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,548 @@
|
||||
package com.tiedup.remake.minigame;
|
||||
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Server-side state for the continuous Struggle mini-game session.
|
||||
*
|
||||
* New system: Player holds a direction key to continuously reduce resistance.
|
||||
* - Progression: 1 resistance per second when holding correct direction
|
||||
* - Direction changes randomly every 3-5 seconds
|
||||
* - Shock collar check: 10% chance every 5 seconds (if locked collar equipped)
|
||||
* - No cooldown, no penalty for wrong key (just no progress)
|
||||
*/
|
||||
public class ContinuousStruggleMiniGameState {
|
||||
|
||||
/**
|
||||
* Possible directions for struggling.
|
||||
*/
|
||||
public enum Direction {
|
||||
UP(0), // W / Forward
|
||||
LEFT(1), // A / Strafe Left
|
||||
DOWN(2), // S / Back
|
||||
RIGHT(3); // D / Strafe Right
|
||||
|
||||
private final int index;
|
||||
|
||||
Direction(int index) {
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
public int getIndex() {
|
||||
return index;
|
||||
}
|
||||
|
||||
public static Direction fromIndex(int index) {
|
||||
return switch (index) {
|
||||
case 0 -> UP;
|
||||
case 1 -> LEFT;
|
||||
case 2 -> DOWN;
|
||||
case 3 -> RIGHT;
|
||||
default -> UP;
|
||||
};
|
||||
}
|
||||
|
||||
public static Direction random(Random random) {
|
||||
return fromIndex(random.nextInt(4));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* State of the continuous struggle session.
|
||||
*/
|
||||
public enum State {
|
||||
/** Actively struggling */
|
||||
ACTIVE,
|
||||
/** Temporarily interrupted by shock collar */
|
||||
SHOCKED,
|
||||
/** Successfully escaped (resistance reached 0) */
|
||||
ESCAPED,
|
||||
/** Cancelled by player or damage */
|
||||
CANCELLED,
|
||||
}
|
||||
|
||||
/**
|
||||
* Type of state update sent to client.
|
||||
*/
|
||||
public enum UpdateType {
|
||||
START,
|
||||
DIRECTION_CHANGE,
|
||||
RESISTANCE_UPDATE,
|
||||
SHOCK,
|
||||
ESCAPE,
|
||||
END,
|
||||
}
|
||||
|
||||
// ==================== CONSTANTS ====================
|
||||
|
||||
/** Minimum interval for direction change (3 seconds = 60 ticks) */
|
||||
private static final int DIRECTION_CHANGE_MIN_TICKS = 60;
|
||||
|
||||
/** Maximum interval for direction change (5 seconds = 100 ticks) */
|
||||
private static final int DIRECTION_CHANGE_MAX_TICKS = 100;
|
||||
|
||||
/** Interval for shock collar check (5 seconds = 100 ticks) */
|
||||
private static final int SHOCK_CHECK_INTERVAL_TICKS = 100;
|
||||
|
||||
/** Shock probability (10% = 10 out of 100) */
|
||||
private static final int SHOCK_PROBABILITY = 10;
|
||||
|
||||
/** Default ticks per 1 resistance point (20 ticks = 1 second per resistance) */
|
||||
private static final int DEFAULT_TICKS_PER_RESISTANCE = 20;
|
||||
|
||||
/** Shock stun duration in ticks (1 second) */
|
||||
private static final int SHOCK_STUN_TICKS = 20;
|
||||
|
||||
/** Kidnapper notification interval (2 seconds = 40 ticks) */
|
||||
private static final int KIDNAPPER_NOTIFY_INTERVAL_TICKS = 40;
|
||||
|
||||
/** Struggle sound interval (1.5 seconds = 30 ticks) */
|
||||
private static final int STRUGGLE_SOUND_INTERVAL_TICKS = 30;
|
||||
|
||||
/** Session timeout in milliseconds (10 minutes) */
|
||||
private static final long SESSION_TIMEOUT_MS = 10L * 60L * 1000L;
|
||||
|
||||
// ==================== FIELDS ====================
|
||||
|
||||
private final UUID sessionId;
|
||||
private final UUID playerId;
|
||||
private final long createdAt;
|
||||
|
||||
/** Current required direction (0-3 for W/A/S/D) */
|
||||
private Direction currentDirection;
|
||||
|
||||
/** Current resistance remaining */
|
||||
private int currentResistance;
|
||||
|
||||
/** Maximum resistance (initial value) for display percentage */
|
||||
private final int maxResistance;
|
||||
|
||||
/** Accumulated progress toward next resistance point (0.0 to 1.0) */
|
||||
private float accumulatedProgress;
|
||||
|
||||
/** Tick when direction last changed */
|
||||
private long lastDirectionChangeTick;
|
||||
|
||||
/** Tick when shock was last checked */
|
||||
private long lastShockCheckTick;
|
||||
|
||||
/** Tick when kidnappers were last notified */
|
||||
private long lastKidnapperNotifyTick;
|
||||
|
||||
/** Tick when struggle sound was last played */
|
||||
private long lastStruggleSoundTick;
|
||||
|
||||
/** Ticks until direction change */
|
||||
private int ticksUntilDirectionChange;
|
||||
|
||||
/** Current session state */
|
||||
private State state;
|
||||
|
||||
/** Ticks remaining in shock stun */
|
||||
private int shockStunTicksRemaining;
|
||||
|
||||
/** Whether player is currently holding the correct key */
|
||||
private boolean isHoldingCorrectKey;
|
||||
|
||||
/** Direction player is currently holding (-1 if none) */
|
||||
private int heldDirection;
|
||||
|
||||
/** Whether the bind is locked (affects display only, locks add +250 resistance) */
|
||||
private boolean isLocked;
|
||||
|
||||
/** Target accessory slot (null = bind, otherwise = accessory) */
|
||||
private Integer targetSlot;
|
||||
|
||||
/** Target V2 body region (null = V1 session, non-null = V2 session) */
|
||||
@Nullable
|
||||
private BodyRegionV2 targetRegion;
|
||||
|
||||
/** Target furniture entity ID (non-zero = furniture struggle session) */
|
||||
private int furnitureEntityId;
|
||||
|
||||
/** Target seat ID for furniture struggles (null = not a furniture session) */
|
||||
@Nullable
|
||||
private String furnitureSeatId;
|
||||
|
||||
/** Resistance reduction per tick (computed from ticksPerResistance) */
|
||||
private final float resistancePerTick;
|
||||
|
||||
private final Random random = new Random();
|
||||
|
||||
// ==================== CONSTRUCTOR ====================
|
||||
|
||||
public ContinuousStruggleMiniGameState(
|
||||
UUID playerId,
|
||||
int targetResistance,
|
||||
boolean isLocked
|
||||
) {
|
||||
this(
|
||||
playerId,
|
||||
targetResistance,
|
||||
isLocked,
|
||||
null,
|
||||
DEFAULT_TICKS_PER_RESISTANCE
|
||||
);
|
||||
}
|
||||
|
||||
public ContinuousStruggleMiniGameState(
|
||||
UUID playerId,
|
||||
int targetResistance,
|
||||
boolean isLocked,
|
||||
Integer targetSlot
|
||||
) {
|
||||
this(
|
||||
playerId,
|
||||
targetResistance,
|
||||
isLocked,
|
||||
targetSlot,
|
||||
DEFAULT_TICKS_PER_RESISTANCE
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor with target slot and configurable rate.
|
||||
*
|
||||
* @param playerId Player UUID
|
||||
* @param targetResistance Current resistance
|
||||
* @param isLocked Whether the target is locked
|
||||
* @param targetSlot Target accessory slot (null = bind)
|
||||
* @param ticksPerResistance Ticks per 1 resistance point (from game rule)
|
||||
*/
|
||||
public ContinuousStruggleMiniGameState(
|
||||
UUID playerId,
|
||||
int targetResistance,
|
||||
boolean isLocked,
|
||||
Integer targetSlot,
|
||||
int ticksPerResistance
|
||||
) {
|
||||
this.sessionId = UUID.randomUUID();
|
||||
this.playerId = playerId;
|
||||
this.createdAt = System.currentTimeMillis();
|
||||
this.resistancePerTick = 1.0f / Math.max(1, ticksPerResistance);
|
||||
|
||||
this.currentResistance = targetResistance;
|
||||
this.maxResistance = targetResistance;
|
||||
this.isLocked = isLocked;
|
||||
this.targetSlot = targetSlot;
|
||||
|
||||
// Initialize direction
|
||||
this.currentDirection = Direction.random(random);
|
||||
this.accumulatedProgress = 0.0f;
|
||||
|
||||
// Initialize timers (will be set properly on first tick)
|
||||
this.lastDirectionChangeTick = 0;
|
||||
this.lastShockCheckTick = 0;
|
||||
this.lastKidnapperNotifyTick = 0;
|
||||
this.ticksUntilDirectionChange = randomDirectionChangeInterval();
|
||||
|
||||
// Initial state
|
||||
this.state = State.ACTIVE;
|
||||
this.shockStunTicksRemaining = 0;
|
||||
this.isHoldingCorrectKey = false;
|
||||
this.heldDirection = -1;
|
||||
}
|
||||
|
||||
// ==================== GETTERS ====================
|
||||
|
||||
public UUID getSessionId() {
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
public UUID getPlayerId() {
|
||||
return playerId;
|
||||
}
|
||||
|
||||
public Direction getCurrentDirection() {
|
||||
return currentDirection;
|
||||
}
|
||||
|
||||
public int getCurrentDirectionIndex() {
|
||||
return currentDirection.getIndex();
|
||||
}
|
||||
|
||||
public int getCurrentResistance() {
|
||||
return currentResistance;
|
||||
}
|
||||
|
||||
public int getMaxResistance() {
|
||||
return maxResistance;
|
||||
}
|
||||
|
||||
public float getProgressPercentage() {
|
||||
if (maxResistance == 0) return 0.0f;
|
||||
return 1.0f - ((float) currentResistance / (float) maxResistance);
|
||||
}
|
||||
|
||||
public State getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public boolean isLocked() {
|
||||
return isLocked;
|
||||
}
|
||||
|
||||
public Integer getTargetSlot() {
|
||||
return targetSlot;
|
||||
}
|
||||
|
||||
public boolean isAccessoryStruggle() {
|
||||
return targetSlot != null;
|
||||
}
|
||||
|
||||
/** Get the V2 target region (null for V1 sessions). */
|
||||
@Nullable
|
||||
public BodyRegionV2 getTargetRegion() {
|
||||
return targetRegion;
|
||||
}
|
||||
|
||||
/** Set the V2 target region. */
|
||||
public void setTargetRegion(@Nullable BodyRegionV2 region) {
|
||||
this.targetRegion = region;
|
||||
}
|
||||
|
||||
/** Whether this is a V2 region-based struggle session. */
|
||||
public boolean isV2Struggle() {
|
||||
return targetRegion != null;
|
||||
}
|
||||
|
||||
/** Get the furniture entity ID (0 = not a furniture session). */
|
||||
public int getFurnitureEntityId() {
|
||||
return furnitureEntityId;
|
||||
}
|
||||
|
||||
/** Get the furniture seat ID (null = not a furniture session). */
|
||||
@Nullable
|
||||
public String getFurnitureSeatId() {
|
||||
return furnitureSeatId;
|
||||
}
|
||||
|
||||
/** Set furniture struggle context. */
|
||||
public void setFurnitureContext(int entityId, String seatId) {
|
||||
this.furnitureEntityId = entityId;
|
||||
this.furnitureSeatId = seatId;
|
||||
}
|
||||
|
||||
/** Whether this is a furniture escape struggle session. */
|
||||
public boolean isFurnitureStruggle() {
|
||||
return furnitureEntityId != 0 && furnitureSeatId != null;
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return state == State.ACTIVE;
|
||||
}
|
||||
|
||||
public boolean isShocked() {
|
||||
return state == State.SHOCKED;
|
||||
}
|
||||
|
||||
public boolean isComplete() {
|
||||
return state == State.ESCAPED || state == State.CANCELLED;
|
||||
}
|
||||
|
||||
public boolean isExpired() {
|
||||
return System.currentTimeMillis() - createdAt > SESSION_TIMEOUT_MS;
|
||||
}
|
||||
|
||||
public boolean isHoldingCorrectKey() {
|
||||
return isHoldingCorrectKey;
|
||||
}
|
||||
|
||||
public int getHeldDirection() {
|
||||
return heldDirection;
|
||||
}
|
||||
|
||||
// ==================== STATE UPDATES ====================
|
||||
|
||||
/**
|
||||
* Update the held direction from client input.
|
||||
*
|
||||
* @param direction Direction index (-1 if not holding any key)
|
||||
* @param isHolding True if player is actively holding the key
|
||||
*/
|
||||
public void updateHeldDirection(int direction, boolean isHolding) {
|
||||
this.heldDirection = direction;
|
||||
this.isHoldingCorrectKey =
|
||||
isHolding && (direction == currentDirection.getIndex());
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a server tick.
|
||||
*
|
||||
* @param currentTick Current game tick
|
||||
* @return True if state changed significantly (needs client update)
|
||||
*/
|
||||
public TickResult tick(long currentTick) {
|
||||
if (isComplete()) {
|
||||
return TickResult.NO_CHANGE;
|
||||
}
|
||||
|
||||
// Handle shock stun
|
||||
if (state == State.SHOCKED) {
|
||||
shockStunTicksRemaining--;
|
||||
if (shockStunTicksRemaining <= 0) {
|
||||
state = State.ACTIVE;
|
||||
return TickResult.SHOCK_END;
|
||||
}
|
||||
return TickResult.NO_CHANGE;
|
||||
}
|
||||
|
||||
boolean needsUpdate = false;
|
||||
TickResult result = TickResult.NO_CHANGE;
|
||||
|
||||
// Direction change check
|
||||
ticksUntilDirectionChange--;
|
||||
if (ticksUntilDirectionChange <= 0) {
|
||||
changeDirection(currentTick);
|
||||
result = TickResult.DIRECTION_CHANGE;
|
||||
}
|
||||
|
||||
// Progress resistance if holding correct key
|
||||
if (isHoldingCorrectKey) {
|
||||
accumulatedProgress += resistancePerTick;
|
||||
|
||||
if (accumulatedProgress >= 1.0f) {
|
||||
int resistanceReduced = (int) accumulatedProgress;
|
||||
accumulatedProgress -= resistanceReduced;
|
||||
currentResistance = Math.max(
|
||||
0,
|
||||
currentResistance - resistanceReduced
|
||||
);
|
||||
|
||||
if (result == TickResult.NO_CHANGE) {
|
||||
result = TickResult.RESISTANCE_UPDATE;
|
||||
}
|
||||
|
||||
// Check for escape
|
||||
if (currentResistance <= 0) {
|
||||
state = State.ESCAPED;
|
||||
return TickResult.ESCAPED;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of a tick update.
|
||||
*/
|
||||
public enum TickResult {
|
||||
NO_CHANGE,
|
||||
DIRECTION_CHANGE,
|
||||
RESISTANCE_UPDATE,
|
||||
SHOCK_START,
|
||||
SHOCK_END,
|
||||
ESCAPED,
|
||||
KIDNAPPER_NOTIFY,
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if shock collar should trigger.
|
||||
*
|
||||
* @param currentTick Current game tick
|
||||
* @return True if shock should be triggered
|
||||
*/
|
||||
public boolean shouldTriggerShock(long currentTick) {
|
||||
if (state != State.ACTIVE) return false;
|
||||
if (
|
||||
currentTick - lastShockCheckTick < SHOCK_CHECK_INTERVAL_TICKS
|
||||
) return false;
|
||||
|
||||
lastShockCheckTick = currentTick;
|
||||
return random.nextInt(100) < SHOCK_PROBABILITY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a shock event.
|
||||
*/
|
||||
public void triggerShock() {
|
||||
if (state != State.ACTIVE) return;
|
||||
state = State.SHOCKED;
|
||||
shockStunTicksRemaining = SHOCK_STUN_TICKS;
|
||||
isHoldingCorrectKey = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if kidnappers should be notified.
|
||||
*
|
||||
* @param currentTick Current game tick
|
||||
* @return True if kidnappers should be notified
|
||||
*/
|
||||
public boolean shouldNotifyKidnappers(long currentTick) {
|
||||
if (state != State.ACTIVE || !isHoldingCorrectKey) return false;
|
||||
if (
|
||||
currentTick - lastKidnapperNotifyTick <
|
||||
KIDNAPPER_NOTIFY_INTERVAL_TICKS
|
||||
) return false;
|
||||
|
||||
lastKidnapperNotifyTick = currentTick;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if struggle sound should play.
|
||||
*
|
||||
* @param currentTick Current game tick
|
||||
* @return True if struggle sound should be played
|
||||
*/
|
||||
public boolean shouldPlayStruggleSound(long currentTick) {
|
||||
if (state != State.ACTIVE || !isHoldingCorrectKey) return false;
|
||||
if (
|
||||
currentTick - lastStruggleSoundTick < STRUGGLE_SOUND_INTERVAL_TICKS
|
||||
) return false;
|
||||
|
||||
lastStruggleSoundTick = currentTick;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the session.
|
||||
*/
|
||||
public void cancel() {
|
||||
state = State.CANCELLED;
|
||||
}
|
||||
|
||||
// ==================== PRIVATE HELPERS ====================
|
||||
|
||||
private void changeDirection(long currentTick) {
|
||||
Direction newDirection;
|
||||
do {
|
||||
newDirection = Direction.random(random);
|
||||
} while (newDirection == currentDirection);
|
||||
|
||||
currentDirection = newDirection;
|
||||
lastDirectionChangeTick = currentTick;
|
||||
ticksUntilDirectionChange = randomDirectionChangeInterval();
|
||||
|
||||
// LOW FIX: Always reset held key status to prevent lucky guess exploit
|
||||
// Player must release and re-press the key to react to direction change
|
||||
// even if they were already holding the new correct key
|
||||
isHoldingCorrectKey = false;
|
||||
}
|
||||
|
||||
private int randomDirectionChangeInterval() {
|
||||
return (
|
||||
DIRECTION_CHANGE_MIN_TICKS +
|
||||
random.nextInt(
|
||||
DIRECTION_CHANGE_MAX_TICKS - DIRECTION_CHANGE_MIN_TICKS + 1
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format(
|
||||
"ContinuousStruggleMiniGameState{session=%s, player=%s, dir=%s, resistance=%d/%d, state=%s}",
|
||||
sessionId.toString().substring(0, 8),
|
||||
playerId.toString().substring(0, 8),
|
||||
currentDirection,
|
||||
currentResistance,
|
||||
maxResistance,
|
||||
state
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.tiedup.remake.minigame;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.entities.EntityKidnapper;
|
||||
import com.tiedup.remake.entities.EntityMaid;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
/**
|
||||
* Helper for notifying nearby guards (Kidnappers, Maids, Traders) about
|
||||
* escape-related noise (struggle, lockpick, knife cutting).
|
||||
*
|
||||
* <p>Extracted from {@link MiniGameSessionManager} (M15 split) so that
|
||||
* any system can trigger guard alerts without depending on a session manager.
|
||||
*/
|
||||
public final class GuardNotificationHelper {
|
||||
|
||||
/**
|
||||
* Radius in blocks for struggle noise to alert nearby NPCs.
|
||||
* 32 blocks is the standard hearing range for mobs in Minecraft.
|
||||
* NPCs within this range will be notified of struggle attempts.
|
||||
*/
|
||||
private static final double STRUGGLE_NOISE_RADIUS = 32.0;
|
||||
|
||||
private GuardNotificationHelper() {}
|
||||
|
||||
/**
|
||||
* Notify nearby guards about escape noise (struggle, lockpick, knife cutting).
|
||||
* Public entry point for external systems (e.g. GenericKnife).
|
||||
*
|
||||
* @param player The player who is making noise
|
||||
*/
|
||||
public static void notifyNearbyGuards(ServerPlayer player) {
|
||||
notifyNearbyKidnappersOfStruggle(player);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify nearby guards (Kidnappers, Maids, Traders) when a player starts struggling.
|
||||
* This creates "noise" that guards can detect.
|
||||
*
|
||||
* Guards will only react if they have LINE OF SIGHT to the struggling prisoner.
|
||||
* If they see the prisoner, they will:
|
||||
* 1. Shock the prisoner (punishment)
|
||||
* 2. Approach to tighten their binds (reset resistance)
|
||||
*
|
||||
* @param player The player who started struggling
|
||||
*/
|
||||
public static void notifyNearbyKidnappersOfStruggle(ServerPlayer player) {
|
||||
if (player == null) return;
|
||||
|
||||
ServerLevel level = player.serverLevel();
|
||||
if (level == null) return;
|
||||
|
||||
int notifiedCount = 0;
|
||||
|
||||
// MEDIUM FIX: Performance optimization - use single entity search instead of 3
|
||||
// Old code did 3 separate searches for EntityKidnapper, EntityMaid, EntitySlaveTrader
|
||||
// with the SAME bounding box, causing 3x iterations over entities in the area.
|
||||
// New code does 1 search for LivingEntity and filters by instanceof.
|
||||
java.util.List<net.minecraft.world.entity.LivingEntity> nearbyEntities =
|
||||
level.getEntitiesOfClass(
|
||||
net.minecraft.world.entity.LivingEntity.class,
|
||||
player.getBoundingBox().inflate(STRUGGLE_NOISE_RADIUS),
|
||||
e -> e.isAlive()
|
||||
);
|
||||
|
||||
for (net.minecraft.world.entity.LivingEntity entity : nearbyEntities) {
|
||||
if (
|
||||
entity instanceof EntityKidnapper kidnapper &&
|
||||
!kidnapper.isTiedUp()
|
||||
) {
|
||||
kidnapper.onStruggleDetected(player);
|
||||
notifiedCount++;
|
||||
} else if (
|
||||
entity instanceof EntityMaid maid &&
|
||||
!maid.isTiedUp() &&
|
||||
!maid.isFreed()
|
||||
) {
|
||||
maid.onStruggleDetected(player);
|
||||
notifiedCount++;
|
||||
} else if (
|
||||
entity instanceof
|
||||
com.tiedup.remake.entities.EntitySlaveTrader trader
|
||||
) {
|
||||
trader.onStruggleDetected(player);
|
||||
notifiedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (notifiedCount > 0) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[GuardNotificationHelper] Notified {} guards about struggle from {}",
|
||||
notifiedCount,
|
||||
player.getName().getString()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
package com.tiedup.remake.minigame;
|
||||
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Phase 2: Server-side state for a Lockpick mini-game session.
|
||||
*
|
||||
* Tracks:
|
||||
* - Session UUID (anti-cheat)
|
||||
* - Sweet spot position and width
|
||||
* - Current lockpick position
|
||||
* - Remaining lockpick uses
|
||||
*/
|
||||
public class LockpickMiniGameState {
|
||||
|
||||
private final UUID sessionId;
|
||||
private final UUID playerId;
|
||||
private final long createdAt;
|
||||
private final int targetSlot;
|
||||
|
||||
/**
|
||||
* Session timeout in milliseconds (2 minutes)
|
||||
*/
|
||||
private static final long SESSION_TIMEOUT_MS = 2L * 60L * 1000L;
|
||||
|
||||
/**
|
||||
* Position of the sweet spot center (0.0 to 1.0)
|
||||
* BUG FIX: Removed final to allow regeneration after failed attempts.
|
||||
*/
|
||||
private float sweetSpotCenter;
|
||||
|
||||
/**
|
||||
* Width of the sweet spot (0.0 to 1.0, e.g. 0.15 = 15%)
|
||||
*/
|
||||
private final float sweetSpotWidth;
|
||||
|
||||
/**
|
||||
* Current lockpick position (0.0 to 1.0)
|
||||
*/
|
||||
private float currentPosition;
|
||||
|
||||
/**
|
||||
* Number of remaining lockpick uses
|
||||
*/
|
||||
private int remainingUses;
|
||||
|
||||
/**
|
||||
* Whether the session is complete
|
||||
*/
|
||||
private boolean complete;
|
||||
|
||||
/**
|
||||
* Whether the lock was successfully picked
|
||||
*/
|
||||
private boolean success;
|
||||
|
||||
private final Random random = new Random();
|
||||
|
||||
public LockpickMiniGameState(
|
||||
UUID playerId,
|
||||
int targetSlot,
|
||||
float sweetSpotWidth
|
||||
) {
|
||||
this.sessionId = UUID.randomUUID();
|
||||
this.playerId = playerId;
|
||||
this.targetSlot = targetSlot;
|
||||
this.createdAt = System.currentTimeMillis();
|
||||
|
||||
// Generate random sweet spot position
|
||||
// Keep it away from edges (at least width/2 from 0 and 1)
|
||||
float minCenter = sweetSpotWidth / 2;
|
||||
float maxCenter = 1.0f - sweetSpotWidth / 2;
|
||||
this.sweetSpotCenter =
|
||||
minCenter + random.nextFloat() * (maxCenter - minCenter);
|
||||
this.sweetSpotWidth = sweetSpotWidth;
|
||||
|
||||
// Start position at random location
|
||||
this.currentPosition = random.nextFloat();
|
||||
this.remainingUses = 5; // Default, will be updated by caller
|
||||
this.complete = false;
|
||||
this.success = false;
|
||||
}
|
||||
|
||||
// ==================== GETTERS ====================
|
||||
|
||||
public UUID getSessionId() {
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
public UUID getPlayerId() {
|
||||
return playerId;
|
||||
}
|
||||
|
||||
public int getTargetSlot() {
|
||||
return targetSlot;
|
||||
}
|
||||
|
||||
public float getSweetSpotCenter() {
|
||||
return sweetSpotCenter;
|
||||
}
|
||||
|
||||
public float getSweetSpotWidth() {
|
||||
return sweetSpotWidth;
|
||||
}
|
||||
|
||||
public float getCurrentPosition() {
|
||||
return currentPosition;
|
||||
}
|
||||
|
||||
public int getRemainingUses() {
|
||||
return remainingUses;
|
||||
}
|
||||
|
||||
public boolean isComplete() {
|
||||
return complete;
|
||||
}
|
||||
|
||||
public boolean isSuccess() {
|
||||
return success;
|
||||
}
|
||||
|
||||
public boolean isExpired() {
|
||||
return System.currentTimeMillis() - createdAt > SESSION_TIMEOUT_MS;
|
||||
}
|
||||
|
||||
// ==================== SETTERS ====================
|
||||
|
||||
public void setRemainingUses(int uses) {
|
||||
this.remainingUses = uses;
|
||||
}
|
||||
|
||||
public void setCurrentPosition(float position) {
|
||||
this.currentPosition = Math.max(0.0f, Math.min(1.0f, position));
|
||||
}
|
||||
|
||||
// ==================== GAME LOGIC ====================
|
||||
|
||||
/**
|
||||
* Move the lockpick position.
|
||||
*
|
||||
* @param delta Amount to move (-1.0 to 1.0, scaled by sensitivity)
|
||||
*/
|
||||
public void move(float delta) {
|
||||
currentPosition = Math.max(
|
||||
0.0f,
|
||||
Math.min(1.0f, currentPosition + delta)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if current position is within the sweet spot.
|
||||
*/
|
||||
public boolean isInSweetSpot() {
|
||||
float minPos = sweetSpotCenter - sweetSpotWidth / 2;
|
||||
float maxPos = sweetSpotCenter + sweetSpotWidth / 2;
|
||||
return currentPosition >= minPos && currentPosition <= maxPos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to pick the lock at current position.
|
||||
*
|
||||
* @return Result of the attempt
|
||||
*/
|
||||
public PickAttemptResult attemptPick() {
|
||||
if (complete) {
|
||||
return PickAttemptResult.SESSION_COMPLETE;
|
||||
}
|
||||
|
||||
if (isInSweetSpot()) {
|
||||
// Success!
|
||||
complete = true;
|
||||
success = true;
|
||||
return PickAttemptResult.SUCCESS;
|
||||
}
|
||||
|
||||
// Failed - use up a lockpick
|
||||
remainingUses--;
|
||||
|
||||
if (remainingUses <= 0) {
|
||||
// Out of lockpicks
|
||||
complete = true;
|
||||
return PickAttemptResult.OUT_OF_PICKS;
|
||||
}
|
||||
|
||||
// Generate new sweet spot position for next attempt
|
||||
regenerateSweetSpot();
|
||||
|
||||
return PickAttemptResult.MISSED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Regenerate sweet spot position (called after failed attempt).
|
||||
* BUG FIX: Actually regenerate the sweet spot to prevent players from memorizing position.
|
||||
* Problem: Players could memorize position and lockpicking became trivial.
|
||||
*/
|
||||
private void regenerateSweetSpot() {
|
||||
// Generate new random sweet spot position
|
||||
// Keep it away from edges (at least width/2 from 0 and 1)
|
||||
float minCenter = sweetSpotWidth / 2;
|
||||
float maxCenter = 1.0f - sweetSpotWidth / 2;
|
||||
this.sweetSpotCenter =
|
||||
minCenter + random.nextFloat() * (maxCenter - minCenter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get distance from current position to sweet spot center.
|
||||
* Used for feedback (closer = warmer).
|
||||
*
|
||||
* @return Distance as 0.0 (exact) to 1.0 (max distance)
|
||||
*/
|
||||
public float getDistanceToSweetSpot() {
|
||||
return Math.abs(currentPosition - sweetSpotCenter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get warmth feedback (how close to sweet spot).
|
||||
*
|
||||
* @return Warmth level: COLD, WARM, HOT, IN_SPOT
|
||||
*/
|
||||
public WarmthLevel getWarmthLevel() {
|
||||
if (isInSweetSpot()) {
|
||||
return WarmthLevel.IN_SPOT;
|
||||
}
|
||||
|
||||
float distance = getDistanceToSweetSpot();
|
||||
if (distance < sweetSpotWidth) {
|
||||
return WarmthLevel.HOT;
|
||||
} else if (distance < sweetSpotWidth * 2) {
|
||||
return WarmthLevel.WARM;
|
||||
}
|
||||
return WarmthLevel.COLD;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of a pick attempt.
|
||||
*/
|
||||
public enum PickAttemptResult {
|
||||
/**
|
||||
* Successfully picked the lock!
|
||||
*/
|
||||
SUCCESS,
|
||||
|
||||
/**
|
||||
* Missed the sweet spot, lockpick used
|
||||
*/
|
||||
MISSED,
|
||||
|
||||
/**
|
||||
* Ran out of lockpicks
|
||||
*/
|
||||
OUT_OF_PICKS,
|
||||
|
||||
/**
|
||||
* Session already complete
|
||||
*/
|
||||
SESSION_COMPLETE,
|
||||
}
|
||||
|
||||
/**
|
||||
* Warmth feedback level.
|
||||
*/
|
||||
public enum WarmthLevel {
|
||||
COLD,
|
||||
WARM,
|
||||
HOT,
|
||||
IN_SPOT,
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format(
|
||||
"LockpickMiniGameState{session=%s, player=%s, slot=%d, pos=%.2f, sweet=%.2f(w=%.2f), uses=%d}",
|
||||
sessionId.toString().substring(0, 8),
|
||||
playerId.toString().substring(0, 8),
|
||||
targetSlot,
|
||||
currentPosition,
|
||||
sweetSpotCenter,
|
||||
sweetSpotWidth,
|
||||
remainingUses
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package com.tiedup.remake.minigame;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.sounds.SoundEvents;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Manages lockpick mini-game sessions.
|
||||
*
|
||||
* <p>Extracted from {@link MiniGameSessionManager} (M15 split) to give lockpick
|
||||
* sessions their own focused manager. Singleton, thread-safe via ConcurrentHashMap.
|
||||
*/
|
||||
public class LockpickSessionManager {
|
||||
|
||||
private static final LockpickSessionManager INSTANCE =
|
||||
new LockpickSessionManager();
|
||||
|
||||
/**
|
||||
* Active lockpick mini-game sessions by player UUID
|
||||
*/
|
||||
private final Map<UUID, LockpickMiniGameState> lockpickSessions =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
private LockpickSessionManager() {}
|
||||
|
||||
public static LockpickSessionManager getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a new lockpick session for a player.
|
||||
* If player already has an active session, it will be replaced (handles ESC cancel case).
|
||||
*
|
||||
* @param player The server player
|
||||
* @param targetSlot The bondage slot being picked
|
||||
* @param sweetSpotWidth The width of the sweet spot (based on tool)
|
||||
* @return The new session
|
||||
*/
|
||||
public LockpickMiniGameState startLockpickSession(
|
||||
ServerPlayer player,
|
||||
int targetSlot,
|
||||
float sweetSpotWidth
|
||||
) {
|
||||
UUID playerId = player.getUUID();
|
||||
|
||||
// Check for existing session - remove it (handles ESC cancel case)
|
||||
LockpickMiniGameState existing = lockpickSessions.get(playerId);
|
||||
if (existing != null) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[LockpickSessionManager] Replacing existing lockpick session for {}",
|
||||
player.getName().getString()
|
||||
);
|
||||
lockpickSessions.remove(playerId);
|
||||
}
|
||||
|
||||
// Create new session
|
||||
LockpickMiniGameState session = new LockpickMiniGameState(
|
||||
playerId,
|
||||
targetSlot,
|
||||
sweetSpotWidth
|
||||
);
|
||||
lockpickSessions.put(playerId, session);
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[LockpickSessionManager] Started lockpick session {} for {} (slot: {}, width: {}%)",
|
||||
session.getSessionId().toString().substring(0, 8),
|
||||
player.getName().getString(),
|
||||
targetSlot,
|
||||
(int) (sweetSpotWidth * 100)
|
||||
);
|
||||
|
||||
// Notify nearby guards about lockpicking attempt
|
||||
GuardNotificationHelper.notifyNearbyKidnappersOfStruggle(player);
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Play lockpick attempt sound and notify guards.
|
||||
* Called when player attempts to pick (tests position).
|
||||
*
|
||||
* @param player The player attempting to pick
|
||||
*/
|
||||
public void onLockpickAttempt(ServerPlayer player) {
|
||||
// Play metallic clicking sound
|
||||
player
|
||||
.serverLevel()
|
||||
.playSound(
|
||||
null,
|
||||
player.getX(),
|
||||
player.getY(),
|
||||
player.getZ(),
|
||||
SoundEvents.CHAIN_HIT,
|
||||
SoundSource.PLAYERS,
|
||||
0.6f,
|
||||
1.2f + player.getRandom().nextFloat() * 0.3f
|
||||
);
|
||||
|
||||
// Notify guards
|
||||
GuardNotificationHelper.notifyNearbyKidnappersOfStruggle(player);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active lockpick session for a player.
|
||||
*
|
||||
* @param playerId The player UUID
|
||||
* @return The session, or null if none active
|
||||
*/
|
||||
@Nullable
|
||||
public LockpickMiniGameState getLockpickSession(UUID playerId) {
|
||||
LockpickMiniGameState session = lockpickSessions.get(playerId);
|
||||
if (session != null && session.isExpired()) {
|
||||
lockpickSessions.remove(playerId);
|
||||
return null;
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a lockpick session.
|
||||
*
|
||||
* @param playerId The player UUID
|
||||
* @param sessionId The session UUID to validate
|
||||
* @return true if session is valid and active
|
||||
*/
|
||||
public boolean validateLockpickSession(UUID playerId, UUID sessionId) {
|
||||
LockpickMiniGameState session = getLockpickSession(playerId);
|
||||
if (session == null) {
|
||||
return false;
|
||||
}
|
||||
return session.getSessionId().equals(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* End a lockpick session.
|
||||
*
|
||||
* @param playerId The player UUID
|
||||
* @param success Whether the session ended in success
|
||||
*/
|
||||
public void endLockpickSession(UUID playerId, boolean success) {
|
||||
LockpickMiniGameState session = lockpickSessions.remove(playerId);
|
||||
if (session != null) {
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[LockpickSessionManager] Ended lockpick session for player {} (success: {})",
|
||||
playerId.toString().substring(0, 8),
|
||||
success
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up all lockpick sessions for a player (called on disconnect).
|
||||
*
|
||||
* @param playerId The player UUID
|
||||
*/
|
||||
public void cleanupPlayer(UUID playerId) {
|
||||
lockpickSessions.remove(playerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Periodic cleanup of expired lockpick sessions.
|
||||
* Should be called from server tick handler.
|
||||
*/
|
||||
public void tickCleanup(long currentTick) {
|
||||
// Only run every 100 ticks (5 seconds)
|
||||
if (currentTick % 100 != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
lockpickSessions
|
||||
.entrySet()
|
||||
.removeIf(entry -> entry.getValue().isExpired());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count of active lockpick sessions (for debugging).
|
||||
*/
|
||||
public int getActiveSessionCount() {
|
||||
return lockpickSessions.size();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.tiedup.remake.minigame;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Lightweight facade over {@link LockpickSessionManager} and
|
||||
* {@link StruggleSessionManager}.
|
||||
*
|
||||
* <p>After the M15 split, this class only delegates cross-cutting concerns
|
||||
* (cleanup on disconnect, aggregate session count). Callers that need a
|
||||
* specific session type should use the sub-manager directly.
|
||||
*/
|
||||
public class MiniGameSessionManager {
|
||||
|
||||
private static final MiniGameSessionManager INSTANCE =
|
||||
new MiniGameSessionManager();
|
||||
|
||||
private MiniGameSessionManager() {}
|
||||
|
||||
public static MiniGameSessionManager getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up all sessions for a player (called on disconnect).
|
||||
* Delegates to both sub-managers.
|
||||
*
|
||||
* @param playerId The player UUID
|
||||
*/
|
||||
public void cleanupPlayer(UUID playerId) {
|
||||
LockpickSessionManager.getInstance().cleanupPlayer(playerId);
|
||||
StruggleSessionManager.getInstance().cleanupPlayer(playerId);
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MiniGameSessionManager] Cleaned up all sessions for player {}",
|
||||
playerId.toString().substring(0, 8)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count of active sessions across both sub-managers (for debugging).
|
||||
*/
|
||||
public int getActiveSessionCount() {
|
||||
return LockpickSessionManager.getInstance().getActiveSessionCount()
|
||||
+ StruggleSessionManager.getInstance().getActiveSessionCount();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,842 @@
|
||||
package com.tiedup.remake.minigame;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.items.ItemShockCollar;
|
||||
import com.tiedup.remake.items.base.ItemBind;
|
||||
import com.tiedup.remake.items.base.ItemCollar;
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
|
||||
import com.tiedup.remake.minigame.ContinuousStruggleMiniGameState.TickResult;
|
||||
import com.tiedup.remake.minigame.ContinuousStruggleMiniGameState.UpdateType;
|
||||
import com.tiedup.remake.network.ModNetwork;
|
||||
import com.tiedup.remake.network.minigame.PacketContinuousStruggleState;
|
||||
import com.tiedup.remake.network.sync.SyncManager;
|
||||
import com.tiedup.remake.state.PlayerBindState;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.sounds.SoundEvents;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Manages continuous struggle mini-game sessions.
|
||||
*
|
||||
* <p>Extracted from {@link MiniGameSessionManager} (M15 split). Handles all
|
||||
* struggle variants: bind struggle, accessory struggle, V2 region struggle,
|
||||
* and furniture escape struggle.
|
||||
*
|
||||
* <p>Singleton, thread-safe via ConcurrentHashMap.
|
||||
*/
|
||||
public class StruggleSessionManager {
|
||||
|
||||
private static final StruggleSessionManager INSTANCE =
|
||||
new StruggleSessionManager();
|
||||
|
||||
/**
|
||||
* Active continuous struggle mini-game sessions by player UUID
|
||||
*/
|
||||
private final Map<
|
||||
UUID,
|
||||
ContinuousStruggleMiniGameState
|
||||
> continuousSessions = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Mapping from legacy V1 slot indices to V2 BodyRegionV2.
|
||||
* Used to convert V1 session targetSlot ordinals to V2 regions.
|
||||
* Index: 0=ARMS, 1=MOUTH, 2=EYES, 3=EARS, 4=NECK, 5=TORSO, 6=HANDS.
|
||||
*/
|
||||
private static final BodyRegionV2[] SLOT_TO_REGION = {
|
||||
BodyRegionV2.ARMS, // 0 = BIND
|
||||
BodyRegionV2.MOUTH, // 1 = GAG
|
||||
BodyRegionV2.EYES, // 2 = BLINDFOLD
|
||||
BodyRegionV2.EARS, // 3 = EARPLUGS
|
||||
BodyRegionV2.NECK, // 4 = COLLAR
|
||||
BodyRegionV2.TORSO, // 5 = CLOTHES
|
||||
BodyRegionV2.HANDS, // 6 = MITTENS
|
||||
};
|
||||
|
||||
private StruggleSessionManager() {}
|
||||
|
||||
public static StruggleSessionManager getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
// ==================== SESSION START VARIANTS ====================
|
||||
|
||||
/**
|
||||
* Start a new continuous struggle session for a player.
|
||||
*
|
||||
* @param player The server player
|
||||
* @param targetResistance Current bind resistance
|
||||
* @param isLocked Whether the bind is locked
|
||||
* @return The new session
|
||||
*/
|
||||
public ContinuousStruggleMiniGameState startContinuousStruggleSession(
|
||||
ServerPlayer player,
|
||||
int targetResistance,
|
||||
boolean isLocked
|
||||
) {
|
||||
UUID playerId = player.getUUID();
|
||||
|
||||
// Remove any existing continuous session
|
||||
ContinuousStruggleMiniGameState existing = continuousSessions.get(
|
||||
playerId
|
||||
);
|
||||
if (existing != null) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[StruggleSessionManager] Replacing existing continuous struggle session for {}",
|
||||
player.getName().getString()
|
||||
);
|
||||
continuousSessions.remove(playerId);
|
||||
}
|
||||
|
||||
// Create new session with configurable rate
|
||||
int ticksPerResistance =
|
||||
com.tiedup.remake.core.SettingsAccessor.getStruggleContinuousRate(
|
||||
player.level().getGameRules()
|
||||
);
|
||||
ContinuousStruggleMiniGameState session =
|
||||
new ContinuousStruggleMiniGameState(
|
||||
playerId,
|
||||
targetResistance,
|
||||
isLocked,
|
||||
null,
|
||||
ticksPerResistance
|
||||
);
|
||||
continuousSessions.put(playerId, session);
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[StruggleSessionManager] Started continuous struggle session {} for {} (resistance: {}, locked: {})",
|
||||
session.getSessionId().toString().substring(0, 8),
|
||||
player.getName().getString(),
|
||||
targetResistance,
|
||||
isLocked
|
||||
);
|
||||
|
||||
// Set struggle animation state and sync to client
|
||||
PlayerBindState state = PlayerBindState.getInstance(player);
|
||||
if (state != null) {
|
||||
state.setStruggling(true, player.level().getGameTime());
|
||||
SyncManager.syncStruggleState(player);
|
||||
}
|
||||
|
||||
// Notify nearby kidnappers
|
||||
GuardNotificationHelper.notifyNearbyKidnappersOfStruggle(player);
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a new continuous struggle session for an accessory.
|
||||
*
|
||||
* @param player The server player
|
||||
* @param targetSlot Target accessory slot ordinal
|
||||
* @param lockResistance Current lock resistance
|
||||
* @return The new session
|
||||
*/
|
||||
public ContinuousStruggleMiniGameState startContinuousAccessoryStruggleSession(
|
||||
ServerPlayer player,
|
||||
int targetSlot,
|
||||
int lockResistance
|
||||
) {
|
||||
UUID playerId = player.getUUID();
|
||||
|
||||
// Remove any existing session
|
||||
ContinuousStruggleMiniGameState existing = continuousSessions.get(
|
||||
playerId
|
||||
);
|
||||
if (existing != null) {
|
||||
continuousSessions.remove(playerId);
|
||||
}
|
||||
|
||||
// Create new session with target slot and configurable rate
|
||||
int ticksPerResistance =
|
||||
com.tiedup.remake.core.SettingsAccessor.getStruggleContinuousRate(
|
||||
player.level().getGameRules()
|
||||
);
|
||||
ContinuousStruggleMiniGameState session =
|
||||
new ContinuousStruggleMiniGameState(
|
||||
playerId,
|
||||
lockResistance,
|
||||
true,
|
||||
targetSlot,
|
||||
ticksPerResistance
|
||||
);
|
||||
continuousSessions.put(playerId, session);
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[StruggleSessionManager] Started continuous accessory struggle session {} for {} (slot: {}, resistance: {})",
|
||||
session.getSessionId().toString().substring(0, 8),
|
||||
player.getName().getString(),
|
||||
targetSlot,
|
||||
lockResistance
|
||||
);
|
||||
|
||||
// Set struggle animation state and sync to client
|
||||
PlayerBindState state = PlayerBindState.getInstance(player);
|
||||
if (state != null) {
|
||||
state.setStruggling(true, player.level().getGameTime());
|
||||
SyncManager.syncStruggleState(player);
|
||||
}
|
||||
|
||||
// Notify nearby kidnappers
|
||||
GuardNotificationHelper.notifyNearbyKidnappersOfStruggle(player);
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a new continuous struggle session for a V2 bondage item.
|
||||
*
|
||||
* @param player The server player
|
||||
* @param targetRegion V2 body region to struggle against
|
||||
* @param targetResistance Current resistance value
|
||||
* @param isLocked Whether the item is locked
|
||||
* @return The new session, or null if creation failed
|
||||
*/
|
||||
public ContinuousStruggleMiniGameState startV2StruggleSession(
|
||||
ServerPlayer player,
|
||||
com.tiedup.remake.v2.BodyRegionV2 targetRegion,
|
||||
int targetResistance,
|
||||
boolean isLocked
|
||||
) {
|
||||
UUID playerId = player.getUUID();
|
||||
|
||||
// RISK-001 fix: reject if an active session already exists (prevents direction re-roll exploit)
|
||||
ContinuousStruggleMiniGameState existing = continuousSessions.get(
|
||||
playerId
|
||||
);
|
||||
if (existing != null) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[StruggleSessionManager] Rejected V2 session: active session already exists for {}",
|
||||
player.getName().getString()
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
int ticksPerResistance =
|
||||
com.tiedup.remake.core.SettingsAccessor.getStruggleContinuousRate(
|
||||
player.level().getGameRules()
|
||||
);
|
||||
ContinuousStruggleMiniGameState session =
|
||||
new ContinuousStruggleMiniGameState(
|
||||
playerId,
|
||||
targetResistance,
|
||||
isLocked,
|
||||
null,
|
||||
ticksPerResistance
|
||||
);
|
||||
session.setTargetRegion(targetRegion);
|
||||
continuousSessions.put(playerId, session);
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[StruggleSessionManager] Started V2 struggle session {} for {} (region: {}, resistance: {}, locked: {})",
|
||||
session.getSessionId().toString().substring(0, 8),
|
||||
player.getName().getString(),
|
||||
targetRegion.name(),
|
||||
targetResistance,
|
||||
isLocked
|
||||
);
|
||||
|
||||
// Set struggle animation state and sync to client
|
||||
PlayerBindState state = PlayerBindState.getInstance(player);
|
||||
if (state != null) {
|
||||
state.setStruggling(true, player.level().getGameTime());
|
||||
SyncManager.syncStruggleState(player);
|
||||
}
|
||||
|
||||
GuardNotificationHelper.notifyNearbyKidnappersOfStruggle(player);
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a new continuous struggle session for a furniture seat escape.
|
||||
*
|
||||
* <p>The session behaves identically to a V2 struggle (direction-hold to
|
||||
* reduce resistance) but on completion it unlocks the seat and dismounts
|
||||
* the player instead of removing a bondage item.</p>
|
||||
*
|
||||
* @param player The server player (must be seated and locked)
|
||||
* @param furnitureEntityId Entity ID of the furniture
|
||||
* @param seatId The locked seat ID
|
||||
* @param totalDifficulty Combined resistance (seat base + item bonus)
|
||||
* @return The new session, or null if creation failed
|
||||
*/
|
||||
public ContinuousStruggleMiniGameState startFurnitureStruggleSession(
|
||||
ServerPlayer player,
|
||||
int furnitureEntityId,
|
||||
String seatId,
|
||||
int totalDifficulty
|
||||
) {
|
||||
UUID playerId = player.getUUID();
|
||||
|
||||
// Reject if an active session already exists
|
||||
ContinuousStruggleMiniGameState existing = continuousSessions.get(playerId);
|
||||
if (existing != null) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[StruggleSessionManager] Rejected furniture session: active session already exists for {}",
|
||||
player.getName().getString()
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
int ticksPerResistance =
|
||||
com.tiedup.remake.core.SettingsAccessor.getStruggleContinuousRate(
|
||||
player.level().getGameRules()
|
||||
);
|
||||
ContinuousStruggleMiniGameState session =
|
||||
new ContinuousStruggleMiniGameState(
|
||||
playerId,
|
||||
totalDifficulty,
|
||||
true, // furniture seats are always "locked" in context
|
||||
null,
|
||||
ticksPerResistance
|
||||
);
|
||||
session.setFurnitureContext(furnitureEntityId, seatId);
|
||||
continuousSessions.put(playerId, session);
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[StruggleSessionManager] Started furniture struggle session {} for {} (entity: {}, seat: '{}', difficulty: {})",
|
||||
session.getSessionId().toString().substring(0, 8),
|
||||
player.getName().getString(),
|
||||
furnitureEntityId,
|
||||
seatId,
|
||||
totalDifficulty
|
||||
);
|
||||
|
||||
// Set struggle animation state and sync to client
|
||||
PlayerBindState state = PlayerBindState.getInstance(player);
|
||||
if (state != null) {
|
||||
state.setStruggling(true, player.level().getGameTime());
|
||||
SyncManager.syncStruggleState(player);
|
||||
}
|
||||
|
||||
// Play struggle loop sound from furniture definition (plays once on start;
|
||||
// true looping sound would require client-side sound management -- future scope)
|
||||
net.minecraft.world.entity.Entity furnitureEntity = player.level().getEntity(furnitureEntityId);
|
||||
if (furnitureEntity instanceof com.tiedup.remake.v2.furniture.EntityFurniture furniture) {
|
||||
com.tiedup.remake.v2.furniture.FurnitureDefinition def = furniture.getDefinition();
|
||||
if (def != null && def.feedback().struggleLoopSound() != null) {
|
||||
player.level().playSound(null, player.getX(), player.getY(), player.getZ(),
|
||||
net.minecraft.sounds.SoundEvent.createVariableRangeEvent(def.feedback().struggleLoopSound()),
|
||||
SoundSource.PLAYERS, 0.6f, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
GuardNotificationHelper.notifyNearbyKidnappersOfStruggle(player);
|
||||
return session;
|
||||
}
|
||||
|
||||
// ==================== SESSION QUERY ====================
|
||||
|
||||
/**
|
||||
* Get active continuous struggle session for a player.
|
||||
*
|
||||
* @param playerId The player UUID
|
||||
* @return The session, or null if none active
|
||||
*/
|
||||
@Nullable
|
||||
public ContinuousStruggleMiniGameState getContinuousStruggleSession(
|
||||
UUID playerId
|
||||
) {
|
||||
ContinuousStruggleMiniGameState session = continuousSessions.get(
|
||||
playerId
|
||||
);
|
||||
if (session != null && session.isExpired()) {
|
||||
continuousSessions.remove(playerId);
|
||||
return null;
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a continuous struggle session.
|
||||
*
|
||||
* @param playerId The player UUID
|
||||
* @param sessionId The session UUID to validate
|
||||
* @return true if session is valid and active
|
||||
*/
|
||||
public boolean validateContinuousStruggleSession(
|
||||
UUID playerId,
|
||||
UUID sessionId
|
||||
) {
|
||||
ContinuousStruggleMiniGameState session = getContinuousStruggleSession(
|
||||
playerId
|
||||
);
|
||||
if (session == null) {
|
||||
return false;
|
||||
}
|
||||
return session.getSessionId().equals(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* End a continuous struggle session.
|
||||
*
|
||||
* @param playerId The player UUID
|
||||
* @param success Whether the session ended in success (escape)
|
||||
*/
|
||||
public void endContinuousStruggleSession(UUID playerId, boolean success) {
|
||||
ContinuousStruggleMiniGameState session = continuousSessions.remove(
|
||||
playerId
|
||||
);
|
||||
if (session != null) {
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[StruggleSessionManager] Ended continuous struggle session for player {} (success: {})",
|
||||
playerId.toString().substring(0, 8),
|
||||
success
|
||||
);
|
||||
|
||||
// Clear struggle animation state
|
||||
// We need to find the player to clear the animation
|
||||
// This will be handled by the caller if they have access to the player
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== TICK ====================
|
||||
|
||||
/**
|
||||
* Tick all continuous struggle sessions.
|
||||
* Should be called every server tick.
|
||||
*
|
||||
* @param server The server instance for player lookups
|
||||
* @param currentTick Current game tick
|
||||
*/
|
||||
public void tickContinuousSessions(
|
||||
net.minecraft.server.MinecraftServer server,
|
||||
long currentTick
|
||||
) {
|
||||
if (continuousSessions.isEmpty()) return;
|
||||
|
||||
// Collect sessions to process (avoid ConcurrentModificationException)
|
||||
List<Map.Entry<UUID, ContinuousStruggleMiniGameState>> toProcess =
|
||||
new ArrayList<>(continuousSessions.entrySet());
|
||||
|
||||
for (Map.Entry<
|
||||
UUID,
|
||||
ContinuousStruggleMiniGameState
|
||||
> entry : toProcess) {
|
||||
UUID playerId = entry.getKey();
|
||||
ContinuousStruggleMiniGameState session = entry.getValue();
|
||||
|
||||
// Get player
|
||||
ServerPlayer player = server.getPlayerList().getPlayer(playerId);
|
||||
if (player == null) {
|
||||
// Player disconnected, clean up
|
||||
continuousSessions.remove(playerId);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for expired/completed sessions
|
||||
if (session.isExpired() || session.isComplete()) {
|
||||
continuousSessions.remove(playerId);
|
||||
clearStruggleAnimation(player);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Tick the session
|
||||
TickResult result = session.tick(currentTick);
|
||||
|
||||
// Handle tick result
|
||||
switch (result) {
|
||||
case DIRECTION_CHANGE -> {
|
||||
sendContinuousStruggleUpdate(
|
||||
player,
|
||||
session,
|
||||
UpdateType.DIRECTION_CHANGE
|
||||
);
|
||||
}
|
||||
case RESISTANCE_UPDATE -> {
|
||||
// Update actual bind resistance
|
||||
updateBindResistance(player, session);
|
||||
// Send update to client immediately for real-time feedback
|
||||
sendContinuousStruggleUpdate(
|
||||
player,
|
||||
session,
|
||||
UpdateType.RESISTANCE_UPDATE
|
||||
);
|
||||
}
|
||||
case ESCAPED -> {
|
||||
handleStruggleEscape(player, session);
|
||||
}
|
||||
case NO_CHANGE -> {
|
||||
// No action needed
|
||||
}
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
|
||||
// Check shock collar (separate from tick result)
|
||||
if (
|
||||
shouldCheckShockCollar(player) &&
|
||||
session.shouldTriggerShock(currentTick)
|
||||
) {
|
||||
handleShockCollar(player, session);
|
||||
}
|
||||
|
||||
// Check kidnapper notification
|
||||
if (session.shouldNotifyKidnappers(currentTick)) {
|
||||
GuardNotificationHelper.notifyNearbyKidnappersOfStruggle(player);
|
||||
}
|
||||
|
||||
// Play struggle sound periodically
|
||||
if (session.shouldPlayStruggleSound(currentTick)) {
|
||||
playStruggleSound(player);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== PRIVATE HELPERS ====================
|
||||
|
||||
/**
|
||||
* Play struggle sound for player and nearby entities.
|
||||
*/
|
||||
private void playStruggleSound(ServerPlayer player) {
|
||||
// Play leather creaking sound - audible to player and nearby
|
||||
player
|
||||
.serverLevel()
|
||||
.playSound(
|
||||
null, // null = all players can hear
|
||||
player.getX(),
|
||||
player.getY(),
|
||||
player.getZ(),
|
||||
SoundEvents.ARMOR_EQUIP_LEATHER,
|
||||
SoundSource.PLAYERS,
|
||||
0.8f, // volume
|
||||
0.9f + player.getRandom().nextFloat() * 0.2f // slight pitch variation
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if shock collar check is applicable for this player.
|
||||
*/
|
||||
private boolean shouldCheckShockCollar(ServerPlayer player) {
|
||||
ItemStack collar = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK);
|
||||
if (collar.isEmpty()) return false;
|
||||
|
||||
// Only shock collars can trigger during struggle
|
||||
if (!(collar.getItem() instanceof ItemShockCollar)) return false;
|
||||
|
||||
// Must be locked
|
||||
if (collar.getItem() instanceof ItemCollar collarItem) {
|
||||
return collarItem.isLocked(collar);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle shock collar trigger.
|
||||
*/
|
||||
private void handleShockCollar(
|
||||
ServerPlayer player,
|
||||
ContinuousStruggleMiniGameState session
|
||||
) {
|
||||
PlayerBindState state = PlayerBindState.getInstance(player);
|
||||
if (state != null) {
|
||||
state.shockKidnapped(" (Your struggle was interrupted!)", 2.0f);
|
||||
}
|
||||
|
||||
session.triggerShock();
|
||||
sendContinuousStruggleUpdate(player, session, UpdateType.SHOCK);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[StruggleSessionManager] Shock collar triggered for {} during struggle",
|
||||
player.getName().getString()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the actual bind resistance based on session state.
|
||||
*/
|
||||
private void updateBindResistance(
|
||||
ServerPlayer player,
|
||||
ContinuousStruggleMiniGameState session
|
||||
) {
|
||||
// V2 region-based resistance update
|
||||
if (session.isV2Struggle()) {
|
||||
com.tiedup.remake.v2.BodyRegionV2 region =
|
||||
session.getTargetRegion();
|
||||
ItemStack stack =
|
||||
com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper
|
||||
.getInRegion(player, region);
|
||||
if (stack.isEmpty()) return;
|
||||
|
||||
if (
|
||||
stack.getItem() instanceof
|
||||
com.tiedup.remake.items.base.IHasResistance resistanceItem
|
||||
) {
|
||||
resistanceItem.setCurrentResistance(
|
||||
stack,
|
||||
session.getCurrentResistance()
|
||||
);
|
||||
com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper.sync(
|
||||
player
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (session.isAccessoryStruggle()) {
|
||||
// Handle accessory resistance update (V1 path -- slot index is legacy ordinal)
|
||||
Integer slotIndex = session.getTargetSlot();
|
||||
if (slotIndex == null || slotIndex < 0 || slotIndex >= SLOT_TO_REGION.length) return;
|
||||
|
||||
BodyRegionV2 region = SLOT_TO_REGION[slotIndex];
|
||||
ItemStack accessoryStack = V2EquipmentHelper.getInRegion(player, region);
|
||||
if (accessoryStack.isEmpty()) return;
|
||||
|
||||
if (
|
||||
accessoryStack.getItem() instanceof
|
||||
com.tiedup.remake.items.base.ILockable lockable
|
||||
) {
|
||||
// Update the lock resistance to match session state
|
||||
lockable.setCurrentLockResistance(
|
||||
accessoryStack,
|
||||
session.getCurrentResistance()
|
||||
);
|
||||
// Sync V2 equipment state
|
||||
V2EquipmentHelper.sync(player);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Update bind resistance
|
||||
ItemStack bindStack = V2EquipmentHelper.getInRegion(player, BodyRegionV2.ARMS);
|
||||
if (
|
||||
bindStack.isEmpty() ||
|
||||
!(bindStack.getItem() instanceof ItemBind bind)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
bind.setCurrentResistance(bindStack, session.getCurrentResistance());
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle successful escape from struggle.
|
||||
*/
|
||||
private void handleStruggleEscape(
|
||||
ServerPlayer player,
|
||||
ContinuousStruggleMiniGameState session
|
||||
) {
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[StruggleSessionManager] Player {} escaped from struggle!",
|
||||
player.getName().getString()
|
||||
);
|
||||
|
||||
// Send escape update to client
|
||||
sendContinuousStruggleUpdate(player, session, UpdateType.ESCAPE);
|
||||
|
||||
// Clear animation state
|
||||
clearStruggleAnimation(player);
|
||||
|
||||
// Furniture escape: unlock seat + dismount
|
||||
if (session.isFurnitureStruggle()) {
|
||||
handleFurnitureEscape(player, session);
|
||||
continuousSessions.remove(player.getUUID());
|
||||
return;
|
||||
}
|
||||
|
||||
// V2 region-based escape
|
||||
if (session.isV2Struggle()) {
|
||||
com.tiedup.remake.v2.BodyRegionV2 region =
|
||||
session.getTargetRegion();
|
||||
// BUG-001 fix: break lock before unequip (consistent with V1 accessory escape)
|
||||
ItemStack stackInRegion =
|
||||
com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper
|
||||
.getInRegion(player, region);
|
||||
if (!stackInRegion.isEmpty()
|
||||
&& stackInRegion.getItem() instanceof com.tiedup.remake.items.base.ILockable lockable) {
|
||||
lockable.breakLock(stackInRegion);
|
||||
}
|
||||
ItemStack removed =
|
||||
com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper
|
||||
.unequipFromRegion(player, region, true);
|
||||
if (!removed.isEmpty()) {
|
||||
if (!player.getInventory().add(removed)) {
|
||||
player.drop(removed, false);
|
||||
}
|
||||
}
|
||||
continuousSessions.remove(player.getUUID());
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the bind
|
||||
if (!session.isAccessoryStruggle()) {
|
||||
PlayerBindState state = PlayerBindState.getInstance(player);
|
||||
if (state != null) {
|
||||
ItemStack bind = state.unequip(BodyRegionV2.ARMS);
|
||||
if (!bind.isEmpty()) {
|
||||
state.kidnappedDropItem(bind);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Handle accessory escape (V1 path -- slot index is legacy ordinal)
|
||||
Integer slotIndex = session.getTargetSlot();
|
||||
if (slotIndex != null && slotIndex >= 0 && slotIndex < SLOT_TO_REGION.length) {
|
||||
BodyRegionV2 region = SLOT_TO_REGION[slotIndex];
|
||||
ItemStack accessoryStack = V2EquipmentHelper.getInRegion(player, region);
|
||||
if (!accessoryStack.isEmpty()) {
|
||||
// Break the lock on the accessory
|
||||
if (
|
||||
accessoryStack.getItem() instanceof
|
||||
com.tiedup.remake.items.base.ILockable lockable
|
||||
) {
|
||||
lockable.breakLock(accessoryStack);
|
||||
}
|
||||
// Remove the accessory from the region and drop it
|
||||
ItemStack removed = V2EquipmentHelper.unequipFromRegion(
|
||||
player, region, true
|
||||
);
|
||||
if (!removed.isEmpty()) {
|
||||
// Drop the item at player's feet
|
||||
player.drop(removed, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove session
|
||||
continuousSessions.remove(player.getUUID());
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle successful furniture escape: unlock the seat, dismount the player,
|
||||
* play sounds, and broadcast state to tracking clients.
|
||||
*/
|
||||
private void handleFurnitureEscape(
|
||||
ServerPlayer player,
|
||||
ContinuousStruggleMiniGameState session
|
||||
) {
|
||||
int furnitureEntityId = session.getFurnitureEntityId();
|
||||
String seatId = session.getFurnitureSeatId();
|
||||
|
||||
net.minecraft.world.entity.Entity entity = player.level().getEntity(furnitureEntityId);
|
||||
if (entity == null || entity.isRemoved()) {
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[StruggleSessionManager] Furniture entity {} no longer exists for escape",
|
||||
furnitureEntityId
|
||||
);
|
||||
// Player still needs to be freed even if entity is gone
|
||||
player.stopRiding();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(entity instanceof com.tiedup.remake.v2.furniture.ISeatProvider provider)) {
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[StruggleSessionManager] Entity {} is not an ISeatProvider",
|
||||
furnitureEntityId
|
||||
);
|
||||
player.stopRiding();
|
||||
return;
|
||||
}
|
||||
|
||||
// Unlock the seat
|
||||
provider.setSeatLocked(seatId, false);
|
||||
|
||||
// Clear persistent data tag (reconnection system)
|
||||
net.minecraft.nbt.CompoundTag persistentData = player.getPersistentData();
|
||||
persistentData.remove("tiedup_locked_furniture");
|
||||
|
||||
// Dismount the player
|
||||
player.stopRiding();
|
||||
|
||||
// Play escape sound: prefer furniture-specific sound, fall back to CHAIN_BREAK
|
||||
net.minecraft.sounds.SoundEvent escapeSound = net.minecraft.sounds.SoundEvents.CHAIN_BREAK;
|
||||
if (entity instanceof com.tiedup.remake.v2.furniture.EntityFurniture furniture) {
|
||||
com.tiedup.remake.v2.furniture.FurnitureDefinition def = furniture.getDefinition();
|
||||
if (def != null && def.feedback().escapeSound() != null) {
|
||||
escapeSound = net.minecraft.sounds.SoundEvent.createVariableRangeEvent(
|
||||
def.feedback().escapeSound()
|
||||
);
|
||||
}
|
||||
}
|
||||
player.serverLevel().playSound(
|
||||
null,
|
||||
player.getX(), player.getY(), player.getZ(),
|
||||
escapeSound,
|
||||
net.minecraft.sounds.SoundSource.PLAYERS,
|
||||
1.0f, 1.0f
|
||||
);
|
||||
|
||||
// Broadcast updated furniture state to all tracking clients
|
||||
if (entity instanceof com.tiedup.remake.v2.furniture.EntityFurniture furniture) {
|
||||
com.tiedup.remake.v2.furniture.network.PacketSyncFurnitureState.sendToTracking(furniture);
|
||||
}
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[StruggleSessionManager] {} escaped furniture {} seat '{}'",
|
||||
player.getName().getString(), furnitureEntityId, seatId
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a continuous struggle state update to the client.
|
||||
*/
|
||||
private void sendContinuousStruggleUpdate(
|
||||
ServerPlayer player,
|
||||
ContinuousStruggleMiniGameState session,
|
||||
UpdateType updateType
|
||||
) {
|
||||
ModNetwork.sendToPlayer(
|
||||
new PacketContinuousStruggleState(
|
||||
session.getSessionId(),
|
||||
updateType,
|
||||
session.getCurrentDirectionIndex(),
|
||||
session.getCurrentResistance(),
|
||||
session.getMaxResistance(),
|
||||
session.isLocked()
|
||||
),
|
||||
player
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear struggle animation state for a player and sync to clients.
|
||||
*/
|
||||
private void clearStruggleAnimation(ServerPlayer player) {
|
||||
PlayerBindState state = PlayerBindState.getInstance(player);
|
||||
if (state != null) {
|
||||
state.setStruggling(false, 0);
|
||||
SyncManager.syncStruggleState(player);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== CLEANUP ====================
|
||||
|
||||
/**
|
||||
* Clean up all struggle sessions for a player (called on disconnect).
|
||||
*
|
||||
* @param playerId The player UUID
|
||||
*/
|
||||
public void cleanupPlayer(UUID playerId) {
|
||||
continuousSessions.remove(playerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Periodic cleanup of expired struggle sessions.
|
||||
* Should be called from server tick handler.
|
||||
*/
|
||||
public void tickCleanup(long currentTick) {
|
||||
// Only run every 100 ticks (5 seconds)
|
||||
if (currentTick % 100 != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
continuousSessions
|
||||
.entrySet()
|
||||
.removeIf(entry -> entry.getValue().isExpired());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count of active struggle sessions (for debugging).
|
||||
*/
|
||||
public int getActiveSessionCount() {
|
||||
return continuousSessions.size();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user