Clean repo for open source release
Remove build artifacts, dev tool configs, unused dependencies, and third-party source dumps. Add proper README, update .gitignore, clean up Makefile.
This commit is contained in:
@@ -0,0 +1,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();
|
||||
}
|
||||
}
|
||||
118
src/main/java/com/tiedup/remake/compat/mca/MCACompat.java
Normal file
118
src/main/java/com/tiedup/remake/compat/mca/MCACompat.java
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
87
src/main/java/com/tiedup/remake/compat/mca/MCAHandler.java
Normal file
87
src/main/java/com/tiedup/remake/compat/mca/MCAHandler.java
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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. ");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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)];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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() -> int (typically -15 to 15)</li>
|
||||
* <li>modifyMoodValue(int) -> 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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() -> VillagerBrain</li>
|
||||
* <li>villagerBrain.getPersonality() -> Personality enum</li>
|
||||
* <li>personality.name() -> 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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user