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.
6.7 KiB
D-01 Branch E : Resistance & Lock System Rework
Prérequis : Branch D (V1 cleanup) mergée. Branche :
feature/d01-branch-e-resistanceObjectif : Redesign complet du système de résistance/lock.
Nouveau modèle
Principes
- La résistance vient de l'item — définie dans le JSON via
ResistanceComponent, point final. - 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.
- ARMS = toujours actif — un bind aux bras nécessite toujours un struggle pour s'en libérer soi-même, locké ou non.
- Non-ARMS + pas locké = libre — un gag/blindfold/collar non-locké peut être retiré librement (par soi-même ou un autre joueur).
- Non-ARMS + locké = struggle requis — le lock active la résistance de l'item.
- 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()retournetrue(comme un lock, mais sans padlock)ILockable.canAttachPadlock()retournefalse(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()(ouIHasResistance.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
currentResistanceinitialisé dans NBT dès l'equip- CollarRegistry clean après removals légitimes
- Pas de duplication d'item via tying task