Remove build artifacts, dev tool configs, unused dependencies, and third-party source dumps. Add proper README, update .gitignore, clean up Makefile.
549 lines
16 KiB
Java
549 lines
16 KiB
Java
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
|
|
);
|
|
}
|
|
}
|