Files
TiedUp-/docs/plans/D01-branch-E-resistance-rework.md
NotEvil 3d61c9e9e6 feat(D-01/C): consumer migration — 85 files migrated to V2 helpers
Phase 1 (state): PlayerBindState, PlayerCaptorManager, PlayerEquipment,
  PlayerDataRetrieval, PlayerLifecycle, PlayerShockCollar, StruggleAccessory
Phase 2 (client): AnimationTickHandler, NpcAnimationTickHandler, 5 render
  handlers, DamselModel, 3 client mixins, SelfBondageInputHandler,
  SlaveManagementScreen, ActionPanel, SlaveEntryWidget, ModKeybindings
Phase 3 (entities): 28 entity/AI files migrated to CollarHelper,
  BindModeHelper, PoseTypeHelper, createStack()
Phase 4 (network): PacketSlaveAction, PacketMasterEquip,
  PacketAssignCellToCollar, PacketNpcCommand, PacketFurnitureForcemount
Phase 5 (events): RestraintTaskTickHandler, PetPlayRestrictionHandler,
  PlayerEnslavementHandler, ChatEventHandler, LaborAttackPunishmentHandler
Phase 6 (commands): BondageSubCommand, CollarCommand, NPCCommand,
  KidnapSetCommand
Phase 7 (compat): MCAKidnappedAdapter, MCA mixins
Phase 8 (misc): GagTalkManager, PetRequestManager, HangingCagePiece,
  BondageItemBlockEntity, TrappedChestBlockEntity, DispenserBehaviors,
  BondageItemLoaderUtility, RestraintApplicator, StruggleSessionManager,
  MovementStyleResolver, CampLifecycleManager

Some files retain dual V1/V2 checks (instanceof V1 || V2Helper) for
coexistence — V1-only branches removed in Branch D.
2026-04-15 00:16:50 +02:00

6.7 KiB

D-01 Branch E : Resistance & Lock System Rework

Prérequis : Branch D (V1 cleanup) mergée. Branche : feature/d01-branch-e-resistance Objectif : Redesign complet du système de résistance/lock.


Nouveau modèle

Principes

  1. La résistance vient de l'item — définie dans le JSON via ResistanceComponent, point final.
  2. Le lock est binaire — on/off. Pas de "lock resistance" séparée. Le lock active la nécessité de struggle pour les items non-ARMS.
  3. ARMS = toujours actif — un bind aux bras nécessite toujours un struggle pour s'en libérer soi-même, locké ou non.
  4. Non-ARMS + pas locké = libre — un gag/blindfold/collar non-locké peut être retiré librement (par soi-même ou un autre joueur).
  5. Non-ARMS + locké = struggle requis — le lock active la résistance de l'item.
  6. Un autre joueur peut aider — retirer un item non-locké sur un autre joueur ne nécessite pas de struggle (aide).

Matrice de struggle

Région Locké ? Self-remove Autre joueur remove
ARMS Non Struggle (résistance item) Libre (aide)
ARMS Oui Struggle (résistance item) Struggle (résistance item)
Non-ARMS Non Libre Libre
Non-ARMS Oui Struggle (résistance item) Struggle (résistance item)

Items organiques (slime, vine, web, tape)

Ces items sont "lockés par nature" — pas de padlock possible mais impossible à retirer sans struggle.

Option : Nouveau composant BuiltInLockComponent dans le JSON :

"components": {
    "resistance": {"id": "slime"},
    "built_in_lock": {}
}

BuiltInLockComponent :

  • blocksUnequip() retourne true (comme un lock, mais sans padlock)
  • ILockable.canAttachPadlock() retourne false (déjà le cas pour les organiques)
  • L'item se comporte comme un ARMS bind : toujours struggle required

Alternative : Flag "always_locked": true sur la definition JSON. Plus simple, pas besoin de nouveau composant.


Problèmes actuels que ce rework corrige

P1. Singleton MAX scan

DataDrivenBondageItem.getBaseResistance(LivingEntity) retourne le MAX de tous les items data-driven équipés. Un gag résistance 50 hérite de la résistance 200 du chain bind.

Fix : Initialiser currentResistance dans le NBT à l'equip depuis ResistanceComponent.getBaseResistance(). Plus jamais de fallback au MAX scan runtime. Le getBaseResistance(LivingEntity) du singleton devient un no-op/fallback qui n'est plus utilisé par le struggle.

P2. isItemLocked() dead code

StruggleState.struggle() ne call jamais isItemLocked(). Le x10 penalty n'est jamais appliqué.

Fix : Supprimer le concept de "locked penalty". Avec le nouveau modèle, le lock active le struggle, il ne le ralentit pas. Si l'item est locké, il faut struggle avec la résistance complète de l'item. Si non locké (et non-ARMS), pas de struggle du tout.

P3. Lock resistance / item resistance déconnectés

ILockable.getLockResistance() vs IHasResistance.getBaseResistance() sont deux systèmes indépendants.

Fix : Supprimer ILockable.getLockResistance() / getCurrentLockResistance() / setCurrentLockResistance() / initializeLockResistance() / clearLockResistance(). La résistance du lockpick minigame utilise directement la résistance de l'item (ou un multiplicateur fixe).

P4. Dice-roll ignore le lock

Fix : Avec le nouveau modèle, le dice-roll ne change pas. C'est canStruggle() qui gate l'accès :

// StruggleBinds.canStruggle()
// ARMS: toujours struggle-able (self)
return true;

// StruggleCollar/StruggleAccessory.canStruggle()
// Non-ARMS: seulement si locké
return isLocked(stack) || hasBuiltInLock(stack);

Bugs pré-existants à corriger dans cette branche

B1. V1 ItemCollar.onUnequipped() — suppressed path skip unregister

Quand isRemovalAlertSuppressed() est true, ItemCollar.onUnequipped() return early SANS appeler CollarRegistry.unregisterWearer(). Entrées fantômes persistées.

Fichier : items/base/ItemCollar.java lignes 1382-1395 Fix : Ajouter unregisterWearer() dans le branch suppressed.

B2. DataDrivenItemRegistry.clear() pas synchronisé

clear() écrit SNAPSHOT = EMPTY sans acquérir RELOAD_LOCK. Race avec mergeAll().

Fichier : v2/bondage/datadriven/DataDrivenItemRegistry.java ligne 142 Fix : Synchroniser sur RELOAD_LOCK.

B3. V2TyingPlayerTask.heldStack reference stale

Le held item peut être remplacé entre début et fin du tying → item dupliqué.

Fichier : tasks/V2TyingPlayerTask.java ligne 80 Fix : Valider heldStack non-vide et matching avant equip dans onComplete().


Tâches

E1. Initialiser currentResistance à l'equip

Dans DataDrivenBondageItem.onEquipped() et les hooks V1 onEquipped() :

  • Lire ResistanceComponent.getBaseResistance() (ou IHasResistance.getBaseResistance() pour V1)
  • Écrire immédiatement dans le NBT via setCurrentResistance(stack, base)
  • Élimine le MAX scan comme source d'initialisation

E2. Refactor canStruggle() — nouveau modèle

  • StruggleBinds.canStruggle() : ARMS → toujours true (self) si item existe
  • Nouveau StruggleAccessory (ou refactor de StruggleCollar) : non-ARMS → true seulement si locké ou built-in lock
  • Supprimer isItemLocked() penalty (dead code de toute façon)

E3. "Aide" — remove non-locké par un autre joueur

Modifier AbstractV2BondageItem.interactLivingEntity() :

  • Si clic sur un joueur qui porte l'item ET item non-locké ET clicker n'a pas l'item en main → retirer l'item (aide)
  • Ou via un packet dédié (clic droit main vide sur joueur attaché)

E4. BuiltInLockComponent ou flag always_locked

Pour les items organiques qui ne peuvent pas avoir de padlock mais nécessitent un struggle.

E5. Cleanup ILockable — supprimer lock resistance

Supprimer : getLockResistance(), getCurrentLockResistance(), setCurrentLockResistance(), initializeLockResistance(), clearLockResistance().

Le lockpick minigame utilise la résistance de l'item directement (ou un multiplicateur config).

E6. Fix bugs pré-existants (B1, B2, B3)


Vérification

  • V2 bind résistance 50 + V2 gag résistance 80 : chacun a sa propre résistance (pas MAX)
  • Gag non-locké → retirable sans struggle
  • Gag locké → struggle avec résistance du gag
  • Bind ARMS non-locké → self-struggle requis, autre joueur peut aider (libre)
  • Bind ARMS locké → self-struggle requis, autre joueur aussi struggle
  • Slime bind (built-in lock) → struggle obligatoire, pas de padlock possible
  • currentResistance initialisé dans NBT dès l'equip
  • CollarRegistry clean après removals légitimes
  • Pas de duplication d'item via tying task