package com.tiedup.remake.state.struggle; import com.tiedup.remake.core.SettingsAccessor; import com.tiedup.remake.core.SystemMessageManager; import com.tiedup.remake.core.SystemMessageManager.MessageCategory; import com.tiedup.remake.core.TiedUpMod; import com.tiedup.remake.state.PlayerBindState; import com.tiedup.remake.util.time.Timer; import net.minecraft.world.entity.player.Player; import net.minecraft.world.level.GameRules; /** * * Handles the logic for players/NPCs struggling against restraints: * - Cooldown timer between attempts * - Probability-based success (dice roll) * - Resistance decrease on success * - Automatic removal when resistance reaches 0 * * v2.5: This is now used for NPC struggle only. * Players use the continuous struggle mini-game (ContinuousStruggleMiniGameState). * Knife bonuses have been removed - knives now work by active cutting. * * Architecture: * - StruggleState (abstract base) * ├─ StruggleBinds (struggle against bind restraints) * └─ StruggleCollar (struggle against collar/ownership) */ public abstract class StruggleState { /** * Cooldown timer for struggle attempts. * Reset after each struggle attempt. * Volatile for thread safety across network/tick threads. */ protected volatile Timer struggleCooldownTimer; /** * Main struggle logic (dice roll based). * Used for NPC struggle - players use QTE mini-game instead. * * Flow: * 1. Check if struggle is enabled (GameRule) * 2. Check if item can be struggled out of * 3. Check cooldown timer * 4. Roll dice for success (probability %) * 5. If success: decrease resistance by random amount * 6. If resistance <= 0: call successAction() * 7. Set cooldown for next attempt * * @param state The player's bind state */ public synchronized void struggle(PlayerBindState state) { Player player = state.getPlayer(); if (player == null || player.level() == null) { return; } GameRules gameRules = player.level().getGameRules(); // 1. Check if struggle system is enabled if (!SettingsAccessor.isStruggleEnabled(gameRules)) { return; } // 2. Check if item can be struggled out of (not locked or forced) if (!canStruggle(state)) { return; } // 3. Check cooldown timer (prevent spamming) if ( struggleCooldownTimer != null && !struggleCooldownTimer.isExpired() ) { TiedUpMod.LOGGER.debug( "[STRUGGLE] Cooldown not expired yet - ignoring struggle attempt" ); return; } // Start struggle animation (after cooldown check passes) if (!state.isStruggling()) { state.setStruggling(true, player.level().getGameTime()); com.tiedup.remake.network.sync.SyncManager.syncStruggleState( player ); } if (!onAttempt(state)) { return; // Interrupted by pain } // 4. Roll for success int probability = SettingsAccessor.getProbabilityStruggle(gameRules); int roll = player.getRandom().nextInt(100) + 1; boolean success = roll <= probability; if (success) { // Calculate resistance decrease int currentResistance = getResistanceState(state); int minDecrease = SettingsAccessor.getStruggleMinDecrease( gameRules ); int maxDecrease = SettingsAccessor.getStruggleMaxDecrease( gameRules ); int decrease = minDecrease + player.getRandom().nextInt(maxDecrease - minDecrease + 1); int newResistance = Math.max(0, currentResistance - decrease); setResistanceState(state, newResistance); // Feedback notifySuccess(player, newResistance); // Check for escape if (newResistance <= 0) { TiedUpMod.LOGGER.debug( "[STRUGGLE] {} broke free! (resistance reached 0)", player.getName().getString() ); successAction(state); } } else { notifyFailure(player); } // 5. Set cooldown int cooldownTicks = SettingsAccessor.getStruggleTimer(gameRules); struggleCooldownTimer = new Timer(cooldownTicks / 20, player.level()); } /** * Get the message category for successful struggle attempts. */ protected abstract MessageCategory getSuccessCategory(); /** * Get the message category for failed struggle attempts. */ protected abstract MessageCategory getFailCategory(); /** * Send success notification to the player. * * @param player The player to notify * @param newResistance The new resistance value after decrease */ protected void notifySuccess(Player player, int newResistance) { SystemMessageManager.sendWithResistance( player, getSuccessCategory(), newResistance ); } /** * Send failure notification to the player. * * @param player The player to notify */ protected void notifyFailure(Player player) { SystemMessageManager.sendToPlayer(player, getFailCategory()); } /** * Called when the player successfully struggles free (resistance reaches 0). * Subclasses implement this to handle the escape action. * * @param state The player's bind state */ protected abstract void successAction(PlayerBindState state); /** * Called when a struggle attempt is performed (regardless of success/fail). * Can be used for side effects like random shocks. * * @param state The player's bind state * @return true to proceed with struggle, false to interrupt */ protected boolean onAttempt(PlayerBindState state) { return true; // Default: proceed } /** * Get the current resistance value from the player's state. * * @param state The player's bind state * @return Current resistance value */ protected abstract int getResistanceState(PlayerBindState state); /** * Set the current resistance value in the player's state. * * @param state The player's bind state * @param resistance The new resistance value */ protected abstract void setResistanceState( PlayerBindState state, int resistance ); /** * Check if the player can struggle. * Returns false if the item cannot be struggled out of. * * @param state The player's bind state * @return True if struggling is allowed */ protected abstract boolean canStruggle(PlayerBindState state); /** * Check if debug logging is enabled. * * @return True if debug logging should be printed */ protected boolean isDebugEnabled() { return true; } }