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:
NotEvil
2026-04-12 00:51:22 +02:00
parent 2e7a1d403b
commit f6466360b6
1947 changed files with 238025 additions and 1 deletions

View File

@@ -0,0 +1,460 @@
package com.tiedup.remake.compat.mca;
import com.tiedup.remake.compat.mca.ai.MCABondageAIController;
import com.tiedup.remake.compat.mca.ai.MCABondageAILevel;
import com.tiedup.remake.compat.mca.dialogue.MCADialogueManager;
import com.tiedup.remake.compat.mca.personality.MCAMoodManager;
import com.tiedup.remake.compat.mca.personality.MCAPersonality;
import com.tiedup.remake.compat.mca.personality.MCAPersonalityManager;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.state.IRestrainable;
import java.util.Map;
import java.util.WeakHashMap;
import org.jetbrains.annotations.Nullable;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.ItemStack;
/**
* Central coordinator for all MCA-TiedUp integration.
*
* Responsibilities:
* - Manages AI state for tied MCA villagers
* - Coordinates sync operations
* - Provides unified API for bondage operations
* - Handles capture/release lifecycle
*
* This is the main entry point for all MCA bondage operations.
* Other code should go through this manager rather than directly
* manipulating capabilities or AI.
*/
public class MCABondageManager {
// Singleton instance
private static final MCABondageManager INSTANCE = new MCABondageManager();
// Track AI controllers for villagers (weak ref to avoid memory leaks)
private final Map<LivingEntity, MCABondageAIController> aiControllers =
new WeakHashMap<>();
private MCABondageManager() {
// Private constructor for singleton
}
/**
* Get the singleton instance.
*/
public static MCABondageManager getInstance() {
return INSTANCE;
}
// ========================================
// LIFECYCLE EVENTS
// ========================================
/** Dialogue broadcast radius in blocks */
private static final double DIALOGUE_RADIUS = 16.0;
/**
* Called when MCA villager is tied up.
* Initializes or updates AI control and syncs state.
*
* @param villager The MCA villager entity
* @param bind The bind item being applied
*/
public void onVillagerTied(LivingEntity villager, ItemStack bind) {
if (!MCACompat.isMCAVillager(villager)) return;
MCAPersonality personality =
MCAPersonalityManager.getInstance().getPersonality(villager);
TiedUpMod.LOGGER.debug(
"[MCA Manager] Villager tied: {} with {} (personality: {})",
villager.getName().getString(),
bind.getItem().getClass().getSimpleName(),
personality.getMcaId()
);
// Update AI level with personality
MCABondageAIController controller = getOrCreateAIController(villager);
controller.setPersonality(personality);
controller.updateAILevel();
// Mood change
MCAMoodManager.getInstance().onTied(villager);
// Dialogue
String dialogue = MCADialogueManager.getBeingTiedDialogue(villager);
MCADialogueManager.broadcastDialogue(
villager,
dialogue,
DIALOGUE_RADIUS
);
// Sync to clients
syncBondageState(villager);
}
/**
* Called when MCA villager is untied.
* Updates AI control and syncs state.
*
* @param villager The MCA villager entity
*/
public void onVillagerUntied(LivingEntity villager) {
if (!MCACompat.isMCAVillager(villager)) return;
TiedUpMod.LOGGER.debug(
"[MCA Manager] Villager untied: {}",
villager.getName().getString()
);
// Update AI level (may restore normal behavior)
MCABondageAIController controller = aiControllers.get(villager);
if (controller != null) {
controller.updateAILevel();
// If fully free, cleanup controller
if (controller.getCurrentLevel() == MCABondageAILevel.NONE) {
controller.cleanup();
aiControllers.remove(villager);
}
}
// Sync to clients
syncBondageState(villager);
}
/**
* Called when MCA villager is captured (leashed to a captor).
*
* @param villager The MCA villager entity
* @param captor The entity capturing the villager
*/
public void onVillagerCaptured(LivingEntity villager, Entity captor) {
if (!MCACompat.isMCAVillager(villager)) return;
TiedUpMod.LOGGER.debug(
"[MCA Manager] Villager captured: {} by {}",
villager.getName().getString(),
captor.getName().getString()
);
// Update AI to include follow behavior
MCABondageAIController controller = getOrCreateAIController(villager);
controller.updateAILevel();
// Sync to clients
syncBondageState(villager);
}
/**
* Called when MCA villager is freed from capture.
*
* @param villager The MCA villager entity
*/
public void onVillagerFreed(LivingEntity villager) {
if (!MCACompat.isMCAVillager(villager)) return;
TiedUpMod.LOGGER.debug(
"[MCA Manager] Villager freed: {}",
villager.getName().getString()
);
// Update AI level
MCABondageAIController controller = aiControllers.get(villager);
if (controller != null) {
controller.updateAILevel();
}
// Mood change (happy to be freed, usually)
MCAMoodManager.getInstance().onFreed(villager);
// Dialogue
String dialogue = MCADialogueManager.getFreedDialogue(villager);
MCADialogueManager.broadcastDialogue(
villager,
dialogue,
DIALOGUE_RADIUS
);
// Sync to clients
syncBondageState(villager);
}
/**
* Called when collar is added/removed.
*
* @param villager The MCA villager entity
* @param hasCollar Whether the villager now has a collar
*/
public void onCollarChanged(LivingEntity villager, boolean hasCollar) {
if (!MCACompat.isMCAVillager(villager)) return;
TiedUpMod.LOGGER.debug(
"[MCA Manager] Collar changed for {}: {}",
villager.getName().getString(),
hasCollar ? "added" : "removed"
);
// Update AI level
MCABondageAIController controller = getOrCreateAIController(villager);
controller.updateAILevel();
// Mood and dialogue
if (hasCollar) {
MCAMoodManager.getInstance().onCollared(villager);
String dialogue = MCADialogueManager.getCollarPutOnDialogue(
villager
);
MCADialogueManager.broadcastDialogue(
villager,
dialogue,
DIALOGUE_RADIUS
);
} else {
MCAMoodManager.getInstance().onCollarRemoved(villager);
}
// Sync to clients
syncBondageState(villager);
}
/**
* Called when gag state changes.
*
* @param villager The MCA villager entity
* @param isGagged Whether the villager is now gagged
*/
public void onGagChanged(LivingEntity villager, boolean isGagged) {
if (!MCACompat.isMCAVillager(villager)) return;
// Update AI level (gagged+blindfolded = OVERRIDE)
MCABondageAIController controller = aiControllers.get(villager);
if (controller != null) {
controller.updateAILevel();
}
// Mood change
if (isGagged) {
MCAMoodManager.getInstance().onGagged(villager);
}
// Sync to clients
syncBondageState(villager);
}
/**
* Called when blindfold state changes.
*
* @param villager The MCA villager entity
* @param isBlindfolded Whether the villager is now blindfolded
*/
public void onBlindfoldChanged(
LivingEntity villager,
boolean isBlindfolded
) {
if (!MCACompat.isMCAVillager(villager)) return;
// Update AI level (gagged+blindfolded = OVERRIDE)
MCABondageAIController controller = aiControllers.get(villager);
if (controller != null) {
controller.updateAILevel();
}
// Mood change
if (isBlindfolded) {
MCAMoodManager.getInstance().onBlindfolded(villager);
}
// Sync to clients
syncBondageState(villager);
}
/**
* Called when any sensory restriction changes (gag/blindfold).
* Legacy method for compatibility.
*
* @param villager The MCA villager entity
*/
public void onSensoryRestrictionChanged(LivingEntity villager) {
if (!MCACompat.isMCAVillager(villager)) return;
// Update AI level (gagged+blindfolded = OVERRIDE)
MCABondageAIController controller = aiControllers.get(villager);
if (controller != null) {
controller.updateAILevel();
}
// Sync to clients
syncBondageState(villager);
}
// ========================================
// AI CONTROL
// ========================================
/**
* Get or create AI controller for villager.
*
* @param villager The MCA villager entity
* @return The AI controller (never null for valid MCA villagers)
*/
public MCABondageAIController getOrCreateAIController(
LivingEntity villager
) {
return aiControllers.computeIfAbsent(
villager,
MCABondageAIController::new
);
}
/**
* Get AI controller for villager if it exists.
*
* @param villager The MCA villager entity
* @return The AI controller, or null if none exists
*/
@Nullable
public MCABondageAIController getAIController(LivingEntity villager) {
return aiControllers.get(villager);
}
/**
* Get current AI level for villager.
*
* @param villager The MCA villager entity
* @return The AI level (NONE if no controller exists)
*/
public MCABondageAILevel getAILevel(LivingEntity villager) {
MCABondageAIController controller = aiControllers.get(villager);
return controller != null
? controller.getCurrentLevel()
: MCABondageAILevel.NONE;
}
/**
* Force a specific AI level (for debugging/commands).
*
* @param villager The MCA villager entity
* @param level The AI level to set
*/
public void setAILevel(LivingEntity villager, MCABondageAILevel level) {
MCABondageAIController controller = getOrCreateAIController(villager);
controller.setLevel(level);
}
/**
* Check if villager should have restricted AI.
*
* @param villager The MCA villager entity
* @return true if AI is restricted in any way
*/
public boolean shouldRestrictAI(LivingEntity villager) {
return getAILevel(villager) != MCABondageAILevel.NONE;
}
// ========================================
// SYNC
// ========================================
/**
* Sync all bondage state to tracking clients.
* Delegates to MCANetworkHandler.
*
* @param villager The MCA villager entity
*/
public void syncBondageState(LivingEntity villager) {
if (villager.level().isClientSide()) return;
if (!MCACompat.isMCAVillager(villager)) return;
villager
.getCapability(MCACompat.MCA_KIDNAPPED)
.ifPresent(cap -> {
com.tiedup.remake.compat.mca.network.MCANetworkHandler.syncBondageState(
villager,
cap
);
});
}
/**
* Sync bondage state to a specific player.
* Used when player starts tracking the villager.
*
* @param villager The MCA villager entity
* @param tracker The player to sync to
*/
public void syncBondageStateTo(
LivingEntity villager,
net.minecraft.server.level.ServerPlayer tracker
) {
if (villager.level().isClientSide()) return;
if (!MCACompat.isMCAVillager(villager)) return;
villager
.getCapability(MCACompat.MCA_KIDNAPPED)
.ifPresent(cap -> {
com.tiedup.remake.compat.mca.network.MCANetworkHandler.syncBondageStateTo(
villager,
cap,
tracker
);
});
}
// ========================================
// QUERIES
// ========================================
/**
* Get IRestrainable adapter for MCA villager.
* Convenience method that delegates to MCACompat.
*
* @param villager The MCA villager entity
* @return IRestrainable adapter, or null if not an MCA villager
*/
@Nullable
public IRestrainable getKidnappedState(LivingEntity villager) {
return MCACompat.getKidnappedState(villager);
}
/**
* Check if entity is a managed MCA villager.
*
* @param entity The entity to check
* @return true if this entity is tracked by the manager
*/
public boolean isManaged(Entity entity) {
if (!(entity instanceof LivingEntity living)) return false;
return aiControllers.containsKey(living);
}
// ========================================
// CLEANUP
// ========================================
/**
* Remove all tracking for a villager.
* Called when villager dies or is removed.
*
* @param villager The MCA villager entity
*/
public void removeVillager(LivingEntity villager) {
MCABondageAIController controller = aiControllers.remove(villager);
if (controller != null) {
controller.cleanup();
}
}
/**
* Clear all tracking data.
* Called on world unload.
*/
public void clearAll() {
for (MCABondageAIController controller : aiControllers.values()) {
controller.cleanup();
}
aiControllers.clear();
}
}

View File

@@ -0,0 +1,118 @@
package com.tiedup.remake.compat.mca;
import com.tiedup.remake.compat.mca.capability.MCAKidnappedCapability;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.state.IRestrainable;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.LivingEntity;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import net.minecraftforge.client.event.EntityRenderersEvent;
import net.minecraftforge.common.capabilities.Capability;
import net.minecraftforge.common.capabilities.CapabilityManager;
import net.minecraftforge.common.capabilities.CapabilityToken;
import net.minecraftforge.fml.ModList;
import net.minecraftforge.registries.ForgeRegistries;
import org.jetbrains.annotations.Nullable;
/**
* MCA (Minecraft Comes Alive) compatibility module.
* Proxy that delegates to MCAHandler only if mod is loaded.
*/
public class MCACompat {
public static final Capability<MCAKidnappedCapability> MCA_KIDNAPPED =
CapabilityManager.get(new CapabilityToken<>() {});
public static final ResourceLocation MCA_KIDNAPPED_CAP_ID =
ResourceLocation.fromNamespaceAndPath(
TiedUpMod.MOD_ID,
"mca_kidnapped"
);
private static final String MCA_MOD_ID = "mca";
private static boolean mcaLoaded = false;
public static void init() {
mcaLoaded = ModList.get().isLoaded(MCA_MOD_ID);
if (mcaLoaded) {
TiedUpMod.LOGGER.info(
"[MCA Compat] MCA detected! Initializing handler."
);
try {
MCAHandler.init();
} catch (Throwable e) {
TiedUpMod.LOGGER.error(
"[MCA Compat] Failed to load MCA handler (class missing?)",
e
);
mcaLoaded = false;
}
}
}
public static boolean isMCALoaded() {
return mcaLoaded;
}
public static boolean isMCAVillager(Entity entity) {
return mcaLoaded && MCAHandler.isMCAVillager(entity);
}
@Nullable
public static IRestrainable getKidnappedState(LivingEntity entity) {
if (!mcaLoaded) return null;
return MCAHandler.getKidnappedState(entity);
}
public static boolean shouldAttachCapability(Entity entity) {
return isMCAVillager(entity);
}
public static void syncBondageState(LivingEntity entity) {
MCABondageManager.getInstance().syncBondageState(entity);
}
@OnlyIn(Dist.CLIENT)
public static void registerRenderLayers(
EntityRenderersEvent.AddLayers event
) {
if (!mcaLoaded) return;
int layersAdded = 0;
for (EntityType<
?
> entityType : ForgeRegistries.ENTITY_TYPES.getValues()) {
ResourceLocation typeId = ForgeRegistries.ENTITY_TYPES.getKey(
entityType
);
if (
typeId == null || !MCA_MOD_ID.equals(typeId.getNamespace())
) continue;
// Check if it's a villager type
if (!typeId.getPath().contains("villager")) continue;
try {
var renderer = event.getRenderer((EntityType) entityType);
if (renderer != null) {
MCAHandler.addBondageLayer(renderer, event);
layersAdded++;
}
} catch (Exception e) {
TiedUpMod.LOGGER.warn(
"[MCA Compat] Failed to add layer to {}: {}",
typeId,
e.getMessage()
);
}
}
if (layersAdded > 0) {
TiedUpMod.LOGGER.info(
"[MCA Compat] Added layers to {} MCA renderers",
layersAdded
);
}
}
}

View File

@@ -0,0 +1,87 @@
package com.tiedup.remake.compat.mca;
import com.tiedup.remake.compat.mca.capability.MCAKidnappedAdapter;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.state.IRestrainable;
import net.minecraft.client.model.HumanoidModel;
import net.minecraft.client.renderer.entity.LivingEntityRenderer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import net.minecraftforge.client.event.EntityRenderersEvent;
/**
* MCA Handler using reflection to avoid compilation issues with obfuscated jars.
*/
public class MCAHandler {
private static Class<?> villagerLikeClass;
private static Class<?> villagerEntityClass;
public static void init() {
try {
// MCA Forge uses "forge.net.mca" package prefix
villagerLikeClass = Class.forName(
"forge.net.mca.entity.VillagerLike"
);
villagerEntityClass = Class.forName(
"forge.net.mca.entity.VillagerEntityMCA"
);
TiedUpMod.LOGGER.info(
"[MCA Compat] Handler initialized successfully via reflection"
);
} catch (ClassNotFoundException e) {
TiedUpMod.LOGGER.error(
"[MCA Compat] Failed to load MCA classes via reflection",
e
);
}
}
public static boolean isMCAVillager(Entity entity) {
if (entity == null) return false;
// 1. Check loaded classes via reflection
if (
villagerLikeClass != null && villagerLikeClass.isInstance(entity)
) return true;
if (
villagerEntityClass != null &&
villagerEntityClass.isInstance(entity)
) return true;
// 2. Check EntityType namespace (fallback if classes not found/loaded)
net.minecraft.resources.ResourceLocation typeId =
net.minecraftforge.registries.ForgeRegistries.ENTITY_TYPES.getKey(
entity.getType()
);
if (typeId != null && "mca".equals(typeId.getNamespace())) {
String path = typeId.getPath();
if (path.contains("villager")) {
return true;
}
}
// 3. Check class name string (ultimate fallback)
String className = entity.getClass().getName();
return className.contains("mca") && className.contains("Villager");
}
public static IRestrainable getKidnappedState(LivingEntity entity) {
if (!isMCAVillager(entity)) return null;
return entity
.getCapability(MCACompat.MCA_KIDNAPPED)
.map(cap -> new MCAKidnappedAdapter(entity, cap))
.orElse(null);
}
@OnlyIn(Dist.CLIENT)
public static void addBondageLayer(
LivingEntityRenderer<?, ?> renderer,
EntityRenderersEvent.AddLayers event
) {
// V1 MCA bondage render layer removed — V2 render layer handles MCA villagers
}
}

View File

@@ -0,0 +1,532 @@
package com.tiedup.remake.compat.mca.ai;
import com.tiedup.remake.compat.mca.MCABondageManager;
import com.tiedup.remake.compat.mca.MCACompat;
import com.tiedup.remake.compat.mca.ai.goals.MCAFleeGoal;
import com.tiedup.remake.compat.mca.ai.goals.MCAFollowCaptorGoal;
import com.tiedup.remake.compat.mca.ai.goals.MCAPanicGoal;
import com.tiedup.remake.compat.mca.ai.goals.MCAStayGoal;
import com.tiedup.remake.compat.mca.personality.MCAPersonality;
import com.tiedup.remake.compat.mca.personality.MCAPersonalityManager;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.util.RestraintEffectUtils;
import java.lang.ref.WeakReference;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.PathfinderMob;
import net.minecraft.world.entity.ai.goal.FloatGoal;
import net.minecraft.world.entity.ai.goal.Goal;
import net.minecraft.world.entity.ai.goal.WrappedGoal;
import net.minecraft.world.entity.player.Player;
/**
* Controls AI behavior for a single MCA villager during bondage.
*
* Non-invasive design:
* - Stores original AI state before modification
* - Restores fully when freed
* - Configurable per capture state
*
* The controller manages transitions between AI levels,
* injecting and removing goals as needed.
*/
public class MCABondageAIController {
private final WeakReference<LivingEntity> villagerRef;
private MCABondageAILevel currentLevel = MCABondageAILevel.NONE;
// Backup of original goals (for restoration in OVERRIDE mode)
private Set<Goal> originalGoals = null;
private boolean mcaBrainSuspended = false;
// TiedUp-specific goals we've added
private final List<Goal> injectedGoals = new ArrayList<>();
// Personality for behavior modification
private MCAPersonality personality = MCAPersonality.UNKNOWN;
/**
* Create AI controller for an MCA villager.
*
* @param villager The MCA villager entity
*/
public MCABondageAIController(LivingEntity villager) {
this.villagerRef = new WeakReference<>(villager);
}
/**
* Get the current AI control level.
*/
public MCABondageAILevel getCurrentLevel() {
return currentLevel;
}
/**
* Get the villager this controller manages.
*/
public LivingEntity getVillager() {
return villagerRef.get();
}
/**
* Get the villager's personality.
*/
public MCAPersonality getPersonality() {
return personality;
}
/**
* Set the villager's personality.
* This affects AI behavior (flee speed, panic duration, etc.)
*/
public void setPersonality(MCAPersonality personality) {
this.personality =
personality != null ? personality : MCAPersonality.UNKNOWN;
}
// ========================================
// LEVEL MANAGEMENT
// ========================================
/**
* Recalculate and apply appropriate AI level based on current state.
* Call this after any bondage state change.
*/
public void updateAILevel() {
LivingEntity villager = villagerRef.get();
if (villager == null) return;
IBondageState state = MCABondageManager.getInstance().getKidnappedState(
villager
);
if (state == null) {
setLevel(MCABondageAILevel.NONE);
return;
}
// Determine level from state
MCABondageAILevel newLevel = calculateLevel(state);
setLevel(newLevel);
}
/**
* Calculate the appropriate AI level based on bondage state.
*/
private MCABondageAILevel calculateLevel(IBondageState state) {
// OVERRIDE: Fully tied (arms AND legs) OR gagged AND blindfolded
if (state.hasArmsBound() && state.hasLegsBound()) {
return MCABondageAILevel.OVERRIDE;
}
if (state.isGagged() && state.isBlindfolded()) {
return MCABondageAILevel.OVERRIDE;
}
// MODIFIED: Tied (arms or legs)
if (state.isTiedUp()) {
return MCABondageAILevel.MODIFIED;
}
// BASIC: Collar only
if (state.hasCollar()) {
return MCABondageAILevel.BASIC;
}
// NONE: No restrictions
return MCABondageAILevel.NONE;
}
/**
* Set a specific AI level.
* Handles transitions between levels properly.
*
* @param level The level to set
*/
public void setLevel(MCABondageAILevel level) {
if (level == currentLevel) return;
LivingEntity villager = villagerRef.get();
if (villager == null) return;
TiedUpMod.LOGGER.debug(
"[MCA AI] Level change for {}: {} -> {}",
villager.getName().getString(),
currentLevel,
level
);
// Clean up current level effects
transitionFromLevel(currentLevel);
// Apply new level effects
MCABondageAILevel oldLevel = currentLevel;
currentLevel = level;
transitionToLevel(level, oldLevel);
}
/**
* Clean up effects from the previous level.
*/
private void transitionFromLevel(MCABondageAILevel level) {
switch (level) {
case OVERRIDE -> {
restoreMCABrain();
restoreOriginalGoals();
}
case MODIFIED -> removeInjectedGoals();
case BASIC -> removeSpeedReduction();
case NONE -> {
/* nothing to clean */
}
}
}
/**
* Apply effects for the new level.
*/
private void transitionToLevel(
MCABondageAILevel level,
MCABondageAILevel oldLevel
) {
switch (level) {
case NONE -> {
/* nothing to apply */
}
case BASIC -> applySpeedReduction();
case MODIFIED -> {
if (!oldLevel.hasSpeedReduction()) {
applySpeedReduction();
}
injectBondageGoals();
}
case OVERRIDE -> {
if (!oldLevel.hasSpeedReduction()) {
applySpeedReduction();
}
backupAndClearGoals();
suspendMCABrain();
injectOverrideGoals();
}
}
}
// ========================================
// SPEED REDUCTION
// ========================================
private void applySpeedReduction() {
LivingEntity villager = villagerRef.get();
if (villager == null || villager.level().isClientSide()) return;
RestraintEffectUtils.applyBindSpeedReduction(villager);
// Stop current navigation
if (villager instanceof Mob mob) {
mob.getNavigation().stop();
}
TiedUpMod.LOGGER.debug(
"[MCA AI] Applied speed reduction to {}",
villager.getName().getString()
);
}
private void removeSpeedReduction() {
LivingEntity villager = villagerRef.get();
if (villager == null || villager.level().isClientSide()) return;
RestraintEffectUtils.removeBindSpeedReduction(villager);
TiedUpMod.LOGGER.debug(
"[MCA AI] Removed speed reduction from {}",
villager.getName().getString()
);
}
// ========================================
// GOAL INJECTION (MODIFIED LEVEL)
// ========================================
private void injectBondageGoals() {
LivingEntity villager = villagerRef.get();
if (!(villager instanceof PathfinderMob mob)) return;
IBondageState state = MCABondageManager.getInstance().getKidnappedState(
villager
);
if (state == null) return;
// Get combined flee/panic speed from personality
float fleeMultiplier =
MCAPersonalityManager.getInstance().getCombinedCompliance(villager);
// Invert compliance for flee speed (compliant = slow flee, rebellious = fast flee)
float fleeSpeedMod = 2.0f - fleeMultiplier; // Range ~0.5 to ~1.7
fleeSpeedMod = Math.max(0.5f, Math.min(1.5f, fleeSpeedMod));
// Priority 1: Follow captor when leashed
MCAFollowCaptorGoal followGoal = new MCAFollowCaptorGoal(
mob,
1.0,
10.0f,
2.0f
);
mob.goalSelector.addGoal(1, followGoal);
injectedGoals.add(followGoal);
// Priority 2: Panic when being tied (personality affects speed)
double panicSpeed = 1.2 * personality.getFleeSpeedMultiplier();
MCAPanicGoal panicGoal = new MCAPanicGoal(mob, panicSpeed);
mob.goalSelector.addGoal(2, panicGoal);
injectedGoals.add(panicGoal);
// Priority 3: Flee from players if not collared (personality affects speed)
if (!state.hasCollar()) {
double walkSpeed = 1.0 * personality.getFleeSpeedMultiplier();
double sprintSpeed = 1.2 * personality.getFleeSpeedMultiplier();
MCAFleeGoal fleeGoal = new MCAFleeGoal(
mob,
Player.class,
8.0f,
walkSpeed,
sprintSpeed
);
mob.goalSelector.addGoal(3, fleeGoal);
injectedGoals.add(fleeGoal);
}
TiedUpMod.LOGGER.debug(
"[MCA AI] Injected {} bondage goals for {} (personality: {}, flee mod: {})",
injectedGoals.size(),
villager.getName().getString(),
personality.getMcaId(),
String.format("%.2f", personality.getFleeSpeedMultiplier())
);
}
private void removeInjectedGoals() {
LivingEntity villager = villagerRef.get();
if (!(villager instanceof Mob mob)) return;
for (Goal goal : injectedGoals) {
mob.goalSelector.removeGoal(goal);
}
int count = injectedGoals.size();
injectedGoals.clear();
TiedUpMod.LOGGER.debug(
"[MCA AI] Removed {} injected goals from {}",
count,
villager.getName().getString()
);
}
// ========================================
// FULL AI OVERRIDE
// ========================================
private void backupAndClearGoals() {
LivingEntity villager = villagerRef.get();
if (!(villager instanceof Mob mob)) return;
// Backup original goals
originalGoals = new HashSet<>();
for (WrappedGoal wrappedGoal : mob.goalSelector.getAvailableGoals()) {
originalGoals.add(wrappedGoal.getGoal());
}
// Clear all goals
mob.goalSelector.removeAllGoals(g -> true);
TiedUpMod.LOGGER.debug(
"[MCA AI] Backed up {} goals for {}",
originalGoals.size(),
villager.getName().getString()
);
}
private void restoreOriginalGoals() {
if (originalGoals == null) return;
LivingEntity villager = villagerRef.get();
if (!(villager instanceof Mob mob)) return;
// Remove our override goals first
for (Goal goal : injectedGoals) {
mob.goalSelector.removeGoal(goal);
}
injectedGoals.clear();
// Restore original goals
// Note: We don't know original priorities, so use default of 5
for (Goal goal : originalGoals) {
mob.goalSelector.addGoal(5, goal);
}
int count = originalGoals.size();
originalGoals = null;
TiedUpMod.LOGGER.debug(
"[MCA AI] Restored {} original goals for {}",
count,
villager.getName().getString()
);
}
private void injectOverrideGoals() {
LivingEntity villager = villagerRef.get();
if (!(villager instanceof Mob mob)) return;
// Essential goals only
// Priority 0: Always float (don't drown)
FloatGoal floatGoal = new FloatGoal(mob);
mob.goalSelector.addGoal(0, floatGoal);
injectedGoals.add(floatGoal);
// Priority 1: Stay in place
MCAStayGoal stayGoal = new MCAStayGoal(mob);
mob.goalSelector.addGoal(1, stayGoal);
injectedGoals.add(stayGoal);
TiedUpMod.LOGGER.debug(
"[MCA AI] Injected override goals for {}",
villager.getName().getString()
);
}
// ========================================
// MCA BRAIN CONTROL
// ========================================
/**
* Suspend MCA's brain activities (for OVERRIDE level).
* Uses reflection to access VillagerBrain if available.
*/
private void suspendMCABrain() {
LivingEntity villager = villagerRef.get();
if (villager == null) return;
try {
// MCA villagers implement VillagerLike which has getVillagerBrain()
// We try to set their move state to STAY
Method getVillagerBrain = villager
.getClass()
.getMethod("getVillagerBrain");
Object mcaBrain = getVillagerBrain.invoke(villager);
if (mcaBrain != null) {
// Try to find setMoveState method
// MCA uses MoveState enum with values like MOVE, STAY, etc.
for (Method method : mcaBrain.getClass().getMethods()) {
if (method.getName().equals("setMoveState")) {
// Find the MoveState enum
Class<?>[] paramTypes = method.getParameterTypes();
if (paramTypes.length >= 1 && paramTypes[0].isEnum()) {
Object[] enumConstants =
paramTypes[0].getEnumConstants();
// Find STAY constant
for (Object constant : enumConstants) {
if (constant.toString().equals("STAY")) {
method.invoke(mcaBrain, constant, null);
mcaBrainSuspended = true;
TiedUpMod.LOGGER.debug(
"[MCA AI] Suspended brain for {}",
villager.getName().getString()
);
return;
}
}
}
}
}
}
} catch (Exception e) {
TiedUpMod.LOGGER.debug(
"[MCA AI] Could not suspend brain for {}: {}",
villager.getName().getString(),
e.getMessage()
);
}
}
/**
* Restore MCA's brain to normal state.
*/
private void restoreMCABrain() {
if (!mcaBrainSuspended) return;
LivingEntity villager = villagerRef.get();
if (villager == null) return;
try {
Method getVillagerBrain = villager
.getClass()
.getMethod("getVillagerBrain");
Object mcaBrain = getVillagerBrain.invoke(villager);
if (mcaBrain != null) {
for (Method method : mcaBrain.getClass().getMethods()) {
if (method.getName().equals("setMoveState")) {
Class<?>[] paramTypes = method.getParameterTypes();
if (paramTypes.length >= 1 && paramTypes[0].isEnum()) {
Object[] enumConstants =
paramTypes[0].getEnumConstants();
// Find MOVE constant
for (Object constant : enumConstants) {
if (constant.toString().equals("MOVE")) {
method.invoke(mcaBrain, constant, null);
mcaBrainSuspended = false;
TiedUpMod.LOGGER.debug(
"[MCA AI] Restored brain for {}",
villager.getName().getString()
);
return;
}
}
}
}
}
}
} catch (Exception e) {
TiedUpMod.LOGGER.debug(
"[MCA AI] Could not restore brain for {}: {}",
villager.getName().getString(),
e.getMessage()
);
}
mcaBrainSuspended = false;
}
// ========================================
// CLEANUP
// ========================================
/**
* Clean up all AI modifications.
* Call when villager is freed or removed.
*/
public void cleanup() {
// Restore from current level
transitionFromLevel(currentLevel);
currentLevel = MCABondageAILevel.NONE;
// Clear any remaining state
injectedGoals.clear();
originalGoals = null;
mcaBrainSuspended = false;
LivingEntity villager = villagerRef.get();
if (villager != null) {
TiedUpMod.LOGGER.debug(
"[MCA AI] Cleaned up controller for {}",
villager.getName().getString()
);
}
}
}

View File

@@ -0,0 +1,90 @@
package com.tiedup.remake.compat.mca.ai;
/**
* Defines levels of AI control for MCA villagers during bondage.
*
* The AI system progressively restricts villager behavior based on
* their bondage state, from normal behavior to complete immobilization.
*/
public enum MCABondageAILevel {
/**
* NONE: Normal MCA behavior, no TiedUp interference.
* Used when: Not tied, no collar
*/
NONE,
/**
* BASIC: Minimal restrictions.
* Effects:
* - Reduced movement speed (via attribute modifier)
* - Navigation stopped when first applied
* Used when: Collar only (not tied)
*/
BASIC,
/**
* MODIFIED: Custom goals injected alongside MCA goals.
* Effects:
* - All BASIC effects
* - FollowCaptorGoal when leashed
* - PanicGoal when being restrained
* - FleeGoal when not collared
* Used when: Tied up (arms or legs bound)
*/
MODIFIED,
/**
* OVERRIDE: Complete AI replacement.
* Effects:
* - All MODIFIED effects
* - MCA brain activities suspended
* - Only TiedUp goals active (StayGoal)
* Used when: Fully tied (arms AND legs) OR gagged AND blindfolded
*/
OVERRIDE;
/**
* Check if this level has speed reduction applied.
*/
public boolean hasSpeedReduction() {
return this != NONE;
}
/**
* Check if this level injects custom goals.
*/
public boolean hasCustomGoals() {
return this == MODIFIED || this == OVERRIDE;
}
/**
* Check if this level suspends MCA's brain.
*/
public boolean suspendsMCABrain() {
return this == OVERRIDE;
}
/**
* Get the next level up (more restrictive).
*/
public MCABondageAILevel nextLevel() {
return switch (this) {
case NONE -> BASIC;
case BASIC -> MODIFIED;
case MODIFIED -> OVERRIDE;
case OVERRIDE -> OVERRIDE;
};
}
/**
* Get the previous level (less restrictive).
*/
public MCABondageAILevel previousLevel() {
return switch (this) {
case NONE -> NONE;
case BASIC -> NONE;
case MODIFIED -> BASIC;
case OVERRIDE -> MODIFIED;
};
}
}

View File

@@ -0,0 +1,129 @@
package com.tiedup.remake.compat.mca.ai.chatai;
import com.tiedup.remake.compat.mca.MCACompat;
import com.tiedup.remake.compat.mca.personality.MCAPersonalityManager;
import com.tiedup.remake.compat.mca.personality.TiedUpTrait;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.state.ICaptor;
import java.util.List;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
/**
* AI Context Module for MCA's OpenAI chat system.
*
* <p>Injects TiedUp bondage state into the AI context so the LLM
* can respond appropriately when a villager is restrained.
*
* <p>Similar to MCA's PersonalityModule, TraitsModule, etc.
* This module adds contextual information about:
* <ul>
* <li>Current bondage state (tied, gagged, blindfolded, collared)</li>
* <li>TiedUp trait (MASO, REBELLIOUS, BROKEN, TRAINED)</li>
* <li>Captor relationship</li>
* </ul>
*/
public class TiedUpModule {
/**
* Apply TiedUp context to the AI input list.
*
* @param input The list of context strings being built
* @param villager The MCA villager entity
* @param player The player interacting with the villager
*/
public static void apply(
List<String> input,
LivingEntity villager,
Player player
) {
IBondageState state = MCACompat.getKidnappedState(villager);
if (state == null) return;
// Skip if no bondage state
if (
!state.isTiedUp() &&
!state.hasCollar() &&
!state.isGagged() &&
!state.isBlindfolded()
) {
return;
}
// Current bondage state
if (state.isTiedUp()) {
input.add("$villager is currently tied up and restrained. ");
if (state.hasArmsBound() && state.hasLegsBound()) {
input.add(
"$villager cannot move freely - both arms and legs are bound. "
);
} else if (state.hasArmsBound()) {
input.add("$villager's arms are bound behind their back. ");
} else if (state.hasLegsBound()) {
input.add("$villager's legs are bound together. ");
}
}
if (state.isGagged()) {
input.add(
"$villager is gagged and cannot speak clearly - only muffled sounds come out. "
);
}
if (state.isBlindfolded()) {
input.add(
"$villager is blindfolded and cannot see anything around them. "
);
}
if (state.hasCollar()) {
input.add(
"$villager is wearing a collar, marking them as someone's property. "
);
}
// TiedUp Trait affects personality/behavior
TiedUpTrait trait = MCAPersonalityManager.getInstance().getTrait(
villager
);
switch (trait) {
case MASO -> input.add(
"$villager secretly enjoys being restrained and may respond positively to bondage. "
);
case REBELLIOUS -> input.add(
"$villager is extremely rebellious and defiant - they will never willingly submit. "
);
case BROKEN -> input.add(
"$villager has been broken psychologically and shows no resistance, responding with empty compliance. "
);
case TRAINED -> input.add(
"$villager has been trained to be obedient and accepts their situation calmly. "
);
default -> {
// NONE - no special trait context
}
}
// Captor relationship
if (state.isCaptive()) {
ICaptor captor = state.getCaptor();
if (captor != null && player != null) {
// Use getEntity() - returns Entity, not LivingEntity
var captorEntity = captor.getEntity();
if (
captorEntity != null &&
captorEntity.getUUID().equals(player.getUUID())
) {
input.add(
"$player is $villager's captor who tied them up. "
);
} else {
input.add("$villager has been captured by someone else. ");
}
} else {
input.add("$villager has been captured by someone else. ");
}
}
}
}

View File

@@ -0,0 +1,200 @@
package com.tiedup.remake.compat.mca.ai.goals;
import com.tiedup.remake.compat.mca.MCABondageManager;
import com.tiedup.remake.state.IBondageState;
import java.util.EnumSet;
import java.util.List;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.PathfinderMob;
import net.minecraft.world.entity.ai.goal.Goal;
import net.minecraft.world.entity.ai.util.DefaultRandomPos;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
/**
* AI Goal: MCA villager flees from entities of a specific class.
*
* Used when villager is tied but not collared (tries to escape).
* Flees from players within detection range.
*/
public class MCAFleeGoal extends Goal {
private final PathfinderMob mob;
private final Class<? extends LivingEntity> fleeFromClass;
private final float maxDistance;
private final double walkSpeedModifier;
private final double sprintSpeedModifier;
private LivingEntity fleeTarget;
private int pathRecalcDelay;
/** How often to recalculate path (in ticks) */
private static final int PATH_RECALC_INTERVAL = 10;
/**
* Create flee goal.
*
* @param mob The MCA villager mob (must be PathfinderMob)
* @param fleeFromClass Class of entities to flee from
* @param maxDistance Detection range
* @param walkSpeed Walking speed multiplier
* @param sprintSpeed Sprint speed multiplier (when threat is close)
*/
public MCAFleeGoal(
PathfinderMob mob,
Class<? extends LivingEntity> fleeFromClass,
float maxDistance,
double walkSpeed,
double sprintSpeed
) {
this.mob = mob;
this.fleeFromClass = fleeFromClass;
this.maxDistance = maxDistance;
this.walkSpeedModifier = walkSpeed;
this.sprintSpeedModifier = sprintSpeed;
this.setFlags(EnumSet.of(Goal.Flag.MOVE));
}
@Override
public boolean canUse() {
// Only flee if NOT collared (collared villagers obey)
IBondageState state = MCABondageManager.getInstance().getKidnappedState(
mob
);
if (state == null) return false;
if (state.hasCollar()) return false;
// Find nearest threat
this.fleeTarget = findNearestThreat();
if (fleeTarget == null) return false;
// Can we find a path away?
Vec3 fleePos = DefaultRandomPos.getPosAway(
mob,
16, // Range
7, // Y variance
fleeTarget.position()
);
return fleePos != null;
}
/**
* Find the nearest entity to flee from.
*/
private LivingEntity findNearestThreat() {
AABB searchBox = mob.getBoundingBox().inflate(maxDistance);
List<? extends LivingEntity> threats = mob
.level()
.getEntitiesOfClass(
fleeFromClass,
searchBox,
entity ->
entity != mob &&
entity.isAlive() &&
mob.distanceTo(entity) <= maxDistance
);
if (threats.isEmpty()) return null;
// Return closest
LivingEntity closest = null;
double closestDist = Double.MAX_VALUE;
for (LivingEntity entity : threats) {
double dist = mob.distanceToSqr(entity);
if (dist < closestDist) {
closestDist = dist;
closest = entity;
}
}
return closest;
}
@Override
public boolean canContinueToUse() {
// Stop if got collared
IBondageState state = MCABondageManager.getInstance().getKidnappedState(
mob
);
if (state != null && state.hasCollar()) return false;
// Stop if target gone
if (fleeTarget == null || !fleeTarget.isAlive()) return false;
// Continue as long as threat is within range
return fleeTarget.distanceToSqr(mob) < (maxDistance * maxDistance);
}
@Override
public void start() {
pathRecalcDelay = 0;
navigateAway();
}
@Override
public void stop() {
this.fleeTarget = null;
mob.getNavigation().stop();
}
@Override
public void tick() {
if (fleeTarget == null) return;
// Recalculate path periodically
if (--pathRecalcDelay <= 0) {
pathRecalcDelay = PATH_RECALC_INTERVAL;
// Update threat target
LivingEntity newThreat = findNearestThreat();
if (newThreat != null) {
this.fleeTarget = newThreat;
}
navigateAway();
}
// Sprint if threat is very close
double distSqr = mob.distanceToSqr(fleeTarget);
double sprintThreshold = 4.0 * 4.0;
if (distSqr < sprintThreshold) {
mob.getNavigation().setSpeedModifier(sprintSpeedModifier);
} else {
mob.getNavigation().setSpeedModifier(walkSpeedModifier);
}
}
/**
* Navigate away from the current threat.
*/
private void navigateAway() {
Vec3 fleePos = DefaultRandomPos.getPosAway(
mob,
16,
7,
fleeTarget.position()
);
if (fleePos != null) {
mob
.getNavigation()
.moveTo(fleePos.x, fleePos.y, fleePos.z, walkSpeedModifier);
} else {
// Fallback: move directly away
Vec3 awayDir = mob
.position()
.subtract(fleeTarget.position())
.normalize();
Vec3 fallbackPos = mob.position().add(awayDir.scale(8));
mob
.getNavigation()
.moveTo(
fallbackPos.x,
mob.getY(),
fallbackPos.z,
walkSpeedModifier
);
}
}
}

View File

@@ -0,0 +1,115 @@
package com.tiedup.remake.compat.mca.ai.goals;
import java.util.EnumSet;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.ai.goal.Goal;
/**
* AI Goal: MCA villager follows the entity holding their leash.
*
* Similar to vanilla's FollowOwnerGoal but for leash holders.
* Active when villager is leashed to a captor.
*/
public class MCAFollowCaptorGoal extends Goal {
private final Mob mob;
private final double speedModifier;
private final float stopDistance;
private final float startDistance;
private LivingEntity captor;
private int timeToRecalcPath;
/** How often to recalculate path (in ticks) */
private static final int PATH_RECALC_INTERVAL = 10;
/**
* Create follow captor goal.
*
* @param mob The MCA villager mob
* @param speedModifier Speed multiplier when following
* @param startDistance Distance at which to start following
* @param stopDistance Distance at which to stop following
*/
public MCAFollowCaptorGoal(
Mob mob,
double speedModifier,
float startDistance,
float stopDistance
) {
this.mob = mob;
this.speedModifier = speedModifier;
this.startDistance = startDistance;
this.stopDistance = stopDistance;
this.setFlags(EnumSet.of(Goal.Flag.MOVE, Goal.Flag.LOOK));
}
@Override
public boolean canUse() {
if (!mob.isLeashed()) return false;
Entity holder = mob.getLeashHolder();
if (!(holder instanceof LivingEntity living)) return false;
// Don't follow if too close already
double distance = mob.distanceToSqr(holder);
if (distance < startDistance * startDistance) return false;
this.captor = living;
return true;
}
@Override
public boolean canContinueToUse() {
if (!mob.isLeashed()) return false;
if (captor == null || !captor.isAlive()) return false;
// Stop following if close enough
double distance = mob.distanceToSqr(captor);
return distance > stopDistance * stopDistance;
}
@Override
public void start() {
this.timeToRecalcPath = 0;
mob.getNavigation().moveTo(captor, speedModifier);
}
@Override
public void stop() {
this.captor = null;
mob.getNavigation().stop();
}
@Override
public void tick() {
// Look at captor
mob
.getLookControl()
.setLookAt(captor, 10.0f, (float) mob.getMaxHeadXRot());
// Recalculate path periodically
if (--timeToRecalcPath <= 0) {
timeToRecalcPath = PATH_RECALC_INTERVAL;
// Navigate towards captor
if (!mob.getNavigation().moveTo(captor, speedModifier)) {
// If can't find path, try teleporting if very far
double distance = mob.distanceToSqr(captor);
if (distance > 256) {
// 16 blocks squared
// Teleport near captor
double x =
captor.getX() +
(mob.getRandom().nextDouble() - 0.5) * 2;
double y = captor.getY();
double z =
captor.getZ() +
(mob.getRandom().nextDouble() - 0.5) * 2;
mob.teleportTo(x, y, z);
}
}
}
}
}

View File

@@ -0,0 +1,106 @@
package com.tiedup.remake.compat.mca.ai.goals;
import com.tiedup.remake.compat.mca.MCABondageManager;
import com.tiedup.remake.state.IBondageState;
import net.minecraft.world.entity.PathfinderMob;
import net.minecraft.world.entity.ai.goal.PanicGoal;
/**
* AI Goal: MCA villager panics when hurt or restrained.
*
* Extends vanilla PanicGoal with bondage-aware conditions.
* Active when villager is being tied (but can still move).
*/
public class MCAPanicGoal extends PanicGoal {
private final PathfinderMob mob;
/** Ticks since last panic trigger (for brief panic after state changes) */
private int panicCooldown = 0;
/** How long panic lasts after trigger (in ticks) */
private static final int PANIC_DURATION = 60; // 3 seconds
/**
* Create panic goal.
*
* @param mob The MCA villager mob (must be PathfinderMob)
* @param speedModifier Speed multiplier when panicking
*/
public MCAPanicGoal(PathfinderMob mob, double speedModifier) {
super(mob, speedModifier);
this.mob = mob;
}
/**
* Trigger panic for a short duration.
* Call this when something scary happens (being tied, etc.)
*/
public void triggerPanic() {
this.panicCooldown = PANIC_DURATION;
}
@Override
public boolean canUse() {
// Check bondage state
IBondageState state = MCABondageManager.getInstance().getKidnappedState(
mob
);
if (state == null) return false;
// Can't panic if fully tied (can't move)
if (state.hasArmsBound() && state.hasLegsBound()) {
return false;
}
// Can't panic if collared (obedient)
if (state.hasCollar()) {
return false;
}
// Check for recent damage OR active panic cooldown
if (panicCooldown > 0) {
panicCooldown--;
return true;
}
return super.canUse();
}
@Override
public boolean canContinueToUse() {
// Check bondage state
IBondageState state = MCABondageManager.getInstance().getKidnappedState(
mob
);
if (state == null) return false;
// Stop if fully tied or collared
if (state.hasArmsBound() && state.hasLegsBound()) {
return false;
}
if (state.hasCollar()) {
return false;
}
// Continue if cooldown active OR parent says so
if (panicCooldown > 0) {
panicCooldown--;
return true;
}
return super.canContinueToUse();
}
@Override
public void start() {
super.start();
// Could add sound/particle effects here
}
@Override
public void stop() {
super.stop();
panicCooldown = 0;
}
}

View File

@@ -0,0 +1,67 @@
package com.tiedup.remake.compat.mca.ai.goals;
import com.tiedup.remake.compat.mca.MCABondageManager;
import com.tiedup.remake.state.IBondageState;
import java.util.EnumSet;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.ai.goal.Goal;
/**
* AI Goal: MCA villager stays in place when fully restrained.
*
* Prevents all movement, only allows looking around.
* Used in OVERRIDE AI level when villager is fully tied.
*/
public class MCAStayGoal extends Goal {
private final Mob mob;
/**
* Create stay goal.
*
* @param mob The MCA villager mob
*/
public MCAStayGoal(Mob mob) {
this.mob = mob;
// Lock movement and jumping
this.setFlags(EnumSet.of(Goal.Flag.MOVE, Goal.Flag.JUMP));
}
@Override
public boolean canUse() {
IBondageState state = MCABondageManager.getInstance().getKidnappedState(
mob
);
if (state == null) return false;
// Active when tied up (arms and/or legs)
return state.isTiedUp();
}
@Override
public boolean canContinueToUse() {
return canUse();
}
@Override
public void start() {
// Stop all navigation immediately
mob.getNavigation().stop();
}
@Override
public void tick() {
// Keep stopping navigation in case something tries to move
if (!mob.getNavigation().isDone()) {
mob.getNavigation().stop();
}
// Zero out any velocity
mob.setDeltaMovement(mob.getDeltaMovement().multiply(0, 1, 0));
}
@Override
public void stop() {
// Nothing special on stop
}
}

View File

@@ -0,0 +1,928 @@
package com.tiedup.remake.compat.mca.capability;
import com.tiedup.remake.compat.mca.MCABondageManager;
import com.tiedup.remake.compat.mca.MCACompat;
import com.tiedup.remake.items.base.IHasBlindingEffect;
import com.tiedup.remake.items.base.IHasGaggingEffect;
import com.tiedup.remake.items.base.IHasResistance;
import com.tiedup.remake.items.base.ILockable;
import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.state.IRestrainable;
import com.tiedup.remake.state.IRestrainableEntity;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
import com.tiedup.remake.state.ICaptor;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.util.tasks.ItemTask;
import com.tiedup.remake.util.teleport.Position;
import java.util.UUID;
import javax.annotation.Nullable;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
/**
* Adapter that implements IRestrainable for MCA villagers.
*
* Wraps an MCA villager entity and delegates bondage state
* to MCAKidnappedCapability. Uses MCABondageManager for
* lifecycle events and sync operations.
*/
public class MCAKidnappedAdapter implements IRestrainable {
private final LivingEntity entity;
private final MCAKidnappedCapability cap;
public MCAKidnappedAdapter(
LivingEntity entity,
MCAKidnappedCapability cap
) {
this.entity = entity;
this.cap = cap;
}
// ========================================
// 1. CAPTURE LIFECYCLE
// ========================================
@Override
public boolean getCapturedBy(ICaptor captor) {
if (!isEnslavable()) {
return false;
}
// Notify captor (CRITICAL: this updates captor's state so it stops hunting)
captor.addCaptive(this);
// Set captor UUID
Entity captorEntity = captor.getEntity();
if (captorEntity != null) {
cap.setCaptorUUID(captorEntity.getUUID());
}
// For MCA villagers (which are Mobs), use vanilla leash
if (entity instanceof Mob mob && captorEntity != null) {
mob.setLeashedTo(captorEntity, true);
}
// Notify manager
MCABondageManager.getInstance().onVillagerCaptured(
entity,
captorEntity
);
return true;
}
@Override
public void free() {
free(true);
}
@Override
public void free(boolean transportState) {
// Notify captor first, before clearing UUID
ICaptor captor = getCaptor();
if (captor != null) {
captor.removeCaptive(this, transportState);
}
cap.setCaptorUUID(null);
// Remove vanilla leash
if (entity instanceof Mob mob) {
mob.dropLeash(true, transportState);
}
// Notify manager
MCABondageManager.getInstance().onVillagerFreed(entity);
}
@Override
public void transferCaptivityTo(ICaptor newCaptor) {
ICaptor currentCaptor = getCaptor();
if (currentCaptor != null && !currentCaptor.allowCaptiveTransfer()) {
return;
}
free(false);
getCapturedBy(newCaptor);
}
// ========================================
// 2. STATE QUERIES - CAPTURE
// ========================================
@Override
public boolean isEnslavable() {
// Can be captured if tied up OR has collar
return isTiedUp() || hasCollar();
}
@Override
public boolean isCaptive() {
// Check if entity is leashed (vanilla leash for Mobs)
if (entity instanceof Mob mob) {
return mob.isLeashed();
}
return cap.isCaptured();
}
@Override
public boolean canBeTiedUp() {
return !isTiedUp();
}
@Override
public boolean isTiedToPole() {
// MCA villagers can be tied to poles via vanilla leash to fence
if (entity instanceof Mob mob && mob.isLeashed()) {
Entity holder = mob.getLeashHolder();
return holder != null && !(holder instanceof LivingEntity);
}
return false;
}
@Override
public boolean tieToClosestPole(int searchRadius) {
// MCA villagers use vanilla leash mechanics
// This would need to find a fence and attach leash
// For now, return false (not implemented for MCA)
return false;
}
@Override
public boolean isForSell() {
return cap.isForSale();
}
@Override
@Nullable
public ItemTask getSalePrice() {
return cap.getSalePrice();
}
@Override
public void putForSale(ItemTask price) {
cap.setForSale(true);
cap.setSalePrice(price);
}
@Override
public void cancelSale() {
cap.setForSale(false);
cap.setSalePrice(null);
}
@Override
public boolean canBeKidnappedByEvents() {
// MCA villagers can be kidnapped by events
return true;
}
@Override
@Nullable
public ICaptor getCaptor() {
UUID captorUUID = cap.getCaptorUUID();
if (captorUUID == null) {
return null;
}
// Try to find captor entity in world
if (entity.level() instanceof ServerLevel serverLevel) {
Entity captorEntity = serverLevel.getEntity(captorUUID);
if (captorEntity instanceof ICaptor kidnapper) {
return kidnapper;
}
// Players need special handling via PlayerCaptorManager
if (captorEntity instanceof Player player) {
return PlayerBindState.getInstance(player).getCaptorManager();
}
}
return null;
}
@Override
@Nullable
public Entity getTransport() {
// MCA villagers use vanilla leash directly, no proxy entity
return null;
}
// ========================================
// 3. STATE QUERIES - BONDAGE EQUIPMENT
// ========================================
@Override
public boolean isTiedUp() {
return cap.hasBind();
}
@Override
public boolean isGagged() {
return cap.hasGag();
}
@Override
public boolean isBlindfolded() {
return cap.hasBlindfold();
}
@Override
public boolean hasEarplugs() {
return cap.hasEarplugs();
}
@Override
public boolean hasCollar() {
return cap.hasCollar();
}
@Override
public boolean hasLockedCollar() {
ItemStack collar = cap.getCollar();
return (
!collar.isEmpty() &&
collar.getItem() instanceof ILockable lockable &&
lockable.isLocked(collar)
);
}
@Override
public boolean hasNamedCollar() {
ItemStack collar = cap.getCollar();
return !collar.isEmpty() && collar.hasCustomHoverName();
}
@Override
public boolean hasClothes() {
return cap.hasClothes();
}
@Override
public boolean hasMittens() {
return cap.hasMittens();
}
@Override
public boolean hasClothesWithSmallArms() {
// MCA villagers use their own model, not relevant here
return false;
}
@Override
public boolean isBoundAndGagged() {
return isTiedUp() && isGagged();
}
@Override
public boolean hasGaggingEffect() {
ItemStack gag = cap.getGag();
return !gag.isEmpty() && gag.getItem() instanceof IHasGaggingEffect;
}
@Override
public boolean hasBlindingEffect() {
ItemStack blindfold = cap.getBlindfold();
return (
!blindfold.isEmpty() &&
blindfold.getItem() instanceof IHasBlindingEffect
);
}
@Override
public boolean hasKnives() {
// MCA villagers don't have knife inventory by default
return false;
}
// ========================================
// 4. EQUIPMENT MANAGEMENT - PUT ON
// ========================================
public void putBindOn(ItemStack bind) {
cap.setBind(bind);
// Set initial resistance from item
if (bind.getItem() instanceof IHasResistance resistance) {
cap.setBindResistance(resistance.getBaseResistance(entity));
}
// Notify manager (handles AI changes)
MCABondageManager.getInstance().onVillagerTied(entity, bind);
checkBindAfterApply();
}
public void putGagOn(ItemStack gag) {
cap.setGag(gag);
MCABondageManager.getInstance().onSensoryRestrictionChanged(entity);
checkGagAfterApply();
}
public void putBlindfoldOn(ItemStack blindfold) {
cap.setBlindfold(blindfold);
MCABondageManager.getInstance().onSensoryRestrictionChanged(entity);
checkBlindfoldAfterApply();
}
public void putEarplugsOn(ItemStack earplugs) {
cap.setEarplugs(earplugs);
checkEarplugsAfterApply();
}
public void putCollarOn(ItemStack collar) {
cap.setCollar(collar);
if (collar.getItem() instanceof IHasResistance resistance) {
cap.setCollarResistance(resistance.getBaseResistance(entity));
}
MCABondageManager.getInstance().onCollarChanged(entity, true);
checkCollarAfterApply();
}
public void putClothesOn(ItemStack clothes) {
cap.setClothes(clothes);
syncToClients();
}
public void putMittensOn(ItemStack mittens) {
cap.setMittens(mittens);
checkMittensAfterApply();
}
// ========================================
// 5. EQUIPMENT MANAGEMENT - UNEQUIP (V2)
// ========================================
@Override
public ItemStack unequip(BodyRegionV2 region) {
return switch (region) {
case ARMS -> takeBindOff();
case MOUTH -> takeGagOff();
case EYES -> takeBlindfoldOff();
case EARS -> takeEarplugsOff();
case NECK -> takeCollarOff(false);
case TORSO -> takeClothesOff();
case HANDS -> takeMittensOff();
default -> ItemStack.EMPTY;
};
}
@Override
public ItemStack forceUnequip(BodyRegionV2 region) {
// Force-remove: same logic as takeXOff but bypasses isLocked checks.
// NECK and TORSO already have correct handling. See RISK-001.
return switch (region) {
case NECK -> takeCollarOff(true);
case TORSO -> takeClothesOff();
case ARMS -> {
ItemStack current = cap.getBind();
if (current.isEmpty()) yield ItemStack.EMPTY;
cap.setBind(ItemStack.EMPTY);
cap.setBindResistance(0);
MCABondageManager.getInstance().onVillagerUntied(entity);
syncToClients();
yield current;
}
case MOUTH -> {
ItemStack current = cap.getGag();
if (current.isEmpty()) yield ItemStack.EMPTY;
cap.setGag(ItemStack.EMPTY);
MCABondageManager.getInstance().onSensoryRestrictionChanged(entity);
syncToClients();
yield current;
}
case EYES -> {
ItemStack current = cap.getBlindfold();
if (current.isEmpty()) yield ItemStack.EMPTY;
cap.setBlindfold(ItemStack.EMPTY);
MCABondageManager.getInstance().onSensoryRestrictionChanged(entity);
syncToClients();
yield current;
}
case EARS -> {
ItemStack current = cap.getEarplugs();
if (current.isEmpty()) yield ItemStack.EMPTY;
cap.setEarplugs(ItemStack.EMPTY);
syncToClients();
yield current;
}
case HANDS -> {
ItemStack current = cap.getMittens();
if (current.isEmpty()) yield ItemStack.EMPTY;
cap.setMittens(ItemStack.EMPTY);
syncToClients();
yield current;
}
default -> ItemStack.EMPTY;
};
}
// ========================================
// 5b. EQUIPMENT MANAGEMENT - TAKE OFF (local helpers)
// ========================================
public ItemStack takeBindOff() {
ItemStack current = cap.getBind();
if (isLocked(current, false)) {
return ItemStack.EMPTY;
}
cap.setBind(ItemStack.EMPTY);
cap.setBindResistance(0);
// Notify manager (handles AI changes)
MCABondageManager.getInstance().onVillagerUntied(entity);
syncToClients();
return current;
}
public ItemStack takeGagOff() {
ItemStack current = cap.getGag();
if (isLocked(current, false)) {
return ItemStack.EMPTY;
}
cap.setGag(ItemStack.EMPTY);
MCABondageManager.getInstance().onSensoryRestrictionChanged(entity);
syncToClients();
return current;
}
public ItemStack takeBlindfoldOff() {
ItemStack current = cap.getBlindfold();
if (isLocked(current, false)) {
return ItemStack.EMPTY;
}
cap.setBlindfold(ItemStack.EMPTY);
MCABondageManager.getInstance().onSensoryRestrictionChanged(entity);
syncToClients();
return current;
}
public ItemStack takeEarplugsOff() {
ItemStack current = cap.getEarplugs();
if (isLocked(current, false)) {
return ItemStack.EMPTY;
}
cap.setEarplugs(ItemStack.EMPTY);
syncToClients();
return current;
}
public ItemStack takeCollarOff() {
return takeCollarOff(false);
}
public ItemStack takeCollarOff(boolean force) {
ItemStack current = cap.getCollar();
if (!force && isLocked(current, false)) {
return ItemStack.EMPTY;
}
cap.setCollar(ItemStack.EMPTY);
cap.setCollarResistance(0);
MCABondageManager.getInstance().onCollarChanged(entity, false);
syncToClients();
return current;
}
public ItemStack takeClothesOff() {
ItemStack current = cap.getClothes();
cap.setClothes(ItemStack.EMPTY);
syncToClients();
return current;
}
public ItemStack takeMittensOff() {
ItemStack current = cap.getMittens();
if (isLocked(current, false)) {
return ItemStack.EMPTY;
}
cap.setMittens(ItemStack.EMPTY);
syncToClients();
return current;
}
// ========================================
// V2 Region-Based Equipment Access
// ========================================
@Override
public ItemStack getEquipment(BodyRegionV2 region) {
return switch (region) {
case ARMS -> cap.getBind();
case MOUTH -> cap.getGag();
case EYES -> cap.getBlindfold();
case EARS -> cap.getEarplugs();
case NECK -> cap.getCollar();
case TORSO -> cap.getClothes();
case HANDS -> cap.getMittens();
default -> ItemStack.EMPTY;
};
}
@Override
public void equip(BodyRegionV2 region, ItemStack stack) {
switch (region) {
case ARMS -> putBindOn(stack);
case MOUTH -> putGagOn(stack);
case EYES -> putBlindfoldOn(stack);
case EARS -> putEarplugsOn(stack);
case NECK -> putCollarOn(stack);
case TORSO -> putClothesOn(stack);
case HANDS -> putMittensOn(stack);
default -> {}
}
}
// ========================================
// 7. EQUIPMENT MANAGEMENT - REPLACE (V2 region-based)
// ========================================
@Override
public ItemStack replaceEquipment(BodyRegionV2 region, ItemStack newStack, boolean force) {
return switch (region) {
case ARMS -> {
ItemStack old = cap.getBind();
if (!force && isLocked(old, false)) yield ItemStack.EMPTY;
cap.setBind(newStack);
if (newStack.getItem() instanceof IHasResistance resistance) {
cap.setBindResistance(resistance.getBaseResistance(entity));
}
yield old;
}
case MOUTH -> {
ItemStack old = cap.getGag();
if (!force && isLocked(old, false)) yield ItemStack.EMPTY;
cap.setGag(newStack);
yield old;
}
case EYES -> {
ItemStack old = cap.getBlindfold();
if (!force && isLocked(old, false)) yield ItemStack.EMPTY;
cap.setBlindfold(newStack);
yield old;
}
case EARS -> {
ItemStack old = cap.getEarplugs();
if (!force && isLocked(old, false)) yield ItemStack.EMPTY;
cap.setEarplugs(newStack);
yield old;
}
case NECK -> {
ItemStack old = cap.getCollar();
if (!force && isLocked(old, false)) yield ItemStack.EMPTY;
cap.setCollar(newStack);
if (newStack.getItem() instanceof IHasResistance resistance) {
cap.setCollarResistance(resistance.getBaseResistance(entity));
}
yield old;
}
case TORSO -> {
ItemStack old = cap.getClothes();
cap.setClothes(newStack);
yield old;
}
case HANDS -> {
ItemStack old = cap.getMittens();
if (!force && isLocked(old, false)) yield ItemStack.EMPTY;
cap.setMittens(newStack);
yield old;
}
default -> ItemStack.EMPTY;
};
}
// ========================================
// 8. BULK OPERATIONS
// ========================================
@Override
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);
}
@Override
public void untie(boolean drop) {
dropBondageItems(drop);
free();
}
@Override
public void dropBondageItems(boolean drop) {
dropBondageItems(drop, true, true, true, true, true, true);
}
@Override
public void dropBondageItems(boolean drop, boolean dropBind) {
dropBondageItems(drop, dropBind, true, true, true, true, true);
}
@Override
public void dropBondageItems(
boolean drop,
boolean dropBind,
boolean dropGag,
boolean dropBlindfold,
boolean dropEarplugs,
boolean dropCollar,
boolean dropClothes
) {
if (!drop) return;
if (dropBind && !isLocked(cap.getBind(), false)) {
kidnappedDropItem(takeBindOff());
}
if (dropGag && !isLocked(cap.getGag(), false)) {
kidnappedDropItem(takeGagOff());
}
if (dropBlindfold && !isLocked(cap.getBlindfold(), false)) {
kidnappedDropItem(takeBlindfoldOff());
}
if (dropEarplugs && !isLocked(cap.getEarplugs(), false)) {
kidnappedDropItem(takeEarplugsOff());
}
if (dropCollar && !isLocked(cap.getCollar(), false)) {
kidnappedDropItem(takeCollarOff());
}
if (dropClothes) {
kidnappedDropItem(takeClothesOff());
}
}
@Override
public void dropClothes() {
kidnappedDropItem(takeClothesOff());
}
@Override
public int getBondageItemsWhichCanBeRemovedCount() {
int count = 0;
if (
!cap.getBind().isEmpty() && !isLocked(cap.getBind(), false)
) count++;
if (!cap.getGag().isEmpty() && !isLocked(cap.getGag(), false)) count++;
if (
!cap.getBlindfold().isEmpty() &&
!isLocked(cap.getBlindfold(), false)
) count++;
if (
!cap.getEarplugs().isEmpty() && !isLocked(cap.getEarplugs(), false)
) count++;
if (
!cap.getCollar().isEmpty() && !isLocked(cap.getCollar(), false)
) count++;
if (!cap.getClothes().isEmpty()) count++;
return count;
}
// ========================================
// 9. CLOTHES PERMISSION SYSTEM
// ========================================
@Override
public boolean canTakeOffClothes(Player player) {
// MCA villagers allow anyone to remove clothes
return true;
}
@Override
public boolean canChangeClothes(Player player) {
return true;
}
@Override
public boolean canChangeClothes() {
return true;
}
// ========================================
// 10. SPECIAL INTERACTIONS
// ========================================
@Override
public void tighten(Player tightener) {
// Reset bind resistance to maximum
ItemStack bind = cap.getBind();
if (
!bind.isEmpty() &&
bind.getItem() instanceof IHasResistance resistance
) {
cap.setBindResistance(resistance.getBaseResistance(entity));
}
}
@Override
public void applyChloroform(int duration) {
// Apply slowness and weakness effects
entity.addEffect(
new net.minecraft.world.effect.MobEffectInstance(
net.minecraft.world.effect.MobEffects.MOVEMENT_SLOWDOWN,
duration,
4 // Strong slowness
)
);
entity.addEffect(
new net.minecraft.world.effect.MobEffectInstance(
net.minecraft.world.effect.MobEffects.WEAKNESS,
duration,
1
)
);
}
@Override
public void shockKidnapped() {
shockKidnapped("", 1.0F);
}
@Override
public void shockKidnapped(String messageAddon, float damage) {
entity.hurt(entity.damageSources().magic(), damage);
// Could play shock sound here
}
@Override
public void takeBondageItemBy(IRestrainableEntity taker, int slotIndex) {
ItemStack taken = switch (slotIndex) {
case 0 -> takeBindOff();
case 1 -> takeGagOff();
case 2 -> takeBlindfoldOff();
case 3 -> takeEarplugsOff();
case 4 -> takeCollarOff();
case 5 -> takeClothesOff();
default -> ItemStack.EMPTY;
};
if (!taken.isEmpty()) {
taker.kidnappedDropItem(taken);
}
}
// ========================================
// 11. POST-APPLY CALLBACKS
// ========================================
/**
* Sync bondage state to tracking clients.
* Delegates to MCABondageManager.
*/
private void syncToClients() {
MCABondageManager.getInstance().syncBondageState(entity);
}
@Override
public void checkBindAfterApply() {
syncToClients();
}
@Override
public void checkGagAfterApply() {
syncToClients();
}
@Override
public void checkBlindfoldAfterApply() {
syncToClients();
}
@Override
public void checkEarplugsAfterApply() {
syncToClients();
}
@Override
public void checkCollarAfterApply() {
syncToClients();
}
@Override
public void checkMittensAfterApply() {
syncToClients();
}
// ========================================
// 12. DEATH & LIFECYCLE
// ========================================
@Override
public boolean onDeathKidnapped(Level world) {
// Drop all bondage items on death
dropBondageItems(true);
free();
// Cleanup manager state
MCABondageManager.getInstance().removeVillager(entity);
return true;
}
// ========================================
// 13. UTILITY & METADATA
// ========================================
@Override
public UUID getKidnappedUniqueId() {
return entity.getUUID();
}
@Override
public String getKidnappedName() {
return entity.getName().getString();
}
@Override
public String getNameFromCollar() {
ItemStack collar = cap.getCollar();
if (!collar.isEmpty() && collar.hasCustomHoverName()) {
return collar.getHoverName().getString();
}
return getKidnappedName();
}
@Override
public void kidnappedDropItem(ItemStack stack) {
if (!stack.isEmpty() && !entity.level().isClientSide) {
ItemEntity itemEntity = new ItemEntity(
entity.level(),
entity.getX(),
entity.getY() + 0.5,
entity.getZ(),
stack
);
itemEntity.setDefaultPickUpDelay();
entity.level().addFreshEntity(itemEntity);
}
}
@Override
public void teleportToPosition(Position position) {
if (
entity instanceof
net.minecraft.server.level.ServerPlayer serverPlayer
) {
serverPlayer.teleportTo(
position.getX(),
position.getY(),
position.getZ()
);
} else if (entity.level() instanceof ServerLevel) {
entity.teleportTo(
position.getX(),
position.getY(),
position.getZ()
);
}
}
// ========================================
// 14.5. RESISTANCE SYSTEM
// ========================================
@Override
public int getCurrentBindResistance() {
return cap.getBindResistance();
}
@Override
public void setCurrentBindResistance(int resistance) {
cap.setBindResistance(resistance);
}
@Override
public int getCurrentCollarResistance() {
return cap.getCollarResistance();
}
@Override
public void setCurrentCollarResistance(int resistance) {
cap.setCollarResistance(resistance);
}
// ========================================
// 15. ENTITY REFERENCE
// ========================================
@Override
public LivingEntity asLivingEntity() {
return entity;
}
}

View File

@@ -0,0 +1,405 @@
package com.tiedup.remake.compat.mca.capability;
import com.tiedup.remake.compat.mca.personality.TiedUpTrait;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.util.tasks.ItemTask;
import java.util.UUID;
import javax.annotation.Nullable;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.common.util.INBTSerializable;
/**
* Capability that stores bondage state for MCA villagers.
*
* Stores:
* - 7 bondage equipment slots (bind, gag, blindfold, earplugs, collar, clothes, mittens)
* - Captor UUID
* - For sale state and price
* - Resistance values for struggle system
*/
public class MCAKidnappedCapability implements INBTSerializable<CompoundTag> {
// ========================================
// BONDAGE EQUIPMENT SLOTS
// ========================================
private ItemStack bind = ItemStack.EMPTY;
private ItemStack gag = ItemStack.EMPTY;
private ItemStack blindfold = ItemStack.EMPTY;
private ItemStack earplugs = ItemStack.EMPTY;
private ItemStack collar = ItemStack.EMPTY;
private ItemStack clothes = ItemStack.EMPTY;
private ItemStack mittens = ItemStack.EMPTY;
// ========================================
// CAPTURE STATE
// ========================================
/** UUID of the captor entity (null if not captured) */
@Nullable
private UUID captorUUID = null;
/** Whether this villager is for sale */
private boolean forSale = false;
/** Sale price (null if not for sale) */
@Nullable
private ItemTask salePrice = null;
// ========================================
// RESISTANCE VALUES
// ========================================
private int bindResistance = 0;
private int collarResistance = 0;
// ========================================
// TIEDUP TRAIT
// ========================================
/** TiedUp-specific trait (MASO, REBELLIOUS, etc.) */
private TiedUpTrait trait = TiedUpTrait.NONE;
// ========================================
// EQUIPMENT GETTERS
// ========================================
public ItemStack getBind() {
return bind;
}
public ItemStack getGag() {
return gag;
}
public ItemStack getBlindfold() {
return blindfold;
}
public ItemStack getEarplugs() {
return earplugs;
}
public ItemStack getCollar() {
return collar;
}
public ItemStack getClothes() {
return clothes;
}
public ItemStack getMittens() {
return mittens;
}
/**
* Get item by body region.
*/
public ItemStack getItem(BodyRegionV2 region) {
return switch (region) {
case ARMS -> bind;
case MOUTH -> gag;
case EYES -> blindfold;
case EARS -> earplugs;
case NECK -> collar;
case TORSO -> clothes;
case HANDS -> mittens;
default -> ItemStack.EMPTY;
};
}
// ========================================
// EQUIPMENT SETTERS
// ========================================
public void setBind(ItemStack stack) {
this.bind = stack.copy();
}
public void setGag(ItemStack stack) {
this.gag = stack.copy();
}
public void setBlindfold(ItemStack stack) {
this.blindfold = stack.copy();
}
public void setEarplugs(ItemStack stack) {
this.earplugs = stack.copy();
}
public void setCollar(ItemStack stack) {
this.collar = stack.copy();
}
public void setClothes(ItemStack stack) {
this.clothes = stack.copy();
}
public void setMittens(ItemStack stack) {
this.mittens = stack.copy();
}
/**
* Set item by body region.
*/
public void setItem(BodyRegionV2 region, ItemStack stack) {
switch (region) {
case ARMS -> setBind(stack);
case MOUTH -> setGag(stack);
case EYES -> setBlindfold(stack);
case EARS -> setEarplugs(stack);
case NECK -> setCollar(stack);
case TORSO -> setClothes(stack);
case HANDS -> setMittens(stack);
default -> {} // Unsupported region — no-op
}
}
/**
* Clear a region and return the old item.
*/
public ItemStack clearSlot(BodyRegionV2 region) {
ItemStack old = getItem(region).copy();
setItem(region, ItemStack.EMPTY);
return old;
}
// ========================================
// STATE QUERIES
// ========================================
public boolean hasBind() {
return !bind.isEmpty();
}
public boolean hasGag() {
return !gag.isEmpty();
}
public boolean hasBlindfold() {
return !blindfold.isEmpty();
}
public boolean hasEarplugs() {
return !earplugs.isEmpty();
}
public boolean hasCollar() {
return !collar.isEmpty();
}
public boolean hasClothes() {
return !clothes.isEmpty();
}
public boolean hasMittens() {
return !mittens.isEmpty();
}
// ========================================
// CAPTURE STATE
// ========================================
@Nullable
public UUID getCaptorUUID() {
return captorUUID;
}
public void setCaptorUUID(@Nullable UUID uuid) {
this.captorUUID = uuid;
}
public boolean isCaptured() {
return captorUUID != null;
}
public boolean isForSale() {
return forSale;
}
public void setForSale(boolean forSale) {
this.forSale = forSale;
}
@Nullable
public ItemTask getSalePrice() {
return salePrice;
}
public void setSalePrice(@Nullable ItemTask price) {
this.salePrice = price;
}
// ========================================
// RESISTANCE
// ========================================
public int getBindResistance() {
return bindResistance;
}
public void setBindResistance(int resistance) {
this.bindResistance = Math.max(0, resistance);
}
public int getCollarResistance() {
return collarResistance;
}
public void setCollarResistance(int resistance) {
this.collarResistance = Math.max(0, resistance);
}
// ========================================
// TIEDUP TRAIT
// ========================================
public TiedUpTrait getTrait() {
return trait;
}
public void setTrait(TiedUpTrait trait) {
this.trait = trait != null ? trait : TiedUpTrait.NONE;
}
// ========================================
// CLEAR ALL
// ========================================
/**
* Clear all bondage equipment.
*/
public void clearAllEquipment() {
bind = ItemStack.EMPTY;
gag = ItemStack.EMPTY;
blindfold = ItemStack.EMPTY;
earplugs = ItemStack.EMPTY;
collar = ItemStack.EMPTY;
clothes = ItemStack.EMPTY;
mittens = ItemStack.EMPTY;
}
/**
* Clear all state (equipment + capture state).
* Note: Does NOT clear trait - that persists across captures.
*/
public void clearAll() {
clearAllEquipment();
captorUUID = null;
forSale = false;
salePrice = null;
bindResistance = 0;
collarResistance = 0;
// trait is NOT cleared - it persists
}
// ========================================
// NBT SERIALIZATION
// ========================================
private static final String TAG_BIND = "Bind";
private static final String TAG_GAG = "Gag";
private static final String TAG_BLINDFOLD = "Blindfold";
private static final String TAG_EARPLUGS = "Earplugs";
private static final String TAG_COLLAR = "Collar";
private static final String TAG_CLOTHES = "Clothes";
private static final String TAG_MITTENS = "Mittens";
private static final String TAG_CAPTOR = "Captor";
private static final String TAG_FOR_SALE = "ForSale";
private static final String TAG_SALE_PRICE = "SalePrice";
private static final String TAG_BIND_RESISTANCE = "BindResistance";
private static final String TAG_COLLAR_RESISTANCE = "CollarResistance";
private static final String TAG_TRAIT = "TiedUpTrait";
@Override
public CompoundTag serializeNBT() {
CompoundTag tag = new CompoundTag();
// Equipment
if (!bind.isEmpty()) {
tag.put(TAG_BIND, bind.save(new CompoundTag()));
}
if (!gag.isEmpty()) {
tag.put(TAG_GAG, gag.save(new CompoundTag()));
}
if (!blindfold.isEmpty()) {
tag.put(TAG_BLINDFOLD, blindfold.save(new CompoundTag()));
}
if (!earplugs.isEmpty()) {
tag.put(TAG_EARPLUGS, earplugs.save(new CompoundTag()));
}
if (!collar.isEmpty()) {
tag.put(TAG_COLLAR, collar.save(new CompoundTag()));
}
if (!clothes.isEmpty()) {
tag.put(TAG_CLOTHES, clothes.save(new CompoundTag()));
}
if (!mittens.isEmpty()) {
tag.put(TAG_MITTENS, mittens.save(new CompoundTag()));
}
// Capture state
if (captorUUID != null) {
tag.putUUID(TAG_CAPTOR, captorUUID);
}
tag.putBoolean(TAG_FOR_SALE, forSale);
if (salePrice != null) {
tag.put(TAG_SALE_PRICE, salePrice.save());
}
// Resistance
tag.putInt(TAG_BIND_RESISTANCE, bindResistance);
tag.putInt(TAG_COLLAR_RESISTANCE, collarResistance);
// TiedUp Trait
tag.putString(TAG_TRAIT, trait.getId());
return tag;
}
@Override
public void deserializeNBT(CompoundTag tag) {
// Equipment
bind = tag.contains(TAG_BIND)
? ItemStack.of(tag.getCompound(TAG_BIND))
: ItemStack.EMPTY;
gag = tag.contains(TAG_GAG)
? ItemStack.of(tag.getCompound(TAG_GAG))
: ItemStack.EMPTY;
blindfold = tag.contains(TAG_BLINDFOLD)
? ItemStack.of(tag.getCompound(TAG_BLINDFOLD))
: ItemStack.EMPTY;
earplugs = tag.contains(TAG_EARPLUGS)
? ItemStack.of(tag.getCompound(TAG_EARPLUGS))
: ItemStack.EMPTY;
collar = tag.contains(TAG_COLLAR)
? ItemStack.of(tag.getCompound(TAG_COLLAR))
: ItemStack.EMPTY;
clothes = tag.contains(TAG_CLOTHES)
? ItemStack.of(tag.getCompound(TAG_CLOTHES))
: ItemStack.EMPTY;
mittens = tag.contains(TAG_MITTENS)
? ItemStack.of(tag.getCompound(TAG_MITTENS))
: ItemStack.EMPTY;
// Capture state
captorUUID = tag.hasUUID(TAG_CAPTOR) ? tag.getUUID(TAG_CAPTOR) : null;
forSale = tag.getBoolean(TAG_FOR_SALE);
if (tag.contains(TAG_SALE_PRICE)) {
salePrice = ItemTask.load(tag.getCompound(TAG_SALE_PRICE));
} else {
salePrice = null;
}
// Resistance
bindResistance = tag.getInt(TAG_BIND_RESISTANCE);
collarResistance = tag.getInt(TAG_COLLAR_RESISTANCE);
// TiedUp Trait
trait = tag.contains(TAG_TRAIT)
? TiedUpTrait.fromId(tag.getString(TAG_TRAIT))
: TiedUpTrait.NONE;
}
}

View File

@@ -0,0 +1,58 @@
package com.tiedup.remake.compat.mca.capability;
import com.tiedup.remake.compat.mca.MCACompat;
import net.minecraft.core.Direction;
import net.minecraft.nbt.CompoundTag;
import net.minecraftforge.common.capabilities.Capability;
import net.minecraftforge.common.capabilities.ICapabilitySerializable;
import net.minecraftforge.common.util.LazyOptional;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Capability provider for MCA villager bondage state.
*
* Attaches MCAKidnappedCapability to MCA villager entities.
* Handles NBT serialization for persistence.
*/
public class MCAKidnappedProvider
implements ICapabilitySerializable<CompoundTag>
{
private final MCAKidnappedCapability capability;
private final LazyOptional<MCAKidnappedCapability> lazyOptional;
public MCAKidnappedProvider() {
this.capability = new MCAKidnappedCapability();
this.lazyOptional = LazyOptional.of(() -> capability);
}
@Override
public @NotNull <T> LazyOptional<T> getCapability(
@NotNull Capability<T> cap,
@Nullable Direction side
) {
if (cap == MCACompat.MCA_KIDNAPPED) {
return lazyOptional.cast();
}
return LazyOptional.empty();
}
@Override
public CompoundTag serializeNBT() {
return capability.serializeNBT();
}
@Override
public void deserializeNBT(CompoundTag tag) {
capability.deserializeNBT(tag);
}
/**
* Invalidate the LazyOptional when this provider is no longer valid.
* Should be called when the entity is removed.
*/
public void invalidate() {
lazyOptional.invalidate();
}
}

View File

@@ -0,0 +1,422 @@
package com.tiedup.remake.compat.mca.dialogue;
import com.tiedup.remake.compat.mca.personality.MCAMoodManager;
import com.tiedup.remake.compat.mca.personality.MCAPersonality;
import com.tiedup.remake.compat.mca.personality.MCAPersonalityManager;
import com.tiedup.remake.compat.mca.personality.TiedUpTrait;
import java.util.Random;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
/**
* Personality-aware dialogue system for MCA villagers.
*
* <p>Dialogue selection considers:
* <ul>
* <li>Personality type (affects tone and content)</li>
* <li>Current mood (affects positivity/negativity)</li>
* <li>TiedUp trait (MASO has different reactions)</li>
* <li>Bondage state (tied, gagged, collared)</li>
* </ul>
*/
public class MCADialogueManager {
private static final Random RANDOM = new Random();
// ========================================
// BEING TIED DIALOGUES
// ========================================
/**
* Get dialogue when being tied up.
*/
public static String getBeingTiedDialogue(LivingEntity villager) {
MCAPersonality personality =
MCAPersonalityManager.getInstance().getPersonality(villager);
TiedUpTrait trait = MCAPersonalityManager.getInstance().getTrait(
villager
);
// MASO trait overrides personality
if (trait == TiedUpTrait.MASO) {
return pickRandom(
"Oh yes... tie me up~",
"Mmm, tighter please...",
"I've been waiting for this~",
"Don't stop..."
);
}
// BROKEN trait
if (trait == TiedUpTrait.BROKEN) {
return pickRandom("...", "*doesn't resist*", "*stares blankly*");
}
// REBELLIOUS trait
if (trait == TiedUpTrait.REBELLIOUS) {
return pickRandom(
"GET YOUR HANDS OFF ME!",
"I'LL NEVER SUBMIT!",
"You'll pay for this!",
"*fights back violently*"
);
}
return switch (personality) {
case CONFIDENT -> pickRandom(
"You'll regret this!",
"I won't submit to you!",
"This won't hold me!",
"You're making a mistake!"
);
case ATHLETIC -> pickRandom(
"You won't hold me for long!",
"I'll break free from this!",
"These bonds can't stop me!",
"*struggles powerfully*"
);
case SHY -> pickRandom(
"P-please... don't hurt me...",
"*whimpers quietly*",
"No... please no...",
"*trembles*"
);
case SENSITIVE -> pickRandom(
"*sobs*",
"Why... why are you doing this?",
"Please... I'm scared...",
"*cries*"
);
case FRIENDLY -> pickRandom(
"Why are you doing this?",
"This isn't necessary...",
"Can't we talk about this?",
"I thought we were friends..."
);
case FLIRTY -> pickRandom(
"Oh my, how forward of you~",
"I didn't say stop...",
"Mmm, tighter please~",
"If you wanted me tied up, you could have just asked..."
);
case GRUMPY -> pickRandom(
"Ugh, not this again.",
"Of course this is happening.",
"Just my luck...",
"Great. Just great."
);
case LAZY -> pickRandom(
"*sighs* Fine...",
"This better not take long...",
"Whatever...",
"At least I don't have to work..."
);
case ODD -> pickRandom(
"The ropes smell like Tuesday.",
"My left elbow predicts rain.",
"Interesting knot technique.",
"Did you know butterflies can't fly in the rain?"
);
case GREEDY -> pickRandom(
"I'll pay you to stop!",
"How much to let me go?",
"Name your price!",
"This is bad for business..."
);
case PEPPY -> pickRandom(
"Hey! That's not nice!",
"Ow ow ow! Too tight!",
"This is NOT fun!",
"*squirms energetically*"
);
case WITTY -> pickRandom(
"Well, this is a bind.",
"I see you've taken things into your own hands.",
"Tied up with work, I see.",
"Quite the knotty situation."
);
case GLOOMY -> pickRandom(
"I knew this would happen eventually...",
"My life just gets worse...",
"*sighs deeply*",
"Why do I even bother..."
);
default -> pickRandom(
"Help! Someone help me!",
"No! Let me go!",
"Please... stop...",
"*struggles*"
);
};
}
// ========================================
// TIED IDLE DIALOGUES
// ========================================
/**
* Get dialogue for idle state while tied.
* Affected by current mood.
*/
public static String getTiedIdleDialogue(LivingEntity villager) {
MCAPersonality personality =
MCAPersonalityManager.getInstance().getPersonality(villager);
TiedUpTrait trait = MCAPersonalityManager.getInstance().getTrait(
villager
);
int mood = MCAMoodManager.getInstance().getMoodValue(villager);
// MASO enjoying it
if (trait == TiedUpTrait.MASO) {
return pickRandom(
"*content sigh*",
"This is nice...",
"*relaxes into the bonds*",
"Mmm..."
);
}
// BROKEN - no reaction
if (trait == TiedUpTrait.BROKEN) {
return pickRandom("...", "*stares*", "*empty eyes*");
}
// Very low mood - desperate
if (mood < -5) {
return pickRandom(
"*sobs quietly*",
"Someone... please...",
"Why is this happening to me...",
"*whimpers*"
);
}
return switch (personality) {
case CONFIDENT -> pickRandom(
"*struggles against the bonds*",
"I will get out of this.",
"*glares defiantly*",
"You won't break me."
);
case SHY -> pickRandom(
"*trembles*",
"*looks around nervously*",
"*stays very quiet*",
"*avoids eye contact*"
);
case LAZY -> pickRandom(
"*yawns*",
"At least I don't have to work...",
"*naps*",
"Wake me when this is over..."
);
case FLIRTY -> pickRandom(
"*winks*",
"See something you like?",
"*poses suggestively*",
"Like what you see?"
);
case ODD -> pickRandom(
"*hums tunelessly*",
"I wonder what clouds taste like...",
"*counts ceiling tiles*",
"The floor has a nice texture."
);
default -> pickRandom(
"*struggles*",
"*looks around for help*",
"*tests the bonds*",
"*sighs*"
);
};
}
// ========================================
// STRUGGLE DIALOGUES
// ========================================
/**
* Get dialogue for struggling.
*/
public static String getStruggleDialogue(
LivingEntity villager,
boolean success
) {
MCAPersonality personality =
MCAPersonalityManager.getInstance().getPersonality(villager);
TiedUpTrait trait = MCAPersonalityManager.getInstance().getTrait(
villager
);
if (success) {
// MASO disappointed to be free
if (trait == TiedUpTrait.MASO) {
return pickRandom(
"Oh... it's over already?",
"Aww...",
"*slightly disappointed*"
);
}
return switch (personality) {
case CONFIDENT -> "I knew I'd break free!";
case ATHLETIC -> "Finally! I'm free!";
case SHY -> "I-I did it...";
case FLIRTY -> "That was... intense~";
default -> "I'm free!";
};
} else {
// MASO happy to fail
if (trait == TiedUpTrait.MASO) {
return pickRandom("Good... still tied~", "*secretly pleased*");
}
// REBELLIOUS trait: never give up
if (trait == TiedUpTrait.REBELLIOUS) {
return pickRandom(
"I'LL NEVER STOP TRYING!",
"YOU CAN'T BREAK ME!",
"*struggles violently*"
);
}
return switch (personality) {
case CONFIDENT -> "These bonds are tougher than I thought...";
case ATHLETIC -> "*pants* Need to try harder...";
case LAZY -> "Eh, not worth the effort...";
default -> pickRandom(
"*struggles futilely*",
"It's too tight...",
"I can't break free..."
);
};
}
}
// ========================================
// FREED DIALOGUES
// ========================================
/**
* Get dialogue for being freed.
*/
public static String getFreedDialogue(LivingEntity villager) {
MCAPersonality personality =
MCAPersonalityManager.getInstance().getPersonality(villager);
TiedUpTrait trait = MCAPersonalityManager.getInstance().getTrait(
villager
);
// MASO disappointed
if (trait == TiedUpTrait.MASO) {
return pickRandom(
"Aww, already?",
"That was... fun~",
"Maybe again sometime?",
"*reluctantly stands up*"
);
}
// BROKEN - minimal reaction
if (trait == TiedUpTrait.BROKEN) {
return pickRandom("...thank you.", "*nods slowly*", "...");
}
return switch (personality) {
case FRIENDLY -> "Thank you so much! You saved me!";
case SHY -> "T-thank you... *sniffles*";
case CONFIDENT -> "About time. I was about to break free anyway.";
case GRUMPY -> "Hmph. Took you long enough.";
case FLIRTY -> "My hero~ How can I ever repay you?";
case LAZY -> "Finally... that was exhausting.";
case GREEDY -> "I'll remember this! Here's something for your trouble...";
case SENSITIVE -> "*hugs you* Thank you so much!";
default -> "Thank you for freeing me!";
};
}
// ========================================
// COLLAR DIALOGUES
// ========================================
/**
* Get dialogue for collar being put on.
*/
public static String getCollarPutOnDialogue(LivingEntity villager) {
MCAPersonality personality =
MCAPersonalityManager.getInstance().getPersonality(villager);
TiedUpTrait trait = MCAPersonalityManager.getInstance().getTrait(
villager
);
// MASO loves it
if (trait == TiedUpTrait.MASO) {
return pickRandom(
"Yes... I'm yours now~",
"*happy shiver*",
"Finally... a collar~",
"Mark me as yours..."
);
}
// BROKEN accepts it
if (trait == TiedUpTrait.BROKEN) {
return pickRandom("*accepts silently*", "...", "*nods*");
}
return switch (personality) {
case CONFIDENT -> "You think a collar makes me yours? Think again.";
case SHY -> "*whimpers* N-no... please not that...";
case GREEDY -> "Is there something in it for me at least?";
case FLIRTY -> "A collar? How... possessive of you~";
case GRUMPY -> "A collar? Really? How degrading.";
case LAZY -> "Ugh, this is uncomfortable...";
default -> "No... not a collar...";
};
}
// ========================================
// BROADCAST HELPER
// ========================================
/**
* Broadcast a dialogue message from a villager to nearby players.
*
* @param villager The villager speaking
* @param message The dialogue text
* @param radius Radius in blocks
*/
public static void broadcastDialogue(
LivingEntity villager,
String message,
double radius
) {
if (villager.level().isClientSide()) return;
if (message == null || message.isEmpty()) return;
String villagerName = villager.getName().getString();
Component chatMessage = Component.literal(
"<" + villagerName + "> " + message
);
villager
.level()
.getEntitiesOfClass(
Player.class,
villager.getBoundingBox().inflate(radius),
player -> true
)
.forEach(player -> {
player.sendSystemMessage(chatMessage);
});
}
// ========================================
// UTILITY
// ========================================
private static String pickRandom(String... options) {
return options[RANDOM.nextInt(options.length)];
}
}

View File

@@ -0,0 +1,332 @@
package com.tiedup.remake.compat.mca.event;
import com.tiedup.remake.compat.mca.MCACompat;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.compat.mca.capability.MCAKidnappedProvider;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.ItemKey;
import com.tiedup.remake.items.ItemMasterKey;
import com.tiedup.remake.v2.bondage.IV2BondageItem;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.tasks.UntyingPlayerTask;
import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.core.SettingsAccessor;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import net.minecraft.world.InteractionHand;
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.minecraftforge.event.AttachCapabilitiesEvent;
import net.minecraftforge.event.entity.living.LivingDeathEvent;
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
import net.minecraftforge.eventbus.api.EventPriority;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
/**
* Event handlers for MCA compatibility.
*
* - Attaches bondage capability to MCA villagers when they spawn.
* - Handles death of tied MCA villagers.
* - Intercepts MCA menu interaction when holding TiedUp items or untying.
*/
@Mod.EventBusSubscriber(modid = TiedUpMod.MOD_ID)
public class MCACompatEvents {
/**
* Map to track ongoing untying tasks for MCA villagers.
* Key: Villager UUID, Value: The active untying task
*/
private static final Map<UUID, UntyingPlayerTask> mcaUntyingTasks =
new HashMap<>();
/**
* Attach bondage capability to MCA villagers.
*
* Called whenever an entity is created and capabilities are attached.
*/
@SubscribeEvent
public static void onAttachCapabilities(
AttachCapabilitiesEvent<Entity> event
) {
// Only process if MCA is loaded
if (!MCACompat.isMCALoaded()) {
return;
}
Entity entity = event.getObject();
// Check if this is an MCA villager
if (MCACompat.shouldAttachCapability(entity)) {
event.addCapability(
MCACompat.MCA_KIDNAPPED_CAP_ID,
new MCAKidnappedProvider()
);
TiedUpMod.LOGGER.debug(
"[MCA Compat] Attached bondage capability to MCA villager: {} (type: {})",
entity.getName().getString(),
entity.getType().toString()
);
}
}
/**
* Handle death of MCA villagers.
* Drops bondage items and frees captive.
*/
@SubscribeEvent
public static void onLivingDeath(LivingDeathEvent event) {
if (!MCACompat.isMCALoaded()) return;
LivingEntity entity = event.getEntity();
if (!MCACompat.isMCAVillager(entity)) return;
IBondageState state = MCACompat.getKidnappedState(entity);
if (state != null) {
// Trigger death logic (drops items, frees captive)
state.onDeathKidnapped(entity.level());
}
}
/**
* Intercept MCA villager INTERACT_AT (hitbox interaction) when holding TiedUp bondage items.
*
* MCA handles interactAt() which is triggered by INTERACT_AT packets.
* This event fires BEFORE EntityInteract and is where MCA opens their menu.
*
* Uses HIGHEST priority to run before anything else.
*/
@SubscribeEvent(priority = EventPriority.HIGHEST)
public static void onEntityInteractSpecific(
PlayerInteractEvent.EntityInteractSpecific event
) {
if (
!handleMCAInteraction(
event,
event.getTarget(),
event.getEntity(),
event.getHand()
)
) {
return;
}
// Event was handled and canceled in handleMCAInteraction
}
/**
* Intercept MCA villager INTERACT (normal interaction) when holding TiedUp bondage items.
*
* This is a backup in case interactAt doesn't catch it.
*
* Uses HIGHEST priority to run before anything else.
*/
@SubscribeEvent(priority = EventPriority.HIGHEST)
public static void onEntityInteract(
PlayerInteractEvent.EntityInteract event
) {
if (
!handleMCAInteraction(
event,
event.getTarget(),
event.getEntity(),
event.getHand()
)
) {
return;
}
// Event was handled and canceled in handleMCAInteraction
}
/**
* Common handler for both interaction types.
*
* @return true if interaction was handled and event should be considered processed
*/
private static boolean handleMCAInteraction(
PlayerInteractEvent event,
Entity target,
Player player,
InteractionHand hand
) {
// Only process if MCA is loaded
if (!MCACompat.isMCALoaded()) {
return false;
}
// Only intercept for MCA villagers
if (!MCACompat.isMCAVillager(target)) {
return false;
}
// Target must be a LivingEntity for bondage items
if (
!(target instanceof
net.minecraft.world.entity.LivingEntity livingTarget)
) {
return false;
}
ItemStack heldItem = player.getItemInHand(hand);
// 1. Check for TiedUp interaction items (Bondage, Keys)
boolean isTiedUpItem =
heldItem.getItem() instanceof IV2BondageItem ||
heldItem.getItem() instanceof ItemKey ||
heldItem.getItem() instanceof ItemMasterKey;
// 2. Check for Shift-Click Untying logic
// If player is crouching AND target is tied/gagged/collared, shift-click should untie/interact
// regardless of held item (unless it's a specific item that overrides shift-click)
boolean isShiftClickUntying = false;
if (player.isCrouching()) {
IBondageState state = MCACompat.getKidnappedState(livingTarget);
if (
state != null &&
(state.isTiedUp() ||
state.isGagged() ||
state.isBlindfolded() ||
state.hasCollar())
) {
isShiftClickUntying = true;
}
}
// If neither case applies, let MCA handle it
if (!isTiedUpItem && !isShiftClickUntying) {
return false;
}
// Cancel the event FIRST to prevent MCA from handling it
event.setCanceled(true);
// Manually trigger interaction logic
net.minecraft.world.InteractionResult result =
net.minecraft.world.InteractionResult.PASS;
if (isTiedUpItem) {
// Let the item handle it
result = heldItem.interactLivingEntity(player, livingTarget, hand);
} else if (isShiftClickUntying) {
// Shift-click interaction logic for untying/ungagging
IBondageState state = MCACompat.getKidnappedState(livingTarget);
if (state != null) {
// Priority: Untie > Ungag > Unblindfold > etc.
// This mimics EntityDamsel logic or ItemBind.interactLivingEntity logic for untying
// No collar ownership check — any player can untie MCA villagers by design
// (MCA villagers use a separate relationship system, not TiedUp collars)
boolean actionTaken = false;
// Try to remove items in order
if (state.isTiedUp()) {
// Check if player can untie (e.g. has knife if needed, or if resistance allows)
// For now, allow simple untying
if (!state.getEquipment(BodyRegionV2.ARMS).isEmpty()) {
// Ensure there is a bind
// Check lock
ItemStack bind = state.getEquipment(BodyRegionV2.ARMS);
if (
bind.getItem() instanceof
com.tiedup.remake.items.base.ILockable lockable &&
lockable.isLocked(bind)
) {
// Locked - can't untie without key
// Maybe send message?
} else {
// Use timed untying task (same as Damsels - 10 seconds by default)
UUID villagerUuid = livingTarget.getUUID();
UntyingPlayerTask existingTask =
mcaUntyingTasks.get(villagerUuid);
int untyingSeconds =
SettingsAccessor.getUntyingPlayerTime(
player.level().getGameRules()
);
if (
existingTask == null ||
existingTask.isOutdated() ||
existingTask.getTargetEntity() != livingTarget
) {
// Create new untying task
UntyingPlayerTask newTask =
new UntyingPlayerTask(
state,
livingTarget,
untyingSeconds,
player.level(),
player
);
mcaUntyingTasks.put(villagerUuid, newTask);
existingTask = newTask;
TiedUpMod.LOGGER.debug(
"[MCA Compat] Started untying task for {} ({} seconds)",
livingTarget.getName().getString(),
untyingSeconds
);
} else {
// Continue existing task, update helper reference
existingTask.setHelper(player);
}
// Update task progress (sends progress packets, completes if timer expired)
existingTask.update();
// Clean up completed tasks
if (existingTask.isStopped()) {
mcaUntyingTasks.remove(villagerUuid);
}
actionTaken = true;
}
}
} else if (state.isGagged()) {
// Check lock
ItemStack gag = state.getEquipment(BodyRegionV2.MOUTH);
if (
gag.getItem() instanceof
com.tiedup.remake.items.base.ILockable lockable &&
lockable.isLocked(gag)
) {
// Locked
} else {
state.unequip(BodyRegionV2.MOUTH);
state.kidnappedDropItem(gag);
actionTaken = true;
}
}
// Add other removal logic as needed
if (actionTaken) {
result = net.minecraft.world.InteractionResult.SUCCESS;
player.swing(hand, true);
} else {
// If we couldn't remove anything (e.g. locked), maybe open GUI?
// Or simply PASS but keep event canceled to stop MCA menu
result = net.minecraft.world.InteractionResult.CONSUME;
}
}
}
event.setCancellationResult(result);
TiedUpMod.LOGGER.debug(
"[MCA Compat] Intercepted MCA interaction - {} on {} (Item: {}, Shift: {}, Result: {})",
player.getName().getString(),
target.getName().getString(),
isTiedUpItem
? heldItem.getItem().getClass().getSimpleName()
: "Other",
isShiftClickUntying,
result
);
return true;
}
}

View File

@@ -0,0 +1,85 @@
package com.tiedup.remake.compat.mca.network;
import com.tiedup.remake.compat.mca.MCACompat;
import com.tiedup.remake.compat.mca.capability.MCAKidnappedCapability;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.network.ModNetwork;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.LivingEntity;
/**
* Centralized network handling for MCA compatibility.
*
* All MCA-related sync operations should go through this handler.
* This replaces scattered sync calls throughout the code.
*/
public class MCANetworkHandler {
/**
* Sync complete bondage state for a villager to all tracking clients.
* Call this after any bondage equipment change.
*
* @param villager The MCA villager entity
* @param cap The villager's bondage capability
*/
public static void syncBondageState(
LivingEntity villager,
MCAKidnappedCapability cap
) {
if (villager.level().isClientSide()) return;
PacketSyncMCABondage packet = PacketSyncMCABondage.fromEntity(
villager,
cap
);
ModNetwork.sendToAllTrackingEntity(packet, villager);
TiedUpMod.LOGGER.debug(
"[MCA Network] Synced bondage state for {} to all trackers",
villager.getName().getString()
);
}
/**
* Sync bondage state to a specific player.
* Used when player starts tracking the villager (enters render distance).
*
* @param villager The MCA villager entity
* @param cap The villager's bondage capability
* @param tracker The player to sync to
*/
public static void syncBondageStateTo(
LivingEntity villager,
MCAKidnappedCapability cap,
ServerPlayer tracker
) {
PacketSyncMCABondage packet = PacketSyncMCABondage.fromEntity(
villager,
cap
);
ModNetwork.sendToPlayer(packet, tracker);
TiedUpMod.LOGGER.debug(
"[MCA Network] Synced bondage state for {} to tracker {}",
villager.getName().getString(),
tracker.getName().getString()
);
}
/**
* Sync bondage state using the villager's capability directly.
* Convenience method that fetches capability internally.
*
* @param villager The MCA villager entity
*/
public static void syncBondageState(LivingEntity villager) {
if (villager.level().isClientSide()) return;
if (!MCACompat.isMCAVillager(villager)) return;
villager
.getCapability(MCACompat.MCA_KIDNAPPED)
.ifPresent(cap -> {
syncBondageState(villager, cap);
});
}
}

View File

@@ -0,0 +1,152 @@
package com.tiedup.remake.compat.mca.network;
import com.tiedup.remake.compat.mca.MCACompat;
import com.tiedup.remake.compat.mca.capability.MCAKidnappedCapability;
import com.tiedup.remake.core.TiedUpMod;
import java.util.function.Supplier;
import net.minecraft.client.Minecraft;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import net.minecraftforge.network.NetworkEvent;
/**
* Packet to sync MCA villager bondage state from server to client.
*
* Sent when bondage items are added/removed from MCA villagers.
* Contains all 7 bondage slots for the entity.
*/
public class PacketSyncMCABondage {
private final int entityId;
private final ItemStack bind;
private final ItemStack gag;
private final ItemStack blindfold;
private final ItemStack earplugs;
private final ItemStack collar;
private final ItemStack clothes;
private final ItemStack mittens;
public PacketSyncMCABondage(
int entityId,
ItemStack bind,
ItemStack gag,
ItemStack blindfold,
ItemStack earplugs,
ItemStack collar,
ItemStack clothes,
ItemStack mittens
) {
this.entityId = entityId;
this.bind = bind;
this.gag = gag;
this.blindfold = blindfold;
this.earplugs = earplugs;
this.collar = collar;
this.clothes = clothes;
this.mittens = mittens;
}
/**
* Create packet from MCA villager's capability.
*/
public static PacketSyncMCABondage fromEntity(
Entity entity,
MCAKidnappedCapability cap
) {
return new PacketSyncMCABondage(
entity.getId(),
cap.getBind(),
cap.getGag(),
cap.getBlindfold(),
cap.getEarplugs(),
cap.getCollar(),
cap.getClothes(),
cap.getMittens()
);
}
/**
* Decode packet from network buffer.
*/
public static PacketSyncMCABondage decode(FriendlyByteBuf buf) {
int entityId = buf.readVarInt();
ItemStack bind = buf.readItem();
ItemStack gag = buf.readItem();
ItemStack blindfold = buf.readItem();
ItemStack earplugs = buf.readItem();
ItemStack collar = buf.readItem();
ItemStack clothes = buf.readItem();
ItemStack mittens = buf.readItem();
return new PacketSyncMCABondage(
entityId,
bind,
gag,
blindfold,
earplugs,
collar,
clothes,
mittens
);
}
/**
* Encode packet to network buffer.
*/
public void encode(FriendlyByteBuf buf) {
buf.writeVarInt(entityId);
buf.writeItem(bind);
buf.writeItem(gag);
buf.writeItem(blindfold);
buf.writeItem(earplugs);
buf.writeItem(collar);
buf.writeItem(clothes);
buf.writeItem(mittens);
}
/**
* Handle packet on client side.
*/
public void handle(Supplier<NetworkEvent.Context> ctx) {
ctx.get().enqueueWork(() -> handleClient());
ctx.get().setPacketHandled(true);
}
@OnlyIn(Dist.CLIENT)
private void handleClient() {
Minecraft mc = Minecraft.getInstance();
if (mc.level == null) return;
Entity entity = mc.level.getEntity(entityId);
if (entity == null) {
TiedUpMod.LOGGER.debug(
"[MCA Sync] Entity {} not found on client",
entityId
);
return;
}
// Get the capability and update it
entity
.getCapability(MCACompat.MCA_KIDNAPPED)
.ifPresent(cap -> {
cap.setBind(bind);
cap.setGag(gag);
cap.setBlindfold(blindfold);
cap.setEarplugs(earplugs);
cap.setCollar(collar);
cap.setClothes(clothes);
cap.setMittens(mittens);
TiedUpMod.LOGGER.debug(
"[MCA Sync] Updated bondage for {} (bind: {})",
entity.getName().getString(),
!bind.isEmpty()
? bind.getItem().getClass().getSimpleName()
: "none"
);
});
}
}

View File

@@ -0,0 +1,334 @@
package com.tiedup.remake.compat.mca.personality;
import com.tiedup.remake.compat.mca.MCACompat;
import com.tiedup.remake.core.TiedUpMod;
import java.lang.reflect.Method;
import java.util.Random;
import net.minecraft.world.entity.LivingEntity;
/**
* Manages MCA mood modifications during bondage events.
*
* <p>MCA mood system:
* <ul>
* <li>getMoodValue() -&gt; int (typically -15 to 15)</li>
* <li>modifyMoodValue(int) -&gt; void</li>
* </ul>
*
* <p>Mood changes are personality-dependent. Some personalities
* enjoy being restrained (FLIRTY), while others hate it (CONFIDENT, SENSITIVE).
*/
public class MCAMoodManager {
private static final MCAMoodManager INSTANCE = new MCAMoodManager();
private static final Random RANDOM = new Random();
// Cached reflection methods
private Method modifyMoodMethod;
private Method getMoodValueMethod;
private boolean reflectionInitialized = false;
public static MCAMoodManager getInstance() {
return INSTANCE;
}
private MCAMoodManager() {}
// ========================================
// EVENT HANDLERS
// ========================================
/**
* Called when villager is tied up.
* Mood change depends on personality and trait.
*/
public void onTied(LivingEntity entity) {
if (!MCACompat.isMCAVillager(entity)) return;
MCAPersonality personality =
MCAPersonalityManager.getInstance().getPersonality(entity);
TiedUpTrait trait = MCAPersonalityManager.getInstance().getTrait(
entity
);
int moodChange;
if (personality.isUnpredictable()) {
// ODD: random mood change
moodChange = RANDOM.nextInt(11) - 5; // -5 to +5
} else {
moodChange = TiedUpTrait.getCombinedMoodTied(personality, trait);
}
modifyMood(entity, moodChange);
TiedUpMod.LOGGER.debug(
"[MCA Mood] {} tied: mood {} (personality={}, trait={})",
entity.getName().getString(),
moodChange >= 0 ? "+" + moodChange : moodChange,
personality.getMcaId(),
trait.getId()
);
}
/**
* Called when villager is freed.
* Generally positive, but personality affects amount.
*/
public void onFreed(LivingEntity entity) {
if (!MCACompat.isMCAVillager(entity)) return;
MCAPersonality personality =
MCAPersonalityManager.getInstance().getPersonality(entity);
TiedUpTrait trait = MCAPersonalityManager.getInstance().getTrait(
entity
);
int moodChange;
if (personality.isUnpredictable()) {
moodChange = RANDOM.nextInt(11) - 3; // -3 to +7 (slightly positive bias)
} else {
// Base positive + personality modifier
moodChange = switch (personality) {
case SENSITIVE -> +7;
case FRIENDLY -> +6;
case SHY, CONFIDENT -> +5;
case GRUMPY -> +2;
case LAZY -> +1;
case FLIRTY -> +1; // They liked being tied, so meh about freedom
default -> +4;
};
// MASO trait: less happy about being freed
if (trait.enjoysBondage()) {
moodChange = Math.max(0, moodChange - 3);
}
}
modifyMood(entity, moodChange);
TiedUpMod.LOGGER.debug(
"[MCA Mood] {} freed: mood +{}",
entity.getName().getString(),
moodChange
);
}
/**
* Called when collar is put on.
* Generally negative (loss of freedom), but personality-dependent.
*/
public void onCollared(LivingEntity entity) {
if (!MCACompat.isMCAVillager(entity)) return;
MCAPersonality personality =
MCAPersonalityManager.getInstance().getPersonality(entity);
TiedUpTrait trait = MCAPersonalityManager.getInstance().getTrait(
entity
);
int moodChange;
if (personality.isUnpredictable()) {
moodChange = RANDOM.nextInt(13) - 8; // -8 to +4
} else {
moodChange = switch (personality) {
case SENSITIVE -> -8;
case CONFIDENT -> -6;
case SHY -> -4;
case FRIENDLY -> -3;
case GRUMPY -> -4;
case LAZY -> -1;
case FLIRTY -> +1; // Kinky!
default -> -4;
};
// MASO trait bonus
if (trait.enjoysBondage()) {
moodChange += 4;
}
}
modifyMood(entity, moodChange);
}
/**
* Called when collar is removed.
*/
public void onCollarRemoved(LivingEntity entity) {
if (!MCACompat.isMCAVillager(entity)) return;
MCAPersonality personality =
MCAPersonalityManager.getInstance().getPersonality(entity);
TiedUpTrait trait = MCAPersonalityManager.getInstance().getTrait(
entity
);
int moodChange = trait.enjoysBondage() ? +1 : +3;
modifyMood(entity, moodChange);
}
/**
* Called when struggle fails.
*/
public void onStruggleFailed(LivingEntity entity) {
if (!MCACompat.isMCAVillager(entity)) return;
MCAPersonality personality =
MCAPersonalityManager.getInstance().getPersonality(entity);
int moodChange = personality == MCAPersonality.CONFIDENT ? -2 : -1;
modifyMood(entity, moodChange);
}
/**
* Called when struggle succeeds.
*/
public void onStruggleSuccess(LivingEntity entity) {
if (!MCACompat.isMCAVillager(entity)) return;
modifyMood(entity, +3);
}
/**
* Called when gagged.
*/
public void onGagged(LivingEntity entity) {
if (!MCACompat.isMCAVillager(entity)) return;
MCAPersonality personality =
MCAPersonalityManager.getInstance().getPersonality(entity);
TiedUpTrait trait = MCAPersonalityManager.getInstance().getTrait(
entity
);
int moodChange = trait.enjoysBondage() ? +1 : -2;
if (personality == MCAPersonality.SENSITIVE) {
moodChange -= 2;
}
modifyMood(entity, moodChange);
}
/**
* Called when blindfolded.
*/
public void onBlindfolded(LivingEntity entity) {
if (!MCACompat.isMCAVillager(entity)) return;
MCAPersonality personality =
MCAPersonalityManager.getInstance().getPersonality(entity);
TiedUpTrait trait = MCAPersonalityManager.getInstance().getTrait(
entity
);
int moodChange = trait.enjoysBondage() ? +1 : -2;
if (
personality == MCAPersonality.SHY ||
personality == MCAPersonality.SENSITIVE
) {
moodChange -= 2; // Extra scary for anxious personalities
}
modifyMood(entity, moodChange);
}
// ========================================
// REFLECTION-BASED MOOD ACCESS
// ========================================
/**
* Modify mood using MCA's modifyMoodValue method via reflection.
*/
public void modifyMood(LivingEntity entity, int amount) {
if (amount == 0) return;
try {
initializeReflection(entity);
if (modifyMoodMethod == null) {
return;
}
Object brain = entity
.getClass()
.getMethod("getVillagerBrain")
.invoke(entity);
if (brain != null) {
modifyMoodMethod.invoke(brain, amount);
}
} catch (Exception e) {
TiedUpMod.LOGGER.debug(
"[MCA Mood] Failed to modify mood: {}",
e.getMessage()
);
}
}
/**
* Get current mood value.
*
* @return Mood value (typically -15 to 15), or 0 if not available
*/
public int getMoodValue(LivingEntity entity) {
try {
initializeReflection(entity);
if (getMoodValueMethod == null) {
return 0;
}
Object brain = entity
.getClass()
.getMethod("getVillagerBrain")
.invoke(entity);
if (brain != null) {
return (int) getMoodValueMethod.invoke(brain);
}
} catch (Exception e) {
TiedUpMod.LOGGER.debug(
"[MCAMoodManager] Failed to get mood value via reflection",
e
);
}
return 0;
}
private void initializeReflection(LivingEntity entity) {
if (reflectionInitialized) return;
reflectionInitialized = true;
try {
Object brain = entity
.getClass()
.getMethod("getVillagerBrain")
.invoke(entity);
if (brain != null) {
for (Method m : brain.getClass().getMethods()) {
if (
m.getName().equals("modifyMoodValue") &&
m.getParameterCount() == 1
) {
modifyMoodMethod = m;
}
if (
m.getName().equals("getMoodValue") &&
m.getParameterCount() == 0
) {
getMoodValueMethod = m;
}
}
if (modifyMoodMethod != null) {
TiedUpMod.LOGGER.debug(
"[MCA Mood] Found mood methods via reflection"
);
}
}
} catch (Exception e) {
TiedUpMod.LOGGER.debug(
"[MCA Mood] Reflection init failed: {}",
e.getMessage()
);
}
}
}

View File

@@ -0,0 +1,130 @@
package com.tiedup.remake.compat.mca.personality;
/**
* Maps MCA personality types to TiedUp behavior modifiers.
*
* <p>MCA 1.20.1 has these personalities:
* ATHLETIC, CONFIDENT, FRIENDLY, FLIRTY, WITTY, SHY, GLOOMY,
* SENSITIVE, GREEDY, ODD, LAZY, GRUMPY, PEPPY
*
* <p>Each personality affects:
* <ul>
* <li>struggleMultiplier - How effectively they struggle against bonds</li>
* <li>complianceMultiplier - How quickly they accept restraints</li>
* <li>fleeSpeedMultiplier - How fast they flee/panic</li>
* <li>baseMoodTied - Mood change when tied (some like it!)</li>
* </ul>
*/
public enum MCAPersonality {
// Strong personalities - resist more
ATHLETIC("athletic", 1.5f, 0.8f, 1.2f, -3),
CONFIDENT("confident", 1.3f, 1.0f, 1.0f, -4),
// Social personalities - more cooperative
FRIENDLY("friendly", 0.7f, 1.2f, 0.8f, -2),
FLIRTY("flirty", 0.5f, 1.5f, 0.6f, +2), // Enjoys it!
// Clever but not strong
WITTY("witty", 0.9f, 1.1f, 1.1f, -2),
// Anxious personalities - panic more
SHY("shy", 0.8f, 0.7f, 1.4f, -3),
SENSITIVE("sensitive", 0.7f, 0.6f, 1.5f, -6),
GLOOMY("gloomy", 0.6f, 0.8f, 0.9f, -4),
// Special personalities
GREEDY("greedy", 1.0f, 0.5f, 1.0f, -2), // Can be bribed
ODD("odd", 1.1f, 1.0f, 1.2f, 0), // Unpredictable (mood is random)
LAZY("lazy", 0.5f, 1.0f, 0.6f, 0), // Doesn't care much
GRUMPY("grumpy", 1.2f, 0.3f, 0.7f, -3), // Resists, doesn't flee
PEPPY("peppy", 1.0f, 1.2f, 1.3f, -1), // Energetic
// Unknown/fallback
UNKNOWN("unknown", 1.0f, 1.0f, 1.0f, -3);
private final String mcaId;
private final float struggleMultiplier;
private final float complianceMultiplier;
private final float fleeSpeedMultiplier;
private final int baseMoodTied;
MCAPersonality(
String mcaId,
float struggle,
float compliance,
float flee,
int moodTied
) {
this.mcaId = mcaId;
this.struggleMultiplier = struggle;
this.complianceMultiplier = compliance;
this.fleeSpeedMultiplier = flee;
this.baseMoodTied = moodTied;
}
public String getMcaId() {
return mcaId;
}
/**
* Get struggle effectiveness multiplier.
* Higher = struggles more effectively against bonds.
*/
public float getStruggleMultiplier() {
return struggleMultiplier;
}
/**
* Get compliance multiplier.
* Higher = accepts restraints more easily (less resistance time).
*/
public float getComplianceMultiplier() {
return complianceMultiplier;
}
/**
* Get flee/panic speed multiplier.
* Higher = runs away faster when panicking.
*/
public float getFleeSpeedMultiplier() {
return fleeSpeedMultiplier;
}
/**
* Get base mood change when tied.
* Negative = unhappy, Positive = enjoys it.
*/
public int getBaseMoodTied() {
return baseMoodTied;
}
/**
* Check if this personality generally enjoys being restrained.
*/
public boolean enjoysBondage() {
return baseMoodTied > 0;
}
/**
* Check if this personality is unpredictable (ODD).
*/
public boolean isUnpredictable() {
return this == ODD;
}
/**
* Find personality by MCA ID string (case-insensitive).
*
* @param id The MCA personality ID (e.g., "athletic", "shy")
* @return The matching personality, or UNKNOWN if not found
*/
public static MCAPersonality fromMcaId(String id) {
if (id == null) return UNKNOWN;
for (MCAPersonality p : values()) {
if (p.mcaId.equalsIgnoreCase(id)) {
return p;
}
}
return UNKNOWN;
}
}

View File

@@ -0,0 +1,251 @@
package com.tiedup.remake.compat.mca.personality;
import com.tiedup.remake.compat.mca.MCACompat;
import com.tiedup.remake.compat.mca.capability.MCAKidnappedCapability;
import com.tiedup.remake.core.TiedUpMod;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.WeakHashMap;
import org.jetbrains.annotations.Nullable;
import net.minecraft.world.entity.LivingEntity;
/**
* Manages MCA personality and TiedUp trait access for villagers.
*
* <p>Uses reflection to access MCA's VillagerBrain.getPersonality() method,
* since MCA is an optional dependency.
*
* <p>Also manages TiedUp traits stored in the villager's capability.
*/
public class MCAPersonalityManager {
private static final MCAPersonalityManager INSTANCE =
new MCAPersonalityManager();
/** Cache personality per entity (weak refs to allow GC) */
private final Map<LivingEntity, MCAPersonality> personalityCache =
new WeakHashMap<>();
/** Cached reflection objects */
private Method getVillagerBrainMethod;
private Method getPersonalityMethod;
private boolean reflectionInitialized = false;
public static MCAPersonalityManager getInstance() {
return INSTANCE;
}
private MCAPersonalityManager() {}
// ========================================
// MCA PERSONALITY ACCESS (Reflection)
// ========================================
/**
* Get the MCA personality for a villager.
* Uses reflection to access MCA's VillagerBrain.getPersonality().
*
* @param entity The MCA villager entity
* @return The personality, or UNKNOWN if not available
*/
public MCAPersonality getPersonality(LivingEntity entity) {
if (entity == null || !MCACompat.isMCAVillager(entity)) {
return MCAPersonality.UNKNOWN;
}
// Check cache first
MCAPersonality cached = personalityCache.get(entity);
if (cached != null) {
return cached;
}
// Try to get via reflection
String personalityId = getPersonalityIdViaReflection(entity);
MCAPersonality personality = MCAPersonality.fromMcaId(personalityId);
// Cache result (even if UNKNOWN, to avoid repeated reflection failures)
personalityCache.put(entity, personality);
return personality;
}
/**
* Get raw personality ID string from MCA via reflection.
*
* <p>MCA structure:
* <ul>
* <li>entity.getVillagerBrain() -&gt; VillagerBrain</li>
* <li>villagerBrain.getPersonality() -&gt; Personality enum</li>
* <li>personality.name() -&gt; String</li>
* </ul>
*/
@Nullable
private String getPersonalityIdViaReflection(LivingEntity entity) {
try {
initializeReflection(entity);
if (getVillagerBrainMethod == null) {
return null;
}
// Get VillagerBrain
Object brain = getVillagerBrainMethod.invoke(entity);
if (brain == null) {
return null;
}
// Get personality from brain
if (getPersonalityMethod == null) {
// Find getPersonality method on brain
for (Method m : brain.getClass().getMethods()) {
if (
m.getName().equals("getPersonality") &&
m.getParameterCount() == 0
) {
getPersonalityMethod = m;
break;
}
}
}
if (getPersonalityMethod == null) {
return null;
}
Object personality = getPersonalityMethod.invoke(brain);
if (personality != null && personality.getClass().isEnum()) {
return personality.toString().toLowerCase();
}
} catch (Exception e) {
TiedUpMod.LOGGER.debug(
"[MCA Personality] Failed to get personality for {}: {}",
entity.getName().getString(),
e.getMessage()
);
}
return null;
}
private void initializeReflection(LivingEntity entity) {
if (reflectionInitialized) return;
reflectionInitialized = true;
try {
// MCA villagers implement VillagerLike which has getVillagerBrain()
getVillagerBrainMethod = entity
.getClass()
.getMethod("getVillagerBrain");
TiedUpMod.LOGGER.debug(
"[MCA Personality] Found getVillagerBrain method"
);
} catch (NoSuchMethodException e) {
TiedUpMod.LOGGER.debug(
"[MCA Personality] getVillagerBrain not found"
);
}
}
// ========================================
// TIEDUP TRAIT ACCESS (Capability)
// ========================================
/**
* Get the TiedUp trait for a villager.
* Stored in the villager's MCAKidnappedCapability.
*
* @param entity The MCA villager entity
* @return The trait, or NONE if not set
*/
public TiedUpTrait getTrait(LivingEntity entity) {
if (entity == null || !MCACompat.isMCAVillager(entity)) {
return TiedUpTrait.NONE;
}
return entity
.getCapability(MCACompat.MCA_KIDNAPPED)
.map(MCAKidnappedCapability::getTrait)
.orElse(TiedUpTrait.NONE);
}
/**
* Set the TiedUp trait for a villager.
*
* @param entity The MCA villager entity
* @param trait The trait to set
*/
public void setTrait(LivingEntity entity, TiedUpTrait trait) {
if (entity == null || !MCACompat.isMCAVillager(entity)) {
return;
}
entity
.getCapability(MCACompat.MCA_KIDNAPPED)
.ifPresent(cap -> {
cap.setTrait(trait);
TiedUpMod.LOGGER.debug(
"[MCA Personality] Set trait {} for {}",
trait.getId(),
entity.getName().getString()
);
});
}
// ========================================
// COMBINED CALCULATIONS
// ========================================
/**
* Get combined struggle multiplier (personality * trait).
*/
public float getCombinedStruggle(LivingEntity entity) {
MCAPersonality personality = getPersonality(entity);
TiedUpTrait trait = getTrait(entity);
return TiedUpTrait.getCombinedStruggle(personality, trait);
}
/**
* Get combined compliance multiplier (personality * trait).
*/
public float getCombinedCompliance(LivingEntity entity) {
MCAPersonality personality = getPersonality(entity);
TiedUpTrait trait = getTrait(entity);
return TiedUpTrait.getCombinedCompliance(personality, trait);
}
/**
* Get combined mood change when tied (personality base + trait modifier).
*/
public int getCombinedMoodTied(LivingEntity entity) {
MCAPersonality personality = getPersonality(entity);
TiedUpTrait trait = getTrait(entity);
return TiedUpTrait.getCombinedMoodTied(personality, trait);
}
/**
* Check if this villager generally enjoys being restrained
* (based on combined personality and trait).
*/
public boolean enjoysBondage(LivingEntity entity) {
return getCombinedMoodTied(entity) > 0;
}
// ========================================
// CACHE MANAGEMENT
// ========================================
/**
* Clear personality cache for an entity.
* Call this when entity is removed or personality might have changed.
*/
public void clearCache(LivingEntity entity) {
personalityCache.remove(entity);
}
/**
* Clear entire personality cache.
*/
public void clearAllCache() {
personalityCache.clear();
}
}

View File

@@ -0,0 +1,138 @@
package com.tiedup.remake.compat.mca.personality;
/**
* TiedUp-specific traits that combine with MCA base personalities.
*
* <p>These traits are stored in the MCA villager's capability (NBT)
* and can be assigned via command or acquired naturally through gameplay.
*
* <p>Multipliers combine with MCA personality: {@code final = personality * trait}
*
* <h3>Trait Acquisition:</h3>
* <ul>
* <li>NONE - Default, no special trait</li>
* <li>MASO - Rare discovery or assigned (enjoys bondage)</li>
* <li>REBELLIOUS - After repeated failed captures or strong personality</li>
* <li>BROKEN - After very long captivity without liberation</li>
* <li>TRAINED - After prolonged captivity with good treatment (positive mood)</li>
* </ul>
*/
public enum TiedUpTrait {
/** No special trait - uses base personality only */
NONE("none", 1.0f, 1.0f, 0),
/** Masochistic - enjoys being restrained */
MASO("maso", 0.3f, 2.0f, +5),
/** Rebellious - always fights back */
REBELLIOUS("rebellious", 1.8f, 0.3f, -5),
/** Broken - has given up resisting (acquired only) */
BROKEN("broken", 0.1f, 3.0f, 0),
/** Trained - has accepted their situation (acquired only) */
TRAINED("trained", 0.5f, 1.5f, +1);
private final String id;
private final float struggleMultiplier;
private final float complianceMultiplier;
private final int moodModifier;
TiedUpTrait(String id, float struggle, float compliance, int mood) {
this.id = id;
this.struggleMultiplier = struggle;
this.complianceMultiplier = compliance;
this.moodModifier = mood;
}
public String getId() {
return id;
}
/**
* Get struggle multiplier (combines with personality).
*/
public float getStruggleMultiplier() {
return struggleMultiplier;
}
/**
* Get compliance multiplier (combines with personality).
*/
public float getComplianceMultiplier() {
return complianceMultiplier;
}
/**
* Get mood modifier when tied.
* Added to personality's base mood change.
*/
public int getMoodModifier() {
return moodModifier;
}
/**
* Check if this trait makes the villager enjoy bondage.
*/
public boolean enjoysBondage() {
return moodModifier > 0;
}
/**
* Check if this trait represents a broken will.
*/
public boolean isBroken() {
return this == BROKEN;
}
/**
* Find trait by ID string (case-insensitive).
*
* @param id The trait ID (e.g., "maso", "trained")
* @return The matching trait, or NONE if not found
*/
public static TiedUpTrait fromId(String id) {
if (id == null) return NONE;
for (TiedUpTrait t : values()) {
if (t.id.equalsIgnoreCase(id)) {
return t;
}
}
return NONE;
}
/**
* Calculate combined struggle multiplier with personality.
*/
public static float getCombinedStruggle(
MCAPersonality personality,
TiedUpTrait trait
) {
return (
personality.getStruggleMultiplier() * trait.getStruggleMultiplier()
);
}
/**
* Calculate combined compliance multiplier with personality.
*/
public static float getCombinedCompliance(
MCAPersonality personality,
TiedUpTrait trait
) {
return (
personality.getComplianceMultiplier() *
trait.getComplianceMultiplier()
);
}
/**
* Calculate combined mood change when tied.
*/
public static int getCombinedMoodTied(
MCAPersonality personality,
TiedUpTrait trait
) {
return personality.getBaseMoodTied() + trait.getMoodModifier();
}
}

View File

@@ -0,0 +1,294 @@
package com.tiedup.remake.compat.wildfire;
import com.tiedup.remake.compat.wildfire.physics.NpcBreastPhysics;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.AbstractTiedUpNpc;
import com.tiedup.remake.entities.skins.Gender;
import com.wildfire.main.GenderPlayer;
import com.wildfire.main.WildfireGender;
import java.util.Map;
import java.util.WeakHashMap;
import net.minecraft.world.entity.LivingEntity;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import net.minecraftforge.client.event.EntityRenderersEvent;
import net.minecraftforge.fml.ModList;
import org.apache.commons.lang3.tuple.Pair;
/**
* Compatibility module for Wildfire's Female Gender Mod.
*/
public class WildfireCompat {
public static final String MOD_ID = "wildfire_gender";
private static boolean loaded = false;
// Cache for NPC physics to maintain state between frames
// Pair<Left, Right>
private static final Map<
LivingEntity,
Pair<NpcBreastPhysics, NpcBreastPhysics>
> NPC_PHYSICS_CACHE = new WeakHashMap<>();
public static void init() {
loaded = ModList.get().isLoaded(MOD_ID);
if (loaded) {
TiedUpMod.LOGGER.info(
"[Wildfire Compat] Wildfire's Gender Mod detected! Enabling physics."
);
}
}
public static boolean isLoaded() {
return loaded;
}
/**
* Get physics instances for an entity. Creates them if needed.
*/
public static Pair<NpcBreastPhysics, NpcBreastPhysics> getNpcPhysics(
LivingEntity entity,
GenderPlayer genderPlayer
) {
if (!loaded || genderPlayer == null) return null;
return NPC_PHYSICS_CACHE.computeIfAbsent(entity, e -> {
return Pair.of(
new NpcBreastPhysics(genderPlayer),
new NpcBreastPhysics(genderPlayer)
);
});
}
// Default values based on user config (displayed values)
// bust size: 100% = 0.8 raw (Wildfire max)
// separation: -2 displayed = -0.2 raw
// height: 0, depth: 0, rotation: 0
// dualphysics: yes = uniboob: false
private static final float DEFAULT_BUST_SIZE = 0.8f;
private static final float DEFAULT_SEPARATION = -0.2f; // -2 displayed
private static final float DEFAULT_BOUNCE = 0.34f;
private static final float DEFAULT_FLOPPY = 0.75f;
/**
* Get or create GenderPlayer data for any entity with a UUID.
* Wildfire mod uses UUIDs key, so it works for non-players too!
* Respects entity gender (MALE/FEMALE) and randomizes bust size for females.
*/
public static GenderPlayer getGenderInfo(LivingEntity entity) {
if (!loaded) return null;
try {
GenderPlayer plr = WildfireGender.getOrAddPlayerById(
entity.getUUID()
);
if (plr != null && entity instanceof AbstractTiedUpNpc damsel) {
// Determine target gender from entity
Gender modGender = damsel.getGender();
GenderPlayer.Gender targetGender = (modGender == Gender.MALE)
? GenderPlayer.Gender.MALE
: GenderPlayer.Gender.FEMALE;
if (plr.getGender() != targetGender) {
plr.updateGender(targetGender);
}
// Initialize breast settings for females if not yet configured
// Check if bust size is at Wildfire default (0.6) which means not configured by us
if (targetGender == GenderPlayer.Gender.FEMALE) {
float currentBust = plr.getBustSize();
// Wildfire default is 0.6f - if close to that, we haven't configured yet
boolean needsInit = Math.abs(currentBust - 0.6f) < 0.01f;
if (needsInit) {
long seed = entity.getUUID().getLeastSignificantBits();
java.util.Random rand = new java.util.Random(seed);
// Bust size: 0.8 (100%) with slight random variation ±0.1
// Clamped to Wildfire max of 0.8
float bustVariation = (rand.nextFloat() - 0.5f) * 0.2f; // -0.1 to +0.1
float randomSize = Math.min(
0.8f,
DEFAULT_BUST_SIZE + bustVariation
);
randomSize = Math.max(0.4f, randomSize); // min 50% displayed
plr.updateBustSize(randomSize);
// Dual physics enabled (uniboob = false)
plr.getBreasts().updateUniboob(false);
// Separation: -0.2 raw (-2 displayed) with slight variation ±0.05
float sepVariation = (rand.nextFloat() - 0.5f) * 0.1f; // -0.05 to +0.05
float separation = DEFAULT_SEPARATION + sepVariation;
separation = Math.max(-0.3f, Math.min(0f, separation)); // clamp -3 to 0 displayed
plr.getBreasts().updateXOffset(separation);
// Height: 0 (no variation for now)
plr.getBreasts().updateYOffset(0f);
// Depth: 0 (no variation for now)
plr.getBreasts().updateZOffset(0f);
// Cleavage/rotation: 0
plr.getBreasts().updateCleavage(0f);
// Physics: slight random variation around defaults
float bounceVariation =
(rand.nextFloat() - 0.5f) * 0.1f;
float bounce = Math.max(
0.2f,
Math.min(0.5f, DEFAULT_BOUNCE + bounceVariation)
);
plr.updateBounceMultiplier(bounce);
float floppyVariation =
(rand.nextFloat() - 0.5f) * 0.2f;
float floppy = Math.max(
0.5f,
Math.min(1.0f, DEFAULT_FLOPPY + floppyVariation)
);
plr.updateFloppiness(floppy);
// Enable breast physics
plr.updateBreastPhysics(true);
}
}
}
return plr;
} catch (Exception e) {
return null;
}
}
/**
* Force an entity to be female in Wildfire system.
* Also initializes physics/randomization if needed.
*/
public static void setGenderFemale(LivingEntity entity) {
if (!loaded) return;
try {
GenderPlayer plr = WildfireGender.getOrAddPlayerById(
entity.getUUID()
);
if (plr != null) {
boolean changed = false;
if (plr.getGender() != GenderPlayer.Gender.FEMALE) {
plr.updateGender(GenderPlayer.Gender.FEMALE);
changed = true;
}
// Initialize if gender changed or bust is at Wildfire default
float currentBust = plr.getBustSize();
boolean needsInit =
changed || Math.abs(currentBust - 0.6f) < 0.01f;
if (needsInit) {
long seed = entity.getUUID().getLeastSignificantBits();
java.util.Random rand = new java.util.Random(seed);
// Same logic as getGenderInfo
float bustVariation = (rand.nextFloat() - 0.5f) * 0.2f;
float randomSize = Math.min(
0.8f,
DEFAULT_BUST_SIZE + bustVariation
);
randomSize = Math.max(0.4f, randomSize);
plr.updateBustSize(randomSize);
plr.getBreasts().updateUniboob(false);
float sepVariation = (rand.nextFloat() - 0.5f) * 0.1f;
float separation = Math.max(
-0.3f,
Math.min(0f, DEFAULT_SEPARATION + sepVariation)
);
plr.getBreasts().updateXOffset(separation);
plr.getBreasts().updateYOffset(0f);
plr.getBreasts().updateZOffset(0f);
plr.getBreasts().updateCleavage(0f);
float bounceVariation = (rand.nextFloat() - 0.5f) * 0.1f;
float bounce = Math.max(
0.2f,
Math.min(0.5f, DEFAULT_BOUNCE + bounceVariation)
);
plr.updateBounceMultiplier(bounce);
float floppyVariation = (rand.nextFloat() - 0.5f) * 0.2f;
float floppy = Math.max(
0.5f,
Math.min(1.0f, DEFAULT_FLOPPY + floppyVariation)
);
plr.updateFloppiness(floppy);
plr.updateBreastPhysics(true);
}
}
} catch (Exception e) {
TiedUpMod.LOGGER.debug(
"[WildfireCompat] Failed to update breast physics",
e
);
}
}
/**
* Register render layers for Wildfire compatibility.
*
* Adds WildfireDamselLayer to PlayerRenderers to render bondage textures
* on top of Wildfire's breast geometry for players.
*
* NOTE: TiedUp entities (Damsel, Kidnapper, etc.) have their WildfireDamselLayer
* added directly in DamselRenderer's constructor.
*/
@OnlyIn(Dist.CLIENT)
public static void registerRenderLayers(
EntityRenderersEvent.AddLayers event
) {
if (!loaded) return;
// Add WildfireDamselLayer to player renderers for breast-aware bind rendering
// This renders bind textures ON TOP of Wildfire's breast geometry
// Default player renderer (Steve)
net.minecraft.client.renderer.entity.EntityRenderer<
? extends net.minecraft.world.entity.player.Player
> defaultRenderer = event.getSkin("default");
if (
defaultRenderer instanceof
net.minecraft.client.renderer.entity.player.PlayerRenderer playerRenderer
) {
playerRenderer.addLayer(
new com.tiedup.remake.compat.wildfire.render.WildfireDamselLayer<>(
playerRenderer,
event.getEntityModels()
)
);
TiedUpMod.LOGGER.info(
"[Wildfire Compat] Added WildfireDamselLayer to default player renderer"
);
}
// Slim player renderer (Alex)
net.minecraft.client.renderer.entity.EntityRenderer<
? extends net.minecraft.world.entity.player.Player
> slimRenderer = event.getSkin("slim");
if (
slimRenderer instanceof
net.minecraft.client.renderer.entity.player.PlayerRenderer playerRenderer
) {
playerRenderer.addLayer(
new com.tiedup.remake.compat.wildfire.render.WildfireDamselLayer<>(
playerRenderer,
event.getEntityModels()
)
);
TiedUpMod.LOGGER.info(
"[Wildfire Compat] Added WildfireDamselLayer to slim player renderer"
);
}
}
}

View File

@@ -0,0 +1,207 @@
package com.tiedup.remake.compat.wildfire.physics;
import com.wildfire.api.IGenderArmor;
import com.wildfire.main.GenderPlayer;
import com.wildfire.main.WildfireHelper;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.Pose;
import net.minecraft.world.phys.Vec3;
/**
* Re-implementation of Wildfire's BreastPhysics adapted for LivingEntity (NPCs).
* The original class strictly requires Player, which crashes for EntityDamsel.
*/
public class NpcBreastPhysics {
private float bounceVel = 0,
targetBounceY = 0,
velocity = 0,
wfg_femaleBreast,
wfg_preBounce;
private float bounceRotVel = 0,
targetRotVel = 0,
rotVelocity = 0,
wfg_bounceRotation,
wfg_preBounceRotation;
private float bounceVelX = 0,
targetBounceX = 0,
velocityX = 0,
wfg_femaleBreastX,
wfg_preBounceX;
private boolean justSneaking = false; // alreadySleeping logic removed (mobs rarely sleep in beds)
private float breastSize = 0,
preBreastSize = 0;
private Vec3 prePos;
private int lastTick = -1;
private final GenderPlayer genderPlayer;
public NpcBreastPhysics(GenderPlayer genderPlayer) {
this.genderPlayer = genderPlayer;
}
public void update(LivingEntity entity, IGenderArmor armor) {
// Run physics only once per tick to ensure correct motion delta calculation
if (entity.tickCount == this.lastTick) {
return;
}
this.lastTick = entity.tickCount;
this.wfg_preBounce = this.wfg_femaleBreast;
this.wfg_preBounceX = this.wfg_femaleBreastX;
this.wfg_preBounceRotation = this.wfg_bounceRotation;
this.preBreastSize = this.breastSize;
if (this.prePos == null) {
this.prePos = entity.position();
return;
}
// Logic copied and adapted from BreastPhysics.update
float breastWeight = genderPlayer.getBustSize() * 1.25f;
float targetBreastSize = genderPlayer.getBustSize();
if (!genderPlayer.getGender().canHaveBreasts()) {
targetBreastSize = 0;
} else if (!genderPlayer.getArmorPhysicsOverride()) {
float tightness = Mth.clamp(armor.tightness(), 0, 1);
targetBreastSize *= 1 - 0.15F * tightness;
}
if (breastSize < targetBreastSize) {
breastSize += Math.abs(breastSize - targetBreastSize) / 2f;
} else {
breastSize -= Math.abs(breastSize - targetBreastSize) / 2f;
}
Vec3 motion = entity.position().subtract(this.prePos);
this.prePos = entity.position();
float bounceIntensity =
(targetBreastSize * 3f) * genderPlayer.getBounceMultiplier();
if (!genderPlayer.getArmorPhysicsOverride()) {
float resistance = Mth.clamp(armor.physicsResistance(), 0, 1);
bounceIntensity *= 1 - resistance;
}
if (!genderPlayer.getBreasts().isUniboob()) {
bounceIntensity =
bounceIntensity * WildfireHelper.randFloat(0.5f, 1.5f);
}
// Falling logic
if (entity.fallDistance > 0) {
// Random bounce removed for NPCs to avoid jitter, simplified falling
}
this.targetBounceY = (float) motion.y * bounceIntensity;
this.targetBounceY += breastWeight;
this.targetRotVel =
-((entity.yBodyRot - entity.yBodyRotO) / 15f) * bounceIntensity;
// Walking bounce simulation
float f = (float) entity.getDeltaMovement().lengthSqr() / 0.2F;
f = f * f * f;
if (f < 1.0F) {
f = 1.0F;
}
// Use walkAnimation position directly from LivingEntity
this.targetBounceY +=
(Mth.cos(
entity.walkAnimation.position() * 0.6662F + (float) Math.PI
) *
0.5F *
entity.walkAnimation.speed() *
0.5F) /
f;
// Sneaking logic
if (entity.getPose() == Pose.CROUCHING && !this.justSneaking) {
this.justSneaking = true;
this.targetBounceY += bounceIntensity;
}
if (entity.getPose() != Pose.CROUCHING && this.justSneaking) {
this.justSneaking = false;
this.targetBounceY += bounceIntensity;
}
// Physics calculation
float percent = genderPlayer.getFloppiness();
float bounceAmount = 0.45f * (1f - percent) + 0.15f;
bounceAmount = Mth.clamp(bounceAmount, 0.15f, 0.6f);
float delta = 2.25f - bounceAmount;
float distanceFromMin = Math.abs(bounceVel + 0.5f) * 0.5f;
float distanceFromMax = Math.abs(bounceVel - 2.65f) * 0.5f;
if (bounceVel < -0.5f) {
targetBounceY += distanceFromMin;
}
if (bounceVel > 2.5f) {
targetBounceY -= distanceFromMax;
}
if (targetBounceY < -1.5f) targetBounceY = -1.5f;
if (targetBounceY > 2.5f) targetBounceY = 2.5f;
if (targetRotVel < -25f) targetRotVel = -25f;
if (targetRotVel > 25f) targetRotVel = 25f;
this.velocity = Mth.lerp(
bounceAmount,
this.velocity,
(this.targetBounceY - this.bounceVel) * delta
);
this.bounceVel += this.velocity * percent * 1.1625f;
// X
this.velocityX = Mth.lerp(
bounceAmount,
this.velocityX,
(this.targetBounceX - this.bounceVelX) * delta
);
this.bounceVelX += this.velocityX * percent;
this.rotVelocity = Mth.lerp(
bounceAmount,
this.rotVelocity,
(this.targetRotVel - this.bounceRotVel) * delta
);
this.bounceRotVel += this.rotVelocity * percent;
this.wfg_bounceRotation = this.bounceRotVel;
this.wfg_femaleBreastX = this.bounceVelX;
this.wfg_femaleBreast = this.bounceVel;
}
public float getBreastSize(float partialTicks) {
return Mth.lerp(partialTicks, preBreastSize, breastSize);
}
public float getPreBounceY() {
return this.wfg_preBounce;
}
public float getBounceY() {
return this.wfg_femaleBreast;
}
public float getPreBounceX() {
return this.wfg_preBounceX;
}
public float getBounceX() {
return this.wfg_femaleBreastX;
}
public float getPreBounceRotation() {
return this.wfg_preBounceRotation;
}
public float getBounceRotation() {
return this.wfg_bounceRotation;
}
}

View File

@@ -0,0 +1,986 @@
package com.tiedup.remake.compat.wildfire.render;
import com.mojang.blaze3d.systems.RenderSystem;
import com.tiedup.remake.v2.BodyRegionV2;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import com.tiedup.remake.compat.wildfire.WildfireCompat;
import com.tiedup.remake.compat.wildfire.physics.NpcBreastPhysics;
import com.tiedup.remake.entities.EntityDamsel;
import com.tiedup.remake.entities.KidnapperItemSelector;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.util.KidnappedHelper;
import com.wildfire.api.IGenderArmor;
import com.wildfire.main.Breasts;
import com.wildfire.main.GenderPlayer;
import com.wildfire.main.WildfireHelper;
import com.wildfire.main.config.GeneralClientConfig;
import com.wildfire.physics.BreastPhysics;
import com.wildfire.render.WildfireModelRenderer;
import com.wildfire.render.WildfireModelRenderer.BreastModelBox;
import com.wildfire.render.WildfireModelRenderer.OverlayModelBox;
import com.wildfire.render.WildfireModelRenderer.PositionTextureVertex;
import java.util.Locale;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import net.minecraft.client.Minecraft;
import net.minecraft.client.model.HumanoidModel;
import net.minecraft.client.model.geom.EntityModelSet;
import net.minecraft.client.model.geom.ModelPart;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.Sheets;
import net.minecraft.client.renderer.entity.LivingEntityRenderer;
import net.minecraft.client.renderer.entity.RenderLayerParent;
import net.minecraft.client.renderer.entity.layers.RenderLayer;
import net.minecraft.client.renderer.texture.OverlayTexture;
import net.minecraft.client.renderer.texture.TextureAtlas;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.core.BlockPos;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.Mth;
import net.minecraft.world.effect.MobEffectUtil;
import net.minecraft.world.entity.EquipmentSlot;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.ArmorItem;
import net.minecraft.world.item.ArmorMaterial;
import net.minecraft.world.item.DyeableLeatherItem;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.armortrim.ArmorTrim;
import net.minecraft.world.level.block.Blocks;
import net.minecraftforge.client.ForgeHooksClient;
import net.minecraftforge.registries.ForgeRegistries;
import org.apache.commons.lang3.tuple.Pair;
import org.joml.Matrix3f;
import org.joml.Matrix4f;
import org.joml.Quaternionf;
import org.joml.Vector3f;
/**
* Adapted GenderLayer for EntityDamsel / EntityKidnapper.
*/
public class WildfireDamselLayer<
T extends LivingEntity,
M extends HumanoidModel<T>
> extends RenderLayer<T, M> {
private final TextureAtlas armorTrimAtlas;
private final RenderLayerParent<T, M> renderer;
private BreastModelBox lBreast, rBreast;
private final OverlayModelBox lBreastWear, rBreastWear;
private final BreastModelBox lBoobArmor, rBoobArmor;
private float preBreastSize = 0f;
public WildfireDamselLayer(
RenderLayerParent<T, M> renderer,
EntityModelSet modelSet
) {
super(renderer);
this.renderer = renderer;
this.armorTrimAtlas = Minecraft.getInstance()
.getModelManager()
.getAtlas(Sheets.ARMOR_TRIMS_SHEET);
lBreast = new BreastModelBox(
64,
64,
16,
17,
-4F,
0.0F,
0F,
4,
5,
4,
0.0F,
false
);
rBreast = new BreastModelBox(
64,
64,
20,
17,
0,
0.0F,
0F,
4,
5,
4,
0.0F,
false
);
lBreastWear = new OverlayModelBox(
true,
64,
64,
17,
34,
-4F,
0.0F,
0F,
4,
5,
3,
0.0F,
false
);
rBreastWear = new OverlayModelBox(
false,
64,
64,
21,
34,
0,
0.0F,
0F,
4,
5,
3,
0.0F,
false
);
lBoobArmor = new BreastModelBox(
64,
32,
16,
17,
-4F,
0.0F,
0F,
4,
5,
3,
0.0F,
false
);
rBoobArmor = new BreastModelBox(
64,
32,
20,
17,
0,
0.0F,
0F,
4,
5,
3,
0.0F,
false
);
}
// Helper to get armor resource
public ResourceLocation getArmorResource(
T entity,
ItemStack stack,
EquipmentSlot slot,
@Nullable String type
) {
ArmorItem item = (ArmorItem) stack.getItem();
String texture = item.getMaterial().getName();
String domain = "minecraft";
int idx = texture.indexOf(':');
if (idx != -1) {
domain = texture.substring(0, idx);
texture = texture.substring(idx + 1);
}
String s1 = String.format(
Locale.ROOT,
"%s:textures/models/armor/%s_layer_%d%s.png",
domain,
texture,
(slot == EquipmentSlot.LEGS ? 2 : 1),
type == null ? "" : String.format(Locale.ROOT, "_%s", type)
);
s1 = ForgeHooksClient.getArmorTexture(entity, stack, s1, slot, type);
return ResourceLocation.tryParse(s1);
}
private static final ResourceLocation DEFAULT_TEXTURE =
ResourceLocation.withDefaultNamespace(
"textures/entity/player/wide/steve.png"
);
private ResourceLocation getEntityTexture(T entity) {
// Use ISkinnedEntity interface if available (EntityDamsel/EntityKidnapper)
if (
entity instanceof com.tiedup.remake.entities.ISkinnedEntity skinned
) {
ResourceLocation tex = skinned.getSkinTexture();
if (tex != null) return tex;
}
// Fallback to renderer
if (
this.renderer instanceof
net.minecraft.client.renderer.entity.EntityRenderer
) {
ResourceLocation rendererTex = (
(net.minecraft.client.renderer.entity.EntityRenderer<
T
>) this.renderer
).getTextureLocation(entity);
if (rendererTex != null) return rendererTex;
}
return DEFAULT_TEXTURE;
}
@Override
public void render(
@Nonnull PoseStack matrixStack,
@Nonnull MultiBufferSource bufferSource,
int packedLightIn,
@Nonnull T ent,
float limbAngle,
float limbDistance,
float partialTicks,
float animationProgress,
float headYaw,
float headPitch
) {
// FIX: Check if Wildfire config is initialized before accessing it
if (GeneralClientConfig.INSTANCE == null) {
return; // Config not loaded yet, skip rendering
}
if (
GeneralClientConfig.INSTANCE.disableRendering.get() ||
ent.isSpectator()
) {
return;
}
try {
GenderPlayer plr = WildfireCompat.getGenderInfo(ent);
if (plr == null) {
return;
}
ItemStack armorStack = ent.getItemBySlot(EquipmentSlot.CHEST);
IGenderArmor genderArmor = WildfireHelper.getArmorConfig(
armorStack
);
boolean isChestplateOccupied = genderArmor.coversBreasts();
if (
genderArmor.alwaysHidesBreasts() ||
(!plr.showBreastsInArmor() && isChestplateOccupied)
) {
return;
}
HumanoidModel<T> model = this.getParentModel();
if (!plr.getGender().canHaveBreasts()) {
return;
}
Breasts breasts = plr.getBreasts();
float breastOffsetX =
Math.round(
(Math.round(breasts.getXOffset() * 100f) / 100f) * 10
) /
10f;
float breastOffsetY =
-Math.round(
(Math.round(breasts.getYOffset() * 100f) / 100f) * 10
) /
10f;
float breastOffsetZ =
-Math.round(
(Math.round(breasts.getZOffset() * 100f) / 100f) * 10
) /
10f;
BreastPhysics leftBreastPhysics = plr.getLeftBreastPhysics();
// Physics Handling
float bSize;
float lTotal = 0,
lTotalX = 0,
leftBounceRotation = 0;
float rTotal = 0,
rTotalX = 0,
rightBounceRotation = 0;
if (ent instanceof net.minecraft.world.entity.player.Player) {
// For Players, use original physics
bSize = leftBreastPhysics.getBreastSize(partialTicks);
lTotal = Mth.lerp(
partialTicks,
leftBreastPhysics.getPreBounceY(),
leftBreastPhysics.getBounceY()
);
lTotalX = Mth.lerp(
partialTicks,
leftBreastPhysics.getPreBounceX(),
leftBreastPhysics.getBounceX()
);
leftBounceRotation = Mth.lerp(
partialTicks,
leftBreastPhysics.getPreBounceRotation(),
leftBreastPhysics.getBounceRotation()
);
if (breasts.isUniboob()) {
rTotal = lTotal;
rTotalX = lTotalX;
rightBounceRotation = leftBounceRotation;
} else {
BreastPhysics rightBreastPhysics =
plr.getRightBreastPhysics();
rTotal = Mth.lerp(
partialTicks,
rightBreastPhysics.getPreBounceY(),
rightBreastPhysics.getBounceY()
);
rTotalX = Mth.lerp(
partialTicks,
rightBreastPhysics.getPreBounceX(),
rightBreastPhysics.getBounceX()
);
rightBounceRotation = Mth.lerp(
partialTicks,
rightBreastPhysics.getPreBounceRotation(),
rightBreastPhysics.getBounceRotation()
);
}
} else {
// For NPCs, use local physics simulation
Pair<NpcBreastPhysics, NpcBreastPhysics> physics =
WildfireCompat.getNpcPhysics(ent, plr);
if (physics != null) {
NpcBreastPhysics left = physics.getLeft();
NpcBreastPhysics right = physics.getRight();
// Update physics (ideally should be done in tick, but render update is acceptable for visual bounce)
// Note: This makes physics frame-rate dependent, but avoids complex tick handler setup
left.update(ent, genderArmor);
if (!breasts.isUniboob()) {
right.update(ent, genderArmor);
}
bSize = left.getBreastSize(partialTicks);
lTotal = Mth.lerp(
partialTicks,
left.getPreBounceY(),
left.getBounceY()
);
lTotalX = Mth.lerp(
partialTicks,
left.getPreBounceX(),
left.getBounceX()
);
leftBounceRotation = Mth.lerp(
partialTicks,
left.getPreBounceRotation(),
left.getBounceRotation()
);
if (breasts.isUniboob()) {
rTotal = lTotal;
rTotalX = lTotalX;
rightBounceRotation = leftBounceRotation;
} else {
rTotal = Mth.lerp(
partialTicks,
right.getPreBounceY(),
right.getBounceY()
);
rTotalX = Mth.lerp(
partialTicks,
right.getPreBounceX(),
right.getBounceX()
);
rightBounceRotation = Mth.lerp(
partialTicks,
right.getPreBounceRotation(),
right.getBounceRotation()
);
}
} else {
bSize = plr.getBustSize(); // Fallback if physics creation fails
}
}
float outwardAngle =
(Math.round(breasts.getCleavage() * 100f) / 100f) * 100f;
outwardAngle = Math.min(outwardAngle, 10);
float reducer = 0;
if (bSize < 0.84f) reducer++;
if (bSize < 0.72f) reducer++;
if (preBreastSize != bSize) {
lBreast = new BreastModelBox(
64,
64,
16,
17,
-4F,
0.0F,
0F,
4,
5,
(int) (4 - breastOffsetZ - reducer),
0.0F,
false
);
rBreast = new BreastModelBox(
64,
64,
20,
17,
0,
0.0F,
0F,
4,
5,
(int) (4 - breastOffsetZ - reducer),
0.0F,
false
);
preBreastSize = bSize;
}
float overlayAlpha = ent.isInvisible() ? 0.15F : 1;
RenderSystem.setShaderColor(1f, 1f, 1f, 1f);
// Physics calculation handled above
float breastSize = bSize * 1.5f;
if (breastSize > 0.7f) breastSize = 0.7f;
if (bSize > 0.7f) {
breastSize = bSize;
}
if (breastSize < 0.02f) return;
float zOff = 0.0625f - (bSize * 0.0625f);
breastSize = bSize + 0.5f * Math.abs(bSize - 0.7f) * 2f;
float resistance = plr.getArmorPhysicsOverride()
? 0
: Mth.clamp(genderArmor.physicsResistance(), 0, 1);
boolean breathingAnimation =
resistance <= 0.5F &&
(!ent.isUnderWater() ||
MobEffectUtil.hasWaterBreathing(ent) ||
ent
.level()
.getBlockState(
BlockPos.containing(
ent.getX(),
ent.getEyeY(),
ent.getZ()
)
)
.is(Blocks.BUBBLE_COLUMN));
boolean bounceEnabled =
plr.hasBreastPhysics() &&
(!isChestplateOccupied || resistance < 1);
int combineTex = LivingEntityRenderer.getOverlayCoords(ent, 0);
ResourceLocation entityTexture = getEntityTexture(ent);
RenderType type;
boolean bodyVisible = !ent.isInvisible();
if (bodyVisible) {
type = RenderType.entityTranslucent(entityTexture);
} else {
if (!isChestplateOccupied) return;
type = null;
}
renderBreastWithTransforms(
ent,
model.body,
armorStack,
matrixStack,
bufferSource,
type,
packedLightIn,
combineTex,
overlayAlpha,
bounceEnabled,
lTotalX,
lTotal,
leftBounceRotation,
breastSize,
breastOffsetX,
breastOffsetY,
breastOffsetZ,
zOff,
outwardAngle,
breasts.isUniboob(),
isChestplateOccupied,
breathingAnimation,
true
);
renderBreastWithTransforms(
ent,
model.body,
armorStack,
matrixStack,
bufferSource,
type,
packedLightIn,
combineTex,
overlayAlpha,
bounceEnabled,
rTotalX,
rTotal,
rightBounceRotation,
breastSize,
-breastOffsetX,
breastOffsetY,
breastOffsetZ,
zOff,
-outwardAngle,
breasts.isUniboob(),
isChestplateOccupied,
breathingAnimation,
false
);
RenderSystem.setShaderColor(1f, 1f, 1f, 1f);
} catch (Exception e) {
com.tiedup.remake.core.TiedUpMod.LOGGER.debug(
"[WildfireDamselLayer] Failed to render gender layer",
e
);
}
}
private void renderBreastWithTransforms(
T entity,
ModelPart body,
ItemStack armorStack,
PoseStack matrixStack,
MultiBufferSource bufferSource,
@Nullable RenderType breastRenderType,
int packedLightIn,
int combineTex,
float alpha,
boolean bounceEnabled,
float totalX,
float total,
float bounceRotation,
float breastSize,
float breastOffsetX,
float breastOffsetY,
float breastOffsetZ,
float zOff,
float outwardAngle,
boolean uniboob,
boolean isChestplateOccupied,
boolean breathingAnimation,
boolean left
) {
matrixStack.pushPose();
try {
// Transform to Body
matrixStack.translate(
body.x * 0.0625f,
body.y * 0.0625f,
body.z * 0.0625f
);
if (body.zRot != 0.0F) {
matrixStack.mulPose(
new Quaternionf().rotationXYZ(0f, 0f, body.zRot)
);
}
if (body.yRot != 0.0F) {
matrixStack.mulPose(
new Quaternionf().rotationXYZ(0f, body.yRot, 0f)
);
}
if (body.xRot != 0.0F) {
matrixStack.mulPose(
new Quaternionf().rotationXYZ(body.xRot, 0f, 0f)
);
}
// --- Bouncing & Positioning logic same as original ---
if (bounceEnabled) {
matrixStack.translate(totalX / 32f, 0, 0);
matrixStack.translate(0, total / 32f, 0);
}
matrixStack.translate(
breastOffsetX * 0.0625f,
0.05625f + (breastOffsetY * 0.0625f),
zOff - 0.0625f * 2f + (breastOffsetZ * 0.0625f)
);
if (!uniboob) {
matrixStack.translate(-0.0625f * 2 * (left ? 1 : -1), 0, 0);
}
if (bounceEnabled) {
matrixStack.mulPose(
new Quaternionf().rotationXYZ(
0,
(float) (bounceRotation * (Math.PI / 180f)),
0
)
);
}
if (!uniboob) {
matrixStack.translate(0.0625f * 2 * (left ? 1 : -1), 0, 0);
}
float rotationMultiplier = 0;
if (bounceEnabled) {
matrixStack.translate(0, -0.035f * breastSize, 0);
rotationMultiplier = -total / 12f;
}
float totalRotation = breastSize + rotationMultiplier;
if (!bounceEnabled) {
totalRotation = breastSize;
}
if (totalRotation > breastSize + 0.2F) {
totalRotation = breastSize + 0.2F;
}
totalRotation = Math.min(totalRotation, 1);
if (isChestplateOccupied) {
matrixStack.translate(0, 0, 0.01f);
}
matrixStack.mulPose(
new Quaternionf().rotationXYZ(
0,
(float) (outwardAngle * (Math.PI / 180f)),
0
)
);
matrixStack.mulPose(
new Quaternionf().rotationXYZ(
(float) (-35f * totalRotation * (Math.PI / 180f)),
0,
0
)
);
if (breathingAnimation) {
float f5 = -Mth.cos(entity.tickCount * 0.09F) * 0.45F + 0.45F;
matrixStack.mulPose(
new Quaternionf().rotationXYZ(
(float) (f5 * (Math.PI / 180f)),
0,
0
)
);
}
matrixStack.scale(0.9995f, 1f, 1f);
renderBreast(
entity,
armorStack,
matrixStack,
bufferSource,
breastRenderType,
packedLightIn,
combineTex,
alpha,
left
);
} catch (Exception e) {
com.tiedup.remake.core.TiedUpMod.LOGGER.error(
"[WildfireDamselLayer] Render error",
e
);
}
matrixStack.popPose();
}
private void renderBreast(
T entity,
ItemStack armorStack,
PoseStack matrixStack,
MultiBufferSource bufferSource,
@Nullable RenderType breastRenderType,
int packedLightIn,
int packedOverlayIn,
float alpha,
boolean left
) {
if (breastRenderType != null) {
VertexConsumer vertexConsumer = bufferSource.getBuffer(
breastRenderType
);
renderBox(
left ? lBreast : rBreast,
matrixStack,
vertexConsumer,
packedLightIn,
packedOverlayIn,
1F,
1F,
1F,
alpha
);
// Render Wear (jacket overlay layer) - NPC skins are 64x64 and include this area
matrixStack.translate(0, 0, -0.015f);
matrixStack.scale(1.05f, 1.05f, 1.05f);
renderBox(
left ? lBreastWear : rBreastWear,
matrixStack,
vertexConsumer,
packedLightIn,
packedOverlayIn,
1F,
1F,
1F,
alpha
);
}
// Armor Rendering
if (
!armorStack.isEmpty() &&
armorStack.getItem() instanceof ArmorItem armorItem
) {
ResourceLocation armorTexture = getArmorResource(
entity,
armorStack,
EquipmentSlot.CHEST,
null
);
ResourceLocation overlayTexture = null;
float armorR = 1f;
float armorG = 1f;
float armorB = 1f;
if (armorItem instanceof DyeableLeatherItem dyeableItem) {
overlayTexture = getArmorResource(
entity,
armorStack,
EquipmentSlot.CHEST,
"overlay"
);
int color = dyeableItem.getColor(armorStack);
armorR = (float) ((color >> 16) & 255) / 255.0F;
armorG = (float) ((color >> 8) & 255) / 255.0F;
armorB = (float) (color & 255) / 255.0F;
}
matrixStack.pushPose();
matrixStack.translate(left ? 0.001f : -0.001f, 0.015f, -0.015f);
matrixStack.scale(1.05f, 1, 1);
WildfireModelRenderer.BreastModelBox armor = left
? lBoobArmor
: rBoobArmor;
RenderType armorType = RenderType.armorCutoutNoCull(armorTexture);
VertexConsumer armorVertexConsumer = bufferSource.getBuffer(
armorType
);
renderBox(
armor,
matrixStack,
armorVertexConsumer,
packedLightIn,
OverlayTexture.NO_OVERLAY,
armorR,
armorG,
armorB,
1
);
if (overlayTexture != null) {
RenderType overlayType = RenderType.armorCutoutNoCull(
overlayTexture
);
VertexConsumer overlayVertexConsumer = bufferSource.getBuffer(
overlayType
);
renderBox(
armor,
matrixStack,
overlayVertexConsumer,
packedLightIn,
OverlayTexture.NO_OVERLAY,
1,
1,
1,
1
);
}
ArmorTrim.getTrim(
entity.level().registryAccess(),
armorStack
).ifPresent(trim -> {
ArmorMaterial armorMaterial = armorItem.getMaterial();
TextureAtlasSprite sprite = this.armorTrimAtlas.getSprite(
trim.outerTexture(armorMaterial)
);
VertexConsumer trimVertexConsumer = sprite.wrap(
bufferSource.getBuffer(Sheets.armorTrimsSheet())
);
renderBox(
armor,
matrixStack,
trimVertexConsumer,
packedLightIn,
OverlayTexture.NO_OVERLAY,
1,
1,
1,
1
);
});
if (armorStack.hasFoil()) {
renderBox(
armor,
matrixStack,
bufferSource.getBuffer(RenderType.armorEntityGlint()),
packedLightIn,
OverlayTexture.NO_OVERLAY,
1,
1,
1,
1
);
}
matrixStack.popPose();
}
// Bondage Item Rendering - render bind texture on breasts
ItemStack bindStack = getBondageBindItem(entity);
if (!bindStack.isEmpty()) {
ResourceLocation bindTexture = getBindTextureForItem(bindStack);
if (bindTexture != null) {
matrixStack.pushPose();
matrixStack.translate(left ? 0.001f : -0.001f, 0.015f, -0.025f);
matrixStack.scale(1.08f, 1.02f, 1.02f);
WildfireModelRenderer.BreastModelBox bondageBox = left
? lBoobArmor
: rBoobArmor;
RenderType bindType = RenderType.entityCutoutNoCull(
bindTexture
);
VertexConsumer bindVertexConsumer = bufferSource.getBuffer(
bindType
);
renderBox(
bondageBox,
matrixStack,
bindVertexConsumer,
packedLightIn,
OverlayTexture.NO_OVERLAY,
1,
1,
1,
1
);
matrixStack.popPose();
}
}
}
/**
* Get the bondage bind item from an entity.
* Works for both NPCs (EntityDamsel) and Players (via IBondageState).
*/
private ItemStack getBondageBindItem(T entity) {
// For NPCs (AbstractTiedUpNpc and subclasses)
if (entity instanceof com.tiedup.remake.entities.AbstractTiedUpNpc npc) {
return npc.getEquipment(BodyRegionV2.ARMS);
}
// For Players (via capability)
if (entity instanceof net.minecraft.world.entity.player.Player player) {
IBondageState state = KidnappedHelper.getKidnappedState(player);
if (state != null) {
return state.getEquipment(BodyRegionV2.ARMS);
}
}
return ItemStack.EMPTY;
}
/**
* Get the texture for a bondage bind item.
* Uses same path logic as DamselBondageLayer.
*/
@Nullable
private ResourceLocation getBindTextureForItem(ItemStack stack) {
if (stack.isEmpty()) {
return null;
}
ResourceLocation itemId = ForgeRegistries.ITEMS.getKey(stack.getItem());
if (itemId == null) {
return null;
}
String itemName = itemId.getPath();
// V2 items use GLB models, not texture subfolders — fallback to "binds"
String subfolder = "binds";
// Get color suffix from NBT (e.g., "_red", "_blue", or "" if no color)
String colorSuffix = KidnapperItemSelector.getColorSuffix(stack);
return ResourceLocation.fromNamespaceAndPath(
"tiedup",
"textures/models/bondage/" +
subfolder +
"/" +
itemName +
colorSuffix +
".png"
);
}
private static void renderBox(
WildfireModelRenderer.ModelBox model,
PoseStack matrixStack,
VertexConsumer bufferIn,
int packedLightIn,
int packedOverlayIn,
float red,
float green,
float blue,
float alpha
) {
Matrix4f matrix4f = matrixStack.last().pose();
Matrix3f matrix3f = matrixStack.last().normal();
for (WildfireModelRenderer.TexturedQuad quad : model.quads) {
Vector3f vector3f = new Vector3f(
quad.normal.getX(),
quad.normal.getY(),
quad.normal.getZ()
);
vector3f.mul(matrix3f);
for (PositionTextureVertex vertex : quad.vertexPositions) {
bufferIn.vertex(
matrix4f,
vertex.x() / 16.0F,
vertex.y() / 16.0F,
vertex.z() / 16.0F
);
bufferIn.color(red, green, blue, alpha);
bufferIn.uv(
vertex.texturePositionX(),
vertex.texturePositionY()
);
bufferIn.overlayCoords(packedOverlayIn);
bufferIn.uv2(packedLightIn);
bufferIn.normal(vector3f.x(), vector3f.y(), vector3f.z());
bufferIn.endVertex();
}
}
}
}