# 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 : ```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 : ```java // 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()`. ### B4. PlayerShockCollar ignore complètement les V2 collars `checkAutoShockCollar()` dispatche exclusivement sur `instanceof ItemShockCollarAuto` et `instanceof ItemGpsCollar`. Les V2 data-driven collars avec ShockComponent ou GpsComponent ne déclenchent jamais les auto-shocks ni l'enforcement de zones GPS. **Fichier :** `state/components/PlayerShockCollar.java` lignes 139-189 **Fix :** Utiliser `CollarHelper.canShock()`, `CollarHelper.getShockInterval()`, `CollarHelper.hasGPS()` pour la détection, avec fallback V1 pour les méthodes V1-specific (`getSafeSpots()`). ### B5. EntityKidnapperMerchant.remove() memory leak `remove()` appelle `tradingPlayers.clear()` mais ne nettoie PAS la `playerToMerchant` ConcurrentHashMap statique. Entrées stales accumulées sur les serveurs long-running. **Fichier :** `entities/EntityKidnapperMerchant.java` ligne 966-981 **Fix :** Itérer `tradingPlayers` et appeler `playerToMerchant.remove(uuid)` avant le clear. ### B6. Timer division potentiellement inversée (auto-shock) `PlayerShockCollar.java` lignes 153-155 : `collarShock.getInterval() / GameConstants.TICKS_PER_SECOND`. Si Timer attend des ticks, la division réduit l'intervalle de 20x (shock toutes les 0.25s au lieu de 5s). **Fichier :** `state/components/PlayerShockCollar.java` lignes 153-155 et 179-182 **Fix :** Vérifier le contrat du constructeur `Timer`. Si il attend des ticks, supprimer la division. ### B7. StruggleState.isItemLocked() dead code `StruggleState.struggle()` ne call JAMAIS `isItemLocked()`. Le penalty x10 pour les items padlockés n'est jamais appliqué. **Fichier :** `state/struggle/StruggleState.java` ligne 53-133 **Fix :** Inclus dans le rework E2 (nouveau modèle resistance/lock). --- ## 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