Files
TiedUp-/src/main/java/com/tiedup/remake/state/struggle/StruggleState.java
NotEvil 75679c6718 feat(D-01/E): resistance & lock system rework (E1-E7)
E1: Initialize currentResistance in NBT at equip time from
    ResistanceComponent — eliminates MAX-scan fallback bug

E2: BuiltInLockComponent for organic items (already committed)

E3: canStruggle refactor — new model:
    - ARMS: always struggle-able (no lock gating)
    - Non-ARMS: only if locked OR built-in lock
    - Removed dead isItemLocked() from StruggleState + overrides

E4: canUnequip already handled by BuiltInLockComponent.blocksUnequip()
    via ComponentHolder delegation

E5: Help/assist mechanic deferred (needs UI design)

E6: Removed lock resistance from ILockable (5 methods + NBT key deleted)
    - GenericKnife: new knifeCutProgress NBT for cutting locks
    - StruggleAccessory: accessoryStruggleResistance NBT replaces lock resistance
    - PacketV2StruggleStart: uses config-based padlock resistance
    - All lock/unlock packets cleaned of initializeLockResistance/clearLockResistance

E7: Fixed 3 pre-existing bugs:
    - B2: DataDrivenItemRegistry.clear() synchronized on RELOAD_LOCK
    - B3: V2TyingPlayerTask validates heldStack before equip (prevents duplication)
    - B5: EntityKidnapperMerchant.remove() cleans playerToMerchant map (memory leak)
2026-04-15 03:23:49 +02:00

224 lines
7.0 KiB
Java

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;
}
}