Clean repo for open source release

Remove build artifacts, dev tool configs, unused dependencies,
and third-party source dumps. Add proper README, update .gitignore,
clean up Makefile.
This commit is contained in:
NotEvil
2026-04-12 00:51:22 +02:00
parent 2e7a1d403b
commit f6466360b6
1947 changed files with 238025 additions and 1 deletions

View File

@@ -0,0 +1,125 @@
package com.tiedup.remake.util;
import com.tiedup.remake.blocks.entity.IBondageItemHolder;
import com.tiedup.remake.items.base.*;
import java.util.List;
import net.minecraft.ChatFormatting;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
/**
* Utility class for loading bondage items into block entity holders.
*
* <p>This centralizes the logic for:
* <ul>
* <li>Loading items into IBondageItemHolder (traps, bombs, chests)</li>
* <li>Checking if an item is a loadable bondage item</li>
* <li>Adding item tooltips from NBT data</li>
* </ul>
*
* <p>Used by BlockKidnapBomb, BlockRopeTrap, and BlockTrappedChest.
*/
public final class BondageItemLoaderUtility {
private BondageItemLoaderUtility() {}
/**
* Load a bondage item into the holder.
*
* <p>Checks the item type and loads it into the appropriate slot
* if that slot is empty. Consumes one item from the stack (unless creative).
*
* @param holder The bondage item holder (block entity)
* @param stack The item stack to load
* @param player The player loading the item
* @return true if item was loaded, false if slot was occupied or wrong item type
*/
public static boolean loadItemIntoHolder(
IBondageItemHolder holder,
ItemStack stack,
Player player
) {
if (stack.getItem() instanceof ItemBind && holder.getBind().isEmpty()) {
holder.setBind(stack.copyWithCount(1));
if (!player.isCreative()) stack.shrink(1);
return true;
}
if (stack.getItem() instanceof ItemGag && holder.getGag().isEmpty()) {
holder.setGag(stack.copyWithCount(1));
if (!player.isCreative()) stack.shrink(1);
return true;
}
if (
stack.getItem() instanceof ItemBlindfold &&
holder.getBlindfold().isEmpty()
) {
holder.setBlindfold(stack.copyWithCount(1));
if (!player.isCreative()) stack.shrink(1);
return true;
}
if (
stack.getItem() instanceof ItemEarplugs &&
holder.getEarplugs().isEmpty()
) {
holder.setEarplugs(stack.copyWithCount(1));
if (!player.isCreative()) stack.shrink(1);
return true;
}
if (
stack.getItem() instanceof ItemCollar &&
holder.getCollar().isEmpty()
) {
holder.setCollar(stack.copyWithCount(1));
if (!player.isCreative()) stack.shrink(1);
return true;
}
return false;
}
/**
* Check if an item is a loadable bondage item.
*
* <p>Returns true for: Bind, Gag, Blindfold, Earplugs, Collar.
*
* @param stack The item stack to check
* @return true if the item can be loaded into a bondage item holder
*/
public static boolean isLoadableBondageItem(ItemStack stack) {
return (
(stack.getItem() instanceof ItemBind) ||
(stack.getItem() instanceof ItemGag) ||
(stack.getItem() instanceof ItemBlindfold) ||
(stack.getItem() instanceof ItemEarplugs) ||
(stack.getItem() instanceof ItemCollar)
);
}
/**
* Add item to tooltip if present in NBT.
*
* <p>Reads an item from the given NBT key and adds it to the tooltip
* with a "- ItemName" format in gold color.
*
* @param tooltip The tooltip list to add to
* @param beTag The BlockEntity NBT tag
* @param key The NBT key to read (e.g., "bind", "gag")
*/
public static void addItemToTooltip(
List<Component> tooltip,
CompoundTag beTag,
String key
) {
if (beTag.contains(key)) {
ItemStack item = ItemStack.of(beTag.getCompound(key));
if (!item.isEmpty()) {
tooltip.add(
Component.literal("- ")
.append(item.getHoverName())
.withStyle(ChatFormatting.GOLD)
);
}
}
}
}

View File

@@ -0,0 +1,204 @@
package com.tiedup.remake.util;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.network.sync.SyncManager;
import com.tiedup.remake.state.IBondageState;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Predicate;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import org.jetbrains.annotations.Nullable;
/**
* Helper class for common bondage equipment interaction logic.
*
* This class extracts the duplicated interactLivingEntity pattern
* from ItemGag, ItemBlindfold, ItemMittens, ItemEarplugs, and ItemCollar.
*/
public class EquipmentInteractionHelper {
/**
* Standard equipment interaction for bondage items.
*
* Handles the common flow: check state → equip or replace equipment.
*
* @param stack The item stack being used
* @param player The player using the item
* @param target The entity being equipped
* @param isEquipped Predicate to check if target already has this equipment
* @param putOn Consumer to equip the item on target (state, itemCopy)
* @param replace Function to replace existing equipment (state, itemCopy) → oldItem
* @param sendMessage Consumer to send message to target (player, target)
* @param logPrefix Log prefix for this equipment type
* @return InteractionResult
*/
public static InteractionResult equipOnTarget(
ItemStack stack,
Player player,
LivingEntity target,
Predicate<IBondageState> isEquipped,
BiConsumer<IBondageState, ItemStack> putOn,
BiFunction<IBondageState, ItemStack, ItemStack> replace,
BiConsumer<Player, LivingEntity> sendMessage,
String logPrefix
) {
return equipOnTarget(
stack,
player,
target,
isEquipped,
putOn,
replace,
sendMessage,
logPrefix,
null,
null,
null
);
}
/**
* Equipment interaction with additional hooks for special items (like Collar).
*
* @param stack The item stack being used
* @param player The player using the item
* @param target The entity being equipped
* @param isEquipped Predicate to check if target already has this equipment
* @param putOn Consumer to equip the item on target (state, itemCopy)
* @param replace Function to replace existing equipment (state, itemCopy) → oldItem
* @param sendMessage Consumer to send message to target (player, target)
* @param logPrefix Log prefix for this equipment type
* @param preEquipHook Optional hook called before equipping (for owner registration, etc.)
* @param postEquipHook Optional hook called after equipping (for sound, particles, etc.)
* @param canReplace Optional predicate to check if replacement is allowed (for lock check)
* @return InteractionResult
*/
public static InteractionResult equipOnTarget(
ItemStack stack,
Player player,
LivingEntity target,
Predicate<IBondageState> isEquipped,
BiConsumer<IBondageState, ItemStack> putOn,
BiFunction<IBondageState, ItemStack, ItemStack> replace,
BiConsumer<Player, LivingEntity> sendMessage,
String logPrefix,
@Nullable EquipContext preEquipHook,
@Nullable EquipContext postEquipHook,
@Nullable ReplacePredicate canReplace
) {
// Server-side only
if (player.level().isClientSide) {
return InteractionResult.SUCCESS;
}
// Get target state
IBondageState targetState = KidnappedHelper.getKidnappedState(target);
if (targetState == null) {
return InteractionResult.PASS;
}
// Must be tied up
if (!targetState.isTiedUp()) {
return InteractionResult.PASS;
}
// Pre-equip hook (e.g., register collar owner)
if (preEquipHook != null) {
preEquipHook.execute(stack, player, target, targetState);
}
// Case 1: Not equipped - put on new
if (!isEquipped.test(targetState)) {
putOn.accept(targetState, stack.copy());
stack.shrink(1);
sendMessage.accept(player, target);
syncIfPlayer(target);
// Post-equip hook (e.g., play sound)
if (postEquipHook != null) {
postEquipHook.execute(stack, player, target, targetState);
}
TiedUpMod.LOGGER.info(
"[{}] {} put {} on {}",
logPrefix,
player.getName().getString(),
logPrefix.toLowerCase(),
target.getName().getString()
);
return InteractionResult.SUCCESS;
}
// Case 2: Already equipped - replace
else {
// Check if replacement is allowed (e.g., not locked)
if (
canReplace != null &&
!canReplace.canReplace(targetState, player)
) {
return InteractionResult.PASS;
}
ItemStack oldItem = replace.apply(targetState, stack.copy());
if (oldItem != null && !oldItem.isEmpty()) {
stack.shrink(1);
targetState.kidnappedDropItem(oldItem);
sendMessage.accept(player, target);
syncIfPlayer(target);
// Post-equip hook
if (postEquipHook != null) {
postEquipHook.execute(stack, player, target, targetState);
}
TiedUpMod.LOGGER.info(
"[{}] {} replaced {} on {}",
logPrefix,
player.getName().getString(),
logPrefix.toLowerCase(),
target.getName().getString()
);
return InteractionResult.SUCCESS;
}
}
return InteractionResult.PASS;
}
/**
* Sync inventory if target is a ServerPlayer.
*/
private static void syncIfPlayer(LivingEntity target) {
if (target instanceof ServerPlayer serverPlayer) {
SyncManager.syncInventory(serverPlayer);
}
}
/**
* Functional interface for pre/post equip hooks.
*/
@FunctionalInterface
public interface EquipContext {
void execute(
ItemStack stack,
Player player,
LivingEntity target,
IBondageState state
);
}
/**
* Functional interface to check if replacement is allowed.
*/
@FunctionalInterface
public interface ReplacePredicate {
boolean canReplace(IBondageState state, Player player);
}
}

View File

@@ -0,0 +1,103 @@
package com.tiedup.remake.util;
import org.jetbrains.annotations.Nullable;
import net.minecraft.world.food.FoodProperties;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
/**
* Calculates the effects of feeding food items to NPCs.
*
* Effects include:
* - Hunger restoration (based on nutrition)
* - Mood boost (based on food quality)
* - HP healing (based on saturation)
* - Affinity increase (based on nutrition)
*/
public class FoodEffects {
/**
* Calculate the effects of feeding a food item to an NPC.
*
* @param food The food item stack
* @return FeedResult with all effects, or null if not a food item
*/
@Nullable
public static FeedResult calculateFeedEffects(ItemStack food) {
if (food.isEmpty()) return null;
FoodProperties props = food.getItem().getFoodProperties();
if (props == null) return null;
int nutrition = props.getNutrition();
float saturation = props.getSaturationModifier();
// Base hunger restoration (nutrition value directly, feed() multiplies by 5)
float hungerRestore = nutrition;
// Mood boost based on food quality (nutrition 1-8 maps to mood 3-20)
int moodBoost = Math.max(3, (int) (nutrition * 2.5f));
// Special items get bonus mood
if (food.is(Items.CAKE)) {
moodBoost = 25;
} else if (food.is(Items.GOLDEN_APPLE)) {
moodBoost = 30;
} else if (food.is(Items.ENCHANTED_GOLDEN_APPLE)) {
moodBoost = 50;
} else if (food.is(Items.COOKIE)) {
moodBoost = 15;
} else if (food.is(Items.HONEY_BOTTLE)) {
moodBoost = 20;
} else if (food.is(Items.PUMPKIN_PIE)) {
moodBoost = 18;
} else if (
food.is(Items.SWEET_BERRIES) || food.is(Items.GLOW_BERRIES)
) {
moodBoost = 12;
} else if (food.is(Items.CHORUS_FRUIT)) {
moodBoost = 8; // Weird taste
} else if (food.is(Items.ROTTEN_FLESH) || food.is(Items.SPIDER_EYE)) {
moodBoost = -5; // Disgusting
} else if (food.is(Items.POISONOUS_POTATO)) {
moodBoost = -10; // Very bad
}
// Heal HP based on saturation (saturation * 2, min 1)
float healAmount = Math.max(1.0f, saturation * 2.0f);
// Affinity boost based on nutrition (min 1, max 4)
int affinityBoost = Math.max(1, Math.min(4, nutrition / 2));
// Special items get bonus affinity
if (
food.is(Items.GOLDEN_APPLE) || food.is(Items.ENCHANTED_GOLDEN_APPLE)
) {
affinityBoost = 8;
} else if (food.is(Items.CAKE)) {
affinityBoost = 6;
}
return new FeedResult(
hungerRestore,
moodBoost,
healAmount,
affinityBoost
);
}
/**
* Result of feeding calculation.
*
* @param hunger How much hunger to restore (passed to NpcNeeds.feed())
* @param mood How much mood to add
* @param heal How much HP to heal
* @param affinity How much affinity to add with the feeding player
*/
public record FeedResult(
float hunger,
int mood,
float heal,
int affinity
) {}
}

View File

@@ -0,0 +1,296 @@
package com.tiedup.remake.util;
import com.tiedup.remake.core.ModConfig;
/**
* GagMaterial DNA - Defines the "sound" and behavior of different gag materials.
*
* Phase 15: Added PANEL, LATEX, RING, BITE, SPONGE, BAGUETTE
* Phase 16: Enhanced with phonetic properties for realistic speech transformation
*/
public enum GagMaterial {
// Original materials
CLOTH(
new String[] { "m", "h", "f", "p" },
new String[] { "ph", "ff", "mm" },
0.4f,
15.0,
"mm",
"ah", // dominant consonant, vowel
0.3f,
0.5f,
0.9f,
0.3f,
0.6f // plosive, fricative, nasal, liquid, vowel bleed
),
BALL(
new String[] { "b", "h", "m", "p" },
new String[] { "oo", "uu", "mm", "ou" },
0.2f,
10.0,
"mm",
"uu",
0.1f,
0.2f,
0.9f,
0.1f,
0.4f
),
TAPE(
new String[] { "m", "n" },
new String[] { "mm", "nn", "mnm" },
0.05f,
5.0,
"mm",
"mm",
0.0f,
0.1f,
0.8f,
0.0f,
0.1f
),
STUFFED(
new String[] { "m" },
new String[] { "mm" },
0.0f,
3.0,
"mm",
"mm",
0.0f,
0.0f,
0.5f,
0.0f,
0.0f
),
// Phase 15: New materials
PANEL(
new String[] { "m", "n" },
new String[] { "mm", "nn" },
0.05f,
4.0,
"mm",
"mm",
0.0f,
0.1f,
0.7f,
0.0f,
0.1f
),
LATEX(
new String[] { "m", "h" },
new String[] { "mm", "uu" },
0.1f,
6.0,
"mm",
"uu",
0.05f,
0.15f,
0.8f,
0.05f,
0.2f
),
RING(
new String[] { "a", "h", "l" },
new String[] { "aa", "ah", "la" },
0.5f,
12.0,
"h",
"ah",
0.7f,
0.8f,
0.95f,
0.8f,
0.9f // Very open, most sounds pass
),
BITE(
new String[] { "h", "g", "n" },
new String[] { "eh", "ah", "ng" },
0.3f,
10.0,
"gh",
"eh",
0.4f,
0.6f,
0.9f,
0.5f,
0.7f
),
SPONGE(
new String[] { "m" },
new String[] { "mm" },
0.0f,
2.0,
"mm",
"mm",
0.0f,
0.0f,
0.3f,
0.0f,
0.0f // Absorbs almost everything
),
BAGUETTE(
new String[] { "b", "m", "f" },
new String[] { "om", "am", "um" },
0.25f,
8.0,
"mm",
"om",
0.2f,
0.3f,
0.8f,
0.2f,
0.5f
);
public final String[] consonants;
public final String[] vowels;
private final float defaultComprehension;
private final double defaultTalkRange;
// Phase 16: Phonetic properties
private final String dominantConsonant;
private final String dominantVowel;
private final float plosiveBleed; // b,d,g,k,p,t - require lip/tongue
private final float fricativeBleed; // f,h,s,v,z - air-based
private final float nasalBleed; // m,n - through nose
private final float liquidBleed; // l,r - tongue-dependent
private final float vowelBleed; // a,e,i,o,u,y - mouth shape
GagMaterial(
String[] c,
String[] v,
float comp,
double range,
String domCons,
String domVowel,
float plosive,
float fricative,
float nasal,
float liquid,
float vowel
) {
this.consonants = c;
this.vowels = v;
this.defaultComprehension = comp;
this.defaultTalkRange = range;
this.dominantConsonant = domCons;
this.dominantVowel = domVowel;
this.plosiveBleed = plosive;
this.fricativeBleed = fricative;
this.nasalBleed = nasal;
this.liquidBleed = liquid;
this.vowelBleed = vowel;
}
public float getComprehension() {
String key = this.name().toLowerCase();
if (
ModConfig.SERVER != null &&
ModConfig.SERVER.gagComprehension.containsKey(key)
) {
return ModConfig.SERVER.gagComprehension
.get(key)
.get()
.floatValue();
}
return defaultComprehension;
}
public double getTalkRange() {
String key = this.name().toLowerCase();
if (
ModConfig.SERVER != null &&
ModConfig.SERVER.gagRange.containsKey(key)
) {
return ModConfig.SERVER.gagRange.get(key).get();
}
return defaultTalkRange;
}
/**
* Get the dominant consonant sound for this material.
* Used when a consonant is completely blocked.
*/
public String getDominantConsonant() {
return dominantConsonant;
}
/**
* Get the dominant vowel sound for this material.
* Used when a vowel is completely blocked.
*/
public String getDominantVowel() {
return dominantVowel;
}
/**
* Get bleed-through rate for plosive consonants (b,d,g,k,p,t).
*/
public float getPlosiveBleed() {
return plosiveBleed;
}
/**
* Get bleed-through rate for fricative consonants (f,h,s,v,z).
*/
public float getFricativeBleed() {
return fricativeBleed;
}
/**
* Get bleed-through rate for nasal consonants (m,n).
*/
public float getNasalBleed() {
return nasalBleed;
}
/**
* Get bleed-through rate for liquid consonants (l,r).
*/
public float getLiquidBleed() {
return liquidBleed;
}
/**
* Get bleed-through rate for vowels.
*/
public float getVowelBleed() {
return vowelBleed;
}
/**
* Calculate the effective bleed rate for a specific character.
*/
public float getBleedRateFor(char c) {
char lower = Character.toLowerCase(c);
// Vowels
if ("aeiouy".indexOf(lower) >= 0) {
return vowelBleed;
}
// Nasals - almost always pass
if (lower == 'm' || lower == 'n') {
return nasalBleed;
}
// Plosives - blocked by most gags
if ("bdgkpt".indexOf(lower) >= 0) {
return plosiveBleed;
}
// Fricatives - air-based
if ("fhsvz".indexOf(lower) >= 0) {
return fricativeBleed;
}
// Liquids - tongue-dependent
if (lower == 'l' || lower == 'r') {
return liquidBleed;
}
// Default to average of consonant bleeds
return (plosiveBleed + fricativeBleed + nasalBleed + liquidBleed) / 4f;
}
}

View File

@@ -0,0 +1,119 @@
package com.tiedup.remake.util;
/**
* Phase 4 Refactoring: Centralized game constants.
*
* Contains all magic numbers extracted from across the codebase
* for better maintainability and easier tuning.
*/
public final class GameConstants {
private GameConstants() {
// Utility class - no instantiation
}
// ==================== Time Conversions ====================
/** Minecraft ticks per second */
public static final int TICKS_PER_SECOND = 20;
/** Minecraft ticks per second (float for calculations) */
public static final float TICKS_PER_SECOND_F = 20.0f;
// ==================== Shock System ====================
/** Default shock damage when no custom value specified */
public static final float DEFAULT_SHOCK_DAMAGE = 3.0f;
/** Number of particles spawned during shock effect */
public static final int SHOCK_PARTICLE_COUNT = 20;
/** Y offset for shock particle spawn position */
public static final double SHOCK_PARTICLE_Y_OFFSET = 1.0;
/** X spread for shock particles */
public static final double SHOCK_PARTICLE_X_SPREAD = 0.3;
/** Y spread for shock particles */
public static final double SHOCK_PARTICLE_Y_SPREAD = 0.5;
/** Z spread for shock particles */
public static final double SHOCK_PARTICLE_Z_SPREAD = 0.3;
/** Speed/motion for shock particles */
public static final double SHOCK_PARTICLE_SPEED = 0.1;
/** Volume for shock sound effect */
public static final float SHOCK_SOUND_VOLUME = 1.0f;
/** Pitch for shock sound effect */
public static final float SHOCK_SOUND_PITCH = 1.0f;
// ==================== Chloroform Effects ====================
/** Movement slowdown amplifier for chloroform (127 = max) */
public static final int CHLOROFORM_SLOWDOWN_AMPLIFIER = 127;
/** Dig slowdown amplifier for chloroform (127 = max) */
public static final int CHLOROFORM_DIG_SLOWDOWN_AMPLIFIER = 127;
/** Blindness amplifier for chloroform (127 = max) */
public static final int CHLOROFORM_BLINDNESS_AMPLIFIER = 127;
/** Jump amplifier for chloroform (negative jump) */
public static final int CHLOROFORM_JUMP_AMPLIFIER = 150;
// ==================== Safety Thresholds ====================
/** Minimum health threshold - damage won't reduce below this */
public static final float MIN_HEALTH_THRESHOLD = 0.5f;
// ==================== Movement Restrictions ====================
/** Break speed multiplier when player is tied */
public static final float TIED_BREAK_SPEED_MULTIPLIER = 0.1f;
// ==================== Tick Intervals ====================
/** Ticks between shock collar checks */
public static final int SHOCK_COLLAR_CHECK_INTERVAL = 10;
/** Ticks between movement restriction checks */
public static final int MOVEMENT_CHECK_INTERVAL = 5;
// ==================== Struggle System ====================
/** Duration of struggle animation in ticks (4 seconds) */
public static final int STRUGGLE_ANIMATION_DURATION_TICKS = 80;
// ==================== Gag Talk System ====================
/** Message length threshold that triggers suffocation effects */
public static final int GAG_MAX_MESSAGE_LENGTH_BEFORE_SUFFOCATION = 40;
/** Duration of slowness effect from suffocation (ticks) */
public static final int GAG_SUFFOCATION_SLOWNESS_DURATION = 40;
/** Chance of blindness when suffocating from long message */
public static final float GAG_SUFFOCATION_BLINDNESS_CHANCE = 0.3f;
/** Duration of blindness effect from suffocation (ticks) */
public static final int GAG_SUFFOCATION_BLINDNESS_DURATION = 20;
/** Base chance of critical fail when speaking through gag */
public static final float GAG_BASE_CRITICAL_FAIL_CHANCE = 0.05f;
/** Additional critical fail chance per character of message */
public static final float GAG_LENGTH_CRITICAL_FACTOR = 0.005f;
// ==================== Merchant Particles ====================
/** Chance per tick of spawning golden sparkle particles on merchant */
public static final float MERCHANT_SPARKLE_PARTICLE_CHANCE = 0.15F;
/** Horizontal spread (X/Z) for merchant sparkle particles */
public static final double MERCHANT_PARTICLE_SPREAD_XZ = 0.6;
/** Vertical spread (Y) for merchant sparkle particles */
public static final double MERCHANT_PARTICLE_SPREAD_Y = 1.8;
}

View File

@@ -0,0 +1,408 @@
package com.tiedup.remake.util;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import org.jetbrains.annotations.Nullable;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.Tag;
import net.minecraft.world.item.ItemStack;
/**
* Utility class for common NBT operations on ItemStacks.
*
* <p>Centralizes repetitive null-check patterns found throughout the codebase:
* <ul>
* <li>Safe getters with default values</li>
* <li>Safe setters that handle empty stacks</li>
* <li>UUID list operations for owner/key systems</li>
* </ul>
*
* <p>All methods are null-safe and handle empty ItemStacks gracefully.
*/
public final class ItemNBTHelper {
private ItemNBTHelper() {
// Utility class - no instantiation
}
// ========================================
// BOOLEAN OPERATIONS
// ========================================
/**
* Get a boolean value from an ItemStack's NBT.
*
* @param stack The ItemStack to read from
* @param key The NBT key
* @param defaultValue Value to return if stack is empty or key doesn't exist
* @return The boolean value or defaultValue
*/
public static boolean getBoolean(
ItemStack stack,
String key,
boolean defaultValue
) {
if (stack.isEmpty()) return defaultValue;
CompoundTag tag = stack.getTag();
if (tag == null || !tag.contains(key, Tag.TAG_BYTE)) {
return defaultValue;
}
return tag.getBoolean(key);
}
/**
* Get a boolean value from an ItemStack's NBT (defaults to false).
*
* @param stack The ItemStack to read from
* @param key The NBT key
* @return The boolean value or false
*/
public static boolean getBoolean(ItemStack stack, String key) {
return getBoolean(stack, key, false);
}
/**
* Set a boolean value in an ItemStack's NBT.
*
* @param stack The ItemStack to modify
* @param key The NBT key
* @param value The boolean value to set
*/
public static void setBoolean(ItemStack stack, String key, boolean value) {
if (stack.isEmpty()) return;
stack.getOrCreateTag().putBoolean(key, value);
}
// ========================================
// INTEGER OPERATIONS
// ========================================
/**
* Get an integer value from an ItemStack's NBT.
*
* @param stack The ItemStack to read from
* @param key The NBT key
* @param defaultValue Value to return if stack is empty or key doesn't exist
* @return The integer value or defaultValue
*/
public static int getInt(ItemStack stack, String key, int defaultValue) {
if (stack.isEmpty()) return defaultValue;
CompoundTag tag = stack.getTag();
if (tag == null || !tag.contains(key, Tag.TAG_INT)) {
return defaultValue;
}
return tag.getInt(key);
}
/**
* Get an integer value from an ItemStack's NBT (defaults to 0).
*
* @param stack The ItemStack to read from
* @param key The NBT key
* @return The integer value or 0
*/
public static int getInt(ItemStack stack, String key) {
return getInt(stack, key, 0);
}
/**
* Set an integer value in an ItemStack's NBT.
*
* @param stack The ItemStack to modify
* @param key The NBT key
* @param value The integer value to set
*/
public static void setInt(ItemStack stack, String key, int value) {
if (stack.isEmpty()) return;
stack.getOrCreateTag().putInt(key, value);
}
// ========================================
// STRING OPERATIONS
// ========================================
/**
* Get a string value from an ItemStack's NBT.
*
* @param stack The ItemStack to read from
* @param key The NBT key
* @param defaultValue Value to return if stack is empty or key doesn't exist
* @return The string value or defaultValue
*/
@Nullable
public static String getString(
ItemStack stack,
String key,
@Nullable String defaultValue
) {
if (stack.isEmpty()) return defaultValue;
CompoundTag tag = stack.getTag();
if (tag == null || !tag.contains(key, Tag.TAG_STRING)) {
return defaultValue;
}
return tag.getString(key);
}
/**
* Get a string value from an ItemStack's NBT (defaults to null).
*
* @param stack The ItemStack to read from
* @param key The NBT key
* @return The string value or null
*/
@Nullable
public static String getString(ItemStack stack, String key) {
return getString(stack, key, null);
}
/**
* Set a string value in an ItemStack's NBT.
*
* @param stack The ItemStack to modify
* @param key The NBT key
* @param value The string value to set (null removes the key)
*/
public static void setString(
ItemStack stack,
String key,
@Nullable String value
) {
if (stack.isEmpty()) return;
if (value == null) {
remove(stack, key);
} else {
stack.getOrCreateTag().putString(key, value);
}
}
// ========================================
// UUID OPERATIONS
// ========================================
/**
* Get a UUID value from an ItemStack's NBT.
*
* @param stack The ItemStack to read from
* @param key The NBT key
* @return The UUID value or null if not present
*/
@Nullable
public static UUID getUUID(ItemStack stack, String key) {
if (stack.isEmpty()) return null;
CompoundTag tag = stack.getTag();
if (tag == null || !tag.hasUUID(key)) {
return null;
}
return tag.getUUID(key);
}
/**
* Set a UUID value in an ItemStack's NBT.
*
* @param stack The ItemStack to modify
* @param key The NBT key
* @param value The UUID value to set (null removes the key)
*/
public static void setUUID(
ItemStack stack,
String key,
@Nullable UUID value
) {
if (stack.isEmpty()) return;
if (value == null) {
remove(stack, key);
} else {
stack.getOrCreateTag().putUUID(key, value);
}
}
/**
* Check if an ItemStack's NBT contains a UUID at the given key.
*
* @param stack The ItemStack to check
* @param key The NBT key
* @return true if the key exists and contains a UUID
*/
public static boolean hasUUID(ItemStack stack, String key) {
if (stack.isEmpty()) return false;
CompoundTag tag = stack.getTag();
return tag != null && tag.hasUUID(key);
}
// ========================================
// UUID LIST OPERATIONS
// ========================================
/**
* Get a list of UUIDs from an ItemStack's NBT.
* UUIDs are stored as a ListTag of CompoundTags with "UUID" key.
*
* @param stack The ItemStack to read from
* @param key The NBT key for the list
* @return A list of UUIDs (empty list if not present)
*/
public static List<UUID> getUUIDList(ItemStack stack, String key) {
List<UUID> result = new ArrayList<>();
if (stack.isEmpty()) return result;
CompoundTag tag = stack.getTag();
if (tag == null || !tag.contains(key, Tag.TAG_LIST)) {
return result;
}
ListTag listTag = tag.getList(key, Tag.TAG_COMPOUND);
for (int i = 0; i < listTag.size(); i++) {
CompoundTag entry = listTag.getCompound(i);
if (entry.hasUUID("UUID")) {
result.add(entry.getUUID("UUID"));
}
}
return result;
}
/**
* Set a list of UUIDs in an ItemStack's NBT.
* UUIDs are stored as a ListTag of CompoundTags with "UUID" key.
*
* @param stack The ItemStack to modify
* @param key The NBT key for the list
* @param uuids The list of UUIDs to store
*/
public static void setUUIDList(
ItemStack stack,
String key,
List<UUID> uuids
) {
if (stack.isEmpty()) return;
if (uuids == null || uuids.isEmpty()) {
remove(stack, key);
return;
}
ListTag listTag = new ListTag();
for (UUID uuid : uuids) {
CompoundTag entry = new CompoundTag();
entry.putUUID("UUID", uuid);
listTag.add(entry);
}
stack.getOrCreateTag().put(key, listTag);
}
/**
* Add a UUID to a list stored in an ItemStack's NBT.
*
* @param stack The ItemStack to modify
* @param key The NBT key for the list
* @param uuid The UUID to add
* @return true if the UUID was added (not already present)
*/
public static boolean addToUUIDList(
ItemStack stack,
String key,
UUID uuid
) {
if (stack.isEmpty() || uuid == null) return false;
List<UUID> list = getUUIDList(stack, key);
if (list.contains(uuid)) {
return false;
}
list.add(uuid);
setUUIDList(stack, key, list);
return true;
}
/**
* Remove a UUID from a list stored in an ItemStack's NBT.
*
* @param stack The ItemStack to modify
* @param key The NBT key for the list
* @param uuid The UUID to remove
* @return true if the UUID was removed
*/
public static boolean removeFromUUIDList(
ItemStack stack,
String key,
UUID uuid
) {
if (stack.isEmpty() || uuid == null) return false;
List<UUID> list = getUUIDList(stack, key);
if (list.remove(uuid)) {
setUUIDList(stack, key, list);
return true;
}
return false;
}
/**
* Check if a UUID is present in a list stored in an ItemStack's NBT.
*
* @param stack The ItemStack to check
* @param key The NBT key for the list
* @param uuid The UUID to find
* @return true if the UUID is in the list
*/
public static boolean isInUUIDList(ItemStack stack, String key, UUID uuid) {
if (stack.isEmpty() || uuid == null) return false;
return getUUIDList(stack, key).contains(uuid);
}
// ========================================
// GENERIC OPERATIONS
// ========================================
/**
* Check if an ItemStack's NBT contains a key.
*
* @param stack The ItemStack to check
* @param key The NBT key
* @return true if the key exists
*/
public static boolean contains(ItemStack stack, String key) {
if (stack.isEmpty()) return false;
CompoundTag tag = stack.getTag();
return tag != null && tag.contains(key);
}
/**
* Remove a key from an ItemStack's NBT.
*
* @param stack The ItemStack to modify
* @param key The NBT key to remove
*/
public static void remove(ItemStack stack, String key) {
if (stack.isEmpty()) return;
CompoundTag tag = stack.getTag();
if (tag != null) {
tag.remove(key);
}
}
/**
* Get the CompoundTag from an ItemStack (may be null).
*
* @param stack The ItemStack to read from
* @return The CompoundTag or null
*/
@Nullable
public static CompoundTag getTag(ItemStack stack) {
if (stack.isEmpty()) return null;
return stack.getTag();
}
/**
* Get or create the CompoundTag for an ItemStack.
*
* @param stack The ItemStack to modify
* @return The CompoundTag (never null if stack is not empty)
*/
@Nullable
public static CompoundTag getOrCreateTag(ItemStack stack) {
if (stack.isEmpty()) return null;
return stack.getOrCreateTag();
}
}

View File

@@ -0,0 +1,159 @@
package com.tiedup.remake.util;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.state.PlayerBindState;
import java.util.List;
import org.jetbrains.annotations.Nullable;
import net.minecraft.core.BlockPos;
import net.minecraft.core.particles.ParticleTypes;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.AABB;
/**
* Kidnap Explosion - Applies bondage to entities in an area.
*
* Phase 16: Blocks
*
* Used by EntityKidnapBomb when it explodes.
* Applies stored bondage items to all kidnappable entities in radius.
*
* Based on original KidnapExplosion from 1.12.2
*/
public class KidnapExplosion {
private final Level level;
private final BlockPos pos;
private final int radius;
private final ItemStack bind;
private final ItemStack gag;
private final ItemStack blindfold;
private final ItemStack earplugs;
private final ItemStack collar;
private final ItemStack clothes;
public KidnapExplosion(
Level level,
BlockPos pos,
int radius,
ItemStack bind,
ItemStack gag,
ItemStack blindfold,
ItemStack earplugs,
ItemStack collar,
ItemStack clothes
) {
this.level = level;
this.pos = pos;
this.radius = radius;
this.bind = bind;
this.gag = gag;
this.blindfold = blindfold;
this.earplugs = earplugs;
this.collar = collar;
this.clothes = clothes;
}
/**
* Execute the explosion effect.
*/
public void explode() {
explode(null);
}
/**
* Execute the explosion effect, optionally excluding a player.
*
* @param toExclude Player to exclude from effect (usually the one who placed the bomb)
*/
public void explode(@Nullable Player toExclude) {
if (level == null || level.isClientSide || pos == null) {
return;
}
// Play explosion sound
level.playSound(
null,
pos,
SoundEvents.GENERIC_EXPLODE,
SoundSource.BLOCKS,
4.0f,
(1.0f +
(level.random.nextFloat() - level.random.nextFloat()) *
0.2f) *
0.7f
);
// Spawn explosion particle
if (level instanceof ServerLevel serverLevel) {
serverLevel.sendParticles(
ParticleTypes.EXPLOSION,
pos.getX() + 0.5,
pos.getY() + 0.5,
pos.getZ() + 0.5,
1,
0,
0,
0,
0
);
}
// Find all kidnappable entities in radius
AABB area = new AABB(pos).inflate(radius);
List<LivingEntity> entities = level.getEntitiesOfClass(
LivingEntity.class,
area
);
int affected = 0;
for (LivingEntity entity : entities) {
// Skip excluded player
if (toExclude != null && entity.equals(toExclude)) {
continue;
}
// Skip spectators
if (entity instanceof Player player && player.isSpectator()) {
continue;
}
// Get kidnapped state
IBondageState kidnappedState = KidnappedHelper.getKidnappedState(
entity
);
if (kidnappedState == null) {
continue;
}
// Skip already tied entities
if (kidnappedState.isTiedUp()) {
continue;
}
// Apply bondage
kidnappedState.applyBondage(
bind,
gag,
blindfold,
earplugs,
collar,
clothes
);
affected++;
}
TiedUpMod.LOGGER.info(
"[KidnapExplosion] Explosion at {} affected {} entities (radius: {})",
pos,
affected,
radius
);
}
}

View File

@@ -0,0 +1,175 @@
package com.tiedup.remake.util;
import com.tiedup.remake.compat.mca.MCACompat;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.state.IRestrainable;
import com.tiedup.remake.state.PlayerBindState;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import org.jetbrains.annotations.Nullable;
/**
* Phase 14.1: Helper utility for working with IRestrainable entities.
*
* Provides convenient methods for obtaining IRestrainable instances from various entity types.
*
* <h2>Purpose</h2>
* This helper abstracts the complexity of determining whether an entity can be kidnapped/restrained
* and provides a unified way to access the IRestrainable interface across different entity types.
*
* <h2>Supported Entity Types</h2>
* <ul>
* <li><b>Player</b>: Uses {@link PlayerBindState} singleton</li>
* <li><b>EntityDamsel</b> (Phase 14.2): Implements IRestrainable directly</li>
* <li><b>EntityKidnapper</b> (Phase 14.2): Implements IRestrainable directly</li>
* <li><b>Other entities</b>: Returns null (not kidnappable)</li>
* </ul>
*
* <h2>Usage Example</h2>
* <pre>{@code
* public InteractionResult interactLivingEntity(ItemStack stack, Player user,
* LivingEntity target, InteractionHand hand) {
* IRestrainable state = KidnappedHelper.getKidnappedState(target);
* if (state == null) {
* return InteractionResult.PASS; // Entity cannot be restrained
* }
*
* if (state.isTiedUp()) {
* user.sendSystemMessage(Component.literal("Already tied up!"));
* return InteractionResult.FAIL;
* }
*
* state.equip(BodyRegionV2.ARMS, stack.copy());
* return InteractionResult.SUCCESS;
* }
* }</pre>
*/
public class KidnappedHelper {
/**
* Get the IRestrainable instance for any living entity.
*
* <p>This method determines the appropriate IRestrainable implementation based on entity type:
* <ul>
* <li><b>Player</b>: Returns {@link PlayerBindState#getInstance(Player)}</li>
* <li><b>IRestrainable implementer</b> (NPCs): Returns the entity itself (cast to IRestrainable)</li>
* <li><b>Other entities</b>: Returns null (entity cannot be kidnapped)</li>
* </ul>
*
* @param entity The living entity to check
* @return The IRestrainable instance, or null if entity cannot be kidnapped
*/
@Nullable
public static IRestrainable getKidnappedState(LivingEntity entity) {
if (entity == null) {
return null;
}
// For Players: Use PlayerBindState singleton
if (entity instanceof Player player) {
return PlayerBindState.getInstance(player);
}
// For NPCs that implement IRestrainable (EntityDamsel, EntityKidnapper, etc.)
if (entity instanceof IRestrainable kidnapped) {
return kidnapped;
}
// MCA Compatibility: Check if entity is an MCA villager
if (MCACompat.isMCALoaded() && MCACompat.isMCAVillager(entity)) {
IRestrainable mcaState = MCACompat.getKidnappedState(entity);
com.tiedup.remake.core.TiedUpMod.LOGGER.debug(
"[KidnappedHelper] MCA villager {} -> state: {}",
entity.getName().getString(),
mcaState != null ? mcaState.getClass().getSimpleName() : "null"
);
return mcaState;
}
// Entity cannot be kidnapped
return null;
}
/**
* Check if an entity can be kidnapped/restrained.
*
* @param entity The living entity to check
* @return true if the entity implements IRestrainable or is a Player
*/
public static boolean canBeKidnapped(LivingEntity entity) {
return getKidnappedState(entity) != null;
}
/**
* Check if an entity is currently tied up.
*
* <p>Convenience method that combines {@link #getKidnappedState(LivingEntity)}
* and {@link IRestrainable#isTiedUp()}.
*
* @param entity The living entity to check
* @return true if the entity is tied up, false otherwise
*/
public static boolean isTiedUp(LivingEntity entity) {
IRestrainable state = getKidnappedState(entity);
return state != null && state.isTiedUp();
}
/**
* Check if an entity is currently gagged.
*
* <p>Convenience method that combines {@link #getKidnappedState(LivingEntity)}
* and {@link IRestrainable#isGagged()}.
*
* @param entity The living entity to check
* @return true if the entity is gagged, false otherwise
*/
public static boolean isGagged(LivingEntity entity) {
IRestrainable state = getKidnappedState(entity);
return state != null && state.isGagged();
}
/**
* Check if an entity is currently blindfolded.
*
* <p>Convenience method that combines {@link #getKidnappedState(LivingEntity)}
* and {@link IRestrainable#isBlindfolded()}.
*
* @param entity The living entity to check
* @return true if the entity is blindfolded, false otherwise
*/
public static boolean isBlindfolded(LivingEntity entity) {
IRestrainable state = getKidnappedState(entity);
return state != null && state.isBlindfolded();
}
/**
* Check if an entity has a collar.
*
* <p>Convenience method that combines {@link #getKidnappedState(LivingEntity)}
* and {@link IRestrainable#hasCollar()}.
*
* @param entity The living entity to check
* @return true if the entity has a collar, false otherwise
*/
public static boolean hasCollar(LivingEntity entity) {
IRestrainable state = getKidnappedState(entity);
return state != null && state.hasCollar();
}
/**
* Check if an entity is enslaved.
*
* <p>Convenience method that combines {@link #getKidnappedState(LivingEntity)}
* and {@link IRestrainable#isSlave()}.
*
* @param entity The living entity to check
* @return true if the entity is enslaved, false otherwise
*/
/**
* Phase 17: Renamed from isSlave to isCaptive
*/
public static boolean isCaptive(LivingEntity entity) {
IRestrainable state = getKidnappedState(entity);
return state != null && state.isCaptive();
}
}

View File

@@ -0,0 +1,374 @@
package com.tiedup.remake.util;
import com.tiedup.remake.entities.EntityKidnapper;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.levelgen.Heightmap;
import org.jetbrains.annotations.Nullable;
/**
* Helper methods for Kidnapper AI goals.
*
* Phase: Code Quality Refactoring
*
* Consolidates duplicated logic from multiple AI goals:
* - Ground position finding (AbstractKidnapperFleeGoal, KidnapperPatrolGoal)
* - Common "can use" preconditions
*/
public final class KidnapperAIHelper {
private KidnapperAIHelper() {
// Utility class - prevent instantiation
}
// ==================== GROUND POSITION FINDING ====================
/**
* Check if a position is valid for walking (solid ground below, air at feet and head).
*
* @param level The world level
* @param pos The position to check (feet position)
* @return true if the position is walkable
*/
public static boolean isValidGroundPosition(Level level, BlockPos pos) {
// Need solid ground below
BlockPos below = pos.below();
if (!level.getBlockState(below).isSolid()) {
return false;
}
// Need air at feet and head level
if (!level.getBlockState(pos).isAir()) {
return false;
}
if (!level.getBlockState(pos.above()).isAir()) {
return false;
}
return true;
}
/**
* Find a valid ground position near a target position.
* Searches in a vertical range of -5 to +5 blocks.
*
* @param level The world level
* @param targetPos The target position to search around
* @return A valid ground position, or null if none found
*/
@Nullable
public static BlockPos findGroundPos(Level level, BlockPos targetPos) {
return findGroundPos(level, targetPos, 5);
}
/**
* Find a valid ground position near a target position.
*
* @param level The world level
* @param targetPos The target position to search around
* @param ySearchRange Vertical search range (up and down)
* @return A valid ground position, or null if none found
*/
@Nullable
public static BlockPos findGroundPos(
Level level,
BlockPos targetPos,
int ySearchRange
) {
for (int yOffset = -ySearchRange; yOffset <= ySearchRange; yOffset++) {
BlockPos checkPos = targetPos.offset(0, yOffset, 0);
if (isValidGroundPosition(level, checkPos)) {
return checkPos;
}
}
return null;
}
/**
* Find a random ground position within a radius.
*
* @param level The world level
* @param center The center position
* @param radius The search radius
* @param random Random source
* @param attempts Maximum number of attempts
* @return A valid ground position, or null if none found
*/
@Nullable
public static BlockPos findRandomGroundPos(
Level level,
BlockPos center,
int radius,
RandomSource random,
int attempts
) {
for (int i = 0; i < attempts; i++) {
int offsetX = random.nextInt(radius * 2 + 1) - radius;
int offsetZ = random.nextInt(radius * 2 + 1) - radius;
BlockPos targetPos = center.offset(offsetX, 0, offsetZ);
BlockPos groundPos = findGroundPos(level, targetPos);
if (groundPos != null) {
return groundPos;
}
}
return null;
}
// ==================== SAFE TELEPORT POSITION FINDING ====================
/**
* Find a safe position for teleportation within a radius.
* Uses polar coordinates with bias towards farther positions.
*
* <p>Consolidated from EntityKidnapper, AbstractKidnapperFleeGoal, MaidExtractPrisonerGoal.</p>
*
* @param level The server level
* @param center The center position to search from
* @param radius Maximum distance from center
* @param random Random source
* @return Safe block position, or null if none found after 20 attempts
*/
@Nullable
public static BlockPos findSafePosition(
ServerLevel level,
BlockPos center,
int radius,
RandomSource random
) {
// Try up to 20 random positions
for (int attempt = 0; attempt < 20; attempt++) {
// Random angle
double angle = random.nextDouble() * Math.PI * 2;
// Random distance (bias towards farther)
double distance = radius * 0.7 + random.nextDouble() * radius * 0.3;
int targetX = (int) (center.getX() + Math.cos(angle) * distance);
int targetZ = (int) (center.getZ() + Math.sin(angle) * distance);
// Find ground level using heightmap
int targetY = level.getHeight(
Heightmap.Types.MOTION_BLOCKING_NO_LEAVES,
targetX,
targetZ
);
BlockPos candidate = new BlockPos(targetX, targetY, targetZ);
// Check if position is safe
if (isSafeForTeleport(level, candidate)) {
return candidate;
}
}
return null;
}
/**
* Find a safe position with a fallback if none found.
*
* @param level The server level
* @param center The center position to search from
* @param radius Maximum distance from center
* @param random Random source
* @return Safe block position, or fallback position if none found
*/
public static BlockPos findSafePositionOrFallback(
ServerLevel level,
BlockPos center,
int radius,
RandomSource random
) {
BlockPos safe = findSafePosition(level, center, radius, random);
if (safe != null) {
return safe;
}
// Fallback: offset from center
return center.offset(radius, 0, 0);
}
/**
* Check if a position is safe for teleporting an entity.
*
* @param level The level to check in
* @param pos The position to check (feet level)
* @return true if position is safe for teleportation
*/
public static boolean isSafeForTeleport(Level level, BlockPos pos) {
BlockState feetBlock = level.getBlockState(pos);
BlockState headBlock = level.getBlockState(pos.above());
BlockState groundBlock = level.getBlockState(pos.below());
// Ground must be solid
if (!groundBlock.isSolid()) {
return false;
}
// Feet and head must be passable (not solid)
if (feetBlock.isSolid() || headBlock.isSolid()) {
return false;
}
// Check for dangerous blocks
var feetFluid = feetBlock.getFluidState();
if (!feetFluid.isEmpty()) {
return false; // Water or lava
}
// Check for fire, cactus, etc.
if (
feetBlock.getBlock() instanceof
net.minecraft.world.level.block.FireBlock ||
feetBlock.getBlock() instanceof
net.minecraft.world.level.block.CactusBlock ||
feetBlock.getBlock() instanceof
net.minecraft.world.level.block.SweetBerryBushBlock ||
feetBlock.getBlock() instanceof
net.minecraft.world.level.block.MagmaBlock
) {
return false;
}
return true;
}
// ==================== COMMON PRECONDITIONS ====================
/**
* Check common preconditions for kidnapper goals that require an active kidnapper.
* Checks: not tied up.
*
* @param kidnapper The kidnapper entity
* @return true if the kidnapper can act
*/
public static boolean canKidnapperAct(EntityKidnapper kidnapper) {
return !kidnapper.isTiedUp();
}
/**
* Check common preconditions for kidnapper goals that require no captive.
* Checks: not tied up, no captive, no target.
*
* @param kidnapper The kidnapper entity
* @return true if the kidnapper can search for targets
*/
public static boolean canKidnapperSearch(EntityKidnapper kidnapper) {
if (kidnapper.isTiedUp()) return false;
if (kidnapper.hasCaptives()) return false;
if (kidnapper.getTarget() != null) return false;
return true;
}
/**
* Check common preconditions for kidnapper goals that handle captives.
* Checks: not tied up, has captive, not in get-out state.
*
* @param kidnapper The kidnapper entity
* @return true if the kidnapper can handle their captive
*/
public static boolean canKidnapperHandleCaptive(EntityKidnapper kidnapper) {
if (kidnapper.isTiedUp()) return false;
if (!kidnapper.hasCaptives()) return false;
if (kidnapper.isGetOutState()) return false;
return true;
}
/**
* Check common preconditions for kidnapper goals during captive transport.
* Checks: not tied up, has captive, not for sale, not waiting for job.
*
* @param kidnapper The kidnapper entity
* @return true if the kidnapper can transport their captive
*/
public static boolean canKidnapperTransport(EntityKidnapper kidnapper) {
if (!canKidnapperHandleCaptive(kidnapper)) return false;
var captive = kidnapper.getCaptive();
if (captive != null && captive.isForSell()) return false;
if (kidnapper.isWaitingForJobToBeCompleted()) return false;
return true;
}
// ==================== CELL NAVIGATION ====================
/**
* Get the best position to deliver/extract a prisoner at a cell.
* Prefers DELIVERY marker, falls back to spawn point.
*
* <p>Consolidated from KidnapperBringToCellGoal, KidnapperWalkPrisonerGoal,
* MaidExtractPrisonerGoal, MaidReturnPrisonerGoal.</p>
*
* @param cell The cell data
* @param level The world level
* @return Best standing position for the cell
*/
public static BlockPos getDeliveryOrSpawnPoint(
com.tiedup.remake.cells.CellDataV2 cell,
Level level
) {
BlockPos deliveryPoint = cell.getDeliveryPoint();
if (deliveryPoint != null) {
return findStandablePosition(deliveryPoint, level);
}
return cell.getSpawnPoint() != null
? cell.getSpawnPoint()
: cell.getCorePos().above();
}
/**
* Find a standable position near a marker.
* Handles both floor-placed markers (stand above) and air-placed markers (stand at marker).
*
* @param marker The marker position
* @param level The world level
* @return A valid standing position
*/
public static BlockPos findStandablePosition(BlockPos marker, Level level) {
// 1. Check if marker itself is standable (air/passable block, solid below)
if (isStandable(level, marker)) {
return marker;
}
// 2. Check above - classic case where marker is the floor block
if (isStandable(level, marker.above())) {
return marker.above();
}
// 3. Search below - case where marker is floating/placed in air above the floor
for (int y = 1; y <= 5; y++) {
BlockPos below = marker.below(y);
if (isStandable(level, below)) {
return below;
}
}
// Fallback: above the marker
return marker.above();
}
/**
* Check if a position is standable (entity can stand there).
* More permissive than isSafeForTeleport - uses blocksMotion() instead of isSolid().
*
* @param level The world level
* @param pos The position to check (feet level)
* @return true if an entity can stand at this position
*/
public static boolean isStandable(Level level, BlockPos pos) {
net.minecraft.world.level.block.state.BlockState below =
level.getBlockState(pos.below());
net.minecraft.world.level.block.state.BlockState atPos =
level.getBlockState(pos);
net.minecraft.world.level.block.state.BlockState above =
level.getBlockState(pos.above());
return (
below.isSolid() && !atPos.blocksMotion() && !above.blocksMotion()
);
}
}

View File

@@ -0,0 +1,350 @@
package com.tiedup.remake.util;
import com.tiedup.remake.dialogue.GagTalkManager;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.entities.EntityDamsel;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.state.PlayerBindState;
import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.Component;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
/**
* Centralized message dispatcher with earplug awareness.
*
* <p>This utility ensures that all player messages respect the earplug system.
* Players with earplugs equipped will not receive messages sent through this dispatcher
* (unless using the "system" variants which bypass the check).
*
* <p>Use cases:
* <ul>
* <li>{@link #sendTo} - Direct message to a player (earplug-aware)</li>
* <li>{@link #sendFrom} - Message from one player to another (earplug-aware)</li>
* <li>{@link #broadcastToAll} - Broadcast to all players (earplug-aware per player)</li>
* <li>{@link #sendActionBar} - Action bar message (earplug-aware)</li>
* <li>{@link #sendSystemMessage} - Critical system message (bypasses earplugs)</li>
* </ul>
*/
public final class MessageDispatcher {
private MessageDispatcher() {
// Utility class - no instantiation
}
// ========================================
// EARPLUG-AWARE METHODS
// ========================================
/**
* Send a message to a player (respects earplugs).
*
* <p>If the target player has earplugs equipped, the message will NOT be sent.
*
* @param target The player to send the message to
* @param message The message component to send
* @return true if the message was sent, false if blocked by earplugs
*/
public static boolean sendTo(Player target, Component message) {
if (target == null || message == null) {
return false;
}
if (hasEarplugs(target)) {
return false; // Blocked by earplugs
}
target.sendSystemMessage(message);
return true;
}
/**
* Send a message from one player to another (respects receiver's earplugs).
*
* <p>If the receiver has earplugs equipped, the message will NOT be sent.
* The sender parameter is currently unused but available for future
* features like sender notification when message is blocked.
*
* @param sender The player sending the message (for future feedback features)
* @param receiver The player receiving the message
* @param message The message component to send
* @return true if the message was sent, false if blocked by earplugs
*/
public static boolean sendFrom(
Player sender,
Player receiver,
Component message
) {
if (receiver == null || message == null) {
return false;
}
if (hasEarplugs(receiver)) {
// Future: Could notify sender that message was blocked
return false;
}
receiver.sendSystemMessage(message);
return true;
}
/**
* Broadcast a message to all players on the server (respects individual earplugs).
*
* <p>Each player's earplug state is checked individually. Players with earplugs
* will NOT receive the message.
*
* @param server The Minecraft server
* @param message The message component to broadcast
*/
public static void broadcastToAll(
MinecraftServer server,
Component message
) {
if (server == null || message == null) {
return;
}
for (ServerPlayer player : server.getPlayerList().getPlayers()) {
sendTo(player, message);
}
}
/**
* Send a message to the action bar (respects earplugs).
*
* <p>Action bar messages appear above the hotbar and fade after a few seconds.
* If the target has earplugs, the message will NOT be shown.
*
* @param target The player to send the action bar message to
* @param message The message component to display
* @return true if the message was sent, false if blocked by earplugs
*/
public static boolean sendActionBar(Player target, Component message) {
if (target == null || message == null) {
return false;
}
if (hasEarplugs(target)) {
return false;
}
target.displayClientMessage(message, true);
return true;
}
/**
* Send a chat message (not action bar) that respects earplugs.
*
* <p>Chat messages appear in the chat window and persist in chat history.
*
* @param target The player to send the chat message to
* @param message The message component to display
* @return true if the message was sent, false if blocked by earplugs
*/
public static boolean sendChat(Player target, Component message) {
if (target == null || message == null) {
return false;
}
if (hasEarplugs(target)) {
return false;
}
target.displayClientMessage(message, false);
return true;
}
// ========================================
// BYPASS METHODS (ignore earplugs)
// ========================================
/**
* Send a critical system message that IGNORES earplugs.
*
* <p>Use this for important system notifications that the player MUST see,
* such as teleport warnings, server announcements, or error messages.
*
* @param target The player to send the message to
* @param message The message component to send
*/
public static void sendSystemMessage(Player target, Component message) {
if (target == null || message == null) {
return;
}
target.sendSystemMessage(message);
}
/**
* Send a critical action bar message that IGNORES earplugs.
*
* @param target The player to send the action bar message to
* @param message The message component to display
*/
public static void sendSystemActionBar(Player target, Component message) {
if (target == null || message == null) {
return;
}
target.displayClientMessage(message, true);
}
// ========================================
// INTERNAL HELPERS
// ========================================
/**
* Check if a player has earplugs equipped.
*
* @param player The player to check
* @return true if the player has earplugs, false otherwise
*/
private static boolean hasEarplugs(Player player) {
if (player == null) {
return false;
}
PlayerBindState state = PlayerBindState.getInstance(player);
return state != null && state.hasEarplugs();
}
// ========================================
// NPC DIALOGUE METHODS
// ========================================
/**
* Send a dialogue message from an entity to a player.
* Formatted as: "<EntityName> message"
*
* @param entity The entity speaking
* @param player The player receiving the message
* @param message The dialogue text
* @return true if message was sent, false if blocked by earplugs
*/
public static boolean talkTo(
LivingEntity entity,
Player player,
String message
) {
if (entity == null || player == null || message == null) {
return false;
}
if (entity.level().isClientSide()) {
return false;
}
if (hasEarplugs(player)) {
return false;
}
// Apply gag talk if entity is a gagged NPC
String finalMessage = message;
if (entity instanceof com.tiedup.remake.entities.AbstractTiedUpNpc npc && npc.isGagged()) {
ItemStack gag = npc.getEquipment(BodyRegionV2.MOUTH);
IBondageState state = KidnappedHelper.getKidnappedState(npc);
if (state != null && !gag.isEmpty()) {
Component gagged = GagTalkManager.processGagMessage(
state,
gag,
message
);
finalMessage = gagged.getString();
}
}
Component formattedMessage = Component.literal("")
.append(Component.literal("<").withStyle(ChatFormatting.WHITE))
.append(entity.getDisplayName().copy())
.append(Component.literal("> ").withStyle(ChatFormatting.WHITE))
.append(
Component.literal(finalMessage).withStyle(ChatFormatting.GRAY)
);
player.displayClientMessage(formattedMessage, false);
return true;
}
/**
* Send an action message from an entity to a player.
* Formatted as: "* EntityName action *"
*
* @param entity The entity performing the action
* @param player The player receiving the message
* @param action The action text
* @return true if message was sent, false if blocked by earplugs
*/
public static boolean actionTo(
LivingEntity entity,
Player player,
String action
) {
if (entity == null || player == null || action == null) {
return false;
}
if (entity.level().isClientSide()) {
return false;
}
if (hasEarplugs(player)) {
return false;
}
Component formattedMessage = Component.literal("")
.append(Component.literal("* ").withStyle(ChatFormatting.GRAY))
.append(entity.getDisplayName().copy())
.append(
Component.literal(" " + action + " *").withStyle(
ChatFormatting.GRAY
)
);
player.displayClientMessage(formattedMessage, false);
return true;
}
/**
* Send a dialogue message to all players within a radius.
*
* @param entity The entity speaking
* @param message The dialogue text
* @param radius The broadcast radius in blocks
*/
public static void talkToNearby(
LivingEntity entity,
String message,
double radius
) {
if (entity == null || message == null) {
return;
}
var nearbyPlayers = entity
.level()
.getEntitiesOfClass(
Player.class,
entity.getBoundingBox().inflate(radius)
);
for (Player player : nearbyPlayers) {
talkTo(entity, player, message);
}
}
/**
* Send an action message to all players within a radius.
*
* @param entity The entity performing the action
* @param action The action text
* @param radius The broadcast radius in blocks
*/
public static void actionToNearby(
LivingEntity entity,
String action,
double radius
) {
if (entity == null || action == null) {
return;
}
var nearbyPlayers = entity
.level()
.getEntitiesOfClass(
Player.class,
entity.getBoundingBox().inflate(radius)
);
for (Player player : nearbyPlayers) {
actionTo(entity, player, action);
}
}
}

View File

@@ -0,0 +1,305 @@
package com.tiedup.remake.util;
import com.tiedup.remake.core.TiedUpMod;
import net.minecraft.world.level.GameRules;
/**
* Phase 6: Custom GameRules for TiedUp mod.
*
* Manages configurable values for gameplay mechanics.
* GameRules can be changed via /gamerule command in-game.
*
* Based on original mod's configuration system.
*/
public class ModGameRules {
// =====================================================
// RESTRAINT MECHANICS
// =====================================================
/** Time (in seconds) required to tie up a player. Default: 5 */
public static GameRules.Key<GameRules.IntegerValue> TYING_PLAYER_TIME;
/** Time (in seconds) required to untie a tied player. Default: 10 */
public static GameRules.Key<GameRules.IntegerValue> UNTYING_PLAYER_TIME;
/** Whether gagged players can be heard within a short range. Default: true */
public static GameRules.Key<GameRules.BooleanValue> GAG_TALK_PROXIMITY;
/** Resistance added by a padlock when locked on an item. Default: 250 */
public static GameRules.Key<GameRules.IntegerValue> PADLOCK_RESISTANCE;
// =====================================================
// STRUGGLE SYSTEM
// =====================================================
/** Enable/disable struggle system. Default: true */
public static GameRules.Key<GameRules.BooleanValue> STRUGGLE;
/** Success probability for struggling (0-100). Default: 40% */
public static GameRules.Key<GameRules.IntegerValue> PROBABILITY_STRUGGLE;
/** Minimum resistance decrease on successful struggle. Default: 1 */
public static GameRules.Key<GameRules.IntegerValue> STRUGGLE_MIN_DECREASE;
/** Maximum resistance decrease on successful struggle. Default: 10 */
public static GameRules.Key<GameRules.IntegerValue> STRUGGLE_MAX_DECREASE;
/** Cooldown between struggle attempts (ticks, 20 = 1s). Default: 80 (4s) */
public static GameRules.Key<GameRules.IntegerValue> STRUGGLE_TIMER;
/** Ticks per 1 resistance point in continuous struggle. Default: 20 (1/s) */
public static GameRules.Key<GameRules.IntegerValue> STRUGGLE_CONTINUOUS_RATE;
/** Probability of random shock during collar struggle (0-100). Default: 20% */
public static GameRules.Key<GameRules.IntegerValue> STRUGGLE_COLLAR_RANDOM_SHOCK;
// BUG-003: RESISTANCE_ROPE, RESISTANCE_GAG, RESISTANCE_BLINDFOLD, RESISTANCE_COLLAR
// removed. Bind resistance is now read from ModConfig via SettingsAccessor.
// =====================================================
// NPC STRUGGLE
// =====================================================
/** Enable/disable NPC struggle to escape restraints. Default: true */
public static GameRules.Key<GameRules.BooleanValue> NPC_STRUGGLE_ENABLED;
/** Base interval (ticks) between NPC struggle attempts. Default: 6000 (5 min) */
public static GameRules.Key<GameRules.IntegerValue> NPC_STRUGGLE_INTERVAL;
// =====================================================
// COLLAR & SHOCKER
// =====================================================
/** Enable/disable enslavement system. Default: true */
public static GameRules.Key<GameRules.BooleanValue> ENSLAVEMENT_ENABLED;
/** Base radius for shocker controller. Default: 50 blocks */
public static GameRules.Key<GameRules.IntegerValue> SHOCKER_CONTROLLER_BASE_RADIUS;
// =====================================================
// NPC SPAWNING
// =====================================================
/** Enable/disable damsel entity spawning. Default: true */
public static GameRules.Key<GameRules.BooleanValue> DAMSELS_SPAWN;
/** Enable/disable kidnapper entity spawning (all variants). Default: true */
public static GameRules.Key<GameRules.BooleanValue> KIDNAPPERS_SPAWN;
/** Damsel spawn rate (0-100). Default: 100 */
public static GameRules.Key<GameRules.IntegerValue> DAMSEL_SPAWN_RATE;
/** Kidnapper spawn rate (0-100). Default: 100 */
public static GameRules.Key<GameRules.IntegerValue> KIDNAPPER_SPAWN_RATE;
/** Kidnapper Archer spawn rate (0-100). Default: 100 */
public static GameRules.Key<GameRules.IntegerValue> KIDNAPPER_ARCHER_SPAWN_RATE;
/** Kidnapper Elite spawn rate (0-100). Default: 100 */
public static GameRules.Key<GameRules.IntegerValue> KIDNAPPER_ELITE_SPAWN_RATE;
/** Kidnapper Merchant spawn rate (0-100). Default: 100 */
public static GameRules.Key<GameRules.IntegerValue> KIDNAPPER_MERCHANT_SPAWN_RATE;
/** Master spawn rate (0-100). Default: 100 */
public static GameRules.Key<GameRules.IntegerValue> MASTER_SPAWN_RATE;
/** Spawn gender mode: 0=BOTH, 1=FEMALE_ONLY, 2=MALE_ONLY. Default: 0 */
public static GameRules.Key<GameRules.IntegerValue> SPAWN_GENDER_MODE;
// =====================================================
// BOUNTY SYSTEM
// =====================================================
/** Maximum bounties per player. Default: 5 */
public static GameRules.Key<GameRules.IntegerValue> MAX_BOUNTIES;
/** Duration of bounties in seconds. Default: 14400 (4 hours) */
public static GameRules.Key<GameRules.IntegerValue> BOUNTY_DURATION;
/** Radius for bounty delivery detection. Default: 5 blocks */
public static GameRules.Key<GameRules.IntegerValue> BOUNTY_DELIVERY_RADIUS;
// =====================================================
// MISCELLANEOUS
// =====================================================
/** Kidnap bomb explosion radius. Default: 5 blocks */
public static GameRules.Key<GameRules.IntegerValue> KIDNAP_BOMB_RADIUS;
/**
* Register all custom GameRules.
* Called during mod initialization.
*/
public static void register() {
TiedUpMod.LOGGER.info("Registering TiedUp GameRules...");
// Phase 6: Tying/Untying times
// NOTE: Using hardcoded defaults because ModConfig isn't loaded yet during mod construction
GAG_TALK_PROXIMITY = GameRules.register(
"gagTalkProximity",
GameRules.Category.CHAT,
GameRules.BooleanValue.create(true)
);
TYING_PLAYER_TIME = GameRules.register(
"tyingPlayerTime",
GameRules.Category.MISC,
GameRules.IntegerValue.create(5)
);
UNTYING_PLAYER_TIME = GameRules.register(
"untyingPlayerTime",
GameRules.Category.MISC,
GameRules.IntegerValue.create(10)
);
// Phase 7: Struggle/Resistance system
STRUGGLE = GameRules.register(
"struggle",
GameRules.Category.MISC,
GameRules.BooleanValue.create(true)
);
PROBABILITY_STRUGGLE = GameRules.register(
"probabilityStruggle",
GameRules.Category.MISC,
GameRules.IntegerValue.create(40)
);
STRUGGLE_MIN_DECREASE = GameRules.register(
"struggleMinDecrease",
GameRules.Category.MISC,
GameRules.IntegerValue.create(1)
);
STRUGGLE_MAX_DECREASE = GameRules.register(
"struggleMaxDecrease",
GameRules.Category.MISC,
GameRules.IntegerValue.create(10)
);
STRUGGLE_TIMER = GameRules.register(
"struggleTimer",
GameRules.Category.MISC,
GameRules.IntegerValue.create(80)
);
// BUG-003: Removed resistanceRope/Gag/Blindfold/Collar GameRule registrations.
// Bind resistance is now managed by ModConfig via SettingsAccessor.
// Phase 13: Collar/Shocker features
STRUGGLE_COLLAR_RANDOM_SHOCK = GameRules.register(
"struggleCollarRandomShock",
GameRules.Category.MISC,
GameRules.IntegerValue.create(20)
);
SHOCKER_CONTROLLER_BASE_RADIUS = GameRules.register(
"shockerControllerBaseRadius",
GameRules.Category.MISC,
GameRules.IntegerValue.create(50)
);
// Phase 8: Enslavement
ENSLAVEMENT_ENABLED = GameRules.register(
"enslavementEnabled",
GameRules.Category.MISC,
GameRules.BooleanValue.create(true)
);
// Phase 14.2: Damsel spawning
DAMSELS_SPAWN = GameRules.register(
"damselsSpawn",
GameRules.Category.SPAWNING,
GameRules.BooleanValue.create(true)
);
// Kidnapper spawning (includes Elite, Archer, Merchant)
KIDNAPPERS_SPAWN = GameRules.register(
"kidnappersSpawn",
GameRules.Category.SPAWNING,
GameRules.BooleanValue.create(true)
);
// NPC spawn rates (0-100 percentage)
// BUG-001 FIX: Defaults now match ModConfig values instead of all being 100
DAMSEL_SPAWN_RATE = GameRules.register(
"damselSpawnRate",
GameRules.Category.SPAWNING,
GameRules.IntegerValue.create(60)
);
KIDNAPPER_SPAWN_RATE = GameRules.register(
"kidnapperSpawnRate",
GameRules.Category.SPAWNING,
GameRules.IntegerValue.create(70)
);
KIDNAPPER_ARCHER_SPAWN_RATE = GameRules.register(
"kidnapperArcherSpawnRate",
GameRules.Category.SPAWNING,
GameRules.IntegerValue.create(70)
);
KIDNAPPER_ELITE_SPAWN_RATE = GameRules.register(
"kidnapperEliteSpawnRate",
GameRules.Category.SPAWNING,
GameRules.IntegerValue.create(70)
);
KIDNAPPER_MERCHANT_SPAWN_RATE = GameRules.register(
"kidnapperMerchantSpawnRate",
GameRules.Category.SPAWNING,
GameRules.IntegerValue.create(70)
);
MASTER_SPAWN_RATE = GameRules.register(
"masterSpawnRate",
GameRules.Category.SPAWNING,
GameRules.IntegerValue.create(100)
);
// Phase 16: Kidnap bomb radius
KIDNAP_BOMB_RADIUS = GameRules.register(
"kidnapBombRadius",
GameRules.Category.MISC,
GameRules.IntegerValue.create(5)
);
SPAWN_GENDER_MODE = GameRules.register(
"spawnGenderMode",
GameRules.Category.SPAWNING,
GameRules.IntegerValue.create(0)
);
// Phase 17: Bounty system
MAX_BOUNTIES = GameRules.register(
"maxBounties",
GameRules.Category.MISC,
GameRules.IntegerValue.create(5)
);
BOUNTY_DURATION = GameRules.register(
"bountyDuration",
GameRules.Category.MISC,
GameRules.IntegerValue.create(14400)
);
BOUNTY_DELIVERY_RADIUS = GameRules.register(
"bountyDeliveryRadius",
GameRules.Category.MISC,
GameRules.IntegerValue.create(5)
);
PADLOCK_RESISTANCE = GameRules.register(
"padlockResistance",
GameRules.Category.MISC,
GameRules.IntegerValue.create(250)
);
STRUGGLE_CONTINUOUS_RATE = GameRules.register(
"struggleContinuousRate",
GameRules.Category.MISC,
GameRules.IntegerValue.create(20)
);
NPC_STRUGGLE_ENABLED = GameRules.register(
"npcStruggleEnabled",
GameRules.Category.MISC,
GameRules.BooleanValue.create(true)
);
NPC_STRUGGLE_INTERVAL = GameRules.register(
"npcStruggleInterval",
GameRules.Category.MISC,
GameRules.IntegerValue.create(6000)
);
TiedUpMod.LOGGER.info("Registered {} TiedUp GameRules", 28);
}
// All getter methods removed in H4-F.
// Call sites now use SettingsAccessor which reads GameRules fields directly.
// BUG-003: getResistance() was removed earlier. Use SettingsAccessor.getBindResistance().
// Chloroform fake GameRule (just read ModConfig) replaced by SettingsAccessor.getChloroformDuration().
}

View File

@@ -0,0 +1,161 @@
package com.tiedup.remake.util;
import com.tiedup.remake.core.TiedUpMod;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import net.minecraft.util.RandomSource;
/**
* Random name generator for NPCs (Damsels, Kidnappers).
*
* Phase 14.3.5: Name system
*
* Loads names from assets/tiedup/names/female_names.txt
* Contains 5001 female first names from the original mod.
*/
public class NameGenerator {
private static final RandomSource RANDOM = RandomSource.create();
/**
* Female names loaded from file.
* Used for both Damsels and Kidnappers (all NPCs are female).
*/
private static List<String> NAMES = null;
/**
* Fallback names if file loading fails.
*/
private static final List<String> FALLBACK_NAMES = List.of(
"Alice",
"Emma",
"Sophia",
"Olivia",
"Isabella",
"Mia",
"Charlotte",
"Amelia",
"Harper",
"Evelyn"
);
/**
* Load names from the resource file.
* Called lazily on first name request.
* Synchronized to prevent race conditions during parallel entity spawning.
*/
private static synchronized void loadNames() {
if (NAMES != null) return;
NAMES = new ArrayList<>();
String resourcePath = "/assets/tiedup/names/female_names.txt";
try (
InputStream is = NameGenerator.class.getResourceAsStream(
resourcePath
)
) {
if (is == null) {
TiedUpMod.LOGGER.error(
"[NameGenerator] Could not find resource: {}",
resourcePath
);
NAMES = new ArrayList<>(FALLBACK_NAMES);
return;
}
try (
BufferedReader reader = new BufferedReader(
new InputStreamReader(is, StandardCharsets.UTF_8)
)
) {
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (!line.isEmpty()) {
NAMES.add(line);
}
}
}
TiedUpMod.LOGGER.info(
"[NameGenerator] Loaded {} names from file",
NAMES.size()
);
} catch (IOException e) {
TiedUpMod.LOGGER.error(
"[NameGenerator] Failed to load names: {}",
e.getMessage()
);
NAMES = new ArrayList<>(FALLBACK_NAMES);
}
if (NAMES.isEmpty()) {
NAMES = new ArrayList<>(FALLBACK_NAMES);
}
}
/**
* Get a random name from the loaded list.
*
* @return Random name
*/
public static String getRandomName() {
loadNames();
return NAMES.get(RANDOM.nextInt(NAMES.size()));
}
/**
* Get a random damsel name.
*
* @return Random damsel name
*/
public static String getRandomDamselName() {
return getRandomName();
}
/**
* Get a random kidnapper name.
* Same as damsel - all NPCs are female.
*
* @return Random kidnapper name
*/
public static String getRandomKidnapperName() {
return getRandomName();
}
/**
* Get a random maid name.
* Same as damsel - all NPCs are female.
*
* @return Random maid name
*/
public static String getRandomMaidName() {
return getRandomName();
}
/**
* Get a random slave trader name.
* Same as damsel - all NPCs are female.
*
* @return Random trader name
*/
public static String getRandomTraderName() {
return getRandomName();
}
/**
* Get the total number of available names.
*
* @return Number of names loaded
*/
public static int getNameCount() {
loadNames();
return NAMES.size();
}
}

View File

@@ -0,0 +1,18 @@
package com.tiedup.remake.util;
import net.minecraft.resources.ResourceLocation;
/**
* Proxy class that directly uses Patchouli API.
* This class is only loaded if Patchouli is present.
*/
public class PatchouliProxy {
/**
* Opens a Patchouli book GUI.
* This method uses Patchouli API directly (no reflection).
*/
public static void openBook(ResourceLocation bookId) {
vazkii.patchouli.api.PatchouliAPI.get().openBookGUI(bookId);
}
}

View File

@@ -0,0 +1,507 @@
package com.tiedup.remake.util;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.Set;
/**
* Phonetic transformation system for gagged speech.
* Maps original phonemes to muffled equivalents based on gag material.
*/
public class PhoneticMapper {
private static final Random RANDOM = new Random();
// Phonetic categories
private static final Set<Character> VOWELS = Set.of(
'a',
'e',
'i',
'o',
'u',
'y'
);
private static final Set<Character> PLOSIVES = Set.of(
'b',
'd',
'g',
'k',
'p',
't'
);
private static final Set<Character> NASALS = Set.of('m', 'n');
private static final Set<Character> FRICATIVES = Set.of(
'f',
'h',
's',
'v',
'z'
);
private static final Set<Character> LIQUIDS = Set.of('l', 'r');
// Material-specific phoneme mappings
private static final Map<
GagMaterial,
Map<Character, String[]>
> CONSONANT_MAPS = new EnumMap<>(GagMaterial.class);
private static final Map<GagMaterial, Map<Character, String[]>> VOWEL_MAPS =
new EnumMap<>(GagMaterial.class);
static {
initializeClothMappings();
initializeBallMappings();
initializeTapeMappings();
initializeStuffedMappings();
initializePanelMappings();
initializeLatexMappings();
initializeRingMappings();
initializeBiteMappings();
initializeSpongeMappings();
initializeBaguetteMappings();
}
private static void initializeClothMappings() {
Map<Character, String[]> consonants = new HashMap<>();
consonants.put('b', new String[] { "m", "mph" });
consonants.put('c', new String[] { "h", "kh" });
consonants.put('d', new String[] { "n", "nd" });
consonants.put('f', new String[] { "f", "ph" });
consonants.put('g', new String[] { "ng", "gh" });
consonants.put('h', new String[] { "h", "hh" });
consonants.put('j', new String[] { "zh", "jh" });
consonants.put('k', new String[] { "kh", "gh" });
consonants.put('l', new String[] { "l", "hl" });
consonants.put('m', new String[] { "m", "mm" });
consonants.put('n', new String[] { "n", "nn" });
consonants.put('p', new String[] { "mph", "m" });
consonants.put('q', new String[] { "kh", "gh" });
consonants.put('r', new String[] { "r", "hr" });
consonants.put('s', new String[] { "s", "sh" });
consonants.put('t', new String[] { "th", "n" });
consonants.put('v', new String[] { "f", "vh" });
consonants.put('w', new String[] { "wh", "u" });
consonants.put('x', new String[] { "ks", "kh" });
consonants.put('z', new String[] { "z", "s" });
CONSONANT_MAPS.put(GagMaterial.CLOTH, consonants);
Map<Character, String[]> vowels = new HashMap<>();
vowels.put('a', new String[] { "ah", "a" });
vowels.put('e', new String[] { "eh", "e" });
vowels.put('i', new String[] { "ih", "e" });
vowels.put('o', new String[] { "oh", "o" });
vowels.put('u', new String[] { "uh", "u" });
vowels.put('y', new String[] { "ih", "e" });
VOWEL_MAPS.put(GagMaterial.CLOTH, vowels);
}
private static void initializeBallMappings() {
Map<Character, String[]> consonants = new HashMap<>();
// Ball gag forces mouth open around ball - tongue and lips blocked
consonants.put('b', new String[] { "m", "mm" });
consonants.put('c', new String[] { "g", "kh" });
consonants.put('d', new String[] { "n", "nn" });
consonants.put('f', new String[] { "h", "hh" });
consonants.put('g', new String[] { "ng", "g" });
consonants.put('h', new String[] { "h", "hh" });
consonants.put('j', new String[] { "g", "ng" });
consonants.put('k', new String[] { "g", "gh" });
consonants.put('l', new String[] { "u", "l" });
consonants.put('m', new String[] { "m", "mm" });
consonants.put('n', new String[] { "n", "nn" });
consonants.put('p', new String[] { "m", "mm" });
consonants.put('q', new String[] { "g", "gh" });
consonants.put('r', new String[] { "u", "r" });
consonants.put('s', new String[] { "h", "s" });
consonants.put('t', new String[] { "n", "nn" });
consonants.put('v', new String[] { "h", "f" });
consonants.put('w', new String[] { "u", "w" });
consonants.put('x', new String[] { "g", "ks" });
consonants.put('z', new String[] { "s", "z" });
CONSONANT_MAPS.put(GagMaterial.BALL, consonants);
Map<Character, String[]> vowels = new HashMap<>();
// Ball forces all vowels toward "oo/uu" (rounded)
vowels.put('a', new String[] { "a", "o" });
vowels.put('e', new String[] { "u", "o" });
vowels.put('i', new String[] { "u", "i" });
vowels.put('o', new String[] { "o", "oo" });
vowels.put('u', new String[] { "u", "uu" });
vowels.put('y', new String[] { "u", "i" });
VOWEL_MAPS.put(GagMaterial.BALL, vowels);
}
private static void initializeTapeMappings() {
Map<Character, String[]> consonants = new HashMap<>();
// Tape seals mouth almost completely - only nasal sounds
consonants.put('b', new String[] { "m", "mm" });
consonants.put('c', new String[] { "n", "m" });
consonants.put('d', new String[] { "n", "nn" });
consonants.put('f', new String[] { "m", "hm" });
consonants.put('g', new String[] { "n", "ng" });
consonants.put('h', new String[] { "m", "hm" });
consonants.put('j', new String[] { "n", "m" });
consonants.put('k', new String[] { "n", "m" });
consonants.put('l', new String[] { "n", "m" });
consonants.put('m', new String[] { "m", "mm" });
consonants.put('n', new String[] { "n", "nn" });
consonants.put('p', new String[] { "m", "mm" });
consonants.put('q', new String[] { "n", "m" });
consonants.put('r', new String[] { "n", "m" });
consonants.put('s', new String[] { "m", "s" });
consonants.put('t', new String[] { "n", "nn" });
consonants.put('v', new String[] { "m", "f" });
consonants.put('w', new String[] { "m", "u" });
consonants.put('x', new String[] { "n", "m" });
consonants.put('z', new String[] { "n", "m" });
CONSONANT_MAPS.put(GagMaterial.TAPE, consonants);
Map<Character, String[]> vowels = new HashMap<>();
// Tape muffles all vowels to near silence
vowels.put('a', new String[] { "m", "mm" });
vowels.put('e', new String[] { "n", "m" });
vowels.put('i', new String[] { "n", "m" });
vowels.put('o', new String[] { "m", "mm" });
vowels.put('u', new String[] { "m", "u" });
vowels.put('y', new String[] { "n", "m" });
VOWEL_MAPS.put(GagMaterial.TAPE, vowels);
}
private static void initializeStuffedMappings() {
Map<Character, String[]> consonants = new HashMap<>();
// Stuffed gag - nearly silent
for (char c = 'a'; c <= 'z'; c++) {
if (!VOWELS.contains(c)) {
consonants.put(c, new String[] { "mm", "m", "" });
}
}
CONSONANT_MAPS.put(GagMaterial.STUFFED, consonants);
Map<Character, String[]> vowels = new HashMap<>();
for (char c : VOWELS) {
vowels.put(c, new String[] { "mm", "m", "" });
}
VOWEL_MAPS.put(GagMaterial.STUFFED, vowels);
}
private static void initializePanelMappings() {
// Panel gag - similar to tape but slightly more sound
Map<Character, String[]> consonants = new HashMap<>();
consonants.put('b', new String[] { "m", "mph" });
consonants.put('c', new String[] { "n", "m" });
consonants.put('d', new String[] { "n", "nd" });
consonants.put('f', new String[] { "m", "f" });
consonants.put('g', new String[] { "n", "ng" });
consonants.put('h', new String[] { "hm", "m" });
consonants.put('j', new String[] { "n", "m" });
consonants.put('k', new String[] { "n", "m" });
consonants.put('l', new String[] { "n", "m" });
consonants.put('m', new String[] { "m", "mm" });
consonants.put('n', new String[] { "n", "nn" });
consonants.put('p', new String[] { "m", "mph" });
consonants.put('q', new String[] { "n", "m" });
consonants.put('r', new String[] { "n", "m" });
consonants.put('s', new String[] { "s", "m" });
consonants.put('t', new String[] { "n", "m" });
consonants.put('v', new String[] { "m", "f" });
consonants.put('w', new String[] { "m", "u" });
consonants.put('x', new String[] { "n", "m" });
consonants.put('z', new String[] { "n", "m" });
CONSONANT_MAPS.put(GagMaterial.PANEL, consonants);
Map<Character, String[]> vowels = new HashMap<>();
vowels.put('a', new String[] { "m", "ah" });
vowels.put('e', new String[] { "n", "m" });
vowels.put('i', new String[] { "n", "m" });
vowels.put('o', new String[] { "m", "oh" });
vowels.put('u', new String[] { "m", "u" });
vowels.put('y', new String[] { "n", "m" });
VOWEL_MAPS.put(GagMaterial.PANEL, vowels);
}
private static void initializeLatexMappings() {
Map<Character, String[]> consonants = new HashMap<>();
// Latex - tight seal, rubber sounds
consonants.put('b', new String[] { "m", "mm" });
consonants.put('c', new String[] { "n", "m" });
consonants.put('d', new String[] { "n", "nn" });
consonants.put('f', new String[] { "h", "f" });
consonants.put('g', new String[] { "ng", "m" });
consonants.put('h', new String[] { "h", "hh" });
consonants.put('j', new String[] { "n", "m" });
consonants.put('k', new String[] { "n", "m" });
consonants.put('l', new String[] { "n", "m" });
consonants.put('m', new String[] { "m", "mm" });
consonants.put('n', new String[] { "n", "nn" });
consonants.put('p', new String[] { "m", "mm" });
consonants.put('q', new String[] { "n", "m" });
consonants.put('r', new String[] { "n", "m" });
consonants.put('s', new String[] { "s", "h" });
consonants.put('t', new String[] { "n", "nn" });
consonants.put('v', new String[] { "f", "m" });
consonants.put('w', new String[] { "m", "u" });
consonants.put('x', new String[] { "n", "m" });
consonants.put('z', new String[] { "s", "m" });
CONSONANT_MAPS.put(GagMaterial.LATEX, consonants);
Map<Character, String[]> vowels = new HashMap<>();
vowels.put('a', new String[] { "u", "a" });
vowels.put('e', new String[] { "u", "e" });
vowels.put('i', new String[] { "u", "i" });
vowels.put('o', new String[] { "u", "o" });
vowels.put('u', new String[] { "u", "uu" });
vowels.put('y', new String[] { "u", "i" });
VOWEL_MAPS.put(GagMaterial.LATEX, vowels);
}
private static void initializeRingMappings() {
Map<Character, String[]> consonants = new HashMap<>();
// Ring gag - mouth forced open, tongue partially free
consonants.put('b', new String[] { "b", "bh" });
consonants.put('c', new String[] { "k", "kh" });
consonants.put('d', new String[] { "d", "dh" });
consonants.put('f', new String[] { "f", "fh" });
consonants.put('g', new String[] { "g", "gh" });
consonants.put('h', new String[] { "h", "ah" });
consonants.put('j', new String[] { "j", "zh" });
consonants.put('k', new String[] { "k", "kh" });
consonants.put('l', new String[] { "l", "lh" });
consonants.put('m', new String[] { "m", "mh" });
consonants.put('n', new String[] { "n", "nh" });
consonants.put('p', new String[] { "p", "ph" });
consonants.put('q', new String[] { "k", "kh" });
consonants.put('r', new String[] { "r", "rh" });
consonants.put('s', new String[] { "s", "sh" });
consonants.put('t', new String[] { "t", "th" });
consonants.put('v', new String[] { "v", "vh" });
consonants.put('w', new String[] { "w", "wh" });
consonants.put('x', new String[] { "ks", "kh" });
consonants.put('z', new String[] { "z", "zh" });
CONSONANT_MAPS.put(GagMaterial.RING, consonants);
Map<Character, String[]> vowels = new HashMap<>();
// Ring forces mouth open - vowels become "a/ah"
vowels.put('a', new String[] { "a", "ah" });
vowels.put('e', new String[] { "eh", "a" });
vowels.put('i', new String[] { "ih", "a" });
vowels.put('o', new String[] { "oh", "a" });
vowels.put('u', new String[] { "uh", "a" });
vowels.put('y', new String[] { "ih", "a" });
VOWEL_MAPS.put(GagMaterial.RING, vowels);
}
private static void initializeBiteMappings() {
Map<Character, String[]> consonants = new HashMap<>();
// Bite gag - teeth clenched on bar
consonants.put('b', new String[] { "bh", "ph" });
consonants.put('c', new String[] { "kh", "gh" });
consonants.put('d', new String[] { "dh", "th" });
consonants.put('f', new String[] { "fh", "f" });
consonants.put('g', new String[] { "gh", "ng" });
consonants.put('h', new String[] { "h", "hh" });
consonants.put('j', new String[] { "jh", "zh" });
consonants.put('k', new String[] { "kh", "gh" });
consonants.put('l', new String[] { "lh", "hl" });
consonants.put('m', new String[] { "m", "mh" });
consonants.put('n', new String[] { "n", "nh" });
consonants.put('p', new String[] { "ph", "bh" });
consonants.put('q', new String[] { "kh", "gh" });
consonants.put('r', new String[] { "rh", "hr" });
consonants.put('s', new String[] { "sh", "s" });
consonants.put('t', new String[] { "th", "dh" });
consonants.put('v', new String[] { "vh", "fh" });
consonants.put('w', new String[] { "wh", "uh" });
consonants.put('x', new String[] { "ksh", "kh" });
consonants.put('z', new String[] { "zh", "sh" });
CONSONANT_MAPS.put(GagMaterial.BITE, consonants);
Map<Character, String[]> vowels = new HashMap<>();
vowels.put('a', new String[] { "eh", "ah" });
vowels.put('e', new String[] { "eh", "e" });
vowels.put('i', new String[] { "ih", "eh" });
vowels.put('o', new String[] { "oh", "eh" });
vowels.put('u', new String[] { "uh", "eh" });
vowels.put('y', new String[] { "ih", "eh" });
VOWEL_MAPS.put(GagMaterial.BITE, vowels);
}
private static void initializeSpongeMappings() {
// Sponge - absorbs almost all sound
Map<Character, String[]> consonants = new HashMap<>();
for (char c = 'a'; c <= 'z'; c++) {
if (!VOWELS.contains(c)) {
consonants.put(c, new String[] { "mm", "" });
}
}
CONSONANT_MAPS.put(GagMaterial.SPONGE, consonants);
Map<Character, String[]> vowels = new HashMap<>();
for (char c : VOWELS) {
vowels.put(c, new String[] { "mm", "" });
}
VOWEL_MAPS.put(GagMaterial.SPONGE, vowels);
}
private static void initializeBaguetteMappings() {
Map<Character, String[]> consonants = new HashMap<>();
// Baguette - comedic, food-blocked
consonants.put('b', new String[] { "bm", "mm" });
consonants.put('c', new String[] { "km", "gm" });
consonants.put('d', new String[] { "dm", "nm" });
consonants.put('f', new String[] { "fm", "hm" });
consonants.put('g', new String[] { "gm", "ngm" });
consonants.put('h', new String[] { "hm", "h" });
consonants.put('j', new String[] { "jm", "zhm" });
consonants.put('k', new String[] { "km", "gm" });
consonants.put('l', new String[] { "lm", "mm" });
consonants.put('m', new String[] { "mm", "m" });
consonants.put('n', new String[] { "nm", "n" });
consonants.put('p', new String[] { "pm", "mm" });
consonants.put('q', new String[] { "km", "gm" });
consonants.put('r', new String[] { "rm", "mm" });
consonants.put('s', new String[] { "sm", "shm" });
consonants.put('t', new String[] { "tm", "nm" });
consonants.put('v', new String[] { "vm", "fm" });
consonants.put('w', new String[] { "wm", "um" });
consonants.put('x', new String[] { "ksm", "km" });
consonants.put('z', new String[] { "zm", "sm" });
CONSONANT_MAPS.put(GagMaterial.BAGUETTE, consonants);
Map<Character, String[]> vowels = new HashMap<>();
vowels.put('a', new String[] { "am", "om" });
vowels.put('e', new String[] { "em", "um" });
vowels.put('i', new String[] { "im", "um" });
vowels.put('o', new String[] { "om", "o" });
vowels.put('u', new String[] { "um", "u" });
vowels.put('y', new String[] { "im", "um" });
VOWEL_MAPS.put(GagMaterial.BAGUETTE, vowels);
}
/**
* Map a single phoneme to its muffled equivalent.
*
* @param c The original character
* @param material The gag material
* @param bleedChance Chance (0-1) that the original sound passes through
* @return The muffled phoneme
*/
public static String mapPhoneme(
char c,
GagMaterial material,
float bleedChance
) {
char lower = Character.toLowerCase(c);
// Non-alphabetic characters pass through
if (!Character.isLetter(c)) {
return String.valueOf(c);
}
// Bleed-through check: original sound passes
if (RANDOM.nextFloat() < bleedChance) {
return String.valueOf(c);
}
// Get appropriate map
Map<Character, String[]> map = isVowel(lower)
? VOWEL_MAPS.get(material)
: CONSONANT_MAPS.get(material);
if (map == null) {
return String.valueOf(c);
}
String[] options = map.get(lower);
if (options == null || options.length == 0) {
// Default fallback
return isVowel(lower) ? "mm" : "nn";
}
// Pick a random option
String result = options[RANDOM.nextInt(options.length)];
// Preserve case for first character
if (Character.isUpperCase(c) && !result.isEmpty()) {
return (
Character.toUpperCase(result.charAt(0)) + result.substring(1)
);
}
return result;
}
/**
* Check if a character is a vowel.
*/
public static boolean isVowel(char c) {
return VOWELS.contains(Character.toLowerCase(c));
}
/**
* Check if a character is a plosive consonant.
*/
public static boolean isPlosive(char c) {
return PLOSIVES.contains(Character.toLowerCase(c));
}
/**
* Check if a character is a nasal consonant.
*/
public static boolean isNasal(char c) {
return NASALS.contains(Character.toLowerCase(c));
}
/**
* Check if a character is a fricative consonant.
*/
public static boolean isFricative(char c) {
return FRICATIVES.contains(Character.toLowerCase(c));
}
/**
* Check if a character is a liquid consonant.
*/
public static boolean isLiquid(char c) {
return LIQUIDS.contains(Character.toLowerCase(c));
}
/**
* Check if a character can potentially bleed through for a given material.
* Nasals have higher bleed-through, plosives have lower.
*/
public static float getBleedModifier(char c, GagMaterial material) {
char lower = Character.toLowerCase(c);
// Nasals almost always pass through
if (isNasal(lower)) {
return 2.0f;
}
// Plosives are harder to pronounce with most gags
if (isPlosive(lower)) {
return material == GagMaterial.RING ? 1.5f : 0.3f;
}
// Fricatives depend on whether air can escape
if (isFricative(lower)) {
return (
material == GagMaterial.TAPE ||
material == GagMaterial.STUFFED
)
? 0.1f
: 0.8f;
}
// Liquids depend on tongue freedom
if (isLiquid(lower)) {
return (
material == GagMaterial.RING || material == GagMaterial.BITE
)
? 1.2f
: 0.2f;
}
return 1.0f;
}
}

View File

@@ -0,0 +1,349 @@
package com.tiedup.remake.util;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.items.base.IHasResistance;
import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.state.IBondageState;
import java.util.UUID;
import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.ItemStack;
import org.jetbrains.annotations.Nullable;
/**
* Utility class for applying restraints to entities.
*
* Phase 3: Refactoring - Centralizes restraint application logic
*
* This class provides methods for applying various restraints (binds, gags,
* blindfolds, etc.) to targets with consistent validation. Used by:
* - KidnapperCaptureGoal
* - KidnapperPunishGoal
* - EntityRopeArrow
* - Other capture/restraint mechanics
*/
public final class RestraintApplicator {
private RestraintApplicator() {
// Utility class - no instantiation
}
// ==================== BIND ====================
/**
* Apply a bind or tighten existing binds.
*
* @param target The target entity
* @param bind The bind item to apply if needed
* @return true if bind was applied or tightened
*/
public static boolean applyOrTightenBind(
LivingEntity target,
ItemStack bind
) {
IBondageState state = KidnappedHelper.getKidnappedState(target);
if (state == null) {
return false;
}
ItemStack currentBind = state.getEquipment(BodyRegionV2.ARMS);
if (currentBind.isEmpty()) {
// No bind - apply new one
if (bind != null && !bind.isEmpty()) {
state.equip(BodyRegionV2.ARMS, bind.copy());
return true;
}
return false;
}
// Tighten existing bind - reset resistance to max
if (currentBind.getItem() instanceof ItemBind bindItem) {
int maxResistance = bindItem.getBaseResistance(target);
state.setCurrentBindResistance(maxResistance);
return true;
}
return false;
}
/**
* Tighten an entity's binds, resetting resistance to max.
* This happens when a guard catches someone struggling.
*
* <p>Consolidated from EntityKidnapper, EntityMaid, EntitySlaveTrader, KidnapperPunishGoal.</p>
*
* @param state The prisoner's kidnapped state
* @param prisoner The prisoner entity
* @return true if binds were tightened, false if no binds equipped
*/
public static boolean tightenBind(
IBondageState state,
LivingEntity prisoner
) {
if (state == null) return false;
ItemStack bind = state.getEquipment(BodyRegionV2.ARMS);
if (bind.isEmpty()) {
return false;
}
if (bind.getItem() instanceof IHasResistance resistItem) {
int baseResistance = resistItem.getBaseResistance(prisoner);
resistItem.setCurrentResistance(bind, baseResistance);
// Notify the prisoner
if (prisoner instanceof ServerPlayer serverPlayer) {
serverPlayer.sendSystemMessage(
Component.literal(
"Your restraints have been tightened!"
).withStyle(ChatFormatting.RED)
);
}
TiedUpMod.LOGGER.debug(
"[RestraintApplicator] Tightened {}'s binds (resistance reset to {})",
prisoner.getName().getString(),
baseResistance
);
return true;
}
return false;
}
// ==================== GAG ====================
/**
* Apply a gag to the target.
*
* @param target The target entity
* @param gag The gag item to apply
* @return true if successfully applied, false if already gagged or invalid
*/
public static boolean applyGag(LivingEntity target, ItemStack gag) {
IBondageState state = KidnappedHelper.getKidnappedState(target);
if (state == null || gag == null || gag.isEmpty()) {
return false;
}
if (state.isGagged()) {
return false; // Already gagged
}
state.equip(BodyRegionV2.MOUTH, gag.copy());
return true;
}
/**
* Apply a gag only if the target doesn't have one.
*
* @param target The target entity
* @param gag The gag item to apply
* @return true if gag was applied
*/
public static boolean applyGagIfMissing(
LivingEntity target,
ItemStack gag
) {
IBondageState state = KidnappedHelper.getKidnappedState(target);
if (state == null || gag == null || gag.isEmpty()) {
return false;
}
ItemStack currentGag = state.getEquipment(BodyRegionV2.MOUTH);
if (!currentGag.isEmpty()) {
return false; // Already has gag
}
state.equip(BodyRegionV2.MOUTH, gag.copy());
return true;
}
// ==================== BLINDFOLD ====================
/**
* Apply a blindfold to the target.
*
* @param target The target entity
* @param blindfold The blindfold item to apply
* @return true if successfully applied, false if already blindfolded or invalid
*/
public static boolean applyBlindfold(
LivingEntity target,
ItemStack blindfold
) {
IBondageState state = KidnappedHelper.getKidnappedState(target);
if (state == null || blindfold == null || blindfold.isEmpty()) {
return false;
}
if (state.isBlindfolded()) {
return false; // Already blindfolded
}
state.equip(BodyRegionV2.EYES, blindfold.copy());
return true;
}
/**
* Apply a blindfold only if the target doesn't have one.
*
* @param target The target entity
* @param blindfold The blindfold item to apply
* @return true if blindfold was applied
*/
public static boolean applyBlindfoldIfMissing(
LivingEntity target,
ItemStack blindfold
) {
IBondageState state = KidnappedHelper.getKidnappedState(target);
if (state == null || blindfold == null || blindfold.isEmpty()) {
return false;
}
ItemStack currentBlindfold = state.getEquipment(BodyRegionV2.EYES);
if (!currentBlindfold.isEmpty()) {
return false; // Already has blindfold
}
state.equip(BodyRegionV2.EYES, blindfold.copy());
return true;
}
// ==================== MITTENS ====================
/**
* Apply mittens to the target.
*
* @param target The target entity
* @param mittens The mittens item to apply
* @return true if successfully applied, false if already has mittens or invalid
*/
public static boolean applyMittens(LivingEntity target, ItemStack mittens) {
IBondageState state = KidnappedHelper.getKidnappedState(target);
if (state == null || mittens == null || mittens.isEmpty()) {
return false;
}
if (state.hasMittens()) {
return false; // Already has mittens
}
state.equip(BodyRegionV2.HANDS, mittens.copy());
return true;
}
// ==================== EARPLUGS ====================
/**
* Apply earplugs to the target.
*
* @param target The target entity
* @param earplugs The earplugs item to apply
* @return true if successfully applied, false if already has earplugs or invalid
*/
public static boolean applyEarplugs(
LivingEntity target,
ItemStack earplugs
) {
IBondageState state = KidnappedHelper.getKidnappedState(target);
if (state == null || earplugs == null || earplugs.isEmpty()) {
return false;
}
if (state.hasEarplugs()) {
return false; // Already has earplugs
}
state.equip(BodyRegionV2.EARS, earplugs.copy());
return true;
}
// ==================== COLLAR ====================
/**
* Apply a collar to the target with owner information.
*
* @param target The target entity
* @param collar The collar item to apply
* @param ownerUUID The owner's UUID (can be null)
* @param ownerName The owner's name (can be null)
* @return true if successfully applied, false if already has collar or invalid
*/
public static boolean applyCollar(
LivingEntity target,
ItemStack collar,
@Nullable UUID ownerUUID,
@Nullable String ownerName
) {
IBondageState state = KidnappedHelper.getKidnappedState(target);
if (state == null || collar == null || collar.isEmpty()) {
return false;
}
if (state.hasCollar()) {
return false; // Already has collar
}
ItemStack collarCopy = collar.copy();
// Add owner if provided
if (
ownerUUID != null &&
collarCopy.getItem() instanceof ItemCollar collarItem
) {
collarItem.addOwner(
collarCopy,
ownerUUID,
ownerName != null ? ownerName : "Unknown"
);
}
state.equip(BodyRegionV2.NECK, collarCopy);
return true;
}
/**
* Apply a collar without owner information.
*
* @param target The target entity
* @param collar The collar item to apply
* @return true if successfully applied
*/
public static boolean applyCollar(LivingEntity target, ItemStack collar) {
return applyCollar(target, collar, null, null);
}
// ==================== BULK OPERATIONS ====================
/**
* Check if target has any restraints.
*
* @param target The target entity
* @return true if target has any restraints
*/
public static boolean hasAnyRestraint(LivingEntity target) {
IBondageState state = KidnappedHelper.getKidnappedState(target);
if (state == null) {
return false;
}
return (
state.isTiedUp() ||
state.isGagged() ||
state.isBlindfolded() ||
state.hasMittens() ||
state.hasEarplugs() ||
state.hasCollar()
);
}
/**
* Get the kidnapped state for an entity.
*
* @param target The target entity
* @return The IBondageState state, or null if not available
*/
@Nullable
public static IBondageState getState(LivingEntity target) {
return KidnappedHelper.getKidnappedState(target);
}
}

View File

@@ -0,0 +1,447 @@
package com.tiedup.remake.util;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.state.IPlayerLeashAccess;
import java.util.UUID;
import org.jetbrains.annotations.Nullable;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.effect.MobEffectInstance;
import net.minecraft.world.effect.MobEffects;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.ai.attributes.AttributeInstance;
import net.minecraft.world.entity.ai.attributes.AttributeModifier;
import net.minecraft.world.entity.ai.attributes.Attributes;
import net.minecraft.world.entity.decoration.LeashFenceKnotEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.FenceBlock;
import net.minecraft.world.level.block.WallBlock;
import net.minecraft.world.level.block.state.BlockState;
/**
* Phase 5: Utility class for applying/removing restraint effects
*
* Manages attribute modifiers for movement speed reduction and other effects.
* Phase 14.1: Refactored to support LivingEntity (Player + NPCs)
*/
public class RestraintEffectUtils {
// UUID for the movement speed modifier (must be consistent)
private static final UUID BIND_SPEED_MODIFIER_UUID = UUID.fromString(
"7f3c7c8e-9d4e-4c7a-8e5f-1a2b3c4d5e6f"
);
private static final String BIND_SPEED_MODIFIER_NAME = "tiedup.bind_speed";
// Speed reduction: -0.09 (90% reduction) when tied up
// Player base speed is 0.10, so this reduces them to 0.01 (10% speed)
private static final double BIND_SPEED_REDUCTION = -0.09;
// Full immobilization: -0.10 (100% reduction) for WRAP/LATEX_SACK
// Player can only move by jumping
private static final double FULL_IMMOBILIZATION_REDUCTION = -0.10;
private static final boolean DEBUG = false;
/**
* Apply movement speed reduction to a tied entity.
*
* @param entity The living entity to apply the effect to
* @deprecated For players, use {@link com.tiedup.remake.v2.bondage.movement.MovementStyleManager}
* which handles speed via tick-based resolution. This method remains for NPC entities
* (Damsel, MCA villagers) that are not managed by MovementStyleManager.
* Scheduled for removal once NPC movement is migrated to V2.
*/
@Deprecated(forRemoval = true)
public static void applyBindSpeedReduction(LivingEntity entity) {
applyBindSpeedReduction(entity, false);
}
/**
* Apply movement speed reduction to a tied entity.
*
* @param entity The living entity to apply the effect to
* @param fullImmobilization If true, applies 100% speed reduction (for WRAP/LATEX_SACK)
* @deprecated For players, use {@link com.tiedup.remake.v2.bondage.movement.MovementStyleManager}
* which handles speed via tick-based resolution. This method remains for NPC entities
* (Damsel, MCA villagers) that are not managed by MovementStyleManager.
* Scheduled for removal once NPC movement is migrated to V2.
*/
@Deprecated(forRemoval = true)
public static void applyBindSpeedReduction(
LivingEntity entity,
boolean fullImmobilization
) {
if (entity == null) {
TiedUpMod.LOGGER.warn(
"[RESTRAINT-UTIL] Cannot apply speed reduction - entity is null"
);
return;
}
AttributeInstance movementSpeed = entity.getAttribute(
Attributes.MOVEMENT_SPEED
);
if (movementSpeed == null) {
TiedUpMod.LOGGER.error(
"[RESTRAINT-UTIL] Entity {} has no MOVEMENT_SPEED attribute!",
entity.getName().getString()
);
return;
}
// Remove existing modifier if present (to avoid duplicates)
removeBindSpeedReduction(entity);
// Choose reduction amount based on immobilization type
double reduction = fullImmobilization
? FULL_IMMOBILIZATION_REDUCTION
: BIND_SPEED_REDUCTION;
// Create and apply new modifier
AttributeModifier modifier = new AttributeModifier(
BIND_SPEED_MODIFIER_UUID,
BIND_SPEED_MODIFIER_NAME,
reduction,
AttributeModifier.Operation.ADDITION
);
movementSpeed.addPermanentModifier(modifier);
if (DEBUG) {
TiedUpMod.LOGGER.info(
"[RESTRAINT-UTIL] Applied speed reduction to {} (base: {}, modified: {}, full: {})",
entity.getName().getString(),
movementSpeed.getBaseValue(),
movementSpeed.getValue(),
fullImmobilization
);
}
}
/**
* Remove movement speed reduction from an entity.
*
* @param entity The living entity to remove the effect from
* @deprecated For players, use {@link com.tiedup.remake.v2.bondage.movement.MovementStyleManager}
* which handles speed cleanup via tick-based resolution. This method remains for
* NPC entities (Damsel, MCA villagers) that are not managed by MovementStyleManager.
* Scheduled for removal once NPC movement is migrated to V2.
*/
@Deprecated(forRemoval = true)
public static void removeBindSpeedReduction(LivingEntity entity) {
if (entity == null) {
TiedUpMod.LOGGER.warn(
"[RESTRAINT-UTIL] Cannot remove speed reduction - entity is null"
);
return;
}
AttributeInstance movementSpeed = entity.getAttribute(
Attributes.MOVEMENT_SPEED
);
if (movementSpeed == null) {
TiedUpMod.LOGGER.error(
"[RESTRAINT-UTIL] Entity {} has no MOVEMENT_SPEED attribute!",
entity.getName().getString()
);
return;
}
// Remove modifier if present
if (movementSpeed.getModifier(BIND_SPEED_MODIFIER_UUID) != null) {
movementSpeed.removeModifier(BIND_SPEED_MODIFIER_UUID);
if (DEBUG) {
TiedUpMod.LOGGER.info(
"[RESTRAINT-UTIL] Removed speed reduction from {} (restored speed: {})",
entity.getName().getString(),
movementSpeed.getValue()
);
}
} else {
if (DEBUG) {
TiedUpMod.LOGGER.debug(
"[RESTRAINT-UTIL] No speed modifier found on {} (already removed or never applied)",
entity.getName().getString()
);
}
}
}
/**
* Check if an entity currently has the bind speed reduction applied.
*
* @param entity The living entity to check
* @return true if the modifier is active
* @deprecated For players, movement style is tracked by {@link com.tiedup.remake.v2.bondage.movement.MovementStyleManager}.
* Scheduled for removal once NPC movement is migrated to V2.
*/
@Deprecated(forRemoval = true)
public static boolean hasBindSpeedReduction(LivingEntity entity) {
if (entity == null) return false;
AttributeInstance movementSpeed = entity.getAttribute(
Attributes.MOVEMENT_SPEED
);
if (movementSpeed == null) return false;
return movementSpeed.getModifier(BIND_SPEED_MODIFIER_UUID) != null;
}
/**
* Re-apply speed reduction if needed (called on login/respawn).
*
* @param entity The living entity
* @param shouldBeSlowed Whether the entity should have reduced speed
* @deprecated For players, use {@link com.tiedup.remake.v2.bondage.movement.MovementStyleManager}
* which re-resolves on every tick. Scheduled for removal once NPC movement is migrated to V2.
*/
@Deprecated(forRemoval = true)
public static void updateBindSpeedReduction(
LivingEntity entity,
boolean shouldBeSlowed
) {
updateBindSpeedReduction(entity, shouldBeSlowed, false);
}
/**
* Re-apply speed reduction if needed (called on login/respawn).
*
* @param entity The living entity
* @param shouldBeSlowed Whether the entity should have reduced speed
* @param fullImmobilization If true, applies 100% speed reduction (for WRAP/LATEX_SACK)
* @deprecated For players, use {@link com.tiedup.remake.v2.bondage.movement.MovementStyleManager}
* which re-resolves on every tick. Scheduled for removal once NPC movement is migrated to V2.
*/
@Deprecated(forRemoval = true)
public static void updateBindSpeedReduction(
LivingEntity entity,
boolean shouldBeSlowed,
boolean fullImmobilization
) {
boolean currentlySlowed = hasBindSpeedReduction(entity);
if (shouldBeSlowed && !currentlySlowed) {
// Need to apply
applyBindSpeedReduction(entity, fullImmobilization);
if (DEBUG) {
TiedUpMod.LOGGER.info(
"[RESTRAINT-UTIL] Re-applied speed reduction to {} on login/respawn (full={})",
entity.getName().getString(),
fullImmobilization
);
}
} else if (shouldBeSlowed && currentlySlowed) {
// Already slowed - but might need to change immobilization level
// Re-apply with correct level
applyBindSpeedReduction(entity, fullImmobilization);
} else if (!shouldBeSlowed && currentlySlowed) {
// Need to remove
removeBindSpeedReduction(entity);
if (DEBUG) {
TiedUpMod.LOGGER.info(
"[RESTRAINT-UTIL] Removed speed reduction from {} on login/respawn",
entity.getName().getString()
);
}
}
}
// ========================================
// POLE BINDING UTILITIES
// ========================================
/**
* Find the closest fence or wall block within a radius.
*
* @param level The level to search in
* @param center The center position to search from
* @param radius The search radius in blocks
* @return The position of the closest fence/wall, or null if none found
*/
@Nullable
public static BlockPos findClosestFence(
Level level,
BlockPos center,
int radius
) {
if (level == null || center == null) return null;
BlockPos closestFence = null;
double closestDistance = Double.MAX_VALUE;
for (int x = -radius; x <= radius; x++) {
for (int y = -radius; y <= radius; y++) {
for (int z = -radius; z <= radius; z++) {
BlockPos checkPos = center.offset(x, y, z);
BlockState state = level.getBlockState(checkPos);
if (
state.getBlock() instanceof FenceBlock ||
state.getBlock() instanceof WallBlock
) {
double dist = center.distSqr(checkPos);
if (dist < closestDistance) {
closestDistance = dist;
closestFence = checkPos;
}
}
}
}
}
return closestFence;
}
/**
* Tie an entity to the closest fence or wall block.
* Works for both Players (via LeashProxy) and NPCs (via vanilla leash).
*
* @param entity The entity to tie
* @param searchRadius The search radius for fence blocks
* @return true if successfully tied, false otherwise
*/
public static boolean tieToClosestPole(
LivingEntity entity,
int searchRadius
) {
if (entity == null || entity.level().isClientSide) return false;
if (!(entity.level() instanceof ServerLevel serverLevel)) return false;
BlockPos entityPos = entity.blockPosition();
BlockPos closestFence = findClosestFence(
serverLevel,
entityPos,
searchRadius
);
if (closestFence == null) {
if (DEBUG) {
TiedUpMod.LOGGER.debug(
"[RESTRAINT-UTIL] No fence found within {} blocks of {}",
searchRadius,
entity.getName().getString()
);
}
return false;
}
// Get or create a LeashFenceKnotEntity at the fence position
LeashFenceKnotEntity fenceKnot = LeashFenceKnotEntity.getOrCreateKnot(
serverLevel,
closestFence
);
if (fenceKnot == null) {
TiedUpMod.LOGGER.warn(
"[RESTRAINT-UTIL] Failed to create fence knot at {}",
closestFence
);
return false;
}
// Handle differently based on entity type
if (entity instanceof Player player) {
// Player: use LeashProxy system
if (player instanceof IPlayerLeashAccess access) {
access.tiedup$attachLeash(fenceKnot);
TiedUpMod.LOGGER.debug(
"[RESTRAINT-UTIL] Tied player {} to pole at {}",
player.getName().getString(),
closestFence
);
return true;
} else {
TiedUpMod.LOGGER.error(
"[RESTRAINT-UTIL] Player {} does not implement IPlayerLeashAccess!",
player.getName().getString()
);
return false;
}
} else if (entity instanceof Mob mob) {
// NPC (Mob): use vanilla leash mechanics
mob.setLeashedTo(fenceKnot, true);
TiedUpMod.LOGGER.debug(
"[RESTRAINT-UTIL] Tied mob {} to pole at {}",
mob.getName().getString(),
closestFence
);
return true;
}
return false;
}
// ========================================
// CHLOROFORM UTILITIES
// ========================================
/**
* Apply chloroform effects to an entity.
* Effects: Slowness, Mining Fatigue, Blindness, Jump Boost (all at max amplifier).
*
* @param entity The entity to affect
* @param durationSeconds Duration in seconds
*/
public static void applyChloroformEffects(
LivingEntity entity,
int durationSeconds
) {
if (entity == null || entity.level().isClientSide) return;
int tickDuration = durationSeconds * GameConstants.TICKS_PER_SECOND;
entity.addEffect(
new MobEffectInstance(
MobEffects.MOVEMENT_SLOWDOWN,
tickDuration,
GameConstants.CHLOROFORM_SLOWDOWN_AMPLIFIER,
false,
false
)
);
entity.addEffect(
new MobEffectInstance(
MobEffects.DIG_SLOWDOWN,
tickDuration,
GameConstants.CHLOROFORM_DIG_SLOWDOWN_AMPLIFIER,
false,
false
)
);
entity.addEffect(
new MobEffectInstance(
MobEffects.BLINDNESS,
tickDuration,
GameConstants.CHLOROFORM_BLINDNESS_AMPLIFIER,
false,
false
)
);
entity.addEffect(
new MobEffectInstance(
MobEffects.JUMP,
tickDuration,
GameConstants.CHLOROFORM_JUMP_AMPLIFIER,
false,
false
)
);
// Stop navigation for mobs
if (entity instanceof Mob mob) {
mob.getNavigation().stop();
}
if (DEBUG) {
TiedUpMod.LOGGER.debug(
"[RESTRAINT-UTIL] Applied chloroform to {} for {} seconds",
entity.getName().getString(),
durationSeconds
);
}
}
}

View File

@@ -0,0 +1,115 @@
package com.tiedup.remake.util;
import net.minecraft.util.Mth;
/**
* Utility class for smoothing rotation values.
*
* <p>Provides smooth interpolation between rotation angles, properly handling
* wraparound at +/-180 degrees. This prevents sudden jumps when rotating past the
* 180/-180 boundary.
*
* <p>Used by:
* <ul>
* <li>EntityDamsel - smoothing body Y rotation in DOG pose</li>
* <li>PlayerArmHideEventHandler - smoothing player body Y rotation in DOG pose</li>
* </ul>
*
* <p>Example usage:
* <pre>
* RotationSmoother smoother = new RotationSmoother();
* // In tick():
* float smoothedRot = smoother.smooth(targetRotation, 0.1f);
* entity.yBodyRot = smoothedRot;
* </pre>
*/
public class RotationSmoother {
/** Current smoothed rotation value. */
private float current;
/** Whether the smoother has been initialized with an initial value. */
private boolean initialized = false;
/**
* Create a new rotation smoother.
*/
public RotationSmoother() {}
/**
* Create a rotation smoother with an initial value.
*
* @param initialValue Initial rotation value in degrees
*/
public RotationSmoother(float initialValue) {
this.current = initialValue;
this.initialized = true;
}
/**
* Smooth rotation towards target.
*
* <p>Interpolates from current rotation towards target using the given speed.
* Properly handles wraparound at +/-180 degrees using {@link Mth#wrapDegrees}.
*
* @param target Target rotation in degrees
* @param speed Smoothing speed (0.0 = no change, 1.0 = instant snap)
* Typical values: 0.1 (slow), 0.2 (medium), 0.3 (fast)
* @return Smoothed rotation value in degrees
*/
public float smooth(float target, float speed) {
if (!initialized) {
// First call: snap to target
current = target;
initialized = true;
return current;
}
// Calculate delta with wraparound handling
float delta = Mth.wrapDegrees(target - current);
// Apply smoothing
current += delta * speed;
return current;
}
/**
* Get current smoothed rotation value.
*
* @return Current rotation in degrees
*/
public float getCurrent() {
return current;
}
/**
* Set current rotation directly (no smoothing).
*
* <p>Use this to reset the smoother or initialize it to a specific value.
*
* @param value Rotation value in degrees
*/
public void setCurrent(float value) {
this.current = value;
this.initialized = true;
}
/**
* Reset the smoother to uninitialized state.
*
* <p>The next call to {@link #smooth} will snap to the target value.
*/
public void reset() {
this.initialized = false;
}
/**
* Check if the smoother has been initialized.
*
* @return true if initialized, false otherwise
*/
public boolean isInitialized() {
return initialized;
}
}

View File

@@ -0,0 +1,226 @@
package com.tiedup.remake.util;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* Analyzes word structure to preserve rhythm in gagged speech.
*/
public class SyllableAnalyzer {
private static final Set<Character> VOWELS = Set.of(
'a',
'e',
'i',
'o',
'u',
'y'
);
/**
* Count the number of syllables in a word.
*
* @param word The word to analyze
* @return The number of syllables (minimum 1)
*/
public static int countSyllables(String word) {
if (word == null || word.isEmpty()) {
return 0;
}
String cleanWord = word.toLowerCase().replaceAll("[^a-z]", "");
if (cleanWord.isEmpty()) {
return 1;
}
int count = 0;
boolean prevVowel = false;
for (int i = 0; i < cleanWord.length(); i++) {
boolean isVowel = VOWELS.contains(cleanWord.charAt(i));
if (isVowel && !prevVowel) {
count++;
}
prevVowel = isVowel;
}
// Handle silent 'e' at end (common in English/French)
if (cleanWord.length() > 2 && cleanWord.endsWith("e") && count > 1) {
char beforeE = cleanWord.charAt(cleanWord.length() - 2);
// Silent 'e' after consonant
if (!VOWELS.contains(beforeE) && !cleanWord.endsWith("le")) {
count--;
}
}
return Math.max(1, count);
}
/**
* Split a word into approximate syllables.
* This is a simplified algorithm that works for most Western languages.
*
* @param word The word to split
* @return List of syllables
*/
public static List<String> splitIntoSyllables(String word) {
List<String> syllables = new ArrayList<>();
if (word == null || word.isEmpty()) {
return syllables;
}
// Preserve original case and non-letter chars
StringBuilder current = new StringBuilder();
boolean prevVowel = false;
int vowelGroups = 0;
for (int i = 0; i < word.length(); i++) {
char c = word.charAt(i);
char lower = Character.toLowerCase(c);
boolean isVowel = VOWELS.contains(lower);
boolean isLetter = Character.isLetter(c);
if (!isLetter) {
// Non-letter characters stick with current syllable
current.append(c);
continue;
}
// Starting a new vowel group after consonants = potential syllable break
if (
isVowel && !prevVowel && vowelGroups > 0 && current.length() > 0
) {
// Check if we should split before this vowel
// Split if we have at least 2 consonants between vowels
int consonantCount = countTrailingConsonants(
current.toString()
);
if (consonantCount >= 2) {
// Keep one consonant with previous syllable, rest go with new
String syllable = current.substring(
0,
current.length() - consonantCount + 1
);
String carry = current.substring(
current.length() - consonantCount + 1
);
if (!syllable.isEmpty()) {
syllables.add(syllable);
}
current = new StringBuilder(carry);
} else if (consonantCount == 1 && current.length() > 1) {
// Single consonant goes with new syllable
String syllable = current.substring(
0,
current.length() - 1
);
String carry = current.substring(current.length() - 1);
if (!syllable.isEmpty()) {
syllables.add(syllable);
}
current = new StringBuilder(carry);
}
}
current.append(c);
if (isVowel && !prevVowel) {
vowelGroups++;
}
prevVowel = isVowel;
}
// Add remaining
if (current.length() > 0) {
syllables.add(current.toString());
}
// If we somehow got no syllables, return the whole word
if (syllables.isEmpty()) {
syllables.add(word);
}
return syllables;
}
/**
* Count trailing consonants in a string.
*/
private static int countTrailingConsonants(String s) {
int count = 0;
for (int i = s.length() - 1; i >= 0; i--) {
char c = Character.toLowerCase(s.charAt(i));
if (Character.isLetter(c) && !VOWELS.contains(c)) {
count++;
} else {
break;
}
}
return count;
}
/**
* Check if a syllable position is typically stressed.
* Simple heuristic: first syllable and syllables with long vowels.
*
* @param syllable The syllable content
* @param position Position in word (0-indexed)
* @param total Total number of syllables
* @return true if likely stressed
*/
public static boolean isStressedSyllable(
String syllable,
int position,
int total
) {
// First syllable is often stressed in English/Germanic
if (position == 0) {
return true;
}
// Last syllable in short words
if (total <= 2 && position == total - 1) {
return true;
}
// Syllables with double vowels or long vowels tend to be stressed
String lower = syllable.toLowerCase();
if (
lower.contains("aa") ||
lower.contains("ee") ||
lower.contains("oo") ||
lower.contains("ii") ||
lower.contains("uu") ||
lower.contains("ou") ||
lower.contains("ai") ||
lower.contains("ei")
) {
return true;
}
return false;
}
/**
* Get the primary vowel of a syllable.
*
* @param syllable The syllable to analyze
* @return The primary vowel character, or 'a' as default
*/
public static char getPrimaryVowel(String syllable) {
if (syllable == null || syllable.isEmpty()) {
return 'a';
}
for (char c : syllable.toCharArray()) {
if (VOWELS.contains(Character.toLowerCase(c))) {
return Character.toLowerCase(c);
}
}
return 'a';
}
}

View File

@@ -0,0 +1,192 @@
package com.tiedup.remake.util;
import com.tiedup.remake.core.ModSounds;
import net.minecraft.sounds.SoundEvent;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.Level;
/**
* Sound utility functions for TiedUp mod.
*
* Provides convenient methods for playing mod-specific sounds.
* Works for both Players and NPCs (EntityDamsel, etc.)
*
* Phase 14.2.5: Connected to actual ModSounds registry
*/
public class TiedUpSounds {
/**
* Play a lock closing sound at entity's position.
* Used when locking collars, gags, blindfolds, etc.
*
* @param entity The entity at whose position to play the sound
*/
public static void playLockSound(Entity entity) {
playSound(entity, ModSounds.COLLAR_KEY_CLOSE.get(), 1.0f);
}
/**
* Play an unlock sound at entity's position.
* Used when unlocking collars, gags, blindfolds, etc.
*
* @param entity The entity at whose position to play the sound
*/
public static void playUnlockSound(Entity entity) {
playSound(entity, ModSounds.COLLAR_KEY_OPEN.get(), 1.0f);
}
/**
* Play a shock sound at entity's position.
* Used when shocking with collar or shocker controller.
*
* @param entity The entity being shocked
*/
public static void playShockSound(Entity entity) {
playSound(entity, ModSounds.ELECTRIC_SHOCK.get(), 1.0f);
}
/**
* Play a binding sound at entity's position.
* Used when applying binds/ropes.
*
* @param entity The entity being bound
*/
public static void playBindSound(Entity entity) {
playSound(entity, ModSounds.CHAIN.get(), 0.8f);
}
/**
* Play a struggle sound at entity's position.
* Used when struggling against restraints.
*
* @param entity The entity struggling
*/
public static void playStruggleSound(Entity entity) {
// Use chain sound with slightly higher pitch for struggle effect
playSound(entity, ModSounds.CHAIN.get(), 0.6f, 1.2f);
}
/**
* Play a collar equip sound at entity's position.
*
* @param entity The entity receiving the collar
*/
public static void playCollarSound(Entity entity) {
playSound(entity, ModSounds.COLLAR_PUT.get(), 1.0f);
}
/**
* Play a slap sound at entity's position.
* Used for paddle/discipline items.
*
* @param entity The entity being slapped
*/
public static void playSlapSound(Entity entity) {
playSound(entity, ModSounds.SLAP.get(), 1.0f);
}
/**
* Play a whip sound at entity's position.
*
* @param entity The entity being whipped
*/
public static void playWhipSound(Entity entity) {
playSound(entity, ModSounds.WHIP.get(), 1.0f);
}
/**
* Play shocker activation sound.
*
* @param entity The entity activating the shocker
*/
public static void playShockerActivatedSound(Entity entity) {
playSound(entity, ModSounds.SHOCKER_ACTIVATED.get(), 1.0f);
}
/**
* Play earplugs equip sound at entity's position.
* Uses a soft variation of the collar sound.
*
* @param entity The entity receiving the earplugs
*/
public static void playEarplugsEquipSound(Entity entity) {
// Use collar_put with softer volume and higher pitch for earplugs
playSound(entity, ModSounds.COLLAR_PUT.get(), 0.5f, 1.3f);
}
/**
* Play earplugs remove sound at entity's position.
*
* @param entity The entity having earplugs removed
*/
public static void playEarplugsRemoveSound(Entity entity) {
// Use collar_put with softer volume and lower pitch for removal
playSound(entity, ModSounds.COLLAR_PUT.get(), 0.4f, 0.9f);
}
/**
* Play a generic sound at an entity's position.
*
* Based on original Utils.playSound()
*
* @param entity The entity at whose position to play the sound
* @param sound The sound event to play
* @param volume The volume (1.0f = 100%)
*/
public static void playSound(
Entity entity,
SoundEvent sound,
float volume
) {
if (entity == null || entity.level() == null || sound == null) {
return;
}
Level level = entity.level();
// Server-side only (will be synced to clients automatically)
if (!level.isClientSide) {
level.playSound(
null, // Player (null = everyone hears it)
entity.blockPosition(), // Position
sound, // Sound event
SoundSource.NEUTRAL, // Sound category (NEUTRAL for mod sounds)
volume, // Volume
1.0f // Pitch
);
}
}
/**
* Play a sound with custom pitch.
*
* @param entity The entity at whose position to play the sound
* @param sound The sound event to play
* @param volume The volume (1.0f = 100%)
* @param pitch The pitch (1.0f = normal, 0.5f = lower, 2.0f = higher)
*/
public static void playSound(
Entity entity,
SoundEvent sound,
float volume,
float pitch
) {
if (entity == null || entity.level() == null || sound == null) {
return;
}
Level level = entity.level();
if (!level.isClientSide) {
level.playSound(
null,
entity.blockPosition(),
sound,
SoundSource.NEUTRAL,
volume,
pitch
);
}
}
}

View File

@@ -0,0 +1,51 @@
package com.tiedup.remake.util;
import java.util.ArrayList;
import java.util.List;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.AABB;
/**
* General utility functions for TiedUp mod.
*/
public class TiedUpUtils {
/**
* Get all players within a radius.
*
* @param level The world
* @param pos The center position
* @param distance The search radius
* @return List of ServerPlayer found
*/
public static List<ServerPlayer> getPlayersAround(
Level level,
BlockPos pos,
double distance
) {
List<ServerPlayer> players = new ArrayList<>();
if (level == null || level.isClientSide) {
return players;
}
AABB searchBox = new AABB(
pos.getX() - distance,
pos.getY() - distance,
pos.getZ() - distance,
pos.getX() + distance,
pos.getY() + distance,
pos.getZ() + distance
);
List<ServerPlayer> allPlayers = level.getEntitiesOfClass(
ServerPlayer.class,
searchBox
);
players.addAll(allPlayers);
return players;
}
}

View File

@@ -0,0 +1,67 @@
package com.tiedup.remake.util;
import java.util.Optional;
import org.jetbrains.annotations.Nullable;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.player.Player;
/**
* Player type validation helpers.
*
* Provides methods for checking player side (client/server) and type casting.
*
* For kidnapped/bondage state checks, use {@link KidnappedHelper} directly.
*/
public final class ValidationHelper {
private ValidationHelper() {}
/**
* Check if the player is a server-side ServerPlayer.
*
* @param player The player to check
* @return true if player is non-null, server-side, and ServerPlayer instance
*/
public static boolean isServerPlayer(@Nullable Player player) {
return (
player != null &&
!player.level().isClientSide &&
player instanceof ServerPlayer
);
}
/**
* Get the player as ServerPlayer if it's server-side.
*
* @param player The player to check
* @return Optional containing ServerPlayer, or empty if not server-side
*/
public static Optional<ServerPlayer> asServerPlayer(
@Nullable Player player
) {
if (isServerPlayer(player)) {
return Optional.of((ServerPlayer) player);
}
return Optional.empty();
}
/**
* Check if the player is client-side.
*
* @param player The player to check
* @return true if player is non-null and on client side
*/
public static boolean isClientSide(@Nullable Player player) {
return player != null && player.level().isClientSide;
}
/**
* Check if we're on the server side for this player.
*
* @param player The player to check
* @return true if player is non-null and on server side
*/
public static boolean isServerSide(@Nullable Player player) {
return player != null && !player.level().isClientSide;
}
}

View File

@@ -0,0 +1,231 @@
package com.tiedup.remake.util.tasks;
import javax.annotation.Nullable;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.registries.ForgeRegistries;
/**
* Represents an item task (for jobs and sales).
*
* Phase 14.3.5: Task system for jobs and sales
*
* An ItemTask defines:
* - What item is needed (by registry name)
* - How many are needed
*
* Used by:
* - Sale system: Price to buy a slave
* - Job system: Items slave must fetch
*
* Based on original ItemTask from 1.12.2
*/
public class ItemTask {
/** The item registry name (e.g., "minecraft:iron_ingot") */
private final String itemId;
/** The amount required */
private final int amount;
/** Cached item reference */
@Nullable
private transient Item cachedItem;
/**
* Create a new item task.
*
* @param itemId The item registry name
* @param amount The amount required
*/
public ItemTask(String itemId, int amount) {
this.itemId = itemId;
this.amount = Math.max(1, amount);
this.cachedItem = null;
}
/**
* Create a new item task from an Item.
*
* @param item The item
* @param amount The amount required
*/
public ItemTask(Item item, int amount) {
ResourceLocation key = ForgeRegistries.ITEMS.getKey(item);
this.itemId = key != null ? key.toString() : "minecraft:air";
this.amount = Math.max(1, amount);
this.cachedItem = item;
}
/**
* Create a new item task from an ItemStack.
*
* @param stack The item stack (amount is used)
*/
public ItemTask(ItemStack stack) {
this(stack.getItem(), stack.getCount());
}
// ========================================
// GETTERS
// ========================================
public String getItemId() {
return itemId;
}
public int getAmount() {
return amount;
}
/**
* Get the Item instance.
*
* @return The Item or null if not found
*/
@Nullable
public Item getItem() {
if (this.cachedItem == null) {
ResourceLocation loc = ResourceLocation.tryParse(this.itemId);
if (loc != null) {
this.cachedItem = ForgeRegistries.ITEMS.getValue(loc);
}
}
return this.cachedItem;
}
/**
* Create an ItemStack of the required amount.
*
* @return ItemStack or empty if item not found
*/
public ItemStack createStack() {
Item item = getItem();
if (item == null) {
return ItemStack.EMPTY;
}
return new ItemStack(item, this.amount);
}
/**
* Get display name for the item.
*
* @return Display name or item ID if not found
*/
public String getDisplayName() {
Item item = getItem();
if (item == null) {
return this.itemId;
}
return new ItemStack(item).getHoverName().getString();
}
// ========================================
// VALIDATION
// ========================================
/**
* Check if an ItemStack matches this task (same item type).
*
* @param stack The stack to check
* @return true if same item type
*/
public boolean matchesItem(ItemStack stack) {
if (stack.isEmpty()) return false;
Item item = getItem();
if (item == null) return false;
return stack.getItem() == item;
}
/**
* Check if an ItemStack completes this task (same item, enough amount).
*
* @param stack The stack to check
* @return true if completes the task
*/
public boolean isCompletedBy(ItemStack stack) {
return matchesItem(stack) && stack.getCount() >= this.amount;
}
/**
* Consume the required items from a stack.
*
* @param stack The stack to consume from
* @return true if consumed successfully
*/
public boolean consumeFrom(ItemStack stack) {
if (!isCompletedBy(stack)) {
return false;
}
stack.shrink(this.amount);
return true;
}
// ========================================
// NBT SERIALIZATION
// ========================================
/**
* Save this task to NBT.
*
* @return CompoundTag with task data
*/
public CompoundTag save() {
CompoundTag tag = new CompoundTag();
tag.putString("item", this.itemId);
tag.putInt("amount", this.amount);
return tag;
}
/**
* Load a task from NBT.
*
* @param tag The CompoundTag to load from
* @return ItemTask or null if invalid
*/
@Nullable
public static ItemTask load(CompoundTag tag) {
if (tag == null || !tag.contains("item")) {
return null;
}
String itemId = tag.getString("item");
int amount = tag.contains("amount") ? tag.getInt("amount") : 1;
return new ItemTask(itemId, amount);
}
// ========================================
// DISPLAY
// ========================================
/**
* Get a display string for this task.
*
* @return String like "20x Iron Ingot"
*/
public String toDisplayString() {
return this.amount + "x " + getDisplayName();
}
@Override
public String toString() {
return "ItemTask{" + this.amount + "x " + this.itemId + "}";
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof ItemTask other)) return false;
return this.amount == other.amount && this.itemId.equals(other.itemId);
}
@Override
public int hashCode() {
return 31 * this.itemId.hashCode() + this.amount;
}
}

View File

@@ -0,0 +1,148 @@
package com.tiedup.remake.util.tasks;
import com.tiedup.remake.core.TiedUpMod;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* Loads and manages job tasks for slave work system.
*
* Phase 14.3.5: Job system
*
* A job is an ItemTask that a slave must complete
* (fetch X items) within a time limit or face punishment.
*
* Based on original JobLoader from 1.12.2
*/
public class JobLoader {
private static final Random RANDOM = new Random();
/** List of available jobs */
private static final List<ItemTask> JOBS = new ArrayList<>();
/** Whether jobs have been initialized */
private static boolean initialized = false;
/**
* Initialize default jobs.
* Called on mod initialization.
*/
public static void init() {
if (initialized) return;
// Default jobs - common gathering tasks
// Easy jobs
JOBS.add(new ItemTask("minecraft:cobblestone", 16));
JOBS.add(new ItemTask("minecraft:dirt", 16));
JOBS.add(new ItemTask("minecraft:oak_log", 8));
JOBS.add(new ItemTask("minecraft:wheat", 10));
JOBS.add(new ItemTask("minecraft:potato", 10));
JOBS.add(new ItemTask("minecraft:carrot", 10));
JOBS.add(new ItemTask("minecraft:apple", 5));
// Medium jobs
JOBS.add(new ItemTask("minecraft:iron_ore", 5));
JOBS.add(new ItemTask("minecraft:coal", 10));
JOBS.add(new ItemTask("minecraft:raw_iron", 5));
JOBS.add(new ItemTask("minecraft:raw_copper", 10));
JOBS.add(new ItemTask("minecraft:leather", 5));
JOBS.add(new ItemTask("minecraft:string", 8));
JOBS.add(new ItemTask("minecraft:paper", 10));
JOBS.add(new ItemTask("minecraft:book", 3));
// Hard jobs
JOBS.add(new ItemTask("minecraft:raw_gold", 3));
JOBS.add(new ItemTask("minecraft:diamond", 1));
JOBS.add(new ItemTask("minecraft:emerald", 1));
JOBS.add(new ItemTask("minecraft:blaze_rod", 2));
JOBS.add(new ItemTask("minecraft:ender_pearl", 2));
JOBS.add(new ItemTask("minecraft:obsidian", 4));
initialized = true;
TiedUpMod.LOGGER.info(
"[JobLoader] Loaded {} default jobs",
JOBS.size()
);
}
/**
* Get a random job task.
*
* @return Random ItemTask for a job
*/
public static ItemTask getRandomJob() {
if (JOBS.isEmpty()) {
// Fallback if not initialized
return new ItemTask("minecraft:cobblestone", 16);
}
return JOBS.get(RANDOM.nextInt(JOBS.size()));
}
/**
* Get a random job from a difficulty tier.
*
* @param difficulty 0 = easy, 1 = medium, 2 = hard
* @return Random ItemTask from that tier
*/
public static ItemTask getRandomJob(int difficulty) {
if (JOBS.isEmpty()) {
return new ItemTask("minecraft:cobblestone", 16);
}
// Simple tier system based on list indices
int tierSize = JOBS.size() / 3;
int startIndex = difficulty * tierSize;
int endIndex = Math.min(startIndex + tierSize, JOBS.size());
if (startIndex >= JOBS.size()) {
startIndex = 0;
endIndex = tierSize;
}
int range = endIndex - startIndex;
if (range <= 0) range = 1;
return JOBS.get(startIndex + RANDOM.nextInt(range));
}
/**
* Get all available jobs.
*
* @return List of all jobs
*/
public static List<ItemTask> getAllJobs() {
return new ArrayList<>(JOBS);
}
/**
* Check if jobs are available.
*
* @return true if at least one job is configured
*/
public static boolean hasJobs() {
return !JOBS.isEmpty();
}
/**
* Add a custom job.
*
* @param job The job to add
*/
public static void addJob(ItemTask job) {
if (job != null) {
JOBS.add(job);
}
}
/**
* Clear all jobs (for reloading).
*/
public static void clear() {
JOBS.clear();
initialized = false;
}
}

View File

@@ -0,0 +1,103 @@
package com.tiedup.remake.util.tasks;
import com.tiedup.remake.core.TiedUpMod;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* Loads and manages sale prices for slave trading.
*
* Phase 14.3.5: Sale system
*
* Provides default sale prices and can be extended
* to load from config files in the future.
*
* Based on original SaleLoader from 1.12.2
*/
public class SaleLoader {
private static final Random RANDOM = new Random();
/** List of available sale prices */
private static final List<ItemTask> SALES = new ArrayList<>();
/** Whether sales have been initialized */
private static boolean initialized = false;
/**
* Initialize default sales.
* Called on mod initialization.
*/
public static void init() {
if (initialized) return;
// Default sale prices
SALES.add(new ItemTask("minecraft:iron_ingot", 10));
SALES.add(new ItemTask("minecraft:iron_ingot", 20));
SALES.add(new ItemTask("minecraft:gold_ingot", 10));
SALES.add(new ItemTask("minecraft:gold_ingot", 15));
SALES.add(new ItemTask("minecraft:diamond", 3));
SALES.add(new ItemTask("minecraft:diamond", 5));
SALES.add(new ItemTask("minecraft:emerald", 5));
SALES.add(new ItemTask("minecraft:emerald", 10));
initialized = true;
TiedUpMod.LOGGER.info(
"[SaleLoader] Loaded {} default sales",
SALES.size()
);
}
/**
* Get a random sale price.
*
* @return Random ItemTask for sale price
*/
public static ItemTask getRandomSale() {
if (SALES.isEmpty()) {
// Fallback if not initialized
return new ItemTask("minecraft:iron_ingot", 10);
}
return SALES.get(RANDOM.nextInt(SALES.size()));
}
/**
* Get all available sale prices.
*
* @return List of all sales
*/
public static List<ItemTask> getAllSales() {
return new ArrayList<>(SALES);
}
/**
* Check if sales are available.
*
* @return true if at least one sale is configured
*/
public static boolean hasSales() {
return !SALES.isEmpty();
}
/**
* Add a custom sale price.
*
* @param sale The sale to add
*/
public static void addSale(ItemTask sale) {
if (sale != null) {
SALES.add(sale);
}
}
/**
* Clear all sales (for reloading).
*/
public static void clear() {
SALES.clear();
initialized = false;
}
}

View File

@@ -0,0 +1,367 @@
package com.tiedup.remake.util.teleport;
import javax.annotation.Nullable;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.Registries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.Vec3;
/**
* Represents a teleport position with coordinates, rotation, and dimension.
*
* <p>Used by collar teleport commands and warp points in the original mod.</p>
*
* <h2>Use Cases</h2>
* <ul>
* <li>Collar warp points (home, prison, custom locations)</li>
* <li>EntityKidnapper prison teleportation</li>
* <li>Admin teleport commands</li>
* </ul>
*
* <h2>NBT Structure</h2>
* <pre>
* position (CompoundTag)
* ├── x (double)
* ├── y (double)
* ├── z (double)
* ├── yaw (float)
* ├── pitch (float)
* └── dimension (string) - e.g., "minecraft:overworld"
* </pre>
*
* <h2>Original Reference</h2>
* Based on Original/com/yuti/kidnapmod/util/teleport/Position.java
*/
public class Position {
private final double x;
private final double y;
private final double z;
private final float yaw;
private final float pitch;
private final ResourceKey<Level> dimension;
/**
* Create a new position with full rotation.
*
* @param x X coordinate
* @param y Y coordinate
* @param z Z coordinate
* @param yaw Horizontal rotation (0-360)
* @param pitch Vertical rotation (-90 to 90)
* @param dimension Dimension key (e.g., Level.OVERWORLD, Level.NETHER)
*/
public Position(
double x,
double y,
double z,
float yaw,
float pitch,
ResourceKey<Level> dimension
) {
this.x = x;
this.y = y;
this.z = z;
this.yaw = yaw;
this.pitch = pitch;
this.dimension = dimension;
}
/**
* Create a new position without rotation (defaults to 0).
*
* @param x X coordinate
* @param y Y coordinate
* @param z Z coordinate
* @param dimension Dimension key
*/
public Position(
double x,
double y,
double z,
ResourceKey<Level> dimension
) {
this(x, y, z, 0.0f, 0.0f, dimension);
}
/**
* Create a position from BlockPos and dimension.
*
* @param pos Block position
* @param dimension Dimension key
*/
public Position(BlockPos pos, ResourceKey<Level> dimension) {
this(
pos.getX() + 0.5,
pos.getY(),
pos.getZ() + 0.5,
0.0f,
0.0f,
dimension
);
}
/**
* Create a position from Vec3 and dimension.
*
* @param vec Vector position
* @param dimension Dimension key
*/
public Position(Vec3 vec, ResourceKey<Level> dimension) {
this(vec.x, vec.y, vec.z, 0.0f, 0.0f, dimension);
}
/**
* Create a position from an entity's current location.
*
* @param entity The entity
* @return Position at entity's location with rotation
*/
public static Position fromEntity(Entity entity) {
return new Position(
entity.getX(),
entity.getY(),
entity.getZ(),
entity.getYRot(),
entity.getXRot(),
entity.level().dimension()
);
}
// ========================================
// NBT SERIALIZATION
// ========================================
/**
* Save this position to NBT.
*
* @return CompoundTag containing position data
*/
public CompoundTag save() {
CompoundTag tag = new CompoundTag();
tag.putDouble("x", this.x);
tag.putDouble("y", this.y);
tag.putDouble("z", this.z);
tag.putFloat("yaw", this.yaw);
tag.putFloat("pitch", this.pitch);
tag.putString("dimension", this.dimension.location().toString());
return tag;
}
/**
* Save this position to an existing CompoundTag.
*
* @param tag The tag to save to
* @return The modified tag
*/
public CompoundTag save(CompoundTag tag) {
tag.putDouble("x", this.x);
tag.putDouble("y", this.y);
tag.putDouble("z", this.z);
tag.putFloat("yaw", this.yaw);
tag.putFloat("pitch", this.pitch);
tag.putString("dimension", this.dimension.location().toString());
return tag;
}
/**
* Load a position from NBT.
*
* @param tag The CompoundTag to load from
* @return Position or null if invalid
*/
@Nullable
public static Position load(CompoundTag tag) {
if (
tag == null ||
!tag.contains("x") ||
!tag.contains("y") ||
!tag.contains("z")
) {
return null;
}
double x = tag.getDouble("x");
double y = tag.getDouble("y");
double z = tag.getDouble("z");
float yaw = tag.contains("yaw") ? tag.getFloat("yaw") : 0.0f;
float pitch = tag.contains("pitch") ? tag.getFloat("pitch") : 0.0f;
// Parse dimension
ResourceKey<Level> dimension = Level.OVERWORLD; // Default
if (tag.contains("dimension")) {
String dimStr = tag.getString("dimension");
ResourceLocation dimLoc = ResourceLocation.tryParse(dimStr);
if (dimLoc != null) {
dimension = ResourceKey.create(Registries.DIMENSION, dimLoc);
}
}
return new Position(x, y, z, yaw, pitch, dimension);
}
// ========================================
// GETTERS
// ========================================
public double getX() {
return x;
}
public double getY() {
return y;
}
public double getZ() {
return z;
}
public float getYaw() {
return yaw;
}
public float getPitch() {
return pitch;
}
public ResourceKey<Level> getDimension() {
return dimension;
}
/**
* Convert to Vec3.
* @return Vector representation
*/
public Vec3 toVec3() {
return new Vec3(x, y, z);
}
/**
* Convert to BlockPos.
* @return Block position (floored)
*/
public BlockPos toBlockPos() {
return new BlockPos(
(int) Math.floor(x),
(int) Math.floor(y),
(int) Math.floor(z)
);
}
/**
* Get dimension name for display.
* @return Dimension name (e.g., "overworld", "the_nether")
*/
public String getDimensionName() {
return dimension.location().getPath();
}
/**
* Check if this position is in the same dimension as another.
* @param other The other position
* @return true if same dimension
*/
public boolean isSameDimension(Position other) {
return other != null && this.dimension.equals(other.dimension);
}
/**
* Check if this position is in the given dimension.
* @param dim The dimension to check
* @return true if in that dimension
*/
public boolean isInDimension(ResourceKey<Level> dim) {
return this.dimension.equals(dim);
}
/**
* Calculate distance to another position (ignores dimension).
* @param other The other position
* @return Distance in blocks
*/
public double distanceTo(Position other) {
double dx = this.x - other.x;
double dy = this.y - other.y;
double dz = this.z - other.z;
return Math.sqrt(dx * dx + dy * dy + dz * dz);
}
/**
* Calculate horizontal distance to another position (ignores Y and dimension).
* @param other The other position
* @return Horizontal distance in blocks
*/
public double horizontalDistanceTo(Position other) {
double dx = this.x - other.x;
double dz = this.z - other.z;
return Math.sqrt(dx * dx + dz * dz);
}
// ========================================
// OBJECT OVERRIDES
// ========================================
@Override
public String toString() {
return String.format(
"Position{x=%.2f, y=%.2f, z=%.2f, yaw=%.1f, pitch=%.1f, dim=%s}",
x,
y,
z,
yaw,
pitch,
dimension.location()
);
}
/**
* Get a short string for display (e.g., in tooltips).
* @return Short position string like "X: 100, Y: 64, Z: -200"
*/
public String toShortString() {
return String.format("X: %.0f, Y: %.0f, Z: %.0f", x, y, z);
}
/**
* Get a display string with dimension for tooltips.
* @return Display string like "overworld - X: 100, Y: 64, Z: -200"
*/
public String toDisplayString() {
return String.format(
"%s - X: %.0f, Y: %.0f, Z: %.0f",
getDimensionName(),
x,
y,
z
);
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Position other)) return false;
return (
Double.compare(other.x, x) == 0 &&
Double.compare(other.y, y) == 0 &&
Double.compare(other.z, z) == 0 &&
Float.compare(other.yaw, yaw) == 0 &&
Float.compare(other.pitch, pitch) == 0 &&
dimension.equals(other.dimension)
);
}
@Override
public int hashCode() {
int result = Double.hashCode(x);
result = 31 * result + Double.hashCode(y);
result = 31 * result + Double.hashCode(z);
result = 31 * result + Float.hashCode(yaw);
result = 31 * result + Float.hashCode(pitch);
result = 31 * result + dimension.hashCode();
return result;
}
}

View File

@@ -0,0 +1,393 @@
package com.tiedup.remake.util.teleport;
import com.tiedup.remake.core.TiedUpMod;
import java.util.function.Function;
import org.jetbrains.annotations.Nullable;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.portal.PortalInfo;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.common.util.ITeleporter;
/**
* Utility class for teleporting entities across dimensions.
*
* <p>Handles the complexity of cross-dimension teleportation in 1.20.1,
* including proper chunk loading, entity transfer, and rotation preservation.</p>
*
* <h2>Features</h2>
* <ul>
* <li>Same-dimension teleportation</li>
* <li>Cross-dimension teleportation</li>
* <li>Player-specific handling (smooth client sync)</li>
* <li>Rotation preservation</li>
* <li>Safe position validation</li>
* </ul>
*
* <h2>Original Reference</h2>
* Based on Original/com/yuti/kidnapmod/util/teleport/TeleportHelper.java
*/
public class TeleportHelper {
/**
* Teleport an entity to a position.
* Handles both same-dimension and cross-dimension teleportation.
*
* @param entity The entity to teleport
* @param position The target position
* @return The entity after teleportation (may be different instance after dimension change)
*/
@Nullable
public static Entity teleportEntity(Entity entity, Position position) {
if (entity == null || position == null) {
return entity;
}
// Server-side only
if (entity.level().isClientSide) {
TiedUpMod.LOGGER.warn(
"[TeleportHelper] Attempted client-side teleport"
);
return entity;
}
// Get server and target level
MinecraftServer server = entity.getServer();
if (server == null) {
TiedUpMod.LOGGER.error(
"[TeleportHelper] No server found for entity"
);
return entity;
}
ResourceKey<Level> targetDimension = position.getDimension();
ServerLevel targetLevel = server.getLevel(targetDimension);
if (targetLevel == null) {
TiedUpMod.LOGGER.error(
"[TeleportHelper] Target dimension not found: {}",
targetDimension
);
return entity;
}
// Check if same dimension or cross-dimension
if (entity.level().dimension().equals(targetDimension)) {
return teleportSameDimension(entity, position);
} else {
return teleportCrossDimension(entity, targetLevel, position);
}
}
/**
* Teleport an entity within the same dimension.
*
* @param entity The entity to teleport
* @param position The target position
* @return The entity after teleportation
*/
private static Entity teleportSameDimension(
Entity entity,
Position position
) {
if (entity == null || position == null) {
return entity;
}
// For players, use the connection method for smooth sync
if (entity instanceof ServerPlayer player) {
player.connection.teleport(
position.getX(),
position.getY(),
position.getZ(),
position.getYaw(),
position.getPitch()
);
// Reset fall distance to prevent fall damage after teleport
player.fallDistance = 0;
player.resetFallDistance();
TiedUpMod.LOGGER.info(
"[TeleportHelper] Teleported player {} to {}",
player.getName().getString(),
position.toShortString()
);
} else {
// For other entities, set position and rotation directly
entity.teleportTo(
position.getX(),
position.getY(),
position.getZ()
);
entity.setYRot(position.getYaw());
entity.setXRot(position.getPitch());
entity.setYHeadRot(position.getYaw());
// Reset fall distance to prevent fall damage after teleport
entity.fallDistance = 0;
TiedUpMod.LOGGER.info(
"[TeleportHelper] Teleported entity {} to {}",
entity.getName().getString(),
position.toShortString()
);
}
return entity;
}
/**
* Teleport an entity across dimensions.
*
* @param entity The entity to teleport
* @param targetLevel The target dimension
* @param position The target position
* @return The entity after teleportation (may be different instance)
*/
@Nullable
public static Entity teleportCrossDimension(
Entity entity,
ServerLevel targetLevel,
Position position
) {
if (entity == null || targetLevel == null || position == null) {
return entity;
}
// For players, use changeDimension with custom teleporter
if (entity instanceof ServerPlayer player) {
return teleportPlayerCrossDimension(player, targetLevel, position);
}
// For other entities, use changeDimension
Entity newEntity = entity.changeDimension(
targetLevel,
new PositionTeleporter(position)
);
if (newEntity != null) {
TiedUpMod.LOGGER.info(
"[TeleportHelper] Cross-dimension teleport: {} to {} in {}",
entity.getName().getString(),
position.toShortString(),
targetLevel.dimension().location()
);
} else {
TiedUpMod.LOGGER.warn(
"[TeleportHelper] Cross-dimension teleport failed for {}",
entity.getName().getString()
);
}
return newEntity;
}
/**
* Teleport a player across dimensions.
*
* @param player The player to teleport
* @param targetLevel The target dimension
* @param position The target position
* @return The player after teleportation
*/
public static ServerPlayer teleportPlayerCrossDimension(
ServerPlayer player,
ServerLevel targetLevel,
Position position
) {
if (player == null || targetLevel == null || position == null) {
return player;
}
// Use Forge's teleportTo method for cross-dimension
player.changeDimension(targetLevel, new PositionTeleporter(position));
TiedUpMod.LOGGER.info(
"[TeleportHelper] Cross-dimension player teleport: {} to {} in {}",
player.getName().getString(),
position.toShortString(),
targetLevel.dimension().location()
);
return player;
}
/**
* Teleport a living entity to another entity's position.
*
* @param entity The entity to teleport
* @param target The target entity to teleport to
* @return The entity after teleportation
*/
@Nullable
public static Entity teleportToEntity(Entity entity, Entity target) {
if (entity == null || target == null) {
return entity;
}
Position targetPos = Position.fromEntity(target);
return teleportEntity(entity, targetPos);
}
/**
* Check if teleportation to a position is safe.
* Checks for solid ground and non-suffocating blocks.
*
* @param level The level to check in
* @param position The position to check
* @return true if position is safe for teleportation
*/
public static boolean isSafePosition(Level level, Position position) {
if (level == null || position == null) {
return false;
}
// Check if there's solid ground below
var groundPos = position.toBlockPos().below();
var groundState = level.getBlockState(groundPos);
if (!groundState.isSolid()) {
return false; // No solid ground
}
// Check if the position itself is not solid (entity can fit)
var feetPos = position.toBlockPos();
var headPos = feetPos.above();
var feetState = level.getBlockState(feetPos);
var headState = level.getBlockState(headPos);
return !feetState.isSolid() && !headState.isSolid();
}
/**
* Find a safe position near the target position.
* Searches in a small radius for a valid teleport location.
*
* @param level The level to search in
* @param position The target position
* @param radius Search radius
* @return Safe position or original if none found
*/
public static Position findSafePosition(
Level level,
Position position,
int radius
) {
if (level == null || position == null) {
return position;
}
// First check if original position is safe
if (isSafePosition(level, position)) {
return position;
}
// Search in expanding circles
for (int r = 1; r <= radius; r++) {
for (int x = -r; x <= r; x++) {
for (int z = -r; z <= r; z++) {
// Only check edge of current radius
if (Math.abs(x) != r && Math.abs(z) != r) continue;
Position testPos = new Position(
position.getX() + x,
position.getY(),
position.getZ() + z,
position.getYaw(),
position.getPitch(),
position.getDimension()
);
if (isSafePosition(level, testPos)) {
return testPos;
}
}
}
}
// No safe position found, return original
TiedUpMod.LOGGER.warn(
"[TeleportHelper] No safe position found near {}",
position.toShortString()
);
return position;
}
// ========================================
// CUSTOM TELEPORTER FOR FORGE
// ========================================
/**
* Custom teleporter that preserves exact position and rotation.
*/
private static class PositionTeleporter implements ITeleporter {
private final Position position;
public PositionTeleporter(Position position) {
this.position = position;
}
@Override
public Entity placeEntity(
Entity entity,
ServerLevel currentWorld,
ServerLevel destWorld,
float yaw,
Function<Boolean, Entity> repositionEntity
) {
// Reposition the entity first (handles mounting, leashing, etc.)
Entity repositioned = repositionEntity.apply(false);
if (repositioned != null) {
// Set exact position and rotation
repositioned.teleportTo(
position.getX(),
position.getY(),
position.getZ()
);
repositioned.setYRot(position.getYaw());
repositioned.setXRot(position.getPitch());
repositioned.setYHeadRot(position.getYaw());
}
return repositioned;
}
@Nullable
@Override
public PortalInfo getPortalInfo(
Entity entity,
ServerLevel destWorld,
Function<ServerLevel, PortalInfo> defaultPortalInfo
) {
return new PortalInfo(
new Vec3(position.getX(), position.getY(), position.getZ()),
Vec3.ZERO, // No velocity
position.getYaw(),
position.getPitch()
);
}
@Override
public boolean isVanilla() {
return false; // Custom teleporter
}
@Override
public boolean playTeleportSound(
ServerPlayer player,
ServerLevel sourceWorld,
ServerLevel destWorld
) {
return false; // No teleport sound
}
}
}

View File

@@ -0,0 +1,90 @@
package com.tiedup.remake.util.time;
import net.minecraft.world.level.Level;
/**
* Phase 6: Game tick-based timer for progressive tasks.
*
* Unlike the original mod which used wall-clock time (Date objects),
* this uses Minecraft game ticks for:
* - Better security (can't exploit by changing system time)
* - Respect for server TPS
* - Easier debugging and testing
*
* Based on original Timer from 1.12.2 but modernized for 1.20.1
*/
public class Timer {
private final long startTick; // Game time when timer started
private final int ticksToWait; // Total ticks to wait
private final Level level; // World reference for getting current game time
/**
* Create a new timer that counts down from the specified number of seconds.
*
* @param seconds Number of seconds to wait
* @param level The world (used to get current game time)
*/
public Timer(int seconds, Level level) {
this.level = level;
this.startTick = level.getGameTime();
this.ticksToWait = seconds * 20; // Convert seconds to ticks (20 ticks/second)
}
/**
* Get the number of ticks remaining until the timer expires.
*
* @return Remaining ticks (0 or negative if expired)
*/
public int getTicksRemaining() {
long currentTick = level.getGameTime();
long elapsed = currentTick - startTick;
return ticksToWait - (int) elapsed;
}
/**
* Get the number of seconds remaining until the timer expires.
*
* @return Remaining seconds (0 or negative if expired)
*/
public int getSecondsRemaining() {
return getTicksRemaining() / 20;
}
/**
* Check if the timer has expired.
*
* @return true if timer has run out
*/
public boolean isExpired() {
return getTicksRemaining() <= 0;
}
/**
* Get elapsed time in ticks since timer started.
*
* @return Elapsed ticks
*/
public int getElapsedTicks() {
long currentTick = level.getGameTime();
return (int) (currentTick - startTick);
}
/**
* Get elapsed time in seconds since timer started.
*
* @return Elapsed seconds
*/
public int getElapsedSeconds() {
return getElapsedTicks() / 20;
}
/**
* Get total duration in seconds.
*
* @return Total seconds
*/
public int getTotalSeconds() {
return ticksToWait / 20;
}
}