Clean repo for open source release
Remove build artifacts, dev tool configs, unused dependencies, and third-party source dumps. Add proper README, update .gitignore, clean up Makefile.
This commit is contained in:
@@ -0,0 +1,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)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
103
src/main/java/com/tiedup/remake/util/FoodEffects.java
Normal file
103
src/main/java/com/tiedup/remake/util/FoodEffects.java
Normal 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
|
||||
) {}
|
||||
}
|
||||
296
src/main/java/com/tiedup/remake/util/GagMaterial.java
Normal file
296
src/main/java/com/tiedup/remake/util/GagMaterial.java
Normal 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;
|
||||
}
|
||||
}
|
||||
119
src/main/java/com/tiedup/remake/util/GameConstants.java
Normal file
119
src/main/java/com/tiedup/remake/util/GameConstants.java
Normal 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;
|
||||
}
|
||||
408
src/main/java/com/tiedup/remake/util/ItemNBTHelper.java
Normal file
408
src/main/java/com/tiedup/remake/util/ItemNBTHelper.java
Normal 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();
|
||||
}
|
||||
}
|
||||
159
src/main/java/com/tiedup/remake/util/KidnapExplosion.java
Normal file
159
src/main/java/com/tiedup/remake/util/KidnapExplosion.java
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
175
src/main/java/com/tiedup/remake/util/KidnappedHelper.java
Normal file
175
src/main/java/com/tiedup/remake/util/KidnappedHelper.java
Normal 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();
|
||||
}
|
||||
}
|
||||
374
src/main/java/com/tiedup/remake/util/KidnapperAIHelper.java
Normal file
374
src/main/java/com/tiedup/remake/util/KidnapperAIHelper.java
Normal 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
350
src/main/java/com/tiedup/remake/util/MessageDispatcher.java
Normal file
350
src/main/java/com/tiedup/remake/util/MessageDispatcher.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
305
src/main/java/com/tiedup/remake/util/ModGameRules.java
Normal file
305
src/main/java/com/tiedup/remake/util/ModGameRules.java
Normal 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().
|
||||
}
|
||||
161
src/main/java/com/tiedup/remake/util/NameGenerator.java
Normal file
161
src/main/java/com/tiedup/remake/util/NameGenerator.java
Normal 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();
|
||||
}
|
||||
}
|
||||
18
src/main/java/com/tiedup/remake/util/PatchouliProxy.java
Normal file
18
src/main/java/com/tiedup/remake/util/PatchouliProxy.java
Normal 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);
|
||||
}
|
||||
}
|
||||
507
src/main/java/com/tiedup/remake/util/PhoneticMapper.java
Normal file
507
src/main/java/com/tiedup/remake/util/PhoneticMapper.java
Normal 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;
|
||||
}
|
||||
}
|
||||
349
src/main/java/com/tiedup/remake/util/RestraintApplicator.java
Normal file
349
src/main/java/com/tiedup/remake/util/RestraintApplicator.java
Normal 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);
|
||||
}
|
||||
}
|
||||
447
src/main/java/com/tiedup/remake/util/RestraintEffectUtils.java
Normal file
447
src/main/java/com/tiedup/remake/util/RestraintEffectUtils.java
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
115
src/main/java/com/tiedup/remake/util/RotationSmoother.java
Normal file
115
src/main/java/com/tiedup/remake/util/RotationSmoother.java
Normal 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;
|
||||
}
|
||||
}
|
||||
226
src/main/java/com/tiedup/remake/util/SyllableAnalyzer.java
Normal file
226
src/main/java/com/tiedup/remake/util/SyllableAnalyzer.java
Normal 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';
|
||||
}
|
||||
}
|
||||
192
src/main/java/com/tiedup/remake/util/TiedUpSounds.java
Normal file
192
src/main/java/com/tiedup/remake/util/TiedUpSounds.java
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
51
src/main/java/com/tiedup/remake/util/TiedUpUtils.java
Normal file
51
src/main/java/com/tiedup/remake/util/TiedUpUtils.java
Normal 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;
|
||||
}
|
||||
}
|
||||
67
src/main/java/com/tiedup/remake/util/ValidationHelper.java
Normal file
67
src/main/java/com/tiedup/remake/util/ValidationHelper.java
Normal 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;
|
||||
}
|
||||
}
|
||||
231
src/main/java/com/tiedup/remake/util/tasks/ItemTask.java
Normal file
231
src/main/java/com/tiedup/remake/util/tasks/ItemTask.java
Normal 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;
|
||||
}
|
||||
}
|
||||
148
src/main/java/com/tiedup/remake/util/tasks/JobLoader.java
Normal file
148
src/main/java/com/tiedup/remake/util/tasks/JobLoader.java
Normal 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;
|
||||
}
|
||||
}
|
||||
103
src/main/java/com/tiedup/remake/util/tasks/SaleLoader.java
Normal file
103
src/main/java/com/tiedup/remake/util/tasks/SaleLoader.java
Normal 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;
|
||||
}
|
||||
}
|
||||
367
src/main/java/com/tiedup/remake/util/teleport/Position.java
Normal file
367
src/main/java/com/tiedup/remake/util/teleport/Position.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
90
src/main/java/com/tiedup/remake/util/time/Timer.java
Normal file
90
src/main/java/com/tiedup/remake/util/time/Timer.java
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user