Files
TiedUp-/docs/plans/D01-branch-E-resistance-rework.md
NotEvil 449178f57b 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

156 lines
6.7 KiB
Markdown

# 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()`.
---
## 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