Clean repo for open source release

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

View File

@@ -0,0 +1,21 @@
package com.tiedup.remake.v2.bondage.movement;
import org.jetbrains.annotations.Nullable;
/**
* Optional per-item overrides for movement style defaults.
* Parsed from the {@code "movement_modifier"} JSON object.
*
* <p>Null fields fall back to the style's defaults. Only the winning item's
* modifier is used (lower-severity items' modifiers are ignored).</p>
*
* <p>Requires a {@code movement_style} to be set on the same item definition.
* The parser ignores {@code movement_modifier} if {@code movement_style} is absent.</p>
*/
public record MovementModifier(
/** Override speed multiplier, or null to use style default. */
@Nullable Float speedMultiplier,
/** Override jump disabled flag, or null to use style default. */
@Nullable Boolean jumpDisabled
) {}

View File

@@ -0,0 +1,72 @@
package com.tiedup.remake.v2.bondage.movement;
import org.jetbrains.annotations.Nullable;
import com.tiedup.remake.v2.BodyRegionV2;
/**
* Movement styles that change how a bound player physically moves.
* Each style has a severity (higher = more constraining), default speed multiplier,
* and default jump-disabled flag.
*
* <p>When multiple styled items are worn, the style with the highest severity wins.
* If two items share the same severity, the item on the region with the lowest
* {@link com.tiedup.remake.v2.BodyRegionV2#ordinal()} wins.</p>
*
* <p>This enum is shared (server + client). It does NOT contain handler references
* to avoid pulling server-only classes into client code.</p>
*/
public enum MovementStyle {
/** Swaying side-to-side gait, visual zigzag via animation. Jump allowed. */
WADDLE(1, 0.6f, false),
/** Tiny dragging steps, heavy speed reduction. Jump disabled. */
SHUFFLE(2, 0.4f, true),
/** Automatic small hops when moving forward. Jump disabled (auto-hop replaces it). */
HOP(3, 0.35f, true),
/** On all fours, swim-like hitbox (0.6 high). Jump disabled. */
CRAWL(4, 0.2f, true);
private final int severity;
private final float defaultSpeedMultiplier;
private final boolean defaultJumpDisabled;
MovementStyle(int severity, float defaultSpeedMultiplier, boolean defaultJumpDisabled) {
this.severity = severity;
this.defaultSpeedMultiplier = defaultSpeedMultiplier;
this.defaultJumpDisabled = defaultJumpDisabled;
}
/** Higher severity = more constraining. Used for resolution tiebreaking. */
public int getSeverity() {
return severity;
}
/** Default speed multiplier (0.0-1.0) applied via MULTIPLY_BASE AttributeModifier. */
public float getDefaultSpeedMultiplier() {
return defaultSpeedMultiplier;
}
/** Whether jumping is disabled by default for this style. */
public boolean isDefaultJumpDisabled() {
return defaultJumpDisabled;
}
/**
* Safe valueOf that returns null instead of throwing on unknown names.
*
* @param name the style name (case-insensitive)
* @return the style, or null if not recognized
*/
@Nullable
public static MovementStyle fromName(String name) {
if (name == null) return null;
try {
return valueOf(name.toUpperCase());
} catch (IllegalArgumentException e) {
return null;
}
}
}

View File

@@ -0,0 +1,507 @@
package com.tiedup.remake.v2.bondage.movement;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.network.sync.PacketSyncMovementStyle;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.IV2BondageEquipment;
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
import java.util.Map;
import java.util.UUID;
import net.minecraft.network.protocol.game.ClientboundSetEntityMotionPacket;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.effect.MobEffects;
import net.minecraft.world.entity.EntityDimensions;
import net.minecraft.world.entity.Pose;
import net.minecraft.world.entity.ai.attributes.AttributeInstance;
import net.minecraft.world.entity.ai.attributes.AttributeModifier;
import net.minecraft.world.entity.ai.attributes.Attributes;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.event.entity.living.LivingEvent;
import net.minecraftforge.eventbus.api.EventPriority;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Server-side manager for movement style mechanics.
*
* <p>Hooks into two events:
* <ul>
* <li>{@code PlayerTickEvent(Phase.END)} at HIGH priority -- resolves style,
* manages lifecycle transitions, dispatches per-style tick logic. Runs after
* vanilla {@code travel()} so velocity modifications apply correctly.</li>
* <li>{@code LivingJumpEvent} -- suppresses jump for styles with jump disabled.
* {@code LivingJumpEvent} is NOT cancelable; jump is neutralized by subtracting
* the jump impulse from Y velocity.</li>
* </ul>
*
* <p>Per-player state lives on {@link PlayerBindState} to piggyback on existing
* lifecycle cleanup hooks (death, logout, dimension change).</p>
*
* @see MovementStyleResolver for resolution logic
* @see MovementStyle for style definitions
*/
@Mod.EventBusSubscriber(
modid = TiedUpMod.MOD_ID,
bus = Mod.EventBusSubscriber.Bus.FORGE
)
public class MovementStyleManager {
private static final Logger LOGGER = LogManager.getLogger("MovementStyles");
// --- V1 legacy modifier UUID (H6 cleanup) ---
// Source of truth: RestraintEffectUtils.BIND_SPEED_MODIFIER_UUID (same value).
// RestraintEffectUtils used this UUID with ADDITION operation and addPermanentModifier().
// Players upgrading from V1 may still have this modifier saved in their NBT.
// Removed on tick to prevent double stacking with V2 MULTIPLY_BASE modifiers.
private static final UUID V1_BIND_SPEED_MODIFIER_UUID =
UUID.fromString("7f3c7c8e-9d4e-4c7a-8e5f-1a2b3c4d5e6f");
// --- Unique UUIDs for AttributeModifiers (one per style to allow clean removal) ---
private static final UUID WADDLE_SPEED_UUID =
UUID.fromString("d7a1c001-0000-0000-0000-000000000001");
private static final UUID SHUFFLE_SPEED_UUID =
UUID.fromString("d7a1c001-0000-0000-0000-000000000002");
private static final UUID HOP_SPEED_UUID =
UUID.fromString("d7a1c001-0000-0000-0000-000000000003");
private static final UUID CRAWL_SPEED_UUID =
UUID.fromString("d7a1c001-0000-0000-0000-000000000004");
// --- Hop tuning constants ---
private static final double HOP_Y_IMPULSE = 0.28;
private static final double HOP_FORWARD_IMPULSE = 0.18;
private static final int HOP_COOLDOWN_TICKS = 10;
private static final int HOP_STARTUP_DELAY_TICKS = 4;
// --- Movement detection threshold (squared distance) ---
private static final double MOVEMENT_THRESHOLD_SQ = 0.001;
// --- Number of consecutive non-moving ticks before hop startup resets ---
private static final int HOP_STARTUP_RESET_TICKS = 2;
// ==================== Tick Event ====================
/**
* Per-tick movement style processing. Runs at HIGH priority at Phase.END
* so it executes before {@code BondageItemRestrictionHandler} (default priority).
*
* <p>Tick flow:
* <ol>
* <li>Skip conditions: passenger, dead, struggling</li>
* <li>Pending pose restore (crawl deactivated but can't stand yet)</li>
* <li>Resolve current style from equipped items</li>
* <li>Compare with active style, handle transitions</li>
* <li>Dispatch to style-specific tick (unless on ladder)</li>
* <li>Update last position for next tick's movement detection</li>
* </ol>
*/
@SubscribeEvent(priority = EventPriority.HIGH)
public static void onPlayerTick(TickEvent.PlayerTickEvent event) {
if (event.side.isClient() || event.phase != TickEvent.Phase.END) {
return;
}
if (!(event.player instanceof ServerPlayer player)) {
return;
}
PlayerBindState state = PlayerBindState.getInstance(player);
if (state == null) {
return;
}
// --- Skip conditions ---
// Update last position even when suspended to prevent false movement
// detection on resume (e.g., teleport while riding)
if (player.isPassenger() || player.isDeadOrDying() || state.isStruggling()) {
state.lastX = player.getX();
state.lastY = player.getY();
state.lastZ = player.getZ();
return;
}
// --- Pending pose restore (crawl deactivated but can't stand) ---
if (state.pendingPoseRestore) {
tryRestoreStandingPose(player, state);
}
// --- H6: Remove stale V1 permanent modifier if present ---
// Players upgrading from V1 may have a permanent ADDITION modifier saved in NBT.
// This one-time cleanup prevents double stacking with the V2 MULTIPLY_BASE modifier.
cleanupV1Modifier(player);
// --- Resolve current style from equipped items ---
IV2BondageEquipment equipment = V2EquipmentHelper.getEquipment(player);
Map<BodyRegionV2, ItemStack> equipped = equipment != null
? equipment.getAllEquipped() : Map.of();
ResolvedMovement resolved = MovementStyleResolver.resolve(equipped);
// --- Compare with current active style ---
MovementStyle newStyle = resolved.style();
MovementStyle oldStyle = state.getActiveMovementStyle();
if (newStyle != oldStyle) {
// Style changed: deactivate old, activate new
if (oldStyle != null) {
onDeactivate(player, state, oldStyle);
}
if (newStyle != null) {
state.setResolvedMovementSpeed(resolved.speedMultiplier());
state.setResolvedJumpDisabled(resolved.jumpDisabled());
onActivate(player, state, newStyle);
} else {
state.setResolvedMovementSpeed(1.0f);
state.setResolvedJumpDisabled(false);
}
state.setActiveMovementStyle(newStyle);
// Sync to all tracking clients (animation + crawl pose)
ModNetwork.sendToAllTrackingAndSelf(
new PacketSyncMovementStyle(player.getUUID(), newStyle), player);
}
// --- Per-style tick ---
if (state.getActiveMovementStyle() != null) {
// Ladder suspension: skip style tick when on ladder
// (ladder movement is controlled by BondageItemRestrictionHandler)
if (player.onClimbable()) {
state.lastX = player.getX();
state.lastY = player.getY();
state.lastZ = player.getZ();
return;
}
tickStyle(player, state);
}
// Update last position for next tick's movement detection
state.lastX = player.getX();
state.lastY = player.getY();
state.lastZ = player.getZ();
}
// ==================== Jump Suppression ====================
/**
* Suppress jumps for styles with jump disabled.
*
* <p>{@code LivingJumpEvent} is NOT cancelable. Standard approach: subtract
* the known jump impulse from Y velocity, preserving knockback and other
* sources of Y motion.</p>
*
* <p>A {@link ClientboundSetEntityMotionPacket} is sent to minimize the
* client-side 1-frame bounce artifact.</p>
*/
@SubscribeEvent
public static void onLivingJump(LivingEvent.LivingJumpEvent event) {
if (!(event.getEntity() instanceof ServerPlayer player)) {
return;
}
PlayerBindState state = PlayerBindState.getInstance(player);
if (state == null || !state.isResolvedJumpDisabled()) {
return;
}
// Subtract vanilla jump impulse, preserving other Y velocity (knockback, etc.)
// Vanilla: jumpPower = 0.42 + (amplifier + 1) * 0.1 = 0.42 * factor
double jumpVelocity = 0.42 * getJumpBoostFactor(player);
Vec3 motion = player.getDeltaMovement();
player.setDeltaMovement(motion.x, motion.y - jumpVelocity, motion.z);
// Sync to client to minimize visual bounce artifact
player.connection.send(new ClientboundSetEntityMotionPacket(player));
}
/**
* Calculate the Jump Boost potion factor.
* Vanilla adds {@code (amplifier + 1) * 0.1} to the base 0.42 jump height.
* We express this as a multiplicative factor on 0.42 for clean subtraction.
*
* @return 1.0 with no Jump Boost, higher with Jump Boost active
*/
private static double getJumpBoostFactor(Player player) {
var jumpBoost = player.getEffect(MobEffects.JUMP);
if (jumpBoost != null) {
return 1.0 + (jumpBoost.getAmplifier() + 1) * 0.1 / 0.42;
}
return 1.0;
}
// ==================== Lifecycle ====================
private static void onActivate(ServerPlayer player, PlayerBindState state,
MovementStyle style) {
switch (style) {
case WADDLE -> activateWaddle(player, state);
case SHUFFLE -> activateShuffle(player, state);
case HOP -> activateHop(player, state);
case CRAWL -> activateCrawl(player, state);
}
}
private static void onDeactivate(ServerPlayer player, PlayerBindState state,
MovementStyle style) {
switch (style) {
case WADDLE -> deactivateWaddle(player, state);
case SHUFFLE -> deactivateShuffle(player, state);
case HOP -> deactivateHop(player, state);
case CRAWL -> deactivateCrawl(player, state);
}
}
private static void tickStyle(ServerPlayer player, PlayerBindState state) {
switch (state.getActiveMovementStyle()) {
case WADDLE -> tickWaddle(player, state);
case SHUFFLE -> tickShuffle(player, state);
case HOP -> tickHop(player, state);
case CRAWL -> tickCrawl(player, state);
}
}
// ==================== Waddle ====================
private static void activateWaddle(ServerPlayer player, PlayerBindState state) {
applySpeedModifier(player, WADDLE_SPEED_UUID, "tiedup.waddle_speed",
state.getResolvedMovementSpeed());
}
private static void deactivateWaddle(ServerPlayer player, PlayerBindState state) {
removeSpeedModifier(player, WADDLE_SPEED_UUID);
}
private static void tickWaddle(ServerPlayer player, PlayerBindState state) {
// Waddle is animation-only on the server. No velocity manipulation.
// The visual zigzag is handled by the context animation on the client.
}
// ==================== Shuffle ====================
private static void activateShuffle(ServerPlayer player, PlayerBindState state) {
applySpeedModifier(player, SHUFFLE_SPEED_UUID, "tiedup.shuffle_speed",
state.getResolvedMovementSpeed());
}
private static void deactivateShuffle(ServerPlayer player, PlayerBindState state) {
removeSpeedModifier(player, SHUFFLE_SPEED_UUID);
}
private static void tickShuffle(ServerPlayer player, PlayerBindState state) {
// Shuffle: speed reduction via attribute is sufficient. No per-tick work.
}
// ==================== Hop ====================
private static void activateHop(ServerPlayer player, PlayerBindState state) {
// Apply base speed reduction (~15% base speed between hops)
applySpeedModifier(player, HOP_SPEED_UUID, "tiedup.hop_speed",
state.getResolvedMovementSpeed());
state.hopCooldown = 0;
state.hopStartupPending = true;
state.hopStartupTicks = HOP_STARTUP_DELAY_TICKS;
}
private static void deactivateHop(ServerPlayer player, PlayerBindState state) {
removeSpeedModifier(player, HOP_SPEED_UUID);
state.hopCooldown = 0;
state.hopStartupPending = false;
state.hopStartupTicks = 0;
state.hopNotMovingTicks = 0;
}
/**
* Hop tick logic:
* <ul>
* <li>Detect movement via position delta (not player.zza/xxa)</li>
* <li>If moving + on ground + cooldown expired: execute hop (with startup delay on first hop)</li>
* <li>If not moving for >= 2 ticks: reset startup pending</li>
* <li>Decrement cooldown each tick</li>
* </ul>
*/
private static void tickHop(ServerPlayer player, PlayerBindState state) {
boolean isMoving = player.distanceToSqr(state.lastX, state.lastY, state.lastZ)
> MOVEMENT_THRESHOLD_SQ;
// Decrement cooldown
if (state.hopCooldown > 0) {
state.hopCooldown--;
}
if (isMoving && player.onGround() && state.hopCooldown <= 0) {
if (state.hopStartupPending) {
// Startup delay: decrement and wait (latched: completes even if
// player briefly releases input during these 4 ticks)
state.hopStartupTicks--;
if (state.hopStartupTicks <= 0) {
// Startup complete: execute first hop
state.hopStartupPending = false;
executeHop(player, state);
}
} else {
// Normal hop
executeHop(player, state);
}
state.hopNotMovingTicks = 0;
} else if (!isMoving) {
state.hopNotMovingTicks++;
// Reset startup if not moving for >= 2 consecutive ticks
if (state.hopNotMovingTicks >= HOP_STARTUP_RESET_TICKS
&& !state.hopStartupPending) {
state.hopStartupPending = true;
state.hopStartupTicks = HOP_STARTUP_DELAY_TICKS;
}
} else {
// Moving but not on ground or cooldown active — reset not-moving counter
state.hopNotMovingTicks = 0;
}
}
/**
* Execute a single hop: apply Y impulse + forward impulse along look direction.
* Sends {@link ClientboundSetEntityMotionPacket} to sync velocity to client.
*/
private static void executeHop(ServerPlayer player, PlayerBindState state) {
Vec3 look = player.getLookAngle();
// Project look onto horizontal plane and normalize (safe: zero vec normalizes to zero)
Vec3 forward = new Vec3(look.x, 0, look.z).normalize();
Vec3 currentMotion = player.getDeltaMovement();
player.setDeltaMovement(
currentMotion.x + forward.x * HOP_FORWARD_IMPULSE,
HOP_Y_IMPULSE,
currentMotion.z + forward.z * HOP_FORWARD_IMPULSE
);
state.hopCooldown = HOP_COOLDOWN_TICKS;
// Sync velocity to client to prevent rubber-banding
player.connection.send(new ClientboundSetEntityMotionPacket(player));
}
// ==================== Crawl ====================
private static void activateCrawl(ServerPlayer player, PlayerBindState state) {
applySpeedModifier(player, CRAWL_SPEED_UUID, "tiedup.crawl_speed",
state.getResolvedMovementSpeed());
player.setForcedPose(Pose.SWIMMING);
player.refreshDimensions();
state.pendingPoseRestore = false;
}
private static void deactivateCrawl(ServerPlayer player, PlayerBindState state) {
removeSpeedModifier(player, CRAWL_SPEED_UUID);
// Space check: can the player stand up?
EntityDimensions standDims = player.getDimensions(Pose.STANDING);
AABB standBox = standDims.makeBoundingBox(player.position());
boolean canStand = player.level().noCollision(player, standBox);
if (canStand) {
player.setForcedPose(null);
player.refreshDimensions();
} else {
// Can't stand yet -- flag for periodic retry in tick flow (step 2)
state.pendingPoseRestore = true;
}
}
private static void tickCrawl(ServerPlayer player, PlayerBindState state) {
// Guard re-assertion: only re-apply if something cleared the forced pose
// (avoids unnecessary per-tick SynchedEntityData dirty-marking)
if (player.getForcedPose() != Pose.SWIMMING) {
player.setForcedPose(Pose.SWIMMING);
player.refreshDimensions();
}
}
// ==================== Pending Pose Restore ====================
/**
* Try to restore standing pose after crawl deactivation.
* Called every tick regardless of active style (step 2 in tick flow).
* Retries until space is available for the player to stand.
*/
private static void tryRestoreStandingPose(ServerPlayer player,
PlayerBindState state) {
EntityDimensions standDims = player.getDimensions(Pose.STANDING);
AABB standBox = standDims.makeBoundingBox(player.position());
boolean canStand = player.level().noCollision(player, standBox);
if (canStand) {
player.setForcedPose(null);
player.refreshDimensions();
state.pendingPoseRestore = false;
LOGGER.debug("Restored standing pose for {} (pending pose restore cleared)",
player.getName().getString());
}
}
// ==================== V1 Legacy Cleanup (H6) ====================
/**
* Remove the legacy V1 {@code RestraintEffectUtils} speed modifier if present.
*
* <p>V1 used {@code addPermanentModifier()} with UUID {@code 7f3c7c8e-...} and
* {@link AttributeModifier.Operation#ADDITION}. Because permanent modifiers are
* serialized to player NBT, players upgrading mid-session or loading old saves
* may still carry this modifier. Removing it here ensures only the V2
* {@code MULTIPLY_BASE} modifier is active.</p>
*
* <p>This is a no-op if the modifier is not present (cheap UUID lookup).</p>
*/
private static void cleanupV1Modifier(ServerPlayer player) {
AttributeInstance attr = player.getAttribute(Attributes.MOVEMENT_SPEED);
if (attr != null && attr.getModifier(V1_BIND_SPEED_MODIFIER_UUID) != null) {
attr.removeModifier(V1_BIND_SPEED_MODIFIER_UUID);
LOGGER.info("Removed stale V1 speed modifier from player {}",
player.getName().getString());
}
}
// ==================== Attribute Modifier Helpers ====================
/**
* Apply a transient {@code MULTIPLY_BASE} speed modifier.
* Always removes any existing modifier with the same UUID first, because
* {@code addTransientModifier} throws {@link IllegalArgumentException}
* if a modifier with the same UUID already exists.
*
* <p>{@code MULTIPLY_BASE} means the modifier value is added to 1.0 and
* multiplied with the base value. A multiplier of 0.4 requires a modifier
* value of -0.6: {@code base * (1 + (-0.6)) = base * 0.4}.</p>
*
* @param player the target player
* @param uuid unique modifier UUID per style
* @param name modifier name (for debugging in F3 screen)
* @param multiplier the desired speed fraction (0.0-1.0)
*/
private static void applySpeedModifier(ServerPlayer player, UUID uuid, String name,
float multiplier) {
AttributeInstance attr = player.getAttribute(Attributes.MOVEMENT_SPEED);
if (attr == null) return;
// Remove existing modifier first (no-op if not present)
attr.removeModifier(uuid);
// MULTIPLY_BASE: value of -(1 - multiplier) reduces base speed to multiplier fraction
double value = -(1.0 - multiplier);
attr.addTransientModifier(new AttributeModifier(uuid, name,
value, AttributeModifier.Operation.MULTIPLY_BASE));
}
/**
* Remove a speed modifier by UUID. Safe to call even if no modifier
* with this UUID is present.
*/
private static void removeSpeedModifier(ServerPlayer player, UUID uuid) {
AttributeInstance attr = player.getAttribute(Attributes.MOVEMENT_SPEED);
if (attr == null) return;
attr.removeModifier(uuid);
}
}

View File

@@ -0,0 +1,154 @@
package com.tiedup.remake.v2.bondage.movement;
import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.items.base.PoseType;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemDefinition;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemRegistry;
import java.util.Map;
import net.minecraft.world.item.ItemStack;
import org.jetbrains.annotations.Nullable;
/**
* Resolves the winning movement style from a player's equipped bondage items.
*
* <p>Shared class (client + server). Deterministic: same items produce the same result.
* The highest-severity style wins. Tiebreaker: lowest {@link BodyRegionV2#ordinal()}.</p>
*
* <p>The winning item's {@link MovementModifier} (if present) overrides the style's
* default speed/jump values. Modifiers from lower-severity items are ignored.</p>
*
* <h3>V1 Compatibility (H6 fix)</h3>
* <p>V1 items ({@link ItemBind}) stored in V2 capability
* do not have data-driven definitions. This resolver provides a fallback that
* maps V1 bind mode + pose type to a {@link MovementStyle} with speed values matching
* the original V1 behavior, preventing double stacking between the legacy
* {@code RestraintEffectUtils} attribute modifier and the V2 modifier.</p>
*/
public final class MovementStyleResolver {
private MovementStyleResolver() {}
// --- V1 fallback speed values ---
// V1 used ADDITION(-0.09) on base 0.10 = 0.01 effective = 10% speed
// Expressed as MULTIPLY_BASE fraction: 0.10
private static final float V1_STANDARD_SPEED = 0.10f;
// V1 used ADDITION(-0.10) on base 0.10 = 0.00 effective = 0% speed
// Expressed as MULTIPLY_BASE fraction: 0.0 (fully immobile)
private static final float V1_IMMOBILIZED_SPEED = 0.0f;
/**
* Resolve the winning movement style from all equipped items.
*
* <p>Checks V2 data-driven definitions first, then falls back to V1 {@link ItemBind}
* introspection for items without data-driven definitions.</p>
*
* @param equipped map of region to ItemStack (from {@code IV2BondageEquipment.getAllEquipped()})
* @return the resolved movement, or {@link ResolvedMovement#NONE} if no styled items
*/
public static ResolvedMovement resolve(Map<BodyRegionV2, ItemStack> equipped) {
if (equipped == null || equipped.isEmpty()) {
return ResolvedMovement.NONE;
}
MovementStyle bestStyle = null;
float bestSpeed = 1.0f;
boolean bestJumpDisabled = false;
int bestSeverity = -1;
int bestRegionOrdinal = Integer.MAX_VALUE;
for (Map.Entry<BodyRegionV2, ItemStack> entry : equipped.entrySet()) {
BodyRegionV2 region = entry.getKey();
ItemStack stack = entry.getValue();
if (stack.isEmpty()) continue;
// --- Try V2 data-driven definition first ---
DataDrivenItemDefinition def = DataDrivenItemRegistry.get(stack);
if (def != null && def.movementStyle() != null) {
MovementStyle style = def.movementStyle();
int severity = style.getSeverity();
int regionOrdinal = region.ordinal();
if (severity > bestSeverity
|| (severity == bestSeverity && regionOrdinal < bestRegionOrdinal)) {
bestStyle = style;
MovementModifier mod = def.movementModifier();
bestSpeed = (mod != null && mod.speedMultiplier() != null)
? mod.speedMultiplier()
: style.getDefaultSpeedMultiplier();
bestJumpDisabled = (mod != null && mod.jumpDisabled() != null)
? mod.jumpDisabled()
: style.isDefaultJumpDisabled();
bestSeverity = severity;
bestRegionOrdinal = regionOrdinal;
}
continue;
}
// --- V1 fallback: ItemBind without data-driven definition ---
V1Fallback fallback = resolveV1Fallback(stack);
if (fallback != null) {
int severity = fallback.style.getSeverity();
int regionOrdinal = region.ordinal();
if (severity > bestSeverity
|| (severity == bestSeverity && regionOrdinal < bestRegionOrdinal)) {
bestStyle = fallback.style;
bestSpeed = fallback.speed;
bestJumpDisabled = fallback.jumpDisabled;
bestSeverity = severity;
bestRegionOrdinal = regionOrdinal;
}
}
}
if (bestStyle == null) {
return ResolvedMovement.NONE;
}
return new ResolvedMovement(bestStyle, bestSpeed, bestJumpDisabled);
}
// ==================== V1 Fallback ====================
/**
* Attempt to derive a movement style from a V1 {@link ItemBind} item.
*
* <p>Only items with legs bound produce a movement style. The mapping preserves
* the original V1 speed values:</p>
* <ul>
* <li>WRAP / LATEX_SACK: SHUFFLE at 0% speed (full immobilization), jump disabled</li>
* <li>DOG / HUMAN_CHAIR: CRAWL at V1 standard speed (10%), jump disabled</li>
* <li>STANDARD / STRAITJACKET: SHUFFLE at 10% speed, jump disabled</li>
* </ul>
*
* @param stack the ItemStack to inspect
* @return fallback resolution, or null if the item is not a V1 bind or legs are not bound
*/
@Nullable
private static V1Fallback resolveV1Fallback(ItemStack stack) {
if (!(stack.getItem() instanceof ItemBind bindItem)) {
return null;
}
if (!ItemBind.hasLegsBound(stack)) {
return null;
}
PoseType poseType = bindItem.getPoseType();
return switch (poseType) {
case WRAP, LATEX_SACK ->
new V1Fallback(MovementStyle.SHUFFLE, V1_IMMOBILIZED_SPEED, true);
case DOG, HUMAN_CHAIR ->
new V1Fallback(MovementStyle.CRAWL, V1_STANDARD_SPEED, true);
default ->
// STANDARD, STRAITJACKET: shuffle at V1 standard speed
new V1Fallback(MovementStyle.SHUFFLE, V1_STANDARD_SPEED, true);
};
}
/** Internal holder for V1 fallback resolution result. */
private record V1Fallback(MovementStyle style, float speed, boolean jumpDisabled) {}
}

View File

@@ -0,0 +1,24 @@
package com.tiedup.remake.v2.bondage.movement;
import org.jetbrains.annotations.Nullable;
/**
* Result of resolving the winning movement style from all equipped items.
* Contains the final computed values (style defaults merged with item overrides).
*
* <p>A null instance or null style means no movement restriction applies.</p>
*/
public record ResolvedMovement(
/** The winning movement style, or null if no styled items are equipped. */
@Nullable MovementStyle style,
/** Final speed multiplier (style default or item override). */
float speedMultiplier,
/** Final jump-disabled flag (style default or item override). */
boolean jumpDisabled
) {
/** Sentinel for "no movement style active". */
public static final ResolvedMovement NONE = new ResolvedMovement(null, 1.0f, false);
}