From 449178f57b499dee09201ab120e50cc5f9facd9c Mon Sep 17 00:00:00 2001
From: NotEvil
Date: Wed, 15 Apr 2026 00:16:50 +0200
Subject: [PATCH 1/3] =?UTF-8?q?feat(D-01/C):=20consumer=20migration=20?=
=?UTF-8?q?=E2=80=94=2085=20files=20migrated=20to=20V2=20helpers?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
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.
---
docs/AUDIT.md | 413 +++++++++
docs/plans/D01-branch-A-bridge-utilities.md | 284 ++++++
docs/plans/D01-branch-B-json-definitions.md | 128 +++
docs/plans/D01-branch-C-consumer-migration.md | 107 +++
docs/plans/D01-branch-D-v1-cleanup.md | 188 ++++
docs/plans/D01-branch-E-resistance-rework.md | 155 ++++
.../2026-04-13-d01-phase1-component-system.md | 869 ++++++++++++++++++
.../blocks/entity/BondageItemBlockEntity.java | 54 +-
.../entity/TrappedChestBlockEntity.java | 35 +-
.../remake/cells/CampLifecycleManager.java | 8 +-
.../client/FirstPersonMittensRenderer.java | 13 +-
.../tiedup/remake/client/ModKeybindings.java | 9 +-
.../render/DogPoseRenderHandler.java | 9 +-
.../animation/render/PetBedRenderHandler.java | 8 +-
.../render/PlayerArmHideEventHandler.java | 8 +-
.../animation/tick/AnimationTickHandler.java | 21 +-
.../tick/NpcAnimationTickHandler.java | 20 +-
.../events/LeashProxyClientHandler.java | 6 +-
.../events/SelfBondageInputHandler.java | 49 +-
.../gui/screens/SlaveManagementScreen.java | 6 +-
.../client/gui/widgets/ActionPanel.java | 8 +-
.../client/gui/widgets/SlaveEntryWidget.java | 14 +-
.../remake/client/model/DamselModel.java | 25 +-
.../tiedup/remake/commands/CollarCommand.java | 46 +-
.../remake/commands/KidnapSetCommand.java | 63 +-
.../tiedup/remake/commands/NPCCommand.java | 20 +-
.../subcommands/BondageSubCommand.java | 61 +-
.../mca/capability/MCAKidnappedAdapter.java | 17 +-
.../remake/dialogue/GagTalkManager.java | 21 +-
.../conversation/PetRequestManager.java | 10 +-
.../remake/entities/AbstractTiedUpNpc.java | 41 +-
.../entities/BondageServiceHandler.java | 43 +-
.../tiedup/remake/entities/EntityDamsel.java | 18 +-
.../remake/entities/EntityKidnapper.java | 7 +-
.../entities/EntityKidnapperMerchant.java | 45 +-
.../remake/entities/EntityRopeArrow.java | 6 +-
.../entities/KidnapperCaptureEquipment.java | 5 +-
.../entities/KidnapperCollarConfig.java | 74 +-
.../entities/KidnapperItemSelector.java | 23 +-
.../remake/entities/KidnapperJobManager.java | 20 +-
.../remake/entities/LeashProxyEntity.java | 5 +-
.../kidnapper/KidnapperBringToCellGoal.java | 8 +-
.../ai/kidnapper/KidnapperCaptureGoal.java | 26 +-
.../kidnapper/KidnapperWalkPrisonerGoal.java | 10 +-
.../ai/maid/goals/MaidReturnGoal.java | 6 +-
.../ai/master/MasterHumanChairGoal.java | 8 +-
.../entities/ai/master/MasterPunishGoal.java | 35 +-
.../ai/master/MasterRandomEventGoal.java | 31 +-
.../ai/master/MasterTaskWatchGoal.java | 10 +-
.../ai/personality/NpcFollowCommandGoal.java | 4 +-
.../ai/personality/NpcGuardCommandGoal.java | 10 +-
.../components/DamselBondageManager.java | 6 +-
.../components/DamselPersonalitySystem.java | 6 +-
.../components/NpcEquipmentManager.java | 22 +-
.../components/KidnapperCaptiveManager.java | 6 +-
.../components/KidnapperTargetSelector.java | 10 +-
.../master/components/MasterPetManager.java | 45 +-
.../events/camp/CampNpcProtectionHandler.java | 4 +-
.../captivity/PlayerEnslavementHandler.java | 6 +-
.../combat/LaborAttackPunishmentHandler.java | 4 +-
.../PetPlayRestrictionHandler.java | 6 +-
.../restriction/RestraintTaskTickHandler.java | 9 +-
.../events/system/ChatEventHandler.java | 27 +-
.../minigame/StruggleSessionManager.java | 21 +-
.../mixin/MixinMCAVillagerInteraction.java | 6 +-
.../remake/mixin/MixinMCAVillagerLeash.java | 6 +-
.../remake/mixin/client/MixinCamera.java | 6 +-
.../remake/mixin/client/MixinPlayerModel.java | 6 +-
.../MixinVillagerEntityBaseModelMCA.java | 15 +-
.../cell/PacketAssignCellToCollar.java | 18 +-
.../network/personality/PacketNpcCommand.java | 6 +-
.../network/slave/PacketMasterEquip.java | 6 +-
.../network/slave/PacketSlaveAction.java | 52 +-
.../tiedup/remake/personality/ToolMode.java | 4 +-
.../tiedup/remake/state/IBondageState.java | 12 +-
.../tiedup/remake/state/PlayerBindState.java | 12 +-
.../remake/state/PlayerCaptorManager.java | 6 +-
.../state/components/PlayerDataRetrieval.java | 13 +-
.../state/components/PlayerEquipment.java | 8 +-
.../state/components/PlayerLifecycle.java | 6 +-
.../remake/util/BondageItemLoaderUtility.java | 67 +-
.../remake/util/RestraintApplicator.java | 12 +-
.../movement/MovementStyleResolver.java | 13 +-
.../network/PacketFurnitureForcemount.java | 11 +-
.../remake/worldgen/HangingCagePiece.java | 95 +-
85 files changed, 2885 insertions(+), 777 deletions(-)
create mode 100644 docs/AUDIT.md
create mode 100644 docs/plans/D01-branch-A-bridge-utilities.md
create mode 100644 docs/plans/D01-branch-B-json-definitions.md
create mode 100644 docs/plans/D01-branch-C-consumer-migration.md
create mode 100644 docs/plans/D01-branch-D-v1-cleanup.md
create mode 100644 docs/plans/D01-branch-E-resistance-rework.md
create mode 100644 docs/superpowers/plans/2026-04-13-d01-phase1-component-system.md
diff --git a/docs/AUDIT.md b/docs/AUDIT.md
new file mode 100644
index 0000000..54a3b77
--- /dev/null
+++ b/docs/AUDIT.md
@@ -0,0 +1,413 @@
+# TiedUp! — Codebase Audit
+
+> Audit complet du mod, systeme par systeme, pour consolider la base et la coherence du code.
+
+---
+
+## Objectif
+
+Passer en revue **chaque systeme** du mod en profondeur pour :
+1. **Consolider** — identifier et corriger les incohérences, le code mort, la duplication
+2. **Améliorer** — proposer des refactors ciblés là où l'architecture freine le développement
+3. **Documenter** — laisser une trace des décisions et de l'état de chaque système
+
+Ce n'est pas un rewrite. On stabilise ce qui existe.
+
+---
+
+## Règles de l'audit
+
+- **Système par système**, dans l'ordre de dépendance (fondations d'abord)
+- **Deep dive** — on remonte les sources, les dépendants, les call chains via le MCP
+- **Pas de changement sans discussion** — on constate, on discute, puis on corrige
+- **Pas de sur-ingénierie** — on fixe les vrais problèmes, on ne refactor pas pour le plaisir
+- **Reindex MCP** après chaque batch de corrections significatives
+
+### Processus par système
+
+Chaque audit suit ce cycle :
+
+```
+1. EXPLORATION — Lire le code, tracer les dépendances (MCP)
+2. CONSTATS — Documenter les problèmes trouvés (dans ce fichier)
+3. VÉRIFICATION — Relire, confirmer, pas de faux positifs
+4. PROPOSITIONS — Pour chaque constat :
+ → Fix direct (bug, incohérence simple)
+ → Amélioration architecturale (refactor ciblé, avec justification)
+5. DISCUSSION — Valider avec l'utilisateur avant d'implémenter
+6. CORRECTION — Appliquer les changements validés
+7. REINDEX — MCP reindex après corrections
+```
+
+On ne passe au système suivant qu'une fois le cycle terminé.
+
+---
+
+## Ordre d'audit
+
+| # | Système | Packages | Status |
+|---|---------|----------|--------|
+| 1 | Core + Registries | `core`, `ModItems`, `ModEntities`, `ModNetwork`, `ModGameRules` | Done |
+| 2 | State | `state`, `state/components`, `state/hosts`, `state/struggle` | Done |
+| 3 | Items (v1) + V2 Bondage | `items/**`, `v2/bondage/**` | Done |
+| 4 | Network | `network/**` | Done |
+| 5 | Client Animation + GLTF | `client/animation/**`, `client/gltf` | Done |
+| 6 | Furniture | `v2/furniture/**` | Done |
+| 7 | Entities + AI | `entities/**` | Done |
+| 8 | Events | `events/**` | Done |
+| 9 | Dialogue + Personality | `dialogue/**`, `personality` | Done |
+| 10 | Cells / Prison / Blocks | `cells`, `prison/**`, `blocks/**` | Done |
+| 11 | Compat | `compat/mca/**`, `compat/wildfire/**` | Done |
+| 12 | Util + Commands + Worldgen + Resources | `util/**`, `commands/**`, `worldgen`, resources | Done |
+
+---
+
+## Échelle de sévérité
+
+| Niveau | Signification | Action |
+|--------|--------------|--------|
+| **Haute** | Bug actif, bloquant, ou dette qui empêche une feature majeure | Corriger avant de continuer |
+| **Moyenne** | Incohérence ou fragilité qui va poser problème à terme | Planifier la correction |
+| **Basse** | Code smell, naming, organisation — pas urgent | Corriger si on touche le fichier |
+| **Cosmétique** | Style, formatting, commentaires | Optionnel |
+
+---
+
+## Constats
+
+### #1 — Core + Registries
+
+**Positif :**
+- DeferredRegister correct partout
+- Factory pattern propre pour les variant items
+- SettingsAccessor bridge solide (safeGet, BUG-003 fix)
+- Séparation client/server correcte
+- Data-driven reload listeners bien câblés
+
+**Problèmes :** (tous vérifiés ✓)
+
+| ID | Sévérité | Constat | Fichier(s) | Proposition |
+|----|----------|---------|------------|-------------|
+| C-01 | Haute | SystemMessageManager : 80+ messages hardcodés en anglais, zero `Component.translatable()` | `core/SystemMessageManager.java` | **Fix** : Remplacer `Component.literal()` par `Component.translatable()` avec clés dans `en_us.json`. Garder l'enum + les couleurs, changer uniquement le transport du texte. |
+| C-02 | Moyenne | 25 settings triplés entre ModConfig + ModGameRules + SettingsAccessor. Historique de bug (BUG-001 : defaults désyncés) | `core/ModConfig.java`, `core/ModGameRules.java`, `core/SettingsAccessor.java` | **Archi** : Évaluer si les GameRules sont vraiment utiles (ils dupliquent la config serveur). Si oui, centraliser les defaults dans une seule source. Si non, supprimer les GameRules doublons et garder ModConfig + SettingsAccessor. |
+| C-03 | Moyenne | ModNetwork : 74 IDs séquentiels. Pattern standard Forge mais fragile à l'insertion. `PROTOCOL_VERSION` protège partiellement. | `network/ModNetwork.java` | **Pas d'action immédiate** : pattern idiomatique Forge 1.20.1. Bumper `PROTOCOL_VERSION` à chaque ajout/suppression de packet. Documenter cette règle. |
+| C-04 | Basse | ChokeEffect importe EntityMaster (core → entities) pour check "non-lethal when master-owned" | `core/ChokeEffect.java` | **Fix** : Extraire le check via un tag NBT ou capability sur le Player, consultable sans dépendre d'EntityMaster. |
+| C-05 | Basse | 10 types d'entités NPC utilisent DamselRenderer — nom spécifique pour un usage générique | `core/TiedUpMod.java`, `client/renderer/DamselRenderer.java` | **Fix** : Renommer `DamselRenderer` → `NpcRenderer` ou `HumanoidNpcRenderer`. |
+| C-06 | Cosmétique | 47+ FQCNs dans le corps de TiedUpMod au lieu d'imports | `core/TiedUpMod.java` | **Fix** : Remplacer par des imports. Faire quand on touche le fichier. |
+| C-07 | Basse | ModConfig.ServerConfig : 127 valeurs configurables, 628 lignes, 20+ catégories dans une classe | `core/ModConfig.java` | **Archi** : Découper en sous-classes par domaine (StruggleConfig, NpcConfig, EconomyConfig, etc.) au prochain refactor config. Pas urgent. |
+
+### #2 — State
+
+**Positif :**
+- Hiérarchie d'interfaces bien conçue (ISP) : `IRestrainableEntity` → `ICapturable` → `IBondageState` → `IRestrainable` (union)
+- Décomposition en 11 components (PlayerEquipment, PlayerStateQuery, etc.) — bonne intention
+- `IBondageState` n'expose que l'API V2 region-based — l'interface publique est propre
+- `CollarRegistry` et `SocialData` : implémentations `SavedData` propres avec persistence correcte
+- `IPlayerLeashAccess` : séparation mixin clean pour le système de laisse
+- `PlayerCaptorManager` : thread-safe avec `CopyOnWriteArrayList` et `synchronized`
+
+**Problèmes :** (tous vérifiés ✓)
+
+| ID | Sévérité | Constat | Fichier(s) | Proposition |
+|----|----------|---------|------------|-------------|
+| S-01 | Moyenne | PlayerBindState : god class de façade, 1237 lignes, ~80 méthodes de pure délégation vers 11 components | `state/PlayerBindState.java` | **Archi** : Évaluer si les consommateurs peuvent utiliser les components directement via des accesseurs typés (`state.equipment().putBindOn()`) au lieu de passer par la façade. Réduirait la surface de ~80 méthodes de boilerplate. Risque : gros refactor, beaucoup de call sites. |
+| S-02 | Moyenne | 8 champs de mouvement publics (`hopCooldown`, `lastX`, etc.) directement mutés par MovementStyleManager (33 accès directs) | `state/PlayerBindState.java`, `v2/bondage/movement/MovementStyleManager.java` | **Fix** : Extraire dans un `MovementState` component avec getters/setters. MovementStyleManager opère via ce component. |
+| S-03 | Basse | API V1 slot-based (`putBindOn`, `takeGagOff`) coexiste avec V2 region-based sur la classe concrète PlayerBindState. L'interface `IBondageState` est propre (V2 only). | `state/PlayerBindState.java` | **Archi** : Marquer les méthodes V1 `@Deprecated` pour guider la migration. Les call sites (commands, etc.) devraient migrer vers `equip(BodyRegionV2)`. Pas urgent car l'interface est déjà propre. |
+| S-04 | Basse | `hasLegsBound()` lit le slot ARMS (pas LEGS) — design V1 intentionnel : un seul item "bind" couvre bras+jambes via NBT mode. Pas un bug. | `state/IBondageState.java` | **Pas d'action immédiate** : cohérent avec le système actuel. Documenter le design dans un commentaire. Deviendra un vrai problème quand des items LEGS dédiés seront ajoutés en V2. |
+| S-05 | Moyenne | Thread safety incohérente : `volatile` (3 champs), `synchronized` (5 méthodes), rien (le reste). La paire `isStruggling`/`struggleStartTick` peut être observée dans un état inconsistant. | `state/PlayerBindState.java` | **Fix** : Définir une stratégie claire. Les champs accédés cross-thread (mouvement, struggle, captor) doivent être soit volatile soit synchronized. Auditer chaque champ. |
+| S-06 | Basse | `HumanChairHelper` dans `state/` mais c'est un utilitaire pur sans lien avec le state. Utilisé par AI, animation, mixins. | `state/HumanChairHelper.java` | **Fix** : Déplacer dans `items/base/` (à côté de `PoseType` dont il dépend) ou `util/`. Faire quand on touche le fichier. |
+
+### #3 — Items (V1) + V2 Bondage
+
+**Positif :**
+- `IV2BondageItem` bien conçu : multi-region, stack-aware, pose priority, blocked regions
+- `V2EquipmentManager` : conflict resolution solide (swap single, supersede global)
+- `V2EquipmentHelper` : facade propre pour read/write/sync
+- `DataDrivenBondageItem` : singleton + NBT registry pattern intelligent pour items data-driven
+- `ILockable` : système lock/jam/key complet et cohérent
+- `IHasResistance` : résistance NBT avec migration legacy, bien documentée
+- `BodyRegionV2` enum complet (15 régions, global flag)
+- Variant enums + factory pattern (BindVariant, GagVariant, etc.) propres
+
+**Problèmes :** (tous vérifiés ✓)
+
+| ID | Sévérité | Constat | Fichier(s) | Proposition |
+|----|----------|---------|------------|-------------|
+| I-01 | ~~Haute~~ | ~~Deux hiérarchies d'interfaces parallèles~~ | | **RÉSOLU** : suppression V1 (voir Décision D-01) |
+| I-02 | ~~Haute~~ | ~~V1 items bypassent la conflict resolution V2~~ | | **RÉSOLU** : suppression V1 (voir Décision D-01) |
+| I-03 | Moyenne | `DataDrivenBondageItem.getBaseResistance()` scanne tous items équipés et retourne MAX difficulty car `IHasResistance` n'a pas de paramètre ItemStack. Workaround documenté mais approximatif — peut surestimer la résistance. | `v2/bondage/datadriven/DataDrivenBondageItem.java` | **Archi** : Ajouter `getBaseResistance(ItemStack, LivingEntity)` à `IHasResistance` avec default qui délègue à l'ancienne méthode. DataDrivenBondageItem override la version stack-aware. |
+| I-04 | ~~Basse~~ | ~~IBondageItem.getBodyRegion() single-region~~ | | **RÉSOLU** : suppression V1 (voir Décision D-01) |
+| I-05 | ~~Moyenne~~ | ~~V1 items pas @Deprecated~~ | | **RÉSOLU** : suppression V1 (voir Décision D-01) |
+
+### Décision D-01 — Suppression totale du système V1 + Composants data-driven
+
+**Décision :** Le système V1 items est supprimé entièrement. Tous les items deviennent data-driven V2. La logique complexe (shock, GPS, lock, gagging, blinding, resistance, etc.) est extraite en **composants réutilisables** déclarables dans le JSON.
+
+**Périmètre de suppression :**
+- `IBondageItem` (interface)
+- `ItemBind`, `ItemGag`, `ItemBlindfold`, `ItemCollar`, `ItemEarplugs`, `ItemMittens` (abstracts)
+- `GenericBind`, `GenericGag`, `GenericBlindfold`, `GenericEarplugs`, `GenericMittens` (concrets)
+- `BindVariant`, `GagVariant`, `BlindfoldVariant`, `EarplugsVariant`, `MittensVariant` (enums)
+- `ItemClassicCollar`, `ItemShockCollar`, `ItemShockCollarAuto`, `ItemGpsCollar`, `ItemChokeCollar` (collars)
+- `ItemHood`, `ItemMedicalGag`, `ItemBallGag3D` (combos/special)
+- Registrations V1 dans `ModItems`
+- `PlayerEquipment.equipInRegion()` → remplacé par `V2EquipmentManager.tryEquip()`
+
+**Interfaces à conserver / migrer :**
+- `ILockable` — conservé, utilisé par V2 items
+- `IHasResistance` — conservé, refactoré avec paramètre ItemStack (I-03)
+- `IKnife` — conservé (outils, pas des bondage items)
+- `IAdjustable` — à évaluer (potentiellement composant)
+- `IHasBlindingEffect`, `IHasGaggingEffect` — deviennent des composants
+
+**Système de composants envisagé :**
+
+Chaque composant est une logique serveur réutilisable qu'un item data-driven peut déclarer :
+
+```json
+{
+ "type": "tiedup:bondage_item",
+ "display_name": "Shock Collar",
+ "model": "tiedup:models/gltf/shock_collar.glb",
+ "regions": ["NECK"],
+ "components": {
+ "lockable": true,
+ "shock": { "auto_interval": 200, "damage": 2.0 },
+ "gps": { "safe_zone_radius": 50 },
+ "gagging": { "comprehension": 0.2, "range": 10.0 },
+ "blinding": { "overlay": "tiedup:textures/overlay/blindfold.png" },
+ "resistance": { "base": 150 }
+ },
+ "escape_difficulty": 5,
+ "pose_priority": 10
+}
+```
+
+Exemples de composants à extraire de la logique V1 existante :
+| Composant | Source V1 | Comportement |
+|-----------|-----------|-------------|
+| `lockable` | `ILockable` | Lock/unlock, padlock, key matching, jam, lock resistance |
+| `resistance` | `IHasResistance` | Struggle resistance, configurable base value |
+| `shock` | `ItemShockCollar` | Auto-shock intervals, manual shock, damage |
+| `gps` | `ItemGpsCollar` | Safe zone, zone violation detection, owner alerts |
+| `gagging` | `IHasGaggingEffect` | Muffled speech, comprehension %, range limit |
+| `blinding` | `IHasBlindingEffect` | Blindfold overlay, hardcore mode |
+| `choking` | `ItemChokeCollar` | Air drain, darkness, slowness, non-lethal master mode |
+| `adjustable` | `IAdjustable` | Tightness level, visual adjustment |
+
+**Ce refactor est le plus gros chantier identifié par l'audit.** Il fera l'objet d'un plan d'implémentation dédié après la fin de l'audit.
+
+### #4 — Network
+
+**Positif :**
+- `AbstractClientPacket` / `AbstractPlayerSyncPacket` — bon pattern de base, handle enqueue sur main thread, retry queue pour les players pas encore loaded
+- `PacketRateLimiter` — token bucket complet avec catégories (struggle, minigame, action, selfbondage, ui). Thread-safe. Bon anti-spam.
+- `SyncManager` — facade centralisée pour sync inventory/state/enslavement/struggle/clothes. Pattern `sendSync()` générique propre.
+- `NetworkEventHandler` — gère correctement login sync, start-tracking sync, furniture reconnection, et fix MC-262715 (stale riding state)
+- `PacketSlaveAction` — bonnes validations serveur : dimension check, distance check, collar ownership check, GPS permission check
+- `PacketSelfBondage` — rate limited, route correctement V2 via `handleV2SelfBondage()` avec conflict check (`isRegionOccupied` + `isRegionBlocked`)
+
+**Problèmes :**
+
+| ID | Sévérité | Constat | Fichier(s) | Proposition |
+|----|----------|---------|------------|-------------|
+| N-01 | Moyenne | `PacketSelfBondage.handle()` contient 5 branches V1 (`instanceof ItemBind/ItemGag/ItemBlindfold/ItemMittens/ItemEarplugs`) qui devront être supprimées avec D-01. La branche V2 (`instanceof IV2BondageItem`) restera seule. | `network/selfbondage/PacketSelfBondage.java` | **Fix D-01** : Supprimer les branches V1, ne garder que la route V2. Simplifie massivement le fichier. |
+| N-02 | Moyenne | 4 packets dépendent de `ItemCollar` (V1 class) : `PacketSlaveAction`, `PacketMasterEquip`, `PacketAssignCellToCollar`, `PacketNpcCommand`. La logique collar (ownership, canShock, hasGPS) est couplée à la classe Java. | `network/slave/PacketSlaveAction.java`, `network/slave/PacketMasterEquip.java`, `network/cell/PacketAssignCellToCollar.java`, `network/personality/PacketNpcCommand.java` | **Fix D-01** : Quand ItemCollar migre vers le système composants, ces packets devront checker les composants (ex: `hasComponent("shock")`) au lieu de `instanceof ItemCollar`. |
+| N-03 | Basse | `PacketSyncBindState` sync des flags d'état V1 (isTiedUp, isGagged, isBlindfolded, etc.) séparément de `PacketSyncV2Equipment` qui sync le V2 capability. Potentiellement redondant post-suppression V1 — l'état peut être dérivé du V2 equipment. | `network/sync/PacketSyncBindState.java`, `v2/bondage/network/PacketSyncV2Equipment.java` | **Archi post-D-01** : Évaluer si `PacketSyncBindState` peut être supprimé et ses flags dérivés côté client depuis V2 equipment. Réduirait le nombre de packets sync. |
+| N-04 | Basse | `SyncManager.syncAllPlayersTo()` envoie 4 packets distincts par joueur (V2Equipment, BindState, Enslavement, Struggle, + Clothes si applicable). Pour un serveur avec N joueurs, un login génère ~4N packets. | `network/sync/SyncManager.java` | **Archi** : Considérer un packet bulk `PacketSyncFullState` qui combine tout en un seul envoi. Pas urgent — 4N packets est acceptable pour les tailles de serveur visées. |
+| N-05 | Cosmétique | Pas de `MCABondageManager` dans le package network, mais `PacketSyncMCABondage` existe — la logique MCA bondage sync est split entre `network/sync/` et `compat/mca/`. | `network/sync/PacketSyncMCABondage.java`, `compat/mca/` | **Pas d'action** : Acceptable pour un module de compatibilité. |
+
+### #5 — Client Animation + GLTF
+
+**Positif :**
+- **Architecture 3 couches propre** : Context layer (pri 40) → Item layer (pri 42) → Furniture layer (pri 43). Priorités claires, bon découplage.
+- **BondageAnimationManager** : API unifiée `playAnimation/playDirect/playContext/playFurniture` pour players et NPCs. Gestion des remote players (fallback stack), pending queue pour retry, furniture grace ticks pour éviter les stuck poses.
+- **GlbAnimationResolver** : Fallback chain fidèle au ARTIST_GUIDE (FullSitStruggle → SitStruggle → FullStruggle → Struggle → FullIdle → Idle). Support variants (.1, .2) avec random selection.
+- **GltfAnimationApplier** : Multi-item composite animation propre. Cache par state key, skip si unchanged. `applyMultiItemV2Animation()` merge les bones de plusieurs items dans un seul AnimationBuilder.
+- **ContextGlbRegistry** : Hot-reload des GLB de contexte depuis resource packs. Atomic swap pour thread safety render.
+- **AnimationContextResolver** : Résolution claire de contexte (sitting → struggling → movement style → sneaking → walking → idle). Version NPC séparée.
+- **GLTF pipeline (12 fichiers)** : Zéro dépendance V1. Parser, cache, skinning engine, mesh renderer, bone mapper, pose converter — tout est V2 natif.
+
+**Problèmes :**
+
+| ID | Sévérité | Constat | Fichier(s) | Proposition |
+|----|----------|---------|------------|-------------|
+| A-01 | Moyenne | 6 fichiers animation dépendent de `ItemBind` et `PoseType` (V1) pour déterminer le type de pose (STANDARD, DOG, HUMAN_CHAIR) et le bind mode (arms/legs/full). | `tick/AnimationTickHandler.java`, `tick/NpcAnimationTickHandler.java`, `render/PlayerArmHideEventHandler.java`, `render/PetBedRenderHandler.java`, `render/DogPoseRenderHandler.java`, `util/AnimationIdBuilder.java` | **Fix D-01** : Quand les items V1 sont supprimés, la pose type et le bind mode doivent venir du système V2 (data-driven definition ou composant). `PoseType` peut être conservé comme enum mais lu depuis `DataDrivenItemDefinition` au lieu de `ItemBind.getPoseType()`. |
+| A-02 | Basse | `StaticPoseApplier` dépend de `PoseType` — applique des rotations hardcodées par pose type (V1 fallback pour quand le GLTF n'est pas disponible). | `animation/StaticPoseApplier.java` | **Évaluer D-01** : Si tous les items ont un GLB, le static pose applier devient un fallback pur. Peut être conservé comme sécurité ou supprimé. |
+| A-03 | Basse | `GltfAnimationApplier` a un toggle debug F9 hardcodé qui charge un GLB spécifique (`cuffs_prototype.glb`). | `client/gltf/GltfAnimationApplier.java` (l.~350) | **Fix** : Supprimer ou mettre derrière un flag dev. Mineur. |
+| A-04 | Cosmétique | Le fallback animation dans `BondageAnimationManager.tryFallbackAnimation()` contient des patterns V1 spécifiques (`_arms_`, `sit_dog_`, `kneel_dog_`). Post-D-01, ces patterns n'existeront plus. | `animation/BondageAnimationManager.java` | **Fix D-01** : Nettoyer les fallbacks V1 obsolètes. Le système GLB a sa propre fallback chain (GlbAnimationResolver). |
+
+### #6 — Furniture
+
+**Positif :**
+- **Architecture data-driven exemplaire** : `FurnitureDefinition` (record immuable) + `FurnitureRegistry` (volatile atomic swap) + `FurnitureParser` + `FurnitureServerReloadListener`. Exactement le même pattern que les bondage items V2 data-driven.
+- **`ISeatProvider`** : interface propre et générique — conçue pour être implémentée par des monstres aussi (ARTIST_GUIDE: "monster seat system"). Bonne anticipation.
+- **`SeatDefinition`** : record immuable avec tous les champs du guide artiste (blocked regions, lockable, locked difficulty, item difficulty bonus).
+- **`EntityFurniture`** : Entity simple (pas LivingEntity), synced via `IEntityAdditionalSpawnData`. Dimensions variables depuis la definition. Animation state machine (IDLE → OCCUPIED → LOCKING → STRUGGLE → UNLOCKING). Seat assignments persistés en NBT.
+- **`FurniturePlacerItem`** : singleton item avec NBT ID, snap-to-wall, floor-only. Même pattern que `DataDrivenBondageItem`.
+- **`FurnitureAnimationContext`** : Conversion GLB → KeyframeAnimation avec bones sélectifs (blocked regions only). S'intègre proprement avec la furniture layer (pri 43) de BondageAnimationManager.
+- **`FurnitureGltfData`** : Parsing dédié qui sépare furniture armature des Player_* seat skeletons dans un seul GLB. Fidèle à l'ARTIST_GUIDE.
+- **Packets** : Rate limited, distance checks, permission checks (collar ownership pour forcemount).
+- **Reconnection robuste** : `NetworkEventHandler.handleFurnitureReconnection()` restaure les joueurs locked dans un seat après déconnexion, avec teleport si le meuble n'existe plus.
+
+**Problèmes :**
+
+| ID | Sévérité | Constat | Fichier(s) | Proposition |
+|----|----------|---------|------------|-------------|
+| F-01 | Moyenne | `EntityFurniture` et `PacketFurnitureForcemount` dépendent de `ItemCollar` (V1) pour vérifier collar ownership avant forcemount. | `v2/furniture/EntityFurniture.java`, `v2/furniture/network/PacketFurnitureForcemount.java` | **Fix D-01** : Quand ItemCollar migre vers composants, le check ownership doit utiliser le composant `lockable` ou `collar` au lieu de `instanceof ItemCollar`. |
+| F-02 | Basse | `FurnitureAnimationContext.create()` log "V1: skeleton parsing not yet implemented" quand `seatSkeleton` est null. Si le GLB n'a pas de skeleton data parsé, l'animation silencieusement ne se joue pas. | `v2/furniture/client/FurnitureAnimationContext.java` | **Évaluer** : Vérifier que le parser GLB furniture extrait toujours le skeleton. Si oui, le fallback est juste un safety net. Sinon, c'est un bug silencieux. |
+
+**Verdict : Le système furniture est le plus propre du mod.** Zéro dette architecturale, fidèle au guide artiste, extensible (monster seats prêts). Les deux constats sont mineurs — un couplage V1 qui part avec D-01 et un fallback debug à vérifier.
+
+### #7 — Entities + AI
+
+**Hiérarchie d'héritage :**
+
+```
+PathfinderMob
+ └─ AbstractTiedUpNpc (1281 lignes, ~100 méthodes) — implements IRestrainable, IAnimatedPlayer, IV2EquipmentHolder
+ ├─ EntityDamsel (834 lignes) — capturable NPC, personality, dialogue, inventory
+ │ ├─ EntityDamselShiny — variante rare
+ │ └─ EntityLaborGuard — garde de prison
+ └─ EntityKidnapper (2039 lignes, ~170 méthodes) — implements ICaptor, IDialogueSpeaker
+ └─ EntityKidnapperElite
+ ├─ EntityKidnapperMerchant — marchand neutre/hostile
+ ├─ EntityKidnapperArcher — attaque à distance
+ ├─ EntitySlaveTrader — boss de camp
+ ├─ EntityMaid — servante du trader
+ └─ EntityMaster (1192 lignes) — pet play system
+```
+
+**Positif :**
+- **Composant-based decomposition pour Damsel** : `DamselBondageManager`, `DamselPersonalitySystem`, `DamselInventoryManager`, `DamselAIController`, `DamselAnimationController`, `DamselAppearance`, `NpcEquipmentManager`, `NpcCaptivityManager` — 8 components avec interfaces host (`IBondageHost`, `IAIHost`, `IAnimationHost`, etc.). Bonne intention.
+- **Composant-based pour Kidnapper** : `KidnapperAggressionSystem`, `KidnapperAlertManager`, `KidnapperAppearance`, `KidnapperCaptiveManager`, `KidnapperCellManager`, `KidnapperCampManager`, `KidnapperStateManager`, `KidnapperSaleManager`, `KidnapperTargetSelector`, `KidnapperDataSerializer` — 10 components avec interfaces host. Très granulaire.
+- **AI goals bien séparés** : 80+ goals dédiés par type de NPC. Chaque goal est une classe autonome avec une seule responsabilité (KidnapperCaptureGoal, MasterDogwalkGoal, NpcFarmCommandGoal, etc.).
+- **V2 equipment intégré** : `AbstractTiedUpNpc` implémente `IV2EquipmentHolder`, utilise `V2BondageEquipment` directement. Les NPCs sont déjà sur le système V2.
+- **State machines Kidnapper** : `KidnapperState` enum avec états clairs (IDLE, HUNTING, CAPTURING, FLEEING, etc.).
+- **Master NPC complet** : pet play system avec task manager, state machine, punishment, dogwalk, furniture interaction — complexe mais fonctionnel.
+
+**Problèmes :**
+
+| ID | Sévérité | Constat | Fichier(s) | Proposition |
+|----|----------|---------|------------|-------------|
+| E-01 | Haute | **EntityKidnapper = 2039 lignes**, la plus grosse classe du mod. Malgré la décomposition en 10 components, la classe reste un god class. Elle mélange : ICaptor impl, targeting, capture equipment, sale system, job system, camp system, cell integration, alert system, NBT serialization, display name, dialogue, et des dizaines de getters/helpers. | `entities/EntityKidnapper.java` | **Archi** : Continuer la décomposition. Candidates : extraire le système de vente (`startSale`/`completeSale`/`cancelSale`/`abandonCaptive`) dans un component dédié, extraire le dialogue, extraire le ciblage. Objectif : ramener la classe sous 800 lignes. |
+| E-02 | Haute | **AbstractTiedUpNpc = 1281 lignes** avec ~100 méthodes. Même pattern que PlayerBindState (S-01) — c'est une façade de délégation vers les components, mais doit aussi implémenter IRestrainable (30+ méthodes) directement. | `entities/AbstractTiedUpNpc.java` | **Archi** : La taille vient surtout de l'implémentation de IRestrainable. Évaluer si les méthodes bondage peuvent être déléguées à `DamselBondageManager` via un pattern `default` sur IRestrainable (mais IRestrainable est une interface, pas une classe — limité). Ou accepter la taille comme coût de l'implémentation multi-interface. |
+| E-03 | Moyenne | **24 fichiers entities** dépendent de V1 item classes (`ItemBind`, `ItemCollar`, `PoseType`, `BindVariant`, etc.). C'est le package le plus impacté par D-01. | 24 fichiers (voir liste grep) | **Fix D-01** : Migration bulk. Les `instanceof ItemBind` deviennent `instanceof IV2BondageItem`, les `ItemCollar` checks deviennent des component checks. `PoseType` et `BindVariant` sont remplacés par des propriétés data-driven. |
+| E-04 | Moyenne | **Héritage profond** : EntityMaid → EntityKidnapperElite → EntityKidnapper → AbstractTiedUpNpc → PathfinderMob. 5 niveaux. EntityMaid et EntitySlaveTrader héritent de toute la logique kidnapper (capture, targeting, sale) alors qu'ils n'utilisent pas tout. | `entities/EntityMaid.java`, `entities/EntitySlaveTrader.java` | **Archi** : Envisager une refactorisation vers composition plutôt qu'héritage. La Maid n'est PAS un kidnapper — elle ne devrait pas hériter de `canCapture()`, `getCaptureBindTime()`, etc. Long terme : AbstractTiedUpNpc → EntityDamsel (passive) / EntityKidnapper (hostile), et les autres types composent leurs comportements. Pas urgent mais dette croissante. |
+| E-05 | Basse | `EntityDamsel` et `EntityKidnapper` ont des hiérarchies de host interfaces parallèles : `damsel/components/IBondageHost`, `damsel/components/IAIHost` vs `kidnapper/components/IAIHost`, `kidnapper/components/ICaptiveHost`, etc. Certaines pourraient être unifiées. | `entities/damsel/components/*.java`, `entities/kidnapper/components/*.java` | **Pas d'action immédiate** : Les interfaces host sont des contrats internes de chaque sous-arbre. Les unifier créerait un couplage horizontal. Acceptable tel quel. |
+| E-06 | Basse | `EntityMaster` (1192 lignes) contient le pet play system complet. Components `MasterPetManager`, `MasterTaskManager`, `MasterStateManager` existent mais la classe orchestre encore beaucoup de logique. | `entities/EntityMaster.java` | **Archi** : Même recommandation que E-01 — continuer la décomposition. Moins urgent car le système est plus cohérent (une seule responsabilité : pet play). |
+
+### #8 — Events
+
+**27 handlers, 5722 lignes, 8 domaines** (camp, captivity, combat, interaction, lifecycle, restriction, system).
+
+**Positif :** Bonne séparation par domaine. La plupart des handlers sont focalisés (85-200 lignes).
+
+| ID | Sévérité | Constat | Proposition |
+|----|----------|---------|-------------|
+| EV-01 | Moyenne | `RestraintTaskTickHandler` (675 lignes, 12 @SubscribeEvent) — consolide tous les ticks restraint. | Découper par type de tâche (tying, untying, force-feeding, shock checks). |
+| EV-02 | Moyenne | `BondageItemRestrictionHandler` (544 lignes, 12 @SubscribeEvent) — consolide toutes les restrictions. | Découper par type de restriction (legs, arms, gags, etc.). |
+| EV-03 | Moyenne | 7/27 handlers importent des classes V1 (`ItemBind`, `ItemCollar`, `ItemGag`, `IKnife`, `ILockable`). | **Fix D-01** : Migrer vers V2 checks (composants). |
+
+### #9 — Dialogue + Personality
+
+**31 fichiers, 7625 lignes.** Dialogue 100% data-driven (JSON par personality × speaker type). Personality enum-based (11 types) avec state machine (needs, mood, commands).
+
+**Positif :** Pipeline de chargement JSON propre (default → personality override → speaker-type). 18 catégories de dialogue. Dépendance unidirectionnelle (dialogue → personality, pas l'inverse). Seulement 3 fichiers importent du V1.
+
+| ID | Sévérité | Constat | Proposition |
+|----|----------|---------|-------------|
+| DI-01 | Moyenne | 6 god classes dans dialogue/ (EntityDialogueManager 622l, ConversationManager 564l, GagTalkManager 557l, DialogueLoader 469l, DialogueBridge 463l, DialogueManager 428l). | Acceptable pour la complexité du système. `DialogueBridge` (mapping legacy → new) peut être supprimé après D-01. |
+| DI-02 | Basse | `PersonalityState` (709 lignes) — god class conteneur d'état NPC (needs, mood, commands, jobs, home). | Continuer la décomposition si ça grossit. OK pour l'instant. |
+| DI-03 | Basse | 3 fichiers importent V1 (`GagTalkManager` → ItemGag, `ToolMode` → ItemBind, `PetRequestManager` → BindVariant). | **Fix D-01** : Migrer. |
+
+### #10 — Cells / Prison / Blocks
+
+**41 fichiers, 13329 lignes.** Système SavedData massif (CellRegistryV2, PrisonerManager, CampOwnership, ConfiscatedInventoryRegistry).
+
+**Positif :** Architecture SavedData correcte. Spatial indexing dans CellRegistryV2. Séparation services (PrisonerService, ItemService, BondageService).
+
+| ID | Sévérité | Constat | Proposition |
+|----|----------|---------|-------------|
+| CP-01 | Haute | `PrisonerService` (1058 lignes) — plus grosse classe du package, gère tout le lifecycle prisonnier. | Décomposer : labor, ransom, confiscation pourraient être des services séparés. |
+| CP-02 | Moyenne | `MarkerBlockEntity` (1146 lignes) — god class block entity, gère spawning + teleportation + cell deletion. | Extraire la logique spawning et teleportation dans des helpers. |
+| CP-03 | Moyenne | `BondageItemBlockEntity` utilise 6 imports V1 (ItemBind, ItemGag, ItemBlindfold, ItemCollar, ItemEarplugs, GenericClothes) pour valider les items dans les trap blocks. | **Fix D-01** : Remplacer par `instanceof IV2BondageItem` + component checks. |
+| CP-04 | Basse | `CellRegistryV2` (903 lignes) — gros mais justifié par les index spatiaux. | Acceptable. |
+
+### #11 — Compat (MCA + Wildfire)
+
+**24 fichiers, 6536 lignes.** MCA très couplé (21 fichiers, 5 sous-systèmes). Wildfire léger (3 fichiers, rendu only).
+
+| ID | Sévérité | Constat | Proposition |
+|----|----------|---------|-------------|
+| CO-01 | Haute | `MCAKidnappedAdapter` (907 lignes) — implémente IRestrainable complet pour les villagers MCA. God class + dépend de V1 items. | **Fix D-01** : Migrer V1 → V2. Décomposer en components comme AbstractTiedUpNpc. |
+| CO-02 | Moyenne | `WildfireDamselLayer` (988 lignes) — rendu physique très complexe. | Acceptable pour un système de rendu physique. Pas urgent. |
+| CO-03 | Basse | MCA compat utilise WeakHashMap et reflection pour détection — bon pattern de découplage. | Pas d'action. |
+
+### #12 — Util + Commands + Worldgen
+
+**55 fichiers, 15475 lignes.**
+
+| ID | Sévérité | Constat | Proposition |
+|----|----------|---------|-------------|
+| UC-01 | Haute | `BondageSubCommand` (1232 lignes) — monolithique, contient 16 sous-commandes (tie, untie, gag, collar, etc.) dans un seul fichier. | Découper : TieCommands, GagCommands, CollarCommands, etc. |
+| UC-02 | Haute | `RoomTheme` (1368 lignes) — config hardcodée de palettes de blocs pour worldgen. | **Archi** : Externaliser en data-driven (JSON). C'est exactement le type de contenu qui devrait être configurable. |
+| UC-03 | Moyenne | 7 fichiers importent V1 items (commands + RestraintApplicator + MCA adapter + HangingCagePiece). | **Fix D-01** : Migrer. |
+| UC-04 | Basse | `NPCCommand` (764 lignes) — gros mais focalisé sur le spawning/state NPC. | Acceptable, pourrait split par entity type. |
+
+---
+
+## Bilan Final
+
+### Statistiques
+
+- **12/12 systèmes audités**
+- **744 classes, 233k lignes** analysées
+- **38 constats** documentés (+ 4 résolus par D-01)
+- **1 décision architecturale majeure** (D-01 : suppression V1 + composants data-driven)
+
+### Classement par santé
+
+| Rang | Système | Verdict | Problèmes |
+|------|---------|---------|-----------|
+| 1 | Furniture | Exemplaire | 2 mineurs |
+| 2 | Animation + GLTF | Excellent | Résidus V1 seulement |
+| 3 | Network | Solide | Résidus V1 seulement |
+| 4 | Dialogue + Personality | Bon | God classes acceptables |
+| 5 | Events | Bon | 2 handlers trop gros |
+| 6 | Core | Dette technique | i18n, config triple |
+| 7 | State | Croissance organique | God class, thread safety |
+| 8 | Cells / Prison | Fonctionnel mais lourd | 11 god classes |
+| 9 | Compat | Fonctionnel mais couplé | MCA adapter 907l |
+| 10 | Util / Commands / Worldgen | Fonctionnel | BondageSubCmd 1232l, RoomTheme 1368l |
+| 11 | Entities + AI | Riche mais massif | 2039l god class, héritage 5 niveaux |
+| 12 | Items V1/V2 | **Point critique** | **D-01 : suppression totale V1** |
+
+### Priorités de correction
+
+| Priorité | Chantier | Impact |
+|----------|----------|--------|
+| **P0** | **D-01 : Suppression V1 + composants data-driven** | Élimine la dette #1 du mod. Impacte ~60 fichiers. Requiert un plan dédié. |
+| **P1** | C-01 : i18n SystemMessageManager | Requis pour toute traduction du mod. |
+| **P1** | UC-02 : RoomTheme → data-driven | 1368 lignes hardcodées de config worldgen. |
+| **P2** | E-01/E-02 : Décomposition EntityKidnapper/AbstractTiedUpNpc | 2039 + 1281 lignes. Améliore la maintenabilité entities. |
+| **P2** | CP-01 : Décomposition PrisonerService | 1058 lignes. |
+| **P2** | UC-01 : Split BondageSubCommand | 1232 lignes en 1 fichier. |
+| **P3** | S-02 : Encapsuler MovementState | 8 champs publics mutés directement. |
+| **P3** | S-05 : Thread safety cohérente | 3 stratégies sans cohérence dans PlayerBindState. |
+| **P3** | C-02 : Unifier Config/GameRules | 25 settings triplés. |
+| **P4** | Renommages (C-05 DamselRenderer, C-06 FQCNs) et cleanups cosmétiques. |
+
+### D-01 Phase 1 — Suivi implémentation
+
+**Branche :** `feature/d01-component-system` (17 commits, build clean)
+
+**Review adversariale :** 3 critiques + 5 hauts trouvés et corrigés.
+
+**Problèmes notés (non bloquants, à traiter lors de la migration Phase 2) :**
+
+| ID | Constat | Action |
+|----|---------|--------|
+| SMELL-002 | `GaggingComponent` n'a aucun consommateur — `GagTalkManager` lit `GagMaterial.getComprehension()`, pas le composant. | Lors de la migration Phase 2, faire pointer `GagTalkManager` vers le composant pour les items data-driven. |
+| SMELL-003 | Duplication sémantique entre le champ top-level `lockable` (boolean) et le composant `LockableComponent`. Un item doit configurer les deux pour un lock complet. | Lors de la migration Phase 2, le champ `lockable` devrait être dérivé de la présence du composant `lockable`. |
+| NOTE-003 | `test_component_gag.json` est dans les resources de production — visible par les joueurs. | Supprimer ou déplacer avant release. OK pour le dev. |
\ No newline at end of file
diff --git a/docs/plans/D01-branch-A-bridge-utilities.md b/docs/plans/D01-branch-A-bridge-utilities.md
new file mode 100644
index 0000000..12dd9b1
--- /dev/null
+++ b/docs/plans/D01-branch-A-bridge-utilities.md
@@ -0,0 +1,284 @@
+# D-01 Branch A : Bridge Utilities
+
+> **Prérequis :** Phase 1 (component system) mergée dans develop.
+> **Branche :** `feature/d01-branch-a-bridge`
+> **Objectif :** Créer les utilitaires V2 qui remplacent la logique V1, SANS supprimer de code V1. À la fin de cette branche, le mod compile, les items V1 et V2 coexistent, le struggle fonctionne pour les deux, et les nouveaux helpers sont prêts pour la migration des consommateurs.
+
+---
+
+## Décisions actées
+
+- **Stack size** : Stacks de 1 pour tout. Régression acceptée.
+- **Save compat** : Breaking change. Pas de migration. Mod en alpha.
+- **Résistance** : Config-driven via `resistanceId`, pas hardcodé en JSON.
+- **Comprehension/range gags** : Config-driven via `gagMaterial`, délègue à ModConfig au runtime.
+- **IHasGaggingEffect/IHasBlindingEffect** : DataDrivenBondageItem les implémente en checkant les composants.
+
+---
+
+## Tâches
+
+### A1. Modifier ResistanceComponent — config-driven
+
+**Fichier :** `src/main/java/com/tiedup/remake/v2/bondage/component/ResistanceComponent.java`
+
+Actuellement stocke un `int baseResistance` hardcodé. Doit stocker un `String resistanceId` et déléguer à `SettingsAccessor.getBindResistance(resistanceId)` au runtime.
+
+- Remplacer le champ `baseResistance` par `resistanceId` (String)
+- `fromJson()` : parse `"id"` au lieu de `"base"` — `"resistance": {"id": "rope"}`
+- `getBaseResistance()` : `return SettingsAccessor.getBindResistance(resistanceId);`
+- Garder un fallback `"base"` pour backward compat avec test_component_gag.json (ou le mettre à jour)
+
+**Référence :** `SettingsAccessor.getBindResistance(String)` dans `core/SettingsAccessor.java` — normalise les clés et lit depuis ModConfig.
+
+---
+
+### A2. Modifier GaggingComponent — config-driven + GagMaterial
+
+**Fichier :** `src/main/java/com/tiedup/remake/v2/bondage/component/GaggingComponent.java`
+
+Actuellement stocke `comprehension` et `range` hardcodés. Doit stocker un `String material` et déléguer à `GagMaterial`/ModConfig au runtime.
+
+- Ajouter champ `@Nullable String material`
+- `fromJson()` : parse `"material"` — `"gagging": {"material": "ball"}`
+- `getComprehension()` : si material != null → `GagMaterial.valueOf(material).getComprehension()` (lit ModConfig). Sinon → fallback au champ hardcodé (compat).
+- `getRange()` : idem via `GagMaterial.valueOf(material).getTalkRange()`
+- `getMaterial()` : expose le `GagMaterial` enum pour `GagTalkManager`
+- Garder les champs `comprehension`/`range` comme overrides optionnels (si présents dans JSON, ils prennent priorité sur le material)
+
+**Référence :** `GagMaterial` enum dans `items/base/GagVariant.java` ou `util/GagMaterial.java`.
+
+---
+
+### A3. Ajouter `appendTooltip()` à IItemComponent + ComponentHolder
+
+**Fichiers :**
+- `src/main/java/com/tiedup/remake/v2/bondage/component/IItemComponent.java`
+- `src/main/java/com/tiedup/remake/v2/bondage/component/ComponentHolder.java`
+- `src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenBondageItem.java`
+
+Ajouter hook tooltip pour que chaque composant contribue des lignes :
+
+```java
+// IItemComponent
+default void appendTooltip(ItemStack stack, @Nullable Level level, List tooltip, TooltipFlag flag) {}
+
+// ComponentHolder
+public void appendTooltip(ItemStack stack, @Nullable Level level, List tooltip, TooltipFlag flag) {
+ for (IItemComponent c : components.values()) c.appendTooltip(stack, level, tooltip, flag);
+}
+```
+
+Dans `DataDrivenBondageItem.appendHoverText()` : appeler `holder.appendTooltip(...)`.
+
+Implémenter `appendTooltip` dans chaque composant existant :
+- `LockableComponent` : affiche "Locked" / "Lockable"
+- `ResistanceComponent` : affiche la résistance en advanced mode (F3+H)
+- `GaggingComponent` : affiche le type de gag
+- `BlindingComponent` : rien (pas d'info utile)
+- `ShockComponent` : affiche "Shock: Manual" ou "Shock: Auto (Xs)"
+- `GpsComponent` : affiche "GPS Tracking" + zone radius
+- `ChokingComponent` : affiche "Choking Effect"
+- `AdjustableComponent` : rien (ajustement est visuel)
+
+---
+
+### A4. Champ `pose_type` + PoseTypeHelper
+
+**Fichiers :**
+- `src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemDefinition.java` — ajouter `@Nullable String poseType`
+- `src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemParser.java` — parser `"pose_type"`
+- Créer `src/main/java/com/tiedup/remake/v2/bondage/PoseTypeHelper.java`
+
+```java
+public static PoseType getPoseType(ItemStack stack) {
+ // V2: read from data-driven definition
+ DataDrivenItemDefinition def = DataDrivenItemRegistry.get(stack);
+ if (def != null && def.poseType() != null) {
+ try { return PoseType.valueOf(def.poseType()); }
+ catch (IllegalArgumentException e) { return PoseType.STANDARD; }
+ }
+ // V1 fallback: instanceof ItemBind
+ if (stack.getItem() instanceof ItemBind bind) {
+ return bind.getPoseType();
+ }
+ return PoseType.STANDARD;
+}
+```
+
+**Note mixin :** Les mixins (`MixinPlayerModel`, `MixinCamera`, etc.) appellent `itemBind.getPoseType()`. Ils devront migrer vers `PoseTypeHelper.getPoseType(stack)` en Branch C. Le helper doit être dans un package chargé tôt — `v2/bondage/` est OK car le mod est chargé avant les mixins client.
+
+---
+
+### A5. BindModeHelper
+
+**Fichier :** Créer `src/main/java/com/tiedup/remake/v2/bondage/BindModeHelper.java`
+
+Méthodes statiques pures NBT (clé `"bindMode"`) :
+- `hasArmsBound(ItemStack)` → true si mode "arms" ou "full"
+- `hasLegsBound(ItemStack)` → true si mode "legs" ou "full"
+- `getBindModeId(ItemStack)` → "full", "arms", ou "legs"
+- `cycleBindModeId(ItemStack)` → full→arms→legs→full, retourne le nouveau mode
+- `getBindModeTranslationKey(ItemStack)` → clé i18n
+- `isBindItem(ItemStack)` → true si l'item a la région ARMS dans sa definition V2, OU est `instanceof ItemBind`
+
+**Référence :** `ItemBind.java` lignes 64-160 pour les méthodes statiques existantes.
+
+---
+
+### A6. CollarHelper (complet)
+
+**Fichier :** Créer `src/main/java/com/tiedup/remake/v2/bondage/CollarHelper.java`
+
+Extraire TOUTES les méthodes NBT de `ItemCollar` (1407 lignes) + 5 sous-classes en méthodes statiques. Sections :
+
+**Ownership :**
+- `isOwner(ItemStack, Player)`, `isOwner(ItemStack, UUID)`
+- `getOwners(ItemStack)` → Set
+- `addOwner(ItemStack, UUID, String name)`, `removeOwner(ItemStack, UUID)`
+- `getBlacklist(ItemStack)`, `addToBlacklist(ItemStack, UUID)`, `removeFromBlacklist(ItemStack, UUID)`
+- `getWhitelist(ItemStack)`, `addToWhitelist(ItemStack, UUID)`, `removeFromWhitelist(ItemStack, UUID)`
+
+**Collar features :**
+- `isCollar(ItemStack)` → check OwnershipComponent presence OR instanceof ItemCollar
+- `getNickname(ItemStack)`, `setNickname(ItemStack, String)`
+- `isKidnappingModeEnabled(ItemStack)`, `setKidnappingModeEnabled(ItemStack, boolean)`
+- `getCellId(ItemStack)`, `setCellId(ItemStack, UUID)`
+- `shouldTieToPole(ItemStack)`, `setShouldTieToPole(ItemStack, boolean)`
+- `shouldWarnMasters(ItemStack)`, `setShouldWarnMasters(ItemStack, boolean)`
+- `isBondageServiceEnabled(ItemStack)`, `setBondageServiceEnabled(ItemStack, boolean)`
+- `getServiceSentence(ItemStack)`, `setServiceSentence(ItemStack, String)`
+
+**Shock :**
+- `canShock(ItemStack)` → check ShockComponent presence OR instanceof ItemShockCollar
+- `isPublic(ItemStack)`, `setPublic(ItemStack, boolean)`
+- `getShockInterval(ItemStack)` → depuis ShockComponent ou ItemShockCollarAuto
+
+**GPS :**
+- `hasGPS(ItemStack)` → check GpsComponent presence OR instanceof ItemGpsCollar
+- `hasPublicTracking(ItemStack)`, `setPublicTracking(ItemStack, boolean)`
+- `getSafeSpots(ItemStack)`, `addSafeSpot(ItemStack, ...)`, `removeSafeSpot(ItemStack, int)`
+- `isActive(ItemStack)`, `setActive(ItemStack, boolean)`
+
+**Choke :**
+- `isChokeCollar(ItemStack)` → check ChokingComponent presence OR instanceof ItemChokeCollar
+- `isChoking(ItemStack)`, `setChoking(ItemStack, boolean)`
+- `isPetPlayMode(ItemStack)`, `setPetPlayMode(ItemStack, boolean)`
+
+**Alert suppression :**
+- `runWithSuppressedAlert(Runnable)` — ThreadLocal, délègue à `ItemCollar` pendant la coexistence
+- `isRemovalAlertSuppressed()` → lit le ThreadLocal
+
+**Référence :** `ItemCollar.java` (1407 lignes), `ItemShockCollar.java` (133 lignes), `ItemGpsCollar.java` (369 lignes), `ItemChokeCollar.java` (154 lignes), `ItemShockCollarAuto.java` (58 lignes).
+
+---
+
+### A7. OwnershipComponent (complet)
+
+**Fichier :** Créer `src/main/java/com/tiedup/remake/v2/bondage/component/OwnershipComponent.java`
+**Modifier :** `ComponentType.java` — ajouter `OWNERSHIP`
+
+JSON : `"ownership": {}`
+
+Lifecycle hooks :
+- `onEquipped(stack, entity)` : enregistrer dans CollarRegistry (extraire de `ItemCollar.registerCollarInRegistry`). **Note :** le owner initial (le player qui equip) n'est pas dans la signature `onEquipped(stack, entity)`. Options : lire le owner depuis le NBT du stack (déjà écrit par l'interaction flow), ou passer par un tag temporaire.
+- `onUnequipped(stack, entity)` : alerter les owners (si pas supprimé via ThreadLocal), désenregistrer du CollarRegistry, reset auto-shock timer.
+- `appendTooltip` : nickname, lock status, kidnapping mode, cell ID, bondage service, shock status, GPS status.
+- `blocksUnequip` : si locked via ILockable.
+- **Override `dropLockOnUnlock()`** : retourner false pour les collars (pas de padlock drop). Note : ceci doit être sur DataDrivenBondageItem, pas sur le composant (ILockable est sur l'Item, pas sur le composant). → DataDrivenBondageItem override `dropLockOnUnlock()` quand OwnershipComponent est présent.
+
+---
+
+### A8. TyingInteractionHelper + DataDrivenBondageItem extensions
+
+**Fichier :** Créer `src/main/java/com/tiedup/remake/v2/bondage/TyingInteractionHelper.java`
+**Modifier :** `src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenBondageItem.java`
+
+Extraire le flow tying de `ItemBind.interactLivingEntity()` dans `TyingInteractionHelper` :
+- Accepte `(ServerPlayer player, LivingEntity target, ItemStack stack, InteractionHand hand)`
+- Crée TyingPlayerTask, gère la progress bar, consume l'item, equipe via V2
+
+Dans `DataDrivenBondageItem` :
+- `use()` : si regions contient ARMS → shift+click cycle bind mode (son + message action bar). Server-side only.
+- `interactLivingEntity()` : routing par région :
+ - ARMS → `TyingInteractionHelper` (tying task flow)
+ - NECK → collar equip flow (add owner, register CollarRegistry, play sound) — extraire de `ItemCollar.interactLivingEntity()`
+ - MOUTH/EYES/EARS/HANDS → instant equip (existant via AbstractV2BondageItem)
+
+---
+
+### A9. Réécrire StruggleBinds/StruggleCollar pour V2
+
+**Fichiers :**
+- `src/main/java/com/tiedup/remake/state/struggle/StruggleBinds.java`
+- `src/main/java/com/tiedup/remake/state/struggle/StruggleCollar.java`
+
+`StruggleBinds.canStruggle()` :
+- Actuellement : `instanceof ItemBind` → rejette V2
+- Fix : accepter `instanceof IV2BondageItem` avec region ARMS, OU `instanceof ItemBind`
+- Résistance : si V2 → `ResistanceComponent.getBaseResistance()`. Si V1 → existant via `IHasResistance`.
+
+`StruggleCollar` :
+- Actuellement : `instanceof ItemCollar` → rejette V2
+- Fix : accepter items avec `OwnershipComponent` (via `CollarHelper.isCollar(stack)`)
+- Résistance collar : via `ResistanceComponent` ou `IHasResistance`
+
+---
+
+### A10. DataDrivenBondageItem implémente IHasGaggingEffect/IHasBlindingEffect
+
+**Fichier :** `src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenBondageItem.java`
+
+Ajouter `implements IHasGaggingEffect, IHasBlindingEffect` sur la classe. Les 7+ call sites qui font `instanceof IHasGaggingEffect` continueront de fonctionner.
+
+**Problème :** Ces interfaces sont des markers (pas de méthodes). La simple présence de l'interface signifie "a l'effet". Mais `DataDrivenBondageItem` est un singleton — TOUS les items data-driven auront ces interfaces.
+
+**Solution :** Ne PAS implémenter les interfaces marker sur la classe. À la place, lors de la migration Branch C, convertir les call sites vers des component checks. C'est plus propre.
+
+→ **A10 annulé.** Les call sites migreront en Branch C vers `DataDrivenBondageItem.getComponent(stack, GAGGING, ...) != null`.
+
+---
+
+### A11. Fix PacketSelfBondage — routing par région
+
+**Fichier :** `src/main/java/com/tiedup/remake/network/selfbondage/PacketSelfBondage.java`
+
+Dans `handleV2SelfBondage()`, ajouter du routing par région :
+
+```java
+Set regions = v2Item.getOccupiedRegions(stack);
+if (regions.contains(BodyRegionV2.NECK)) {
+ // Cannot self-collar
+ return;
+}
+if (regions.contains(BodyRegionV2.ARMS)) {
+ // Tying task (existing flow)
+ handleV2SelfBind(player, stack, v2Item, state);
+} else {
+ // Accessories: instant equip (no tying delay)
+ handleV2SelfAccessory(player, stack, v2Item, state);
+}
+```
+
+Créer `handleV2SelfAccessory()` basé sur le pattern V1 `handleSelfAccessory()` (instant equip, swap si déjà équipé, locked check).
+
+### A12. NPC speed reduction
+
+**Fichier :** À déterminer (composant ou event handler)
+
+`ItemBind.onEquipped()` appelle `RestraintEffectUtils.applyBindSpeedReduction(entity)` pour les NPCs. Cette logique doit survivre à la migration.
+
+**Option :** Ajouter dans `DataDrivenBondageItem.onEquipped()` (après le component dispatch) un check : si entity n'est PAS un Player ET l'item a la région ARMS → appeler `RestraintEffectUtils.applyBindSpeedReduction(entity)`.
+
+---
+
+## Vérification
+
+- [ ] `make build` — compilation clean
+- [ ] Struggle fonctionne avec V1 items (ropes, chain, collar)
+- [ ] Struggle fonctionne avec V2 items (test_component_gag.json ou nouveau test item)
+- [ ] Self-bondage V2 : ARMS → tying delay, MOUTH → instant, NECK → rejeté
+- [ ] Tooltips : composants contribuent des lignes
+- [ ] PoseTypeHelper résout V1 (ItemBind) et V2 (definition.poseType)
+- [ ] CollarHelper.isOwner() fonctionne sur V1 ET V2 collars
+- [ ] MCP reindex après la branche
diff --git a/docs/plans/D01-branch-B-json-definitions.md b/docs/plans/D01-branch-B-json-definitions.md
new file mode 100644
index 0000000..8436bcf
--- /dev/null
+++ b/docs/plans/D01-branch-B-json-definitions.md
@@ -0,0 +1,128 @@
+# D-01 Branch B : 46 JSON Item Definitions
+
+> **Prérequis :** Branch A (bridge utilities) mergée.
+> **Branche :** `feature/d01-branch-b-definitions`
+> **Objectif :** Créer les définitions JSON data-driven pour les 46 items V1. Chaque item V1 a un équivalent JSON. Les valeurs sont config-driven (pas hardcodées). À la fin, les 46 items V2 existent en parallèle des V1.
+
+---
+
+## Extensions parser nécessaires
+
+Avant de créer les JSON, vérifier que le parser supporte :
+- `pose_type` (string, optionnel) — ajouté en Branch A4
+- `can_attach_padlock` (boolean, default true) — **à ajouter si pas fait en A**
+- GaggingComponent `material` — ajouté en Branch A2
+- ResistanceComponent `id` — ajouté en Branch A1
+
+**Fichiers :** `DataDrivenItemParser.java`, `DataDrivenItemDefinition.java`
+
+---
+
+## Items à créer
+
+Tous dans `src/main/resources/data/tiedup/tiedup_items/`.
+
+### Binds (16 fichiers)
+
+| Fichier | display_name | resistance id | pose_type | Notes |
+|---------|-------------|---------------|-----------|-------|
+| `ropes.json` | Ropes | rope | STANDARD | supports_color: true |
+| `armbinder.json` | Armbinder | armbinder | STANDARD | |
+| `dogbinder.json` | Dogbinder | armbinder | DOG | movement_style: crawl |
+| `chain.json` | Chains | chain | STANDARD | |
+| `ribbon.json` | Ribbon | ribbon | STANDARD | supports_color: true |
+| `slime.json` | Slime Bind | slime | STANDARD | can_attach_padlock: false |
+| `vine_seed.json` | Vine Bind | vine | STANDARD | can_attach_padlock: false |
+| `web_bind.json` | Web Bind | web | STANDARD | can_attach_padlock: false |
+| `shibari.json` | Shibari | rope | STANDARD | supports_color: true |
+| `leather_straps.json` | Leather Straps | armbinder | STANDARD | |
+| `medical_straps.json` | Medical Straps | armbinder | STANDARD | |
+| `beam_cuffs.json` | Beam Cuffs | chain | STANDARD | |
+| `duct_tape.json` | Duct Tape | tape | STANDARD | can_attach_padlock: false |
+| `straitjacket.json` | Straitjacket | straitjacket | STRAITJACKET | |
+| `wrap.json` | Wrap | wrap | WRAP | |
+| `latex_sack.json` | Latex Sack | latex_sack | LATEX_SACK | |
+
+Tous ont : regions `["ARMS"]`, lockable component (sauf organiques), resistance component avec `id`.
+
+**Référence :** `BindVariant.java` pour les registry names, pose types, supports_color. `ModConfig.ServerConfig` pour les resistance IDs (lignes 413-428).
+
+### Gags (19 fichiers)
+
+| Fichier | display_name | gag material | Notes |
+|---------|-------------|-------------|-------|
+| `cloth_gag.json` | Cloth Gag | cloth | |
+| `ropes_gag.json` | Rope Gag | cloth | |
+| `cleave_gag.json` | Cleave Gag | cloth | |
+| `ribbon_gag.json` | Ribbon Gag | cloth | supports_color: true |
+| `ball_gag.json` | Ball Gag | ball | supports_color: true |
+| `ball_gag_strap.json` | Ball Gag (Strap) | ball | |
+| `tape_gag.json` | Tape Gag | tape | can_attach_padlock: false |
+| `wrap_gag.json` | Wrap Gag | tape | |
+| `slime_gag.json` | Slime Gag | tape | can_attach_padlock: false |
+| `vine_gag.json` | Vine Gag | tape | can_attach_padlock: false |
+| `web_gag.json` | Web Gag | tape | can_attach_padlock: false |
+| `panel_gag.json` | Panel Gag | panel | |
+| `beam_panel_gag.json` | Beam Panel Gag | panel | |
+| `chain_panel_gag.json` | Chain Panel Gag | panel | |
+| `latex_gag.json` | Latex Gag | latex | |
+| `tube_gag.json` | Tube Gag | stuffed | |
+| `bite_gag.json` | Bite Gag | bite | |
+| `sponge_gag.json` | Sponge Gag | sponge | |
+| `baguette_gag.json` | Baguette Gag | baguette | |
+
+Tous ont : regions `["MOUTH"]`, gagging component avec material, lockable, resistance `{"id": "gag"}`, adjustable.
+
+**Référence :** `GagVariant.java` pour les materials et registry names. `ModConfig.ServerConfig.gagComprehension/gagRange` pour les valeurs runtime.
+
+### Blindfolds (2 fichiers)
+
+| Fichier | display_name |
+|---------|-------------|
+| `classic_blindfold.json` | Blindfold |
+| `blindfold_mask.json` | Blindfold Mask |
+
+Regions `["EYES"]`, components : blinding, lockable, resistance `{"id": "blindfold"}`, adjustable.
+
+### Earplugs (1 fichier)
+
+`classic_earplugs.json` — regions `["EARS"]`, lockable, resistance `{"id": "blindfold"}` (partage la résistance blindfold dans V1).
+
+### Mittens (1 fichier)
+
+`leather_mittens.json` — regions `["HANDS"]`, lockable.
+
+### Collars (5 fichiers)
+
+| Fichier | display_name | Components spécifiques |
+|---------|-------------|----------------------|
+| `classic_collar.json` | Classic Collar | ownership, lockable, resistance `{"id": "collar"}` |
+| `shock_collar.json` | Shock Collar | + shock `{"damage": 2.0}` |
+| `shock_collar_auto.json` | Auto Shock Collar | + shock `{"damage": 2.0, "auto_interval": 200}` |
+| `gps_collar.json` | GPS Collar | + gps `{"safe_zone_radius": 50}` |
+| `choke_collar.json` | Choke Collar | + choking |
+
+### Combos (3 fichiers)
+
+| Fichier | display_name | Regions | Components |
+|---------|-------------|---------|-----------|
+| `hood.json` | Hood | `["EYES"]` | blinding, gagging `{"material": "cloth"}`, lockable, blocked_regions: `["EYES", "EARS"]` |
+| `medical_gag.json` | Medical Gag | `["MOUTH"]` | gagging `{"material": "stuffed"}`, blinding, lockable |
+| `ball_gag_3d.json` | Ball Gag 3D | `["MOUTH"]` | gagging `{"material": "ball"}`, lockable, adjustable. Model 3D spécifique. |
+
+---
+
+## Supprimer le fichier test
+
+Supprimer `src/main/resources/data/tiedup/tiedup_items/test_component_gag.json` (fichier de test Phase 1, plus nécessaire).
+
+---
+
+## Vérification
+
+- [ ] `make build` — clean
+- [ ] `make run` — les 46 items data-driven apparaissent (via `/tiedup give` ou creative tab section data-driven)
+- [ ] Résistance = valeur config (changer dans config, vérifier que la résistance change)
+- [ ] Gag comprehension = valeur config (changer dans config, vérifier)
+- [ ] Collars ownership fonctionne via le nouveau OwnershipComponent
+- [ ] Items organiques (slime, vine, web, tape) ne peuvent pas recevoir de padlock
diff --git a/docs/plans/D01-branch-C-consumer-migration.md b/docs/plans/D01-branch-C-consumer-migration.md
new file mode 100644
index 0000000..14f2413
--- /dev/null
+++ b/docs/plans/D01-branch-C-consumer-migration.md
@@ -0,0 +1,107 @@
+# D-01 Branch C : Consumer Migration (~97 fichiers)
+
+> **Prérequis :** Branch A + B mergées.
+> **Branche :** `feature/d01-branch-c-migration`
+> **Objectif :** Remplacer TOUTES les références V1 (`instanceof ItemBind`, `ItemCollar.isOwner()`, `BindVariant.ROPES`, etc.) par les helpers/composants V2. Les classes V1 existent encore mais ne sont plus référencées. À la fin, `grep -r "instanceof ItemBind\|instanceof ItemGag\|instanceof ItemCollar\|instanceof ItemBlindfold\|instanceof ItemEarplugs\|instanceof ItemMittens\|BindVariant\|GagVariant" src/` retourne ZÉRO résultats (hors items/ lui-même).
+
+---
+
+## Pattern migration
+
+| V1 | V2 | Notes |
+|----|-----|-------|
+| `instanceof ItemBind` | `BindModeHelper.isBindItem(stack)` | Ou `instanceof IV2BondageItem` si on a juste besoin de savoir que c'est un bondage item |
+| `ItemBind.hasArmsBound(stack)` | `BindModeHelper.hasArmsBound(stack)` | Mêmes NBT keys |
+| `ItemBind.hasLegsBound(stack)` | `BindModeHelper.hasLegsBound(stack)` | |
+| `ItemBind.getBindModeId(stack)` | `BindModeHelper.getBindModeId(stack)` | |
+| `itemBind.getPoseType()` | `PoseTypeHelper.getPoseType(stack)` | |
+| `BindVariant.ROPES` / `ModItems.getBind(variant)` | `DataDrivenBondageItem.createStack(rl("tiedup:ropes"))` | **LAZY !** Ne pas appeler dans des static initializers |
+| `instanceof ItemCollar` + methods | `CollarHelper.isCollar(stack)` + `CollarHelper.method(stack)` | |
+| `instanceof ItemShockCollar` | `CollarHelper.canShock(stack)` | |
+| `instanceof ItemGpsCollar` | `CollarHelper.hasGPS(stack)` | |
+| `instanceof ItemChokeCollar` | `CollarHelper.isChokeCollar(stack)` | |
+| `instanceof IHasGaggingEffect` | `DataDrivenBondageItem.getComponent(stack, GAGGING, GaggingComponent.class) != null` | Pour V2 items. V1 items gardent l'interface pendant la transition. |
+| `instanceof IHasBlindingEffect` | `DataDrivenBondageItem.getComponent(stack, BLINDING, BlindingComponent.class) != null` | Idem |
+| `instanceof ItemGag` + `getGagMaterial()` | `GaggingComponent comp = getComponent(stack, GAGGING, ...)` + `comp.getMaterial()` | |
+| `PoseType` enum direct | Inchangé — l'enum est conservé | |
+| `IHasResistance` methods | Inchangé — l'interface est conservée | |
+| `ILockable` methods | Inchangé — l'interface est conservée | |
+
+---
+
+## Ordre de migration (critique d'abord)
+
+### Phase 1 : State core (12 fichiers)
+
+Ces fichiers sont la fondation — les migrer d'abord assure que le reste fonctionne.
+
+| Fichier | Changements |
+|---------|------------|
+| `state/IBondageState.java` | `hasArmsBound()`/`hasLegsBound()` → `BindModeHelper` |
+| `state/PlayerBindState.java` | `instanceof ItemCollar` → `CollarHelper`, `instanceof ItemBind` → `BindModeHelper` |
+| `state/PlayerCaptorManager.java` | `instanceof ItemCollar` → `CollarHelper.isCollar() + CollarHelper.getOwners()` |
+| `state/HumanChairHelper.java` | `PoseType` import OK (conservé) |
+| `state/components/PlayerEquipment.java` | **Garder `equipInRegion()` V1 fallback** (migre en Branch D). Remplacer `instanceof ItemBind/ItemCollar` dans resistance methods. |
+| `state/components/PlayerDataRetrieval.java` | `instanceof ItemCollar` → `CollarHelper.getNickname()` |
+| `state/components/PlayerLifecycle.java` | imports V1 → V2 helpers |
+| `state/components/PlayerShockCollar.java` | `instanceof ItemShockCollar` → `CollarHelper.canShock()` |
+| `state/struggle/StruggleBinds.java` | Déjà migré en Branch A9 — vérifier |
+| `state/struggle/StruggleCollar.java` | Déjà migré en Branch A9 — vérifier |
+| `state/struggle/StruggleAccessory.java` | Vérifier |
+
+### Phase 2 : Client animation/render (12 fichiers)
+
+| Fichier | Changements |
+|---------|------------|
+| `client/animation/tick/AnimationTickHandler.java` | `instanceof ItemBind` → `PoseTypeHelper.getPoseType()` + `BindModeHelper` |
+| `client/animation/tick/NpcAnimationTickHandler.java` | Idem |
+| `client/animation/render/PlayerArmHideEventHandler.java` | `instanceof ItemBind` → `PoseTypeHelper` |
+| `client/animation/render/DogPoseRenderHandler.java` | Idem |
+| `client/animation/render/PetBedRenderHandler.java` | Idem |
+| `client/animation/util/AnimationIdBuilder.java` | `PoseType` import OK (conservé) |
+| `client/animation/StaticPoseApplier.java` | `PoseType` import OK |
+| `client/model/DamselModel.java` | `PoseType` + `instanceof ItemBind` → helpers |
+| `client/FirstPersonMittensRenderer.java` | `BindVariant.ROPES` → lazy createStack |
+| `mixin/client/MixinPlayerModel.java` | `instanceof ItemBind` → `PoseTypeHelper` |
+| `mixin/client/MixinCamera.java` | Idem |
+| `mixin/client/MixinVillagerEntityBaseModelMCA.java` | Idem |
+
+### Phase 3 : Entity AI goals (15 fichiers)
+
+Principalement `instanceof ItemBind` → `BindModeHelper`, `ModItems.getBind(variant)` → `createStack(rl)`, `instanceof ItemCollar` → `CollarHelper`.
+
+### Phase 4 : Network packets (14 fichiers)
+
+`PacketSelfBondage` déjà migré en A11. Reste : `PacketSlaveAction`, `PacketMasterEquip`, `PacketAssignCellToCollar`, `PacketNpcCommand`, etc. → `CollarHelper`.
+
+### Phase 5 : Events (8 fichiers)
+
+`BondageItemRestrictionHandler`, `RestraintTaskTickHandler`, `PlayerEnslavementHandler`, `ChatEventHandler`, etc.
+
+### Phase 6 : Commands (6 fichiers)
+
+`BondageSubCommand` (1232 lignes) — le plus gros. `BindVariant` → `createStack()`. `NPCCommand`, `CollarCommand`, `KidnapSetCommand`.
+
+### Phase 7 : Entity classes (15 fichiers)
+
+`EntityKidnapper`, `KidnapperCaptureEquipment`, `KidnapperTheme`, `KidnapperItemSelector`, `KidnapperCollarConfig`, etc.
+
+### Phase 8 : Compat MCA (5 fichiers)
+
+`MCAKidnappedAdapter` (907 lignes) — `instanceof IHasGaggingEffect/IHasBlindingEffect` → component checks. `instanceof ItemCollar` → `CollarHelper`.
+
+### Phase 9 : Autres (10 fichiers)
+
+Dialogue (`GagTalkManager`, `PetRequestManager`), worldgen (`HangingCagePiece`), util (`RestraintApplicator`), blocks (`BondageItemBlockEntity`), dispenser, creative tab.
+
+---
+
+## Vérification
+
+- [ ] `make build` — clean
+- [ ] `grep -r "instanceof ItemBind\b" src/ --include="*.java" | grep -v "items/"` → 0 résultats
+- [ ] `grep -r "instanceof ItemGag\b" src/ --include="*.java" | grep -v "items/"` → 0 résultats
+- [ ] `grep -r "instanceof ItemCollar\b" src/ --include="*.java" | grep -v "items/"` → 0 résultats
+- [ ] `grep -r "BindVariant\b" src/ --include="*.java" | grep -v "items/"` → 0 résultats
+- [ ] `grep -r "GagVariant\b" src/ --include="*.java" | grep -v "items/"` → 0 résultats
+- [ ] `make run` — le mod fonctionne normalement
diff --git a/docs/plans/D01-branch-D-v1-cleanup.md b/docs/plans/D01-branch-D-v1-cleanup.md
new file mode 100644
index 0000000..055420b
--- /dev/null
+++ b/docs/plans/D01-branch-D-v1-cleanup.md
@@ -0,0 +1,188 @@
+# D-01 Branch D : V1 Cleanup
+
+> **Prérequis :** Branch A + B + C mergées. Zero références V1 hors du package `items/`.
+> **Branche :** `feature/d01-branch-d-cleanup`
+> **Objectif :** Supprimer toutes les classes V1 bondage. Migrer `equipInRegion()` vers le flow V2 complet. Réécrire le creative tab. À la fin, zéro classe V1 bondage dans le mod.
+
+---
+
+## Décisions
+
+- **Save compat :** Breaking change. Les items V1 dans les inventaires existants seront perdus. Mod en alpha.
+- **Pas de MissingMappingsEvent.** Simplement supprimer les registrations.
+
+---
+
+## Tâches
+
+### D1. Migrer equipInRegion() → V2EquipmentHelper.equipItem()
+
+**Fichier :** `src/main/java/com/tiedup/remake/state/components/PlayerEquipment.java`
+
+Maintenant que tous les items sont `DataDrivenBondageItem` (qui implémente `IV2BondageItem`), le bypass direct `setInRegion()` n'est plus nécessaire.
+
+Remplacer `equipInRegion()` par un appel à `V2EquipmentHelper.equipItem()` qui fait la conflict resolution complète (swap, supersede, blocked regions).
+
+Vérifier que les méthodes `putBindOn()`, `putGagOn()`, `putCollarOn()`, etc. fonctionnent toujours via le nouveau path.
+
+---
+
+### D2. Supprimer les classes V1 (~30 fichiers)
+
+**À supprimer :**
+
+Abstract bases :
+- `items/base/ItemBind.java` (637 lignes)
+- `items/base/ItemGag.java` (93 lignes)
+- `items/base/ItemBlindfold.java` (89 lignes)
+- `items/base/ItemCollar.java` (1407 lignes)
+- `items/base/ItemEarplugs.java` (90 lignes)
+- `items/base/ItemMittens.java` (72 lignes)
+
+Interfaces V1-only :
+- `items/base/IBondageItem.java` (102 lignes)
+- `items/base/IHasGaggingEffect.java` (33 lignes)
+- `items/base/IHasBlindingEffect.java` (33 lignes)
+- `items/base/IAdjustable.java` (49 lignes)
+- `items/base/ItemOwnerTarget.java`
+- `items/base/ItemColor.java`
+
+Variant enums :
+- `items/base/BindVariant.java` (90 lignes)
+- `items/base/GagVariant.java` (163 lignes)
+- `items/base/BlindfoldVariant.java` (48 lignes)
+- `items/base/EarplugsVariant.java` (33 lignes)
+- `items/base/MittensVariant.java` (35 lignes)
+
+Factory classes :
+- `items/GenericBind.java` (68 lignes)
+- `items/GenericGag.java` (72 lignes)
+- `items/GenericBlindfold.java` (37 lignes)
+- `items/GenericEarplugs.java` (37 lignes)
+- `items/GenericMittens.java` (37 lignes)
+
+Collars :
+- `items/ItemClassicCollar.java` (21 lignes)
+- `items/ItemShockCollar.java` (133 lignes)
+- `items/ItemShockCollarAuto.java` (58 lignes)
+- `items/ItemGpsCollar.java` (369 lignes)
+- `items/ItemChokeCollar.java` (154 lignes)
+
+Combos :
+- `items/ItemHood.java` (35 lignes)
+- `items/ItemMedicalGag.java` (24 lignes)
+- `items/bondage3d/gags/ItemBallGag3D.java` (78 lignes)
+- `items/bondage3d/IHas3DModelConfig.java`
+- `items/bondage3d/Model3DConfig.java`
+
+**À CONSERVER :**
+- `items/base/ILockable.java` — utilisé par V2 (AbstractV2BondageItem)
+- `items/base/IHasResistance.java` — utilisé par V2 (DataDrivenBondageItem)
+- `items/base/IKnife.java` — utilisé par GenericKnife (tool)
+- `items/base/PoseType.java` — utilisé par animation system
+- `items/base/KnifeVariant.java` — utilisé par GenericKnife (tool)
+- `items/base/AdjustmentHelper.java` — utilisé par adjustment packets
+- `items/GenericKnife.java` — tool, pas bondage
+- `items/clothes/GenericClothes.java` — déjà V2
+- `items/clothes/ClothesProperties.java`
+- `items/ModItems.java` — garde les tools, supprime les bondage
+- `items/ModCreativeTabs.java` — réécrit (voir D3)
+- Tous les tool items (whip, padlock, key, lockpick, taser, etc.)
+
+---
+
+### D3. Réécrire ModItems — retirer les registrations V1
+
+**Fichier :** `src/main/java/com/tiedup/remake/items/ModItems.java`
+
+Supprimer :
+- `BINDS` map + `registerAllBinds()`
+- `GAGS` map + `registerAllGags()`
+- `BLINDFOLDS` map + `registerAllBlindfolds()`
+- `EARPLUGS` map + `registerAllEarplugs()`
+- `MITTENS` map + `registerAllMittens()`
+- `BALL_GAG_3D`, `MEDICAL_GAG`, `HOOD`
+- `CLASSIC_COLLAR`, `SHOCK_COLLAR`, `SHOCK_COLLAR_AUTO`, `GPS_COLLAR`, `CHOKE_COLLAR`
+- Les helper accessors `getBind()`, `getGag()`, etc.
+
+Garder : CLOTHES, tous les tools (WHIP, PADLOCK, KEY, etc.), KNIVES, spawn eggs.
+
+---
+
+### D4. Réécrire ModCreativeTabs
+
+**Fichier :** `src/main/java/com/tiedup/remake/items/ModCreativeTabs.java`
+
+Remplacer l'itération par variant enums par :
+
+```java
+// Data-driven bondage items
+for (DataDrivenItemDefinition def : DataDrivenItemRegistry.getAll()) {
+ output.accept(DataDrivenBondageItem.createStack(def.id()));
+}
+```
+
+Pour l'ordre : ajouter un champ optionnel `"creative_tab_order"` aux definitions JSON, ou trier par catégorie (regions) puis par nom.
+
+Pour les couleurs : si l'item a `supports_color`, ajouter les variantes colorées. Utiliser `tint_channels` du definition.
+
+---
+
+### D5. Cleanup PoseTypeHelper — retirer le fallback V1
+
+**Fichier :** `src/main/java/com/tiedup/remake/v2/bondage/PoseTypeHelper.java`
+
+Supprimer le fallback `instanceof ItemBind` dans `getPoseType()`. Ne garder que le path data-driven.
+
+---
+
+### D6. Cleanup CollarHelper — retirer les fallbacks V1
+
+**Fichier :** `src/main/java/com/tiedup/remake/v2/bondage/CollarHelper.java`
+
+Les méthodes comme `isCollar(stack)` checkent `instanceof ItemCollar` en fallback V1. Retirer ces checks.
+
+---
+
+### D7. Cleanup BindModeHelper — retirer le fallback V1
+
+Idem — retirer `instanceof ItemBind` fallback dans `isBindItem()`.
+
+---
+
+### D8. Cleanup imports orphelins
+
+Faire un pass sur tout le projet pour retirer les imports V1 orphelins.
+
+```bash
+grep -rn "import com.tiedup.remake.items.base.ItemBind" src/ --include="*.java"
+grep -rn "import com.tiedup.remake.items.base.ItemCollar" src/ --include="*.java"
+grep -rn "import com.tiedup.remake.items.base.IBondageItem" src/ --include="*.java"
+# etc. — tout doit retourner 0
+```
+
+---
+
+## Vérification finale
+
+- [ ] `make build` — clean, zero errors
+- [ ] `make run` — le mod démarre, les items apparaissent dans le creative tab
+- [ ] `grep -r "items.base.ItemBind\|items.base.ItemGag\|items.base.ItemCollar\|items.base.ItemBlindfold\|items.base.ItemEarplugs\|items.base.ItemMittens\|items.base.IBondageItem\|BindVariant\|GagVariant\|BlindfoldVariant\|EarplugsVariant\|MittensVariant" src/main/java/ --include="*.java"` → **0 résultats**
+- [ ] Les items data-driven s'équipent/se déséquipent correctement
+- [ ] Le struggle fonctionne (binds + collars)
+- [ ] Le self-bondage fonctionne (routing par région)
+- [ ] Les collars gardent leur ownership/shock/GPS après equip/unequip
+- [ ] Les tooltips affichent toutes les infos composants
+- [ ] `equipInRegion()` utilise V2EquipmentManager (conflict resolution active)
+- [ ] MCP reindex final
+
+---
+
+## Résultat attendu
+
+- **~6500 lignes de code V1 supprimées**
+- **46 items = 46 fichiers JSON** (data-driven, extensible par resource packs)
+- **1 seul Item singleton** (`DataDrivenBondageItem`)
+- **8 composants** gèrent toute la logique gameplay
+- **3 helpers** (`BindModeHelper`, `PoseTypeHelper`, `CollarHelper`) remplacent les anciennes classes
+- **Zero couplage V1** dans le reste du mod
diff --git a/docs/plans/D01-branch-E-resistance-rework.md b/docs/plans/D01-branch-E-resistance-rework.md
new file mode 100644
index 0000000..750d6d0
--- /dev/null
+++ b/docs/plans/D01-branch-E-resistance-rework.md
@@ -0,0 +1,155 @@
+# 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
diff --git a/docs/superpowers/plans/2026-04-13-d01-phase1-component-system.md b/docs/superpowers/plans/2026-04-13-d01-phase1-component-system.md
new file mode 100644
index 0000000..de7a3be
--- /dev/null
+++ b/docs/superpowers/plans/2026-04-13-d01-phase1-component-system.md
@@ -0,0 +1,869 @@
+# D-01 Phase 1: Data-Driven Item Component System
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Create a reusable component system so data-driven bondage items can declare gameplay behaviors (lockable, shock, GPS, gagging, etc.) in their JSON definition instead of requiring per-item Java classes.
+
+**Architecture:** Each component is a self-contained behavior module implementing `IItemComponent`. Components are declared in item JSON (`"components": {"shock": {...}}`), parsed by an extended `DataDrivenItemParser`, stored on `DataDrivenItemDefinition`, and ticked/queried via `DataDrivenBondageItem` delegation. The existing `ILockable` and `IHasResistance` interfaces are preserved as shared contracts — components implement them.
+
+**Tech Stack:** Java 17, Forge 1.20.1, existing V2 data-driven infrastructure (`DataDrivenItemRegistry`, `DataDrivenItemParser`, `DataDrivenItemDefinition`, `DataDrivenBondageItem`)
+
+**Scope:** This plan builds ONLY the component infrastructure + 3 core components (lockable, resistance, gagging). The remaining 5 components (shock, GPS, blinding, choking, adjustable) follow the same pattern and will be added in subsequent tasks or a follow-up plan.
+
+---
+
+## File Structure
+
+### New files
+
+| File | Responsibility |
+|------|---------------|
+| `v2/bondage/component/IItemComponent.java` | Component interface: lifecycle hooks, tick, query |
+| `v2/bondage/component/ComponentType.java` | Enum of all component types with factory methods |
+| `v2/bondage/component/ComponentHolder.java` | Container: holds instantiated components for an item stack |
+| `v2/bondage/component/LockableComponent.java` | Lock/unlock, padlock, key matching, jam, lock resistance |
+| `v2/bondage/component/ResistanceComponent.java` | Struggle resistance with configurable base value |
+| `v2/bondage/component/GaggingComponent.java` | Muffled speech, comprehension %, range limit |
+
+### Modified files
+
+| File | Changes |
+|------|---------|
+| `v2/bondage/datadriven/DataDrivenItemDefinition.java` | Add `Map componentConfigs` field |
+| `v2/bondage/datadriven/DataDrivenItemParser.java` | Parse `"components"` JSON block |
+| `v2/bondage/datadriven/DataDrivenBondageItem.java` | Delegate lifecycle hooks to components, expose `getComponent()` |
+| `v2/bondage/datadriven/DataDrivenItemRegistry.java` | Instantiate `ComponentHolder` per definition |
+
+---
+
+## Tasks
+
+### Task 1: IItemComponent interface
+
+**Files:**
+- Create: `src/main/java/com/tiedup/remake/v2/bondage/component/IItemComponent.java`
+
+- [ ] **Step 1: Create the component interface**
+
+```java
+package com.tiedup.remake.v2.bondage.component;
+
+import com.google.gson.JsonObject;
+import net.minecraft.world.entity.LivingEntity;
+import net.minecraft.world.item.ItemStack;
+
+/**
+ * A reusable behavior module for data-driven bondage items.
+ * Components are declared in JSON and instantiated per item definition.
+ *
+ * Lifecycle: parse config once (from JSON), then tick/query per equipped entity.
+ */
+public interface IItemComponent {
+
+ /**
+ * Called when the item is equipped on an entity.
+ * @param stack The equipped item stack
+ * @param entity The entity wearing the item
+ */
+ default void onEquipped(ItemStack stack, LivingEntity entity) {}
+
+ /**
+ * Called when the item is unequipped from an entity.
+ * @param stack The unequipped item stack
+ * @param entity The entity that was wearing the item
+ */
+ default void onUnequipped(ItemStack stack, LivingEntity entity) {}
+
+ /**
+ * Called every tick while the item is equipped.
+ * @param stack The equipped item stack
+ * @param entity The entity wearing the item
+ */
+ default void onWornTick(ItemStack stack, LivingEntity entity) {}
+
+ /**
+ * Whether this component prevents the item from being unequipped.
+ * @param stack The equipped item stack
+ * @param entity The entity wearing the item
+ * @return true if unequip should be blocked
+ */
+ default boolean blocksUnequip(ItemStack stack, LivingEntity entity) {
+ return false;
+ }
+}
+```
+
+- [ ] **Step 2: Verify file compiles**
+
+Run: `cd /home/user/Documents/Projet/Open-TiedUp! && make build 2>&1 | tail -5`
+Expected: BUILD SUCCESSFUL
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add src/main/java/com/tiedup/remake/v2/bondage/component/IItemComponent.java
+git commit -m "feat(D-01): add IItemComponent interface for data-driven item behaviors"
+```
+
+---
+
+### Task 2: ComponentType enum
+
+**Files:**
+- Create: `src/main/java/com/tiedup/remake/v2/bondage/component/ComponentType.java`
+
+- [ ] **Step 1: Create the component type registry**
+
+```java
+package com.tiedup.remake.v2.bondage.component;
+
+import com.google.gson.JsonObject;
+import javax.annotation.Nullable;
+import java.util.function.Function;
+
+/**
+ * All known component types. Each type knows how to instantiate itself from JSON config.
+ */
+public enum ComponentType {
+ LOCKABLE("lockable", LockableComponent::fromJson),
+ RESISTANCE("resistance", ResistanceComponent::fromJson),
+ GAGGING("gagging", GaggingComponent::fromJson);
+ // Future: SHOCK, GPS, BLINDING, CHOKING, ADJUSTABLE
+
+ private final String jsonKey;
+ private final Function factory;
+
+ ComponentType(String jsonKey, Function factory) {
+ this.jsonKey = jsonKey;
+ this.factory = factory;
+ }
+
+ public String getJsonKey() {
+ return jsonKey;
+ }
+
+ public IItemComponent create(JsonObject config) {
+ return factory.apply(config);
+ }
+
+ /**
+ * Look up a ComponentType by its JSON key. Returns null if unknown.
+ */
+ @Nullable
+ public static ComponentType fromKey(String key) {
+ for (ComponentType type : values()) {
+ if (type.jsonKey.equals(key)) {
+ return type;
+ }
+ }
+ return null;
+ }
+}
+```
+
+Note: This file will not compile yet because `LockableComponent`, `ResistanceComponent`, and `GaggingComponent` don't exist. We'll create stub classes first, then implement them.
+
+- [ ] **Step 2: Create stub classes so the enum compiles**
+
+Create three empty stubs (they will be fully implemented in Tasks 4-6):
+
+`src/main/java/com/tiedup/remake/v2/bondage/component/LockableComponent.java`:
+```java
+package com.tiedup.remake.v2.bondage.component;
+
+import com.google.gson.JsonObject;
+
+public class LockableComponent implements IItemComponent {
+ private LockableComponent() {}
+
+ public static IItemComponent fromJson(JsonObject config) {
+ return new LockableComponent();
+ }
+}
+```
+
+`src/main/java/com/tiedup/remake/v2/bondage/component/ResistanceComponent.java`:
+```java
+package com.tiedup.remake.v2.bondage.component;
+
+import com.google.gson.JsonObject;
+
+public class ResistanceComponent implements IItemComponent {
+ private ResistanceComponent() {}
+
+ public static IItemComponent fromJson(JsonObject config) {
+ return new ResistanceComponent();
+ }
+}
+```
+
+`src/main/java/com/tiedup/remake/v2/bondage/component/GaggingComponent.java`:
+```java
+package com.tiedup.remake.v2.bondage.component;
+
+import com.google.gson.JsonObject;
+
+public class GaggingComponent implements IItemComponent {
+ private GaggingComponent() {}
+
+ public static IItemComponent fromJson(JsonObject config) {
+ return new GaggingComponent();
+ }
+}
+```
+
+- [ ] **Step 3: Verify all files compile**
+
+Run: `cd /home/user/Documents/Projet/Open-TiedUp! && make build 2>&1 | tail -5`
+Expected: BUILD SUCCESSFUL
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add src/main/java/com/tiedup/remake/v2/bondage/component/
+git commit -m "feat(D-01): add ComponentType enum with stub component classes"
+```
+
+---
+
+### Task 3: ComponentHolder container
+
+**Files:**
+- Create: `src/main/java/com/tiedup/remake/v2/bondage/component/ComponentHolder.java`
+
+- [ ] **Step 1: Create the component container**
+
+```java
+package com.tiedup.remake.v2.bondage.component;
+
+import net.minecraft.world.entity.LivingEntity;
+import net.minecraft.world.item.ItemStack;
+
+import javax.annotation.Nullable;
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.Map;
+
+/**
+ * Holds instantiated components for an item definition.
+ * Immutable after construction. One per DataDrivenItemDefinition.
+ */
+public final class ComponentHolder {
+
+ public static final ComponentHolder EMPTY = new ComponentHolder(Map.of());
+
+ private final Map components;
+
+ public ComponentHolder(Map components) {
+ this.components = components.isEmpty()
+ ? Map.of()
+ : Collections.unmodifiableMap(new EnumMap<>(components));
+ }
+
+ /**
+ * Get a component by type, or null if not present.
+ */
+ @Nullable
+ public IItemComponent get(ComponentType type) {
+ return components.get(type);
+ }
+
+ /**
+ * Get a component by type, cast to the expected class.
+ * Returns null if not present or wrong type.
+ */
+ @Nullable
+ @SuppressWarnings("unchecked")
+ public T get(ComponentType type, Class clazz) {
+ IItemComponent component = components.get(type);
+ if (clazz.isInstance(component)) {
+ return (T) component;
+ }
+ return null;
+ }
+
+ /**
+ * Check if a component type is present.
+ */
+ public boolean has(ComponentType type) {
+ return components.containsKey(type);
+ }
+
+ /**
+ * Fire onEquipped for all components.
+ */
+ public void onEquipped(ItemStack stack, LivingEntity entity) {
+ for (IItemComponent component : components.values()) {
+ component.onEquipped(stack, entity);
+ }
+ }
+
+ /**
+ * Fire onUnequipped for all components.
+ */
+ public void onUnequipped(ItemStack stack, LivingEntity entity) {
+ for (IItemComponent component : components.values()) {
+ component.onUnequipped(stack, entity);
+ }
+ }
+
+ /**
+ * Fire onWornTick for all components.
+ */
+ public void onWornTick(ItemStack stack, LivingEntity entity) {
+ for (IItemComponent component : components.values()) {
+ component.onWornTick(stack, entity);
+ }
+ }
+
+ /**
+ * Check if any component blocks unequip.
+ */
+ public boolean blocksUnequip(ItemStack stack, LivingEntity entity) {
+ for (IItemComponent component : components.values()) {
+ if (component.blocksUnequip(stack, entity)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Whether this holder has any components.
+ */
+ public boolean isEmpty() {
+ return components.isEmpty();
+ }
+}
+```
+
+- [ ] **Step 2: Verify compilation**
+
+Run: `cd /home/user/Documents/Projet/Open-TiedUp! && make build 2>&1 | tail -5`
+Expected: BUILD SUCCESSFUL
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add src/main/java/com/tiedup/remake/v2/bondage/component/ComponentHolder.java
+git commit -m "feat(D-01): add ComponentHolder container for item components"
+```
+
+---
+
+### Task 4: Integrate components into DataDrivenItemDefinition + Parser
+
+**Files:**
+- Modify: `src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemDefinition.java`
+- Modify: `src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemParser.java`
+
+- [ ] **Step 1: Add componentConfigs field to DataDrivenItemDefinition**
+
+Read the current record definition, then add a new field. The record should get a new parameter:
+
+```java
+/** Raw component configs from JSON, keyed by ComponentType. */
+Map componentConfigs
+```
+
+Add after the last existing field in the record. Also add a convenience method:
+
+```java
+/**
+ * Whether this definition declares a specific component.
+ */
+public boolean hasComponent(ComponentType type) {
+ return componentConfigs != null && componentConfigs.containsKey(type);
+}
+```
+
+- [ ] **Step 2: Parse "components" block in DataDrivenItemParser**
+
+Read `DataDrivenItemParser.java` and add parsing for the `"components"` JSON field. After parsing all existing fields, add:
+
+```java
+// Parse components
+Map componentConfigs = new EnumMap<>(ComponentType.class);
+if (json.has("components")) {
+ JsonObject componentsObj = json.getAsJsonObject("components");
+ for (Map.Entry entry : componentsObj.entrySet()) {
+ ComponentType type = ComponentType.fromKey(entry.getKey());
+ if (type != null) {
+ JsonObject config = entry.getValue().isJsonObject()
+ ? entry.getValue().getAsJsonObject()
+ : new JsonObject();
+ componentConfigs.put(type, config);
+ } else {
+ LOGGER.warn("[DataDrivenItemParser] Unknown component type '{}' in item '{}'",
+ entry.getKey(), id);
+ }
+ }
+}
+```
+
+Pass `componentConfigs` to the `DataDrivenItemDefinition` record constructor.
+
+- [ ] **Step 3: Update all existing call sites that construct DataDrivenItemDefinition**
+
+Search for all `new DataDrivenItemDefinition(` calls and add `Map.of()` for the new parameter (for the network sync deserialization path, etc.).
+
+- [ ] **Step 4: Verify compilation**
+
+Run: `cd /home/user/Documents/Projet/Open-TiedUp! && make build 2>&1 | tail -5`
+Expected: BUILD SUCCESSFUL
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemDefinition.java
+git add src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemParser.java
+git commit -m "feat(D-01): parse component configs from item JSON definitions"
+```
+
+---
+
+### Task 5: Instantiate ComponentHolder in DataDrivenItemRegistry
+
+**Files:**
+- Modify: `src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemRegistry.java`
+
+- [ ] **Step 1: Add ComponentHolder cache**
+
+Read `DataDrivenItemRegistry.java`. Add a parallel cache that maps `ResourceLocation` to `ComponentHolder`. When definitions are loaded/reloaded, instantiate components from their `componentConfigs`.
+
+Add field:
+```java
+private static volatile Map COMPONENT_HOLDERS = Map.of();
+```
+
+In the reload/register method, after storing definitions, build component holders:
+```java
+Map holders = new HashMap<>();
+for (Map.Entry entry : newDefinitions.entrySet()) {
+ DataDrivenItemDefinition def = entry.getValue();
+ Map components = new EnumMap<>(ComponentType.class);
+ for (Map.Entry compEntry : def.componentConfigs().entrySet()) {
+ components.put(compEntry.getKey(), compEntry.getKey().create(compEntry.getValue()));
+ }
+ holders.put(entry.getKey(), new ComponentHolder(components));
+}
+COMPONENT_HOLDERS = Collections.unmodifiableMap(holders);
+```
+
+Add accessor:
+```java
+@Nullable
+public static ComponentHolder getComponents(ItemStack stack) {
+ DataDrivenItemDefinition def = get(stack);
+ if (def == null) return null;
+ return COMPONENT_HOLDERS.get(def.id());
+}
+
+@Nullable
+public static ComponentHolder getComponents(ResourceLocation id) {
+ return COMPONENT_HOLDERS.get(id);
+}
+```
+
+- [ ] **Step 2: Verify compilation**
+
+Run: `cd /home/user/Documents/Projet/Open-TiedUp! && make build 2>&1 | tail -5`
+Expected: BUILD SUCCESSFUL
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemRegistry.java
+git commit -m "feat(D-01): instantiate ComponentHolder per item definition on reload"
+```
+
+---
+
+### Task 6: Delegate DataDrivenBondageItem lifecycle to components
+
+**Files:**
+- Modify: `src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenBondageItem.java`
+
+- [ ] **Step 1: Add component delegation in lifecycle hooks**
+
+Read `DataDrivenBondageItem.java`. In `onEquipped()` and `onUnequipped()`, delegate to components:
+
+```java
+@Override
+public void onEquipped(ItemStack stack, LivingEntity entity) {
+ ComponentHolder holder = DataDrivenItemRegistry.getComponents(stack);
+ if (holder != null) {
+ holder.onEquipped(stack, entity);
+ }
+}
+
+@Override
+public void onUnequipped(ItemStack stack, LivingEntity entity) {
+ ComponentHolder holder = DataDrivenItemRegistry.getComponents(stack);
+ if (holder != null) {
+ holder.onUnequipped(stack, entity);
+ }
+}
+```
+
+Override `canUnequip` to check component blocks:
+```java
+@Override
+public boolean canUnequip(ItemStack stack, LivingEntity entity) {
+ ComponentHolder holder = DataDrivenItemRegistry.getComponents(stack);
+ if (holder != null && holder.blocksUnequip(stack, entity)) {
+ return false;
+ }
+ return super.canUnequip(stack, entity);
+}
+```
+
+Add a public static helper for external code to query components:
+```java
+/**
+ * Get a specific component from a data-driven item stack.
+ * @return The component, or null if the item is not data-driven or lacks this component.
+ */
+@Nullable
+public static T getComponent(ItemStack stack, ComponentType type, Class clazz) {
+ ComponentHolder holder = DataDrivenItemRegistry.getComponents(stack);
+ if (holder == null) return null;
+ return holder.get(type, clazz);
+}
+```
+
+- [ ] **Step 2: Verify compilation**
+
+Run: `cd /home/user/Documents/Projet/Open-TiedUp! && make build 2>&1 | tail -5`
+Expected: BUILD SUCCESSFUL
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenBondageItem.java
+git commit -m "feat(D-01): delegate DataDrivenBondageItem lifecycle to components"
+```
+
+---
+
+### Task 7: Implement LockableComponent
+
+**Files:**
+- Modify: `src/main/java/com/tiedup/remake/v2/bondage/component/LockableComponent.java`
+
+- [ ] **Step 1: Implement full lockable logic**
+
+Replace the stub with the full implementation. Extract lock behavior from `ILockable` (which remains as a shared interface). The component reads its config from JSON and delegates to `ILockable` default methods on the item stack:
+
+```java
+package com.tiedup.remake.v2.bondage.component;
+
+import com.google.gson.JsonObject;
+import com.tiedup.remake.items.base.ILockable;
+import net.minecraft.world.entity.LivingEntity;
+import net.minecraft.world.item.ItemStack;
+
+/**
+ * Component: lockable behavior for data-driven items.
+ * Delegates to ILockable interface methods on the item.
+ *
+ * JSON config:
+ * {"lockable": true}
+ * or
+ * {"lockable": {"lock_resistance": 300}}
+ */
+public class LockableComponent implements IItemComponent {
+
+ private final int lockResistance;
+
+ private LockableComponent(int lockResistance) {
+ this.lockResistance = lockResistance;
+ }
+
+ public static IItemComponent fromJson(JsonObject config) {
+ int resistance = 250; // default from SettingsAccessor
+ if (config.has("lock_resistance")) {
+ resistance = config.get("lock_resistance").getAsInt();
+ }
+ return new LockableComponent(resistance);
+ }
+
+ public int getLockResistance() {
+ return lockResistance;
+ }
+
+ @Override
+ public boolean blocksUnequip(ItemStack stack, LivingEntity entity) {
+ // If item implements ILockable, check if locked
+ if (stack.getItem() instanceof ILockable lockable) {
+ return lockable.isLocked(stack);
+ }
+ return false;
+ }
+}
+```
+
+- [ ] **Step 2: Verify compilation**
+
+Run: `cd /home/user/Documents/Projet/Open-TiedUp! && make build 2>&1 | tail -5`
+Expected: BUILD SUCCESSFUL
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add src/main/java/com/tiedup/remake/v2/bondage/component/LockableComponent.java
+git commit -m "feat(D-01): implement LockableComponent with configurable lock resistance"
+```
+
+---
+
+### Task 8: Implement ResistanceComponent
+
+**Files:**
+- Modify: `src/main/java/com/tiedup/remake/v2/bondage/component/ResistanceComponent.java`
+
+- [ ] **Step 1: Implement resistance logic**
+
+```java
+package com.tiedup.remake.v2.bondage.component;
+
+import com.google.gson.JsonObject;
+import net.minecraft.world.entity.LivingEntity;
+import net.minecraft.world.item.ItemStack;
+
+/**
+ * Component: struggle resistance for data-driven items.
+ * Replaces IHasResistance for data-driven items.
+ *
+ * JSON config:
+ * {"resistance": {"base": 150}}
+ */
+public class ResistanceComponent implements IItemComponent {
+
+ private final int baseResistance;
+
+ private ResistanceComponent(int baseResistance) {
+ this.baseResistance = baseResistance;
+ }
+
+ public static IItemComponent fromJson(JsonObject config) {
+ int base = 100; // default
+ if (config.has("base")) {
+ base = config.get("base").getAsInt();
+ }
+ return new ResistanceComponent(base);
+ }
+
+ /**
+ * Get the base resistance for this item.
+ * Used by DataDrivenBondageItem.getBaseResistance() to replace the MAX-scan workaround.
+ */
+ public int getBaseResistance() {
+ return baseResistance;
+ }
+}
+```
+
+- [ ] **Step 2: Update DataDrivenBondageItem.getBaseResistance() to use ResistanceComponent**
+
+In `DataDrivenBondageItem.java`, update `getBaseResistance()`:
+
+```java
+@Override
+public int getBaseResistance(LivingEntity entity) {
+ // Try stack-aware component lookup first (fixes I-03: no more MAX scan)
+ // Note: This method is called WITHOUT a stack parameter by IHasResistance.
+ // We still need the MAX scan as fallback until IHasResistance gets a stack-aware method.
+ if (entity != null) {
+ IV2BondageEquipment equip = V2EquipmentHelper.getEquipment(entity);
+ if (equip != null) {
+ int maxDifficulty = -1;
+ for (Map.Entry entry : equip.getAllEquipped().entrySet()) {
+ ItemStack stack = entry.getValue();
+ if (stack.getItem() == this) {
+ // Try component first
+ ResistanceComponent comp = DataDrivenBondageItem.getComponent(
+ stack, ComponentType.RESISTANCE, ResistanceComponent.class);
+ if (comp != null) {
+ maxDifficulty = Math.max(maxDifficulty, comp.getBaseResistance());
+ continue;
+ }
+ // Fallback to escape_difficulty from definition
+ DataDrivenItemDefinition def = DataDrivenItemRegistry.get(stack);
+ if (def != null) {
+ maxDifficulty = Math.max(maxDifficulty, def.escapeDifficulty());
+ }
+ }
+ }
+ if (maxDifficulty >= 0) return maxDifficulty;
+ }
+ }
+ return 100;
+}
+```
+
+- [ ] **Step 3: Verify compilation**
+
+Run: `cd /home/user/Documents/Projet/Open-TiedUp! && make build 2>&1 | tail -5`
+Expected: BUILD SUCCESSFUL
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add src/main/java/com/tiedup/remake/v2/bondage/component/ResistanceComponent.java
+git add src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenBondageItem.java
+git commit -m "feat(D-01): implement ResistanceComponent, fixes I-03 MAX scan for stack-aware items"
+```
+
+---
+
+### Task 9: Implement GaggingComponent
+
+**Files:**
+- Modify: `src/main/java/com/tiedup/remake/v2/bondage/component/GaggingComponent.java`
+
+- [ ] **Step 1: Implement gagging logic**
+
+```java
+package com.tiedup.remake.v2.bondage.component;
+
+import com.google.gson.JsonObject;
+
+/**
+ * Component: gagging behavior for data-driven items.
+ * Replaces IHasGaggingEffect for data-driven items.
+ *
+ * JSON config:
+ * {"gagging": {"comprehension": 0.2, "range": 10.0}}
+ */
+public class GaggingComponent implements IItemComponent {
+
+ private final double comprehension;
+ private final double range;
+
+ private GaggingComponent(double comprehension, double range) {
+ this.comprehension = comprehension;
+ this.range = range;
+ }
+
+ public static IItemComponent fromJson(JsonObject config) {
+ double comprehension = 0.2; // default: 20% understandable
+ double range = 10.0; // default: 10 blocks
+ if (config.has("comprehension")) {
+ comprehension = config.get("comprehension").getAsDouble();
+ }
+ if (config.has("range")) {
+ range = config.get("range").getAsDouble();
+ }
+ return new GaggingComponent(comprehension, range);
+ }
+
+ /**
+ * How much of the gagged speech is comprehensible (0.0 = nothing, 1.0 = full).
+ */
+ public double getComprehension() {
+ return comprehension;
+ }
+
+ /**
+ * Maximum range in blocks where muffled speech can be heard.
+ */
+ public double getRange() {
+ return range;
+ }
+}
+```
+
+- [ ] **Step 2: Verify compilation**
+
+Run: `cd /home/user/Documents/Projet/Open-TiedUp! && make build 2>&1 | tail -5`
+Expected: BUILD SUCCESSFUL
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add src/main/java/com/tiedup/remake/v2/bondage/component/GaggingComponent.java
+git commit -m "feat(D-01): implement GaggingComponent with comprehension and range"
+```
+
+---
+
+### Task 10: Create a test item JSON using components
+
+**Files:**
+- Create: `src/main/resources/data/tiedup/tiedup_items/test_gag.json`
+
+- [ ] **Step 1: Create a JSON definition that uses the new component system**
+
+```json
+{
+ "type": "tiedup:bondage_item",
+ "display_name": "Test Ball Gag",
+ "model": "tiedup:models/gltf/v2/handcuffs/cuffs_prototype.glb",
+ "regions": ["MOUTH"],
+ "animation_bones": {
+ "idle": []
+ },
+ "pose_priority": 10,
+ "escape_difficulty": 3,
+ "lockable": true,
+ "components": {
+ "lockable": {
+ "lock_resistance": 200
+ },
+ "resistance": {
+ "base": 80
+ },
+ "gagging": {
+ "comprehension": 0.15,
+ "range": 8.0
+ }
+ }
+}
+```
+
+- [ ] **Step 2: Verify the mod loads without errors**
+
+Run: `cd /home/user/Documents/Projet/Open-TiedUp! && make build 2>&1 | tail -5`
+Expected: BUILD SUCCESSFUL
+
+Check that the JSON parses by searching for component-related log output in the run logs (manual verification — start the game client with `make run`, check for errors in log).
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add src/main/resources/data/tiedup/tiedup_items/test_gag.json
+git commit -m "feat(D-01): add test_gag.json demonstrating component system"
+```
+
+---
+
+### Task 11: Verify and clean up
+
+- [ ] **Step 1: Full build verification**
+
+Run: `cd /home/user/Documents/Projet/Open-TiedUp! && make rebuild 2>&1 | tail -10`
+Expected: BUILD SUCCESSFUL with zero errors
+
+- [ ] **Step 2: Verify no regressions in existing items**
+
+Existing data-driven items (in `data/tiedup/tiedup_items/`) should continue working without the `"components"` field. The parser should handle missing components gracefully (empty map).
+
+- [ ] **Step 3: Reindex MCP**
+
+Run the MCP reindex to update the symbol table with new classes.
+
+- [ ] **Step 4: Final commit**
+
+```bash
+git add -A
+git commit -m "feat(D-01): Phase 1 complete - data-driven item component system
+
+Adds IItemComponent interface, ComponentType enum, ComponentHolder container,
+and 3 core components (LockableComponent, ResistanceComponent, GaggingComponent).
+
+Components are declared in item JSON 'components' field, parsed by DataDrivenItemParser,
+instantiated by DataDrivenItemRegistry, and delegated by DataDrivenBondageItem.
+
+Existing items without components continue to work unchanged."
+```
diff --git a/src/main/java/com/tiedup/remake/blocks/entity/BondageItemBlockEntity.java b/src/main/java/com/tiedup/remake/blocks/entity/BondageItemBlockEntity.java
index 8cea85b..279bb5c 100644
--- a/src/main/java/com/tiedup/remake/blocks/entity/BondageItemBlockEntity.java
+++ b/src/main/java/com/tiedup/remake/blocks/entity/BondageItemBlockEntity.java
@@ -6,6 +6,14 @@ import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.items.base.ItemEarplugs;
import com.tiedup.remake.items.base.ItemGag;
import com.tiedup.remake.items.clothes.GenericClothes;
+import com.tiedup.remake.v2.BodyRegionV2;
+import com.tiedup.remake.v2.bondage.BindModeHelper;
+import com.tiedup.remake.v2.bondage.CollarHelper;
+import com.tiedup.remake.v2.bondage.component.ComponentType;
+import com.tiedup.remake.v2.bondage.component.GaggingComponent;
+import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
+import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemDefinition;
+import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemRegistry;
import javax.annotation.Nullable;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
@@ -191,55 +199,45 @@ public abstract class BondageItemBlockEntity
@Override
public void readBondageData(CompoundTag tag) {
- // Read bind with type validation
+ // Read bind with type validation (V1 ItemBind or V2 ARMS-region item)
if (tag.contains("bind")) {
ItemStack bindStack = ItemStack.of(tag.getCompound("bind"));
- if (
- !bindStack.isEmpty() && bindStack.getItem() instanceof ItemBind
- ) {
+ if (!bindStack.isEmpty() && (bindStack.getItem() instanceof ItemBind || BindModeHelper.isBindItem(bindStack))) {
this.bind = bindStack;
}
}
- // Read gag with type validation
+ // Read gag with type validation (V1 ItemGag or V2 GAGGING component)
if (tag.contains("gag")) {
ItemStack gagStack = ItemStack.of(tag.getCompound("gag"));
- if (!gagStack.isEmpty() && gagStack.getItem() instanceof ItemGag) {
+ if (!gagStack.isEmpty() && (gagStack.getItem() instanceof ItemGag
+ || DataDrivenBondageItem.getComponent(gagStack, ComponentType.GAGGING, GaggingComponent.class) != null)) {
this.gag = gagStack;
}
}
- // Read blindfold with type validation
+ // Read blindfold with type validation (V1 ItemBlindfold or V2 EYES-region item)
if (tag.contains("blindfold")) {
- ItemStack blindfoldStack = ItemStack.of(
- tag.getCompound("blindfold")
- );
- if (
- !blindfoldStack.isEmpty() &&
- blindfoldStack.getItem() instanceof ItemBlindfold
- ) {
+ ItemStack blindfoldStack = ItemStack.of(tag.getCompound("blindfold"));
+ if (!blindfoldStack.isEmpty() && (blindfoldStack.getItem() instanceof ItemBlindfold
+ || isDataDrivenForRegion(blindfoldStack, BodyRegionV2.EYES))) {
this.blindfold = blindfoldStack;
}
}
- // Read earplugs with type validation
+ // Read earplugs with type validation (V1 ItemEarplugs or V2 EARS-region item)
if (tag.contains("earplugs")) {
ItemStack earplugsStack = ItemStack.of(tag.getCompound("earplugs"));
- if (
- !earplugsStack.isEmpty() &&
- earplugsStack.getItem() instanceof ItemEarplugs
- ) {
+ if (!earplugsStack.isEmpty() && (earplugsStack.getItem() instanceof ItemEarplugs
+ || isDataDrivenForRegion(earplugsStack, BodyRegionV2.EARS))) {
this.earplugs = earplugsStack;
}
}
- // Read collar with type validation
+ // Read collar with type validation (V1 ItemCollar or V2 collar)
if (tag.contains("collar")) {
ItemStack collarStack = ItemStack.of(tag.getCompound("collar"));
- if (
- !collarStack.isEmpty() &&
- collarStack.getItem() instanceof ItemCollar
- ) {
+ if (!collarStack.isEmpty() && (collarStack.getItem() instanceof ItemCollar || CollarHelper.isCollar(collarStack))) {
this.collar = collarStack;
}
}
@@ -279,6 +277,14 @@ public abstract class BondageItemBlockEntity
return tag;
}
+ // V2 HELPERS
+
+ /** Check if a stack is a data-driven item occupying the given body region. */
+ private static boolean isDataDrivenForRegion(ItemStack stack, BodyRegionV2 region) {
+ DataDrivenItemDefinition def = DataDrivenItemRegistry.get(stack);
+ return def != null && def.occupiedRegions().contains(region);
+ }
+
// NETWORK SYNC
/**
diff --git a/src/main/java/com/tiedup/remake/blocks/entity/TrappedChestBlockEntity.java b/src/main/java/com/tiedup/remake/blocks/entity/TrappedChestBlockEntity.java
index e4db192..73dd2da 100644
--- a/src/main/java/com/tiedup/remake/blocks/entity/TrappedChestBlockEntity.java
+++ b/src/main/java/com/tiedup/remake/blocks/entity/TrappedChestBlockEntity.java
@@ -1,7 +1,19 @@
package com.tiedup.remake.blocks.entity;
-import com.tiedup.remake.items.base.*;
+import com.tiedup.remake.items.base.ItemBind;
+import com.tiedup.remake.items.base.ItemBlindfold;
+import com.tiedup.remake.items.base.ItemCollar;
+import com.tiedup.remake.items.base.ItemEarplugs;
+import com.tiedup.remake.items.base.ItemGag;
import com.tiedup.remake.items.clothes.GenericClothes;
+import com.tiedup.remake.v2.BodyRegionV2;
+import com.tiedup.remake.v2.bondage.BindModeHelper;
+import com.tiedup.remake.v2.bondage.CollarHelper;
+import com.tiedup.remake.v2.bondage.component.ComponentType;
+import com.tiedup.remake.v2.bondage.component.GaggingComponent;
+import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
+import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemDefinition;
+import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemRegistry;
import javax.annotation.Nullable;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
@@ -45,7 +57,7 @@ public class TrappedChestBlockEntity
@Override
public void setBind(ItemStack stack) {
- if (stack.isEmpty() || stack.getItem() instanceof ItemBind) {
+ if (stack.isEmpty() || stack.getItem() instanceof ItemBind || BindModeHelper.isBindItem(stack)) {
this.bind = stack;
setChangedAndSync();
}
@@ -58,7 +70,8 @@ public class TrappedChestBlockEntity
@Override
public void setGag(ItemStack stack) {
- if (stack.isEmpty() || stack.getItem() instanceof ItemGag) {
+ if (stack.isEmpty() || stack.getItem() instanceof ItemGag
+ || DataDrivenBondageItem.getComponent(stack, ComponentType.GAGGING, GaggingComponent.class) != null) {
this.gag = stack;
setChangedAndSync();
}
@@ -71,7 +84,8 @@ public class TrappedChestBlockEntity
@Override
public void setBlindfold(ItemStack stack) {
- if (stack.isEmpty() || stack.getItem() instanceof ItemBlindfold) {
+ if (stack.isEmpty() || stack.getItem() instanceof ItemBlindfold
+ || isDataDrivenForRegion(stack, BodyRegionV2.EYES)) {
this.blindfold = stack;
setChangedAndSync();
}
@@ -84,7 +98,8 @@ public class TrappedChestBlockEntity
@Override
public void setEarplugs(ItemStack stack) {
- if (stack.isEmpty() || stack.getItem() instanceof ItemEarplugs) {
+ if (stack.isEmpty() || stack.getItem() instanceof ItemEarplugs
+ || isDataDrivenForRegion(stack, BodyRegionV2.EARS)) {
this.earplugs = stack;
setChangedAndSync();
}
@@ -97,7 +112,7 @@ public class TrappedChestBlockEntity
@Override
public void setCollar(ItemStack stack) {
- if (stack.isEmpty() || stack.getItem() instanceof ItemCollar) {
+ if (stack.isEmpty() || stack.getItem() instanceof ItemCollar || CollarHelper.isCollar(stack)) {
this.collar = stack;
setChangedAndSync();
}
@@ -183,6 +198,14 @@ public class TrappedChestBlockEntity
writeBondageData(tag);
}
+ // V2 HELPERS
+
+ /** Check if a stack is a data-driven item occupying the given body region. */
+ private static boolean isDataDrivenForRegion(ItemStack stack, BodyRegionV2 region) {
+ DataDrivenItemDefinition def = DataDrivenItemRegistry.get(stack);
+ return def != null && def.occupiedRegions().contains(region);
+ }
+
// NETWORK SYNC
/**
diff --git a/src/main/java/com/tiedup/remake/cells/CampLifecycleManager.java b/src/main/java/com/tiedup/remake/cells/CampLifecycleManager.java
index c1bc7e8..948d8ad 100644
--- a/src/main/java/com/tiedup/remake/cells/CampLifecycleManager.java
+++ b/src/main/java/com/tiedup/remake/cells/CampLifecycleManager.java
@@ -2,8 +2,8 @@ package com.tiedup.remake.cells;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.base.ILockable;
-import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.prison.PrisonerManager;
+import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.state.IRestrainable;
import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.v2.BodyRegionV2;
@@ -231,7 +231,7 @@ public final class CampLifecycleManager {
}
// Suppress collar removal alerts - this is a legitimate release (camp death)
- ItemCollar.runWithSuppressedAlert(() -> {
+ CollarHelper.runWithSuppressedAlert(() -> {
// Unlock collar if owned by the dead camp/trader
unlockCollarIfOwnedBy(prisoner, state, traderUUID);
@@ -285,8 +285,8 @@ public final class CampLifecycleManager {
return;
}
- if (collar.getItem() instanceof ItemCollar collarItem) {
- List owners = collarItem.getOwners(collar);
+ if (CollarHelper.isCollar(collar)) {
+ List owners = CollarHelper.getOwners(collar);
// If the dead trader/camp is an owner, unlock the collar
if (owners.contains(ownerUUID)) {
diff --git a/src/main/java/com/tiedup/remake/client/FirstPersonMittensRenderer.java b/src/main/java/com/tiedup/remake/client/FirstPersonMittensRenderer.java
index 5ab94da..3a7dd40 100644
--- a/src/main/java/com/tiedup/remake/client/FirstPersonMittensRenderer.java
+++ b/src/main/java/com/tiedup/remake/client/FirstPersonMittensRenderer.java
@@ -3,8 +3,8 @@ package com.tiedup.remake.client;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import com.tiedup.remake.core.TiedUpMod;
-import com.tiedup.remake.items.GenericBind;
-import com.tiedup.remake.items.base.BindVariant;
+import com.tiedup.remake.items.base.PoseType;
+import com.tiedup.remake.v2.bondage.PoseTypeHelper;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
@@ -141,12 +141,7 @@ public class FirstPersonMittensRenderer {
net.minecraft.world.item.ItemStack bindStack =
V2EquipmentHelper.getInRegion(player, BodyRegionV2.ARMS);
if (bindStack.isEmpty()) return false;
- if (bindStack.getItem() instanceof GenericBind bind) {
- BindVariant variant = bind.getVariant();
- return (
- variant == BindVariant.WRAP || variant == BindVariant.LATEX_SACK
- );
- }
- return false;
+ PoseType poseType = PoseTypeHelper.getPoseType(bindStack);
+ return poseType == PoseType.WRAP || poseType == PoseType.LATEX_SACK;
}
}
diff --git a/src/main/java/com/tiedup/remake/client/ModKeybindings.java b/src/main/java/com/tiedup/remake/client/ModKeybindings.java
index 549c617..11e60d5 100644
--- a/src/main/java/com/tiedup/remake/client/ModKeybindings.java
+++ b/src/main/java/com/tiedup/remake/client/ModKeybindings.java
@@ -6,7 +6,7 @@ import com.tiedup.remake.client.gui.screens.UnifiedBondageScreen;
import com.tiedup.remake.core.ModConfig;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.base.ILockable;
-import com.tiedup.remake.items.base.ItemCollar;
+import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.network.action.PacketForceSeatModifier;
import com.tiedup.remake.network.action.PacketStruggle;
@@ -428,11 +428,8 @@ public class ModKeybindings {
target,
BodyRegionV2.NECK
);
- if (
- !collarStack.isEmpty() &&
- collarStack.getItem() instanceof ItemCollar collar
- ) {
- return collar.isOwner(collarStack, player);
+ if (!collarStack.isEmpty() && CollarHelper.isCollar(collarStack)) {
+ return CollarHelper.isOwner(collarStack, player);
}
return false;
}
diff --git a/src/main/java/com/tiedup/remake/client/animation/render/DogPoseRenderHandler.java b/src/main/java/com/tiedup/remake/client/animation/render/DogPoseRenderHandler.java
index fa799f5..f61f877 100644
--- a/src/main/java/com/tiedup/remake/client/animation/render/DogPoseRenderHandler.java
+++ b/src/main/java/com/tiedup/remake/client/animation/render/DogPoseRenderHandler.java
@@ -1,8 +1,8 @@
package com.tiedup.remake.client.animation.render;
import com.tiedup.remake.core.TiedUpMod;
-import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.items.base.PoseType;
+import com.tiedup.remake.v2.bondage.PoseTypeHelper;
import com.tiedup.remake.state.HumanChairHelper;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.v2.BodyRegionV2;
@@ -93,14 +93,11 @@ public class DogPoseRenderHandler {
}
ItemStack bindForPose = state.getEquipment(BodyRegionV2.ARMS);
- if (
- bindForPose.isEmpty() ||
- !(bindForPose.getItem() instanceof ItemBind itemBind)
- ) {
+ if (bindForPose.isEmpty()) {
return;
}
- PoseType bindPoseType = itemBind.getPoseType();
+ PoseType bindPoseType = PoseTypeHelper.getPoseType(bindForPose);
// Check for humanChairMode NBT override
bindPoseType = HumanChairHelper.resolveEffectivePose(
bindPoseType,
diff --git a/src/main/java/com/tiedup/remake/client/animation/render/PetBedRenderHandler.java b/src/main/java/com/tiedup/remake/client/animation/render/PetBedRenderHandler.java
index d057531..53e0bbd 100644
--- a/src/main/java/com/tiedup/remake/client/animation/render/PetBedRenderHandler.java
+++ b/src/main/java/com/tiedup/remake/client/animation/render/PetBedRenderHandler.java
@@ -2,8 +2,8 @@ package com.tiedup.remake.client.animation.render;
import com.tiedup.remake.client.state.PetBedClientState;
import com.tiedup.remake.core.TiedUpMod;
-import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.items.base.PoseType;
+import com.tiedup.remake.v2.bondage.PoseTypeHelper;
import com.tiedup.remake.state.HumanChairHelper;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.v2.BodyRegionV2;
@@ -83,11 +83,9 @@ public class PetBedRenderHandler {
PlayerBindState state = PlayerBindState.getInstance(player);
if (state == null) return false;
ItemStack bind = state.getEquipment(BodyRegionV2.ARMS);
- if (
- bind.isEmpty() || !(bind.getItem() instanceof ItemBind itemBind)
- ) return false;
+ if (bind.isEmpty()) return false;
PoseType pose = HumanChairHelper.resolveEffectivePose(
- itemBind.getPoseType(),
+ PoseTypeHelper.getPoseType(bind),
bind
);
return pose == PoseType.DOG || pose == PoseType.HUMAN_CHAIR;
diff --git a/src/main/java/com/tiedup/remake/client/animation/render/PlayerArmHideEventHandler.java b/src/main/java/com/tiedup/remake/client/animation/render/PlayerArmHideEventHandler.java
index e7773f6..3dd08a7 100644
--- a/src/main/java/com/tiedup/remake/client/animation/render/PlayerArmHideEventHandler.java
+++ b/src/main/java/com/tiedup/remake/client/animation/render/PlayerArmHideEventHandler.java
@@ -2,8 +2,8 @@ package com.tiedup.remake.client.animation.render;
import com.tiedup.remake.client.renderer.layers.ClothesRenderHelper;
import com.tiedup.remake.core.TiedUpMod;
-import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.items.base.PoseType;
+import com.tiedup.remake.v2.bondage.PoseTypeHelper;
import com.tiedup.remake.items.clothes.ClothesProperties;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.v2.BodyRegionV2;
@@ -71,10 +71,8 @@ public class PlayerArmHideEventHandler {
// === HIDE ARMS (wrap/latex_sack poses) ===
if (state.hasArmsBound()) {
ItemStack bind = state.getEquipment(BodyRegionV2.ARMS);
- if (
- !bind.isEmpty() && bind.getItem() instanceof ItemBind itemBind
- ) {
- PoseType poseType = itemBind.getPoseType();
+ if (!bind.isEmpty()) {
+ PoseType poseType = PoseTypeHelper.getPoseType(bind);
// Only hide arms for wrap/sack poses (arms are covered by the item)
if (
diff --git a/src/main/java/com/tiedup/remake/client/animation/tick/AnimationTickHandler.java b/src/main/java/com/tiedup/remake/client/animation/tick/AnimationTickHandler.java
index 70bc8a2..4b31a49 100644
--- a/src/main/java/com/tiedup/remake/client/animation/tick/AnimationTickHandler.java
+++ b/src/main/java/com/tiedup/remake/client/animation/tick/AnimationTickHandler.java
@@ -14,8 +14,9 @@ import com.tiedup.remake.client.gltf.GltfAnimationApplier;
import com.tiedup.remake.client.state.ClothesClientCache;
import com.tiedup.remake.client.state.MovementStyleClientState;
import com.tiedup.remake.client.state.PetBedClientState;
-import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.items.base.PoseType;
+import com.tiedup.remake.v2.bondage.BindModeHelper;
+import com.tiedup.remake.v2.bondage.PoseTypeHelper;
import com.tiedup.remake.state.HumanChairHelper;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.v2.BodyRegionV2;
@@ -249,14 +250,10 @@ public class AnimationTickHandler {
PlayerBindState state
) {
ItemStack bind = state.getEquipment(BodyRegionV2.ARMS);
- PoseType poseType = PoseType.STANDARD;
+ PoseType poseType = PoseTypeHelper.getPoseType(bind);
- if (bind.getItem() instanceof ItemBind itemBind) {
- poseType = itemBind.getPoseType();
-
- // Human chair mode: override DOG pose to HUMAN_CHAIR (straight limbs)
- poseType = HumanChairHelper.resolveEffectivePose(poseType, bind);
- }
+ // Human chair mode: override DOG pose to HUMAN_CHAIR (straight limbs)
+ poseType = HumanChairHelper.resolveEffectivePose(poseType, bind);
// Derive bound state from V2 regions (works client-side, synced via capability)
boolean armsBound = V2EquipmentHelper.isRegionOccupied(
@@ -268,10 +265,10 @@ public class AnimationTickHandler {
BodyRegionV2.LEGS
);
- // V1 fallback: if no V2 regions are set but player is tied, derive from ItemBind NBT
- if (!armsBound && !legsBound && bind.getItem() instanceof ItemBind) {
- armsBound = ItemBind.hasArmsBound(bind);
- legsBound = ItemBind.hasLegsBound(bind);
+ // V1 fallback: if no V2 regions are set but player is tied, derive from bind mode NBT
+ if (!armsBound && !legsBound && BindModeHelper.isBindItem(bind)) {
+ armsBound = BindModeHelper.hasArmsBound(bind);
+ legsBound = BindModeHelper.hasLegsBound(bind);
}
boolean isStruggling = state.isStruggling();
diff --git a/src/main/java/com/tiedup/remake/client/animation/tick/NpcAnimationTickHandler.java b/src/main/java/com/tiedup/remake/client/animation/tick/NpcAnimationTickHandler.java
index ae0e805..828c994 100644
--- a/src/main/java/com/tiedup/remake/client/animation/tick/NpcAnimationTickHandler.java
+++ b/src/main/java/com/tiedup/remake/client/animation/tick/NpcAnimationTickHandler.java
@@ -9,8 +9,9 @@ import com.tiedup.remake.client.gltf.GltfAnimationApplier;
import com.tiedup.remake.entities.AbstractTiedUpNpc;
import com.tiedup.remake.entities.EntityMaster;
import com.tiedup.remake.entities.ai.master.MasterState;
-import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.items.base.PoseType;
+import com.tiedup.remake.v2.bondage.BindModeHelper;
+import com.tiedup.remake.v2.bondage.PoseTypeHelper;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.IV2BondageEquipment;
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
@@ -161,13 +162,8 @@ public class NpcAnimationTickHandler {
net.minecraft.world.item.ItemStack bind = entity.getEquipment(
BodyRegionV2.ARMS
);
- PoseType poseType = PoseType.STANDARD;
- boolean hasBind = false;
-
- if (bind.getItem() instanceof ItemBind itemBind) {
- poseType = itemBind.getPoseType();
- hasBind = true;
- }
+ PoseType poseType = PoseTypeHelper.getPoseType(bind);
+ boolean hasBind = BindModeHelper.isBindItem(bind);
// Derive bound state from V2 regions (AbstractTiedUpNpc implements IV2EquipmentHolder)
boolean armsBound = V2EquipmentHelper.isRegionOccupied(
@@ -179,10 +175,10 @@ public class NpcAnimationTickHandler {
BodyRegionV2.LEGS
);
- // V1 fallback: if no V2 regions set but NPC has a bind, derive from ItemBind NBT
- if (!armsBound && !legsBound && bind.getItem() instanceof ItemBind) {
- armsBound = ItemBind.hasArmsBound(bind);
- legsBound = ItemBind.hasLegsBound(bind);
+ // V1 fallback: if no V2 regions set but NPC has a bind, derive from bind mode NBT
+ if (!armsBound && !legsBound && BindModeHelper.isBindItem(bind)) {
+ armsBound = BindModeHelper.hasArmsBound(bind);
+ legsBound = BindModeHelper.hasLegsBound(bind);
}
boolean isStruggling = entity.isStruggling();
diff --git a/src/main/java/com/tiedup/remake/client/events/LeashProxyClientHandler.java b/src/main/java/com/tiedup/remake/client/events/LeashProxyClientHandler.java
index 8a3c237..fb1676b 100644
--- a/src/main/java/com/tiedup/remake/client/events/LeashProxyClientHandler.java
+++ b/src/main/java/com/tiedup/remake/client/events/LeashProxyClientHandler.java
@@ -1,8 +1,8 @@
package com.tiedup.remake.client.events;
import com.tiedup.remake.core.TiedUpMod;
-import com.tiedup.remake.items.ModItems;
-import com.tiedup.remake.items.base.BindVariant;
+import com.tiedup.remake.items.base.PoseType;
+import com.tiedup.remake.v2.bondage.PoseTypeHelper;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.v2.BodyRegionV2;
@@ -138,7 +138,7 @@ public class LeashProxyClientHandler {
ItemStack bind = state.getEquipment(BodyRegionV2.ARMS);
if (
!bind.isEmpty() &&
- bind.getItem() == ModItems.getBind(BindVariant.DOGBINDER)
+ PoseTypeHelper.getPoseType(bind) == PoseType.DOG
) {
return DOGWALK_Y_OFFSET;
}
diff --git a/src/main/java/com/tiedup/remake/client/events/SelfBondageInputHandler.java b/src/main/java/com/tiedup/remake/client/events/SelfBondageInputHandler.java
index a06ba1d..b876fbd 100644
--- a/src/main/java/com/tiedup/remake/client/events/SelfBondageInputHandler.java
+++ b/src/main/java/com/tiedup/remake/client/events/SelfBondageInputHandler.java
@@ -1,13 +1,20 @@
package com.tiedup.remake.client.events;
-import com.tiedup.remake.items.base.*;
+import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.network.selfbondage.PacketSelfBondage;
+import com.tiedup.remake.v2.bondage.BindModeHelper;
+import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.v2.bondage.IV2BondageItem;
+import com.tiedup.remake.v2.bondage.component.BlindingComponent;
+import com.tiedup.remake.v2.bondage.component.ComponentType;
+import com.tiedup.remake.v2.bondage.component.GaggingComponent;
+import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
+import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemDefinition;
+import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemRegistry;
import net.minecraft.client.Minecraft;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.world.InteractionHand;
-import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
@@ -70,7 +77,7 @@ public class SelfBondageInputHandler {
if (!event.getLevel().isClientSide()) return;
ItemStack stack = event.getItemStack();
- if (isSelfBondageItem(stack.getItem())) {
+ if (isSelfBondageItem(stack)) {
event.setCanceled(true);
startSelfBondage();
}
@@ -87,11 +94,11 @@ public class SelfBondageInputHandler {
InteractionHand hand = InteractionHand.MAIN_HAND;
ItemStack stack = player.getMainHandItem();
- if (!isSelfBondageItem(stack.getItem())) {
+ if (!isSelfBondageItem(stack)) {
stack = player.getOffhandItem();
hand = InteractionHand.OFF_HAND;
- if (!isSelfBondageItem(stack.getItem())) {
+ if (!isSelfBondageItem(stack)) {
return; // No bondage item in either hand
}
}
@@ -130,7 +137,7 @@ public class SelfBondageInputHandler {
// Check if still holding bondage item in the active hand
ItemStack stack = player.getItemInHand(activeHand);
- if (!isSelfBondageItem(stack.getItem())) {
+ if (!isSelfBondageItem(stack)) {
stopSelfBondage();
return;
}
@@ -153,27 +160,31 @@ public class SelfBondageInputHandler {
}
/**
- * Check if an item supports self-bondage.
+ * Check if a stack supports self-bondage.
* Collar is explicitly excluded.
*/
- private static boolean isSelfBondageItem(Item item) {
- // Collar cannot be self-equipped (V1 collar guard)
- if (item instanceof ItemCollar) {
+ private static boolean isSelfBondageItem(ItemStack stack) {
+ if (stack.isEmpty()) return false;
+
+ // Collar cannot be self-equipped (V1 collar guard + V2 ownership component)
+ if (stack.getItem() instanceof ItemCollar || CollarHelper.isCollar(stack)) {
return false;
}
// V2 bondage items support self-bondage (left-click hold with tying duration)
- if (item instanceof IV2BondageItem) {
+ if (stack.getItem() instanceof IV2BondageItem) {
return true;
}
- // V1 bondage items (legacy)
- return (
- item instanceof ItemBind ||
- item instanceof ItemGag ||
- item instanceof ItemBlindfold ||
- item instanceof ItemMittens ||
- item instanceof ItemEarplugs
- );
+ // V2 data-driven items: check if it occupies any bondage region
+ DataDrivenItemDefinition def = DataDrivenItemRegistry.get(stack);
+ if (def != null) {
+ return true;
+ }
+
+ // V1 fallback: bind items
+ return BindModeHelper.isBindItem(stack)
+ || DataDrivenBondageItem.getComponent(stack, ComponentType.GAGGING, GaggingComponent.class) != null
+ || DataDrivenBondageItem.getComponent(stack, ComponentType.BLINDING, BlindingComponent.class) != null;
}
}
diff --git a/src/main/java/com/tiedup/remake/client/gui/screens/SlaveManagementScreen.java b/src/main/java/com/tiedup/remake/client/gui/screens/SlaveManagementScreen.java
index e597979..52d895f 100644
--- a/src/main/java/com/tiedup/remake/client/gui/screens/SlaveManagementScreen.java
+++ b/src/main/java/com/tiedup/remake/client/gui/screens/SlaveManagementScreen.java
@@ -3,7 +3,7 @@ package com.tiedup.remake.client.gui.screens;
import com.tiedup.remake.client.gui.util.GuiColors;
import com.tiedup.remake.client.gui.util.GuiLayoutConstants;
import com.tiedup.remake.client.gui.widgets.SlaveEntryWidget;
-import com.tiedup.remake.items.base.ItemCollar;
+import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.network.slave.PacketSlaveAction;
import com.tiedup.remake.state.IBondageState;
@@ -145,8 +145,8 @@ public class SlaveManagementScreen extends BaseScreen {
ItemStack collarStack = kidnapped.getEquipment(
BodyRegionV2.NECK
);
- if (collarStack.getItem() instanceof ItemCollar collar) {
- if (collar.isOwner(collarStack, player)) {
+ if (CollarHelper.isCollar(collarStack)) {
+ if (CollarHelper.isOwner(collarStack, player)) {
addSlaveEntry(kidnapped);
addedUUIDs.add(entity.getUUID());
}
diff --git a/src/main/java/com/tiedup/remake/client/gui/widgets/ActionPanel.java b/src/main/java/com/tiedup/remake/client/gui/widgets/ActionPanel.java
index 9bf0801..72fe69a 100644
--- a/src/main/java/com/tiedup/remake/client/gui/widgets/ActionPanel.java
+++ b/src/main/java/com/tiedup/remake/client/gui/widgets/ActionPanel.java
@@ -7,7 +7,7 @@ import com.tiedup.remake.items.ItemLockpick;
import com.tiedup.remake.items.ModItems;
import com.tiedup.remake.items.base.IHasResistance;
import com.tiedup.remake.items.base.ILockable;
-import com.tiedup.remake.items.base.ItemCollar;
+import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.network.action.PacketSetKnifeCutTarget;
import com.tiedup.remake.network.minigame.PacketLockpickMiniGameStart;
@@ -474,10 +474,10 @@ public class ActionPanel extends AbstractWidget {
// Bondage Service toggle (NECK collar only, prison configured)
if (
selectedRegion == BodyRegionV2.NECK &&
- selectedItem.getItem() instanceof ItemCollar collar
+ CollarHelper.isCollar(selectedItem)
) {
- if (collar.hasCellAssigned(selectedItem)) {
- boolean svcEnabled = collar.isBondageServiceEnabled(
+ if (CollarHelper.hasCellAssigned(selectedItem)) {
+ boolean svcEnabled = CollarHelper.isBondageServiceEnabled(
selectedItem
);
String svcKey = svcEnabled
diff --git a/src/main/java/com/tiedup/remake/client/gui/widgets/SlaveEntryWidget.java b/src/main/java/com/tiedup/remake/client/gui/widgets/SlaveEntryWidget.java
index 86c610c..00fdb2d 100644
--- a/src/main/java/com/tiedup/remake/client/gui/widgets/SlaveEntryWidget.java
+++ b/src/main/java/com/tiedup/remake/client/gui/widgets/SlaveEntryWidget.java
@@ -5,7 +5,7 @@ import static com.tiedup.remake.client.gui.util.GuiLayoutConstants.*;
import com.google.common.collect.ImmutableList;
import com.tiedup.remake.client.gui.util.GuiColors;
import com.tiedup.remake.items.ItemGpsCollar;
-import com.tiedup.remake.items.base.ItemCollar;
+import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.v2.BodyRegionV2;
import java.util.ArrayList;
@@ -560,19 +560,13 @@ public class SlaveEntryWidget
private boolean hasShockCollar() {
if (!slave.hasCollar()) return false;
ItemStack collar = slave.getEquipment(BodyRegionV2.NECK);
- return (
- collar.getItem() instanceof ItemCollar itemCollar &&
- itemCollar.canShock()
- );
+ return CollarHelper.canShock(collar);
}
private boolean hasGPSCollar() {
if (!slave.hasCollar()) return false;
ItemStack collar = slave.getEquipment(BodyRegionV2.NECK);
- return (
- collar.getItem() instanceof ItemCollar itemCollar &&
- itemCollar.hasGPS()
- );
+ return CollarHelper.hasGPS(collar);
}
private boolean isInAnySafeZone(
@@ -580,7 +574,7 @@ public class SlaveEntryWidget
ItemStack collarStack,
LivingEntity entity
) {
- if (!gps.isActive(collarStack)) return true;
+ if (!CollarHelper.isActive(collarStack)) return true;
var safeSpots = gps.getSafeSpots(collarStack);
if (safeSpots.isEmpty()) return true;
diff --git a/src/main/java/com/tiedup/remake/client/model/DamselModel.java b/src/main/java/com/tiedup/remake/client/model/DamselModel.java
index 9668e95..80e1567 100644
--- a/src/main/java/com/tiedup/remake/client/model/DamselModel.java
+++ b/src/main/java/com/tiedup/remake/client/model/DamselModel.java
@@ -9,8 +9,9 @@ import com.tiedup.remake.entities.AbstractTiedUpNpc;
import com.tiedup.remake.entities.EntityKidnapperArcher;
import com.tiedup.remake.entities.EntityMaster;
import com.tiedup.remake.entities.ai.master.MasterState;
-import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.items.base.PoseType;
+import com.tiedup.remake.v2.bondage.BindModeHelper;
+import com.tiedup.remake.v2.bondage.PoseTypeHelper;
import com.tiedup.remake.items.clothes.GenericClothes;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
@@ -230,11 +231,7 @@ public class DamselModel
if (inPose) {
ItemStack bind = entity.getEquipment(BodyRegionV2.ARMS);
- PoseType poseType = PoseType.STANDARD;
-
- if (bind.getItem() instanceof ItemBind itemBind) {
- poseType = itemBind.getPoseType();
- }
+ PoseType poseType = PoseTypeHelper.getPoseType(bind);
// Hide arms for wrap/latex_sack poses
if (poseType == PoseType.WRAP || poseType == PoseType.LATEX_SACK) {
@@ -252,9 +249,7 @@ public class DamselModel
PoseType currentPoseType = PoseType.STANDARD;
if (inPose) {
ItemStack bindForPoseType = entity.getEquipment(BodyRegionV2.ARMS);
- if (bindForPoseType.getItem() instanceof ItemBind itemBindForType) {
- currentPoseType = itemBindForType.getPoseType();
- }
+ currentPoseType = PoseTypeHelper.getPoseType(bindForPoseType);
}
// Check if this is a Master in human chair mode (head should look around freely)
@@ -306,11 +301,7 @@ public class DamselModel
// Animation not yet active (1-frame delay) - apply static pose as fallback
// This ensures immediate visual feedback when bind is applied
ItemStack bind = entity.getEquipment(BodyRegionV2.ARMS);
- PoseType fallbackPoseType = PoseType.STANDARD;
-
- if (bind.getItem() instanceof ItemBind itemBind) {
- fallbackPoseType = itemBind.getPoseType();
- }
+ PoseType fallbackPoseType = PoseTypeHelper.getPoseType(bind);
// Derive bound state from V2 regions (AbstractTiedUpNpc implements IV2EquipmentHolder)
boolean armsBound = V2EquipmentHelper.isRegionOccupied(
@@ -323,10 +314,10 @@ public class DamselModel
);
if (
- !armsBound && !legsBound && bind.getItem() instanceof ItemBind
+ !armsBound && !legsBound && BindModeHelper.isBindItem(bind)
) {
- armsBound = ItemBind.hasArmsBound(bind);
- legsBound = ItemBind.hasLegsBound(bind);
+ armsBound = BindModeHelper.hasArmsBound(bind);
+ legsBound = BindModeHelper.hasLegsBound(bind);
}
// Apply static pose directly to model parts
diff --git a/src/main/java/com/tiedup/remake/commands/CollarCommand.java b/src/main/java/com/tiedup/remake/commands/CollarCommand.java
index fc26120..7384090 100644
--- a/src/main/java/com/tiedup/remake/commands/CollarCommand.java
+++ b/src/main/java/com/tiedup/remake/commands/CollarCommand.java
@@ -7,8 +7,8 @@ import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.tiedup.remake.cells.CellDataV2;
import com.tiedup.remake.cells.CellRegistryV2;
-import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.state.PlayerBindState;
+import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.util.teleport.Position;
import com.tiedup.remake.util.teleport.TeleportHelper;
import com.tiedup.remake.v2.BodyRegionV2;
@@ -160,8 +160,8 @@ public class CollarCommand {
}
if (source.getEntity() instanceof ServerPlayer executor) {
- if (collar.getItem() instanceof ItemCollar collarItem) {
- collarItem.addOwner(collar, executor);
+ if (CollarHelper.isCollar(collar)) {
+ CollarHelper.addOwner(collar, executor);
source.sendSuccess(
() ->
Component.literal(
@@ -195,8 +195,8 @@ public class CollarCommand {
}
if (source.getEntity() instanceof ServerPlayer executor) {
- if (collar.getItem() instanceof ItemCollar collarItem) {
- collarItem.removeOwner(collar, executor.getUUID());
+ if (CollarHelper.isCollar(collar)) {
+ CollarHelper.removeOwner(collar, executor.getUUID());
source.sendSuccess(
() ->
Component.literal(
@@ -230,8 +230,8 @@ public class CollarCommand {
return 0;
}
- if (collar.getItem() instanceof ItemCollar collarItem) {
- collarItem.setNickname(collar, name);
+ if (CollarHelper.isCollar(collar)) {
+ CollarHelper.setNickname(collar, name);
source.sendSuccess(
() ->
Component.literal(
@@ -261,8 +261,8 @@ public class CollarCommand {
return 0;
}
- if (collar.getItem() instanceof ItemCollar collarItem) {
- collarItem.addOwner(collar, owner);
+ if (CollarHelper.isCollar(collar)) {
+ CollarHelper.addOwner(collar, owner);
source.sendSuccess(
() ->
Component.literal(
@@ -296,8 +296,8 @@ public class CollarCommand {
return 0;
}
- if (collar.getItem() instanceof ItemCollar collarItem) {
- collarItem.removeOwner(collar, owner.getUUID());
+ if (CollarHelper.isCollar(collar)) {
+ CollarHelper.removeOwner(collar, owner.getUUID());
source.sendSuccess(
() ->
Component.literal(
@@ -348,8 +348,8 @@ public class CollarCommand {
return 0;
}
- if (collar.getItem() instanceof ItemCollar collarItem) {
- collarItem.setCellId(collar, cell.getId());
+ if (CollarHelper.isCollar(collar)) {
+ CollarHelper.setCellId(collar, cell.getId());
source.sendSuccess(
() ->
Component.literal(
@@ -388,8 +388,8 @@ public class CollarCommand {
return 0;
}
- if (collar.getItem() instanceof ItemCollar collarItem) {
- if (!collarItem.hasCellAssigned(collar)) {
+ if (CollarHelper.isCollar(collar)) {
+ if (!CollarHelper.hasCellAssigned(collar)) {
source.sendFailure(
Component.literal("No cell assigned to collar")
);
@@ -397,7 +397,7 @@ public class CollarCommand {
}
// Get cell position and teleport
- java.util.UUID cellId = collarItem.getCellId(collar);
+ java.util.UUID cellId = CollarHelper.getCellId(collar);
ServerLevel serverLevel = source.getLevel();
CellDataV2 cell = CellRegistryV2.get(serverLevel).getCell(cellId);
@@ -449,7 +449,7 @@ public class CollarCommand {
return 0;
}
- if (collar.getItem() instanceof ItemCollar collarItem) {
+ if (CollarHelper.isCollar(collar)) {
source.sendSuccess(
() ->
Component.literal(
@@ -460,24 +460,24 @@ public class CollarCommand {
false
);
- String nickname = collarItem.getNickname(collar);
+ String nickname = CollarHelper.getNickname(collar);
source.sendSuccess(
() ->
Component.literal(
"§7Nickname: §f" +
- (nickname.isEmpty() ? "None" : nickname)
+ (nickname == null || nickname.isEmpty() ? "None" : nickname)
),
false
);
source.sendSuccess(
() ->
Component.literal(
- "§7Has Owner: §f" + collarItem.hasOwner(collar)
+ "§7Has Owner: §f" + CollarHelper.hasOwner(collar)
),
false
);
// Cell assignment
- java.util.UUID cellId = collarItem.getCellId(collar);
+ java.util.UUID cellId = CollarHelper.getCellId(collar);
if (cellId != null) {
ServerLevel serverLevel = source.getLevel();
CellRegistryV2 registry = CellRegistryV2.get(serverLevel);
@@ -510,10 +510,12 @@ public class CollarCommand {
);
}
+ boolean locked = collar.getItem() instanceof com.tiedup.remake.items.base.ILockable lockable
+ && lockable.isLocked(collar);
source.sendSuccess(
() ->
Component.literal(
- "§7Locked: §f" + collarItem.isLocked(collar)
+ "§7Locked: §f" + locked
),
false
);
diff --git a/src/main/java/com/tiedup/remake/commands/KidnapSetCommand.java b/src/main/java/com/tiedup/remake/commands/KidnapSetCommand.java
index d555007..a269e13 100644
--- a/src/main/java/com/tiedup/remake/commands/KidnapSetCommand.java
+++ b/src/main/java/com/tiedup/remake/commands/KidnapSetCommand.java
@@ -4,12 +4,10 @@ import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.tiedup.remake.items.ModItems;
-import com.tiedup.remake.items.base.BindVariant;
-import com.tiedup.remake.items.base.BlindfoldVariant;
-import com.tiedup.remake.items.base.EarplugsVariant;
-import com.tiedup.remake.items.base.GagVariant;
import com.tiedup.remake.items.base.KnifeVariant;
+import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
import java.util.Optional;
+import net.minecraft.resources.ResourceLocation;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.network.chat.Component;
@@ -82,42 +80,18 @@ public class KidnapSetCommand {
int given = 0;
// Binds
- given += giveItem(
- player,
- new ItemStack(ModItems.getBind(BindVariant.ROPES), 8)
- );
- given += giveItem(
- player,
- new ItemStack(ModItems.getBind(BindVariant.CHAIN), 4)
- );
- given += giveItem(
- player,
- new ItemStack(ModItems.getBind(BindVariant.LEATHER_STRAPS), 4)
- );
+ given += giveDataDrivenItems(player, "ropes", 8);
+ given += giveDataDrivenItems(player, "chain", 4);
+ given += giveDataDrivenItems(player, "leather_straps", 4);
// Gags
- given += giveItem(
- player,
- new ItemStack(ModItems.getGag(GagVariant.CLOTH_GAG), 4)
- );
- given += giveItem(
- player,
- new ItemStack(ModItems.getGag(GagVariant.BALL_GAG), 4)
- );
- given += giveItem(
- player,
- new ItemStack(ModItems.getGag(GagVariant.TAPE_GAG), 4)
- );
+ given += giveDataDrivenItems(player, "cloth_gag", 4);
+ given += giveDataDrivenItems(player, "ball_gag", 4);
+ given += giveDataDrivenItems(player, "tape_gag", 4);
// Blindfolds
- given += giveItem(
- player,
- new ItemStack(ModItems.getBlindfold(BlindfoldVariant.CLASSIC), 4)
- );
- given += giveItem(
- player,
- new ItemStack(ModItems.getBlindfold(BlindfoldVariant.MASK), 2)
- );
+ given += giveDataDrivenItems(player, "classic_blindfold", 4);
+ given += giveDataDrivenItems(player, "blindfold_mask", 2);
// Collars
given += giveItem(
@@ -155,10 +129,7 @@ public class KidnapSetCommand {
given += giveItem(player, new ItemStack(ModItems.MASTER_KEY.get(), 1));
// Earplugs
- given += giveItem(
- player,
- new ItemStack(ModItems.getEarplugs(EarplugsVariant.CLASSIC), 4)
- );
+ given += giveDataDrivenItems(player, "classic_earplugs", 4);
// Rope arrows
given += giveItem(player, new ItemStack(ModItems.ROPE_ARROW.get(), 16));
@@ -182,6 +153,18 @@ public class KidnapSetCommand {
return finalGiven;
}
+ private static int giveDataDrivenItems(ServerPlayer player, String itemName, int count) {
+ int given = 0;
+ for (int i = 0; i < count; i++) {
+ ItemStack stack = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", itemName));
+ if (!stack.isEmpty()) {
+ giveItem(player, stack);
+ given++;
+ }
+ }
+ return given;
+ }
+
private static int giveItem(ServerPlayer player, ItemStack stack) {
if (!player.getInventory().add(stack)) {
// Drop on ground if inventory full
diff --git a/src/main/java/com/tiedup/remake/commands/NPCCommand.java b/src/main/java/com/tiedup/remake/commands/NPCCommand.java
index 88d0251..d2960a1 100644
--- a/src/main/java/com/tiedup/remake/commands/NPCCommand.java
+++ b/src/main/java/com/tiedup/remake/commands/NPCCommand.java
@@ -7,11 +7,9 @@ import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.tiedup.remake.entities.*;
import com.tiedup.remake.entities.skins.EliteKidnapperSkinManager;
import com.tiedup.remake.items.ModItems;
-import com.tiedup.remake.items.base.BindVariant;
-import com.tiedup.remake.items.base.BlindfoldVariant;
-import com.tiedup.remake.items.base.EarplugsVariant;
-import com.tiedup.remake.items.base.GagVariant;
import com.tiedup.remake.state.IBondageState;
+import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
+import net.minecraft.resources.ResourceLocation;
import com.tiedup.remake.v2.BodyRegionV2;
import java.util.List;
import java.util.Optional;
@@ -507,7 +505,7 @@ public class NPCCommand {
npc.equip(
BodyRegionV2.ARMS,
- new ItemStack(ModItems.getBind(BindVariant.ROPES))
+ DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "ropes"))
);
context
.getSource()
@@ -538,7 +536,7 @@ public class NPCCommand {
npc.equip(
BodyRegionV2.MOUTH,
- new ItemStack(ModItems.getGag(GagVariant.CLOTH_GAG))
+ DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "cloth_gag"))
);
context
.getSource()
@@ -571,7 +569,7 @@ public class NPCCommand {
npc.equip(
BodyRegionV2.EYES,
- new ItemStack(ModItems.getBlindfold(BlindfoldVariant.CLASSIC))
+ DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "classic_blindfold"))
);
context
.getSource()
@@ -656,10 +654,10 @@ public class NPCCommand {
com.tiedup.remake.entities.AbstractTiedUpNpc npcEntity
) {
npcEntity.applyBondage(
- new ItemStack(ModItems.getBind(BindVariant.ROPES)),
- new ItemStack(ModItems.getGag(GagVariant.CLOTH_GAG)),
- new ItemStack(ModItems.getBlindfold(BlindfoldVariant.CLASSIC)),
- new ItemStack(ModItems.getEarplugs(EarplugsVariant.CLASSIC)),
+ DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "ropes")),
+ DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "cloth_gag")),
+ DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "classic_blindfold")),
+ DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "classic_earplugs")),
new ItemStack(ModItems.CLASSIC_COLLAR.get()),
ItemStack.EMPTY // No clothes
);
diff --git a/src/main/java/com/tiedup/remake/commands/subcommands/BondageSubCommand.java b/src/main/java/com/tiedup/remake/commands/subcommands/BondageSubCommand.java
index 96f85a6..3e53e76 100644
--- a/src/main/java/com/tiedup/remake/commands/subcommands/BondageSubCommand.java
+++ b/src/main/java/com/tiedup/remake/commands/subcommands/BondageSubCommand.java
@@ -9,12 +9,10 @@ import com.tiedup.remake.commands.CommandHelper;
import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.items.ModItems;
import com.tiedup.remake.items.base.AdjustmentHelper;
-import com.tiedup.remake.items.base.BindVariant;
-import com.tiedup.remake.items.base.BlindfoldVariant;
-import com.tiedup.remake.items.base.EarplugsVariant;
-import com.tiedup.remake.items.base.GagVariant;
-import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.network.ModNetwork;
+import com.tiedup.remake.v2.bondage.CollarHelper;
+import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
+import net.minecraft.resources.ResourceLocation;
import com.tiedup.remake.network.sync.PacketSyncBindState;
import com.tiedup.remake.state.PlayerBindState;
import net.minecraft.commands.CommandSourceStack;
@@ -256,7 +254,7 @@ public class BondageSubCommand {
return 0;
}
- ItemStack ropes = new ItemStack(ModItems.getBind(BindVariant.ROPES));
+ ItemStack ropes = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "ropes"));
state.putBindOn(ropes);
CommandHelper.syncPlayerState(targetPlayer, state);
@@ -387,7 +385,7 @@ public class BondageSubCommand {
return 0;
}
- ItemStack gag = new ItemStack(ModItems.getGag(GagVariant.CLOTH_GAG));
+ ItemStack gag = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "cloth_gag"));
state.putGagOn(gag);
CommandHelper.syncPlayerState(targetPlayer, state);
@@ -440,9 +438,7 @@ public class BondageSubCommand {
return 0;
}
- ItemStack blindfold = new ItemStack(
- ModItems.getBlindfold(BlindfoldVariant.CLASSIC)
- );
+ ItemStack blindfold = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "classic_blindfold"));
state.putBlindfoldOn(blindfold);
CommandHelper.syncPlayerState(targetPlayer, state);
@@ -500,8 +496,7 @@ public class BondageSubCommand {
ItemStack collar = new ItemStack(ModItems.CLASSIC_COLLAR.get());
if (context.getSource().getEntity() instanceof ServerPlayer executor) {
- ItemCollar collarItem = (ItemCollar) collar.getItem();
- collarItem.addOwner(collar, executor);
+ CollarHelper.addOwner(collar, executor);
}
state.putCollarOn(collar);
@@ -1021,9 +1016,7 @@ public class BondageSubCommand {
return 0;
}
- ItemStack earplugs = new ItemStack(
- ModItems.getEarplugs(EarplugsVariant.CLASSIC)
- );
+ ItemStack earplugs = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "classic_earplugs"));
state.putEarplugsOn(earplugs);
CommandHelper.syncPlayerState(targetPlayer, state);
@@ -1067,25 +1060,19 @@ public class BondageSubCommand {
int applied = 0;
if (!state.isTiedUp()) {
- ItemStack ropes = new ItemStack(
- ModItems.getBind(BindVariant.ROPES)
- );
+ ItemStack ropes = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "ropes"));
state.putBindOn(ropes);
applied++;
}
if (!state.isGagged()) {
- ItemStack gag = new ItemStack(
- ModItems.getGag(GagVariant.CLOTH_GAG)
- );
+ ItemStack gag = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "cloth_gag"));
state.putGagOn(gag);
applied++;
}
if (!state.isBlindfolded()) {
- ItemStack blindfold = new ItemStack(
- ModItems.getBlindfold(BlindfoldVariant.CLASSIC)
- );
+ ItemStack blindfold = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "classic_blindfold"));
state.putBlindfoldOn(blindfold);
applied++;
}
@@ -1095,17 +1082,14 @@ public class BondageSubCommand {
if (
context.getSource().getEntity() instanceof ServerPlayer executor
) {
- ItemCollar collarItem = (ItemCollar) collar.getItem();
- collarItem.addOwner(collar, executor);
+ CollarHelper.addOwner(collar, executor);
}
state.putCollarOn(collar);
applied++;
}
if (!state.hasEarplugs()) {
- ItemStack earplugs = new ItemStack(
- ModItems.getEarplugs(EarplugsVariant.CLASSIC)
- );
+ ItemStack earplugs = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "classic_earplugs"));
state.putEarplugsOn(earplugs);
applied++;
}
@@ -1167,21 +1151,15 @@ public class BondageSubCommand {
// First fully restrain
if (!state.isTiedUp()) {
- ItemStack ropes = new ItemStack(
- ModItems.getBind(BindVariant.ROPES)
- );
+ ItemStack ropes = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "ropes"));
state.putBindOn(ropes);
}
if (!state.isGagged()) {
- ItemStack gag = new ItemStack(
- ModItems.getGag(GagVariant.CLOTH_GAG)
- );
+ ItemStack gag = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "cloth_gag"));
state.putGagOn(gag);
}
if (!state.isBlindfolded()) {
- ItemStack blindfold = new ItemStack(
- ModItems.getBlindfold(BlindfoldVariant.CLASSIC)
- );
+ ItemStack blindfold = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "classic_blindfold"));
state.putBlindfoldOn(blindfold);
}
if (!state.hasCollar()) {
@@ -1189,15 +1167,12 @@ public class BondageSubCommand {
if (
context.getSource().getEntity() instanceof ServerPlayer executor
) {
- ItemCollar collarItem = (ItemCollar) collar.getItem();
- collarItem.addOwner(collar, executor);
+ CollarHelper.addOwner(collar, executor);
}
state.putCollarOn(collar);
}
if (!state.hasEarplugs()) {
- ItemStack earplugs = new ItemStack(
- ModItems.getEarplugs(EarplugsVariant.CLASSIC)
- );
+ ItemStack earplugs = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "classic_earplugs"));
state.putEarplugsOn(earplugs);
}
diff --git a/src/main/java/com/tiedup/remake/compat/mca/capability/MCAKidnappedAdapter.java b/src/main/java/com/tiedup/remake/compat/mca/capability/MCAKidnappedAdapter.java
index cd2c349..2d10de1 100644
--- a/src/main/java/com/tiedup/remake/compat/mca/capability/MCAKidnappedAdapter.java
+++ b/src/main/java/com/tiedup/remake/compat/mca/capability/MCAKidnappedAdapter.java
@@ -2,8 +2,10 @@ package com.tiedup.remake.compat.mca.capability;
import com.tiedup.remake.compat.mca.MCABondageManager;
import com.tiedup.remake.compat.mca.MCACompat;
-import com.tiedup.remake.items.base.IHasBlindingEffect;
-import com.tiedup.remake.items.base.IHasGaggingEffect;
+import com.tiedup.remake.v2.bondage.component.BlindingComponent;
+import com.tiedup.remake.v2.bondage.component.ComponentType;
+import com.tiedup.remake.v2.bondage.component.GaggingComponent;
+import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
import com.tiedup.remake.items.base.IHasResistance;
import com.tiedup.remake.items.base.ILockable;
import com.tiedup.remake.items.base.ItemBind;
@@ -277,16 +279,17 @@ public class MCAKidnappedAdapter implements IRestrainable {
@Override
public boolean hasGaggingEffect() {
ItemStack gag = cap.getGag();
- return !gag.isEmpty() && gag.getItem() instanceof IHasGaggingEffect;
+ if (gag.isEmpty()) return false;
+ if (DataDrivenBondageItem.getComponent(gag, ComponentType.GAGGING, GaggingComponent.class) != null) return true;
+ return gag.getItem() instanceof com.tiedup.remake.items.base.IHasGaggingEffect;
}
@Override
public boolean hasBlindingEffect() {
ItemStack blindfold = cap.getBlindfold();
- return (
- !blindfold.isEmpty() &&
- blindfold.getItem() instanceof IHasBlindingEffect
- );
+ if (blindfold.isEmpty()) return false;
+ if (DataDrivenBondageItem.getComponent(blindfold, ComponentType.BLINDING, BlindingComponent.class) != null) return true;
+ return blindfold.getItem() instanceof com.tiedup.remake.items.base.IHasBlindingEffect;
}
@Override
diff --git a/src/main/java/com/tiedup/remake/dialogue/GagTalkManager.java b/src/main/java/com/tiedup/remake/dialogue/GagTalkManager.java
index b822fa5..585e687 100644
--- a/src/main/java/com/tiedup/remake/dialogue/GagTalkManager.java
+++ b/src/main/java/com/tiedup/remake/dialogue/GagTalkManager.java
@@ -5,6 +5,9 @@ import static com.tiedup.remake.util.GameConstants.*;
import com.tiedup.remake.dialogue.EmotionalContext.EmotionType;
import com.tiedup.remake.items.base.ItemGag;
import com.tiedup.remake.state.IBondageState;
+import com.tiedup.remake.v2.bondage.component.ComponentType;
+import com.tiedup.remake.v2.bondage.component.GaggingComponent;
+import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
import com.tiedup.remake.util.GagMaterial;
import com.tiedup.remake.util.PhoneticMapper;
import com.tiedup.remake.util.SyllableAnalyzer;
@@ -58,7 +61,12 @@ public class GagTalkManager {
) {
LivingEntity entity = kidnapped.asLivingEntity();
GagMaterial material = GagMaterial.CLOTH;
- if (gagStack.getItem() instanceof ItemGag gag) {
+ // V2: check data-driven GaggingComponent first
+ GaggingComponent gaggingComp = DataDrivenBondageItem.getComponent(
+ gagStack, ComponentType.GAGGING, GaggingComponent.class);
+ if (gaggingComp != null && gaggingComp.getMaterial() != null) {
+ material = gaggingComp.getMaterial();
+ } else if (gagStack.getItem() instanceof ItemGag gag) {
material = gag.getGagMaterial();
}
@@ -514,8 +522,15 @@ public class GagTalkManager {
}
GagMaterial material = GagMaterial.CLOTH;
- if (gagStack != null && gagStack.getItem() instanceof ItemGag gag) {
- material = gag.getGagMaterial();
+ if (gagStack != null && !gagStack.isEmpty()) {
+ // V2: check data-driven GaggingComponent first
+ GaggingComponent comp = DataDrivenBondageItem.getComponent(
+ gagStack, ComponentType.GAGGING, GaggingComponent.class);
+ if (comp != null && comp.getMaterial() != null) {
+ material = comp.getMaterial();
+ } else if (gagStack.getItem() instanceof ItemGag gag) {
+ material = gag.getGagMaterial();
+ }
}
StringBuilder muffled = new StringBuilder();
diff --git a/src/main/java/com/tiedup/remake/dialogue/conversation/PetRequestManager.java b/src/main/java/com/tiedup/remake/dialogue/conversation/PetRequestManager.java
index 85c0770..ba9dd8b 100644
--- a/src/main/java/com/tiedup/remake/dialogue/conversation/PetRequestManager.java
+++ b/src/main/java/com/tiedup/remake/dialogue/conversation/PetRequestManager.java
@@ -5,9 +5,9 @@ import com.tiedup.remake.dialogue.DialogueBridge;
import com.tiedup.remake.entities.EntityMaster;
import com.tiedup.remake.entities.ai.master.MasterPlaceBlockGoal;
import com.tiedup.remake.entities.ai.master.MasterState;
-import com.tiedup.remake.items.ModItems;
-import com.tiedup.remake.items.base.BindVariant;
import com.tiedup.remake.network.ModNetwork;
+import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
+import net.minecraft.resources.ResourceLocation;
import com.tiedup.remake.network.master.PacketOpenPetRequestMenu;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.v2.BodyRegionV2;
@@ -171,9 +171,7 @@ public class PetRequestManager {
// Put dogbind on player (if not already tied)
if (!state.isTiedUp()) {
- ItemStack dogbind = new ItemStack(
- ModItems.getBind(BindVariant.DOGBINDER)
- );
+ ItemStack dogbind = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "dogbinder"));
state.equip(BodyRegionV2.ARMS, dogbind);
TiedUpMod.LOGGER.debug(
"[PetRequestManager] Equipped dogbind on {} for walk",
@@ -228,7 +226,7 @@ public class PetRequestManager {
}
// Master equips armbinder on pet (classic pet play restraint)
- ItemStack bind = new ItemStack(ModItems.getBind(BindVariant.ARMBINDER));
+ ItemStack bind = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "armbinder"));
state.equip(BodyRegionV2.ARMS, bind);
DialogueBridge.talkTo(master, pet, "petplay.tie_accept");
diff --git a/src/main/java/com/tiedup/remake/entities/AbstractTiedUpNpc.java b/src/main/java/com/tiedup/remake/entities/AbstractTiedUpNpc.java
index ded0b0d..85f3945 100644
--- a/src/main/java/com/tiedup/remake/entities/AbstractTiedUpNpc.java
+++ b/src/main/java/com/tiedup/remake/entities/AbstractTiedUpNpc.java
@@ -4,7 +4,12 @@ import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.damsel.components.*;
import com.tiedup.remake.entities.skins.Gender;
import com.tiedup.remake.items.base.ILockable;
-import com.tiedup.remake.items.base.ItemCollar;
+import com.tiedup.remake.v2.bondage.CollarHelper;
+import com.tiedup.remake.v2.bondage.PoseTypeHelper;
+import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
+import com.tiedup.remake.v2.bondage.component.ComponentType;
+import com.tiedup.remake.v2.bondage.component.GaggingComponent;
+import com.tiedup.remake.v2.bondage.component.BlindingComponent;
import com.tiedup.remake.state.ICaptor;
import com.tiedup.remake.state.IRestrainable;
import com.tiedup.remake.state.IRestrainableEntity;
@@ -455,16 +460,8 @@ public abstract class AbstractTiedUpNpc
*/
public boolean isDogPose() {
ItemStack bind = this.getEquipment(BodyRegionV2.ARMS);
- if (
- bind.getItem() instanceof
- com.tiedup.remake.items.base.ItemBind itemBind
- ) {
- return (
- itemBind.getPoseType() ==
- com.tiedup.remake.items.base.PoseType.DOG
- );
- }
- return false;
+ if (bind.isEmpty()) return false;
+ return PoseTypeHelper.getPoseType(bind) == com.tiedup.remake.items.base.PoseType.DOG;
}
/**
@@ -679,10 +676,8 @@ public abstract class AbstractTiedUpNpc
// Exception: collar owner can leash even if not tied
if (this.hasCollar()) {
ItemStack collar = this.getEquipment(BodyRegionV2.NECK);
- if (collar.getItem() instanceof ItemCollar collarItem) {
- if (collarItem.getOwners(collar).contains(player.getUUID())) {
- return true;
- }
+ if (CollarHelper.isOwner(collar, player)) {
+ return true;
}
}
@@ -801,20 +796,16 @@ public abstract class AbstractTiedUpNpc
public boolean hasGaggingEffect() {
ItemStack gag = this.getEquipment(BodyRegionV2.MOUTH);
if (gag.isEmpty()) return false;
- return (
- gag.getItem() instanceof
- com.tiedup.remake.items.base.IHasGaggingEffect
- );
+ if (DataDrivenBondageItem.getComponent(gag, ComponentType.GAGGING, GaggingComponent.class) != null) return true;
+ return gag.getItem() instanceof com.tiedup.remake.items.base.IHasGaggingEffect;
}
@Override
public boolean hasBlindingEffect() {
ItemStack blindfold = this.getEquipment(BodyRegionV2.EYES);
if (blindfold.isEmpty()) return false;
- return (
- blindfold.getItem() instanceof
- com.tiedup.remake.items.base.IHasBlindingEffect
- );
+ if (DataDrivenBondageItem.getComponent(blindfold, ComponentType.BLINDING, BlindingComponent.class) != null) return true;
+ return blindfold.getItem() instanceof com.tiedup.remake.items.base.IHasBlindingEffect;
}
@Override
@@ -990,9 +981,9 @@ public abstract class AbstractTiedUpNpc
ItemStack collar = this.getEquipment(BodyRegionV2.NECK);
if (collar.isEmpty()) return false;
- if (!(collar.getItem() instanceof ItemCollar itemCollar)) return false;
+ if (!CollarHelper.isCollar(collar)) return false;
- java.util.UUID cellId = itemCollar.getCellId(collar);
+ java.util.UUID cellId = CollarHelper.getCellId(collar);
if (cellId == null) return false;
// Get cell position from registry
diff --git a/src/main/java/com/tiedup/remake/entities/BondageServiceHandler.java b/src/main/java/com/tiedup/remake/entities/BondageServiceHandler.java
index 689871b..f8c554d 100644
--- a/src/main/java/com/tiedup/remake/entities/BondageServiceHandler.java
+++ b/src/main/java/com/tiedup/remake/entities/BondageServiceHandler.java
@@ -1,10 +1,9 @@
package com.tiedup.remake.entities;
import com.tiedup.remake.core.SystemMessageManager;
-import com.tiedup.remake.items.ModItems;
-import com.tiedup.remake.items.base.BindVariant;
-import com.tiedup.remake.items.base.GagVariant;
-import com.tiedup.remake.items.base.ItemCollar;
+import com.tiedup.remake.v2.bondage.CollarHelper;
+import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
+import net.minecraft.resources.ResourceLocation;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.util.MessageDispatcher;
import com.tiedup.remake.util.teleport.Position;
@@ -53,13 +52,10 @@ public class BondageServiceHandler {
if (!npc.hasCollar()) return false;
ItemStack collar = npc.getEquipment(BodyRegionV2.NECK);
- if (collar.getItem() instanceof ItemCollar itemCollar) {
- return (
- itemCollar.hasCellAssigned(collar) &&
- itemCollar.isBondageServiceEnabled(collar)
- );
- }
- return false;
+ return (
+ CollarHelper.hasCellAssigned(collar) &&
+ CollarHelper.isBondageServiceEnabled(collar)
+ );
}
/**
@@ -70,11 +66,9 @@ public class BondageServiceHandler {
public String getMessage() {
if (npc.hasCollar()) {
ItemStack collar = npc.getEquipment(BodyRegionV2.NECK);
- if (collar.getItem() instanceof ItemCollar itemCollar) {
- String message = itemCollar.getServiceSentence(collar);
- if (message != null && !message.isEmpty()) {
- return message;
- }
+ String message = CollarHelper.getServiceSentence(collar);
+ if (message != null && !message.isEmpty()) {
+ return message;
}
}
return DEFAULT_MESSAGE;
@@ -119,9 +113,9 @@ public class BondageServiceHandler {
*/
private void capturePlayer(Player player) {
ItemStack collar = npc.getEquipment(BodyRegionV2.NECK);
- if (!(collar.getItem() instanceof ItemCollar itemCollar)) return;
+ if (!CollarHelper.isCollar(collar)) return;
- java.util.UUID cellId = itemCollar.getCellId(collar);
+ java.util.UUID cellId = CollarHelper.getCellId(collar);
if (cellId == null) return;
// Get cell position from registry
@@ -141,7 +135,7 @@ public class BondageServiceHandler {
);
// Warn masters if configured
- warnOwners(player, itemCollar, collar);
+ warnOwners(player, collar);
// Get player's kidnapped state
PlayerBindState state = PlayerBindState.getInstance(player);
@@ -149,18 +143,18 @@ public class BondageServiceHandler {
// Apply bondage
state.equip(
BodyRegionV2.ARMS,
- new ItemStack(ModItems.getBind(BindVariant.ROPES))
+ DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "ropes"))
);
state.equip(
BodyRegionV2.MOUTH,
- new ItemStack(ModItems.getGag(GagVariant.BALL_GAG))
+ DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "ball_gag"))
);
// Teleport to cell
state.teleportToPosition(cellPosition);
// Tie to pole if configured on collar
- if (itemCollar.shouldTieToPole(collar)) {
+ if (CollarHelper.shouldTieToPole(collar)) {
state.tieToClosestPole(3);
}
}
@@ -178,10 +172,9 @@ public class BondageServiceHandler {
*/
private void warnOwners(
Player capturedPlayer,
- ItemCollar itemCollar,
ItemStack collarStack
) {
- if (!itemCollar.shouldWarnMasters(collarStack)) {
+ if (!CollarHelper.shouldWarnMasters(collarStack)) {
return;
}
@@ -191,7 +184,7 @@ public class BondageServiceHandler {
capturedPlayer.getName().getString() +
" via bondage service!";
- for (UUID ownerUUID : itemCollar.getOwners(collarStack)) {
+ for (UUID ownerUUID : CollarHelper.getOwners(collarStack)) {
Player owner = npc.level().getPlayerByUUID(ownerUUID);
if (owner != null) {
SystemMessageManager.sendChatToPlayer(
diff --git a/src/main/java/com/tiedup/remake/entities/EntityDamsel.java b/src/main/java/com/tiedup/remake/entities/EntityDamsel.java
index 790a3ac..7cda2a3 100644
--- a/src/main/java/com/tiedup/remake/entities/EntityDamsel.java
+++ b/src/main/java/com/tiedup/remake/entities/EntityDamsel.java
@@ -5,7 +5,7 @@ import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.damsel.components.*;
import com.tiedup.remake.entities.skins.DamselSkinManager;
import com.tiedup.remake.entities.skins.Gender;
-import com.tiedup.remake.items.base.ItemCollar;
+import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.state.ICaptor;
import com.tiedup.remake.v2.BodyRegionV2;
import java.util.UUID;
@@ -527,8 +527,8 @@ public class EntityDamsel
if (!this.hasCollar()) return false;
ItemStack collar = this.getEquipment(BodyRegionV2.NECK);
- if (!(collar.getItem() instanceof ItemCollar collarItem)) return false;
- if (!collarItem.getOwners(collar).contains(commander.getUUID())) {
+ if (!CollarHelper.isCollar(collar)) return false;
+ if (!CollarHelper.isOwner(collar, commander.getUUID())) {
if (!this.isGagged()) {
com.tiedup.remake.dialogue.EntityDialogueManager.talkByDialogueId(
this,
@@ -653,8 +653,8 @@ public class EntityDamsel
return net.minecraft.world.InteractionResult.FAIL;
}
ItemStack collar = this.getEquipment(BodyRegionV2.NECK);
- if (collar.getItem() instanceof ItemCollar collarItem) {
- if (!collarItem.isOwner(collar, player)) {
+ if (CollarHelper.isCollar(collar)) {
+ if (!CollarHelper.isOwner(collar, player)) {
if (
player instanceof
net.minecraft.server.level.ServerPlayer sp
@@ -693,9 +693,9 @@ public class EntityDamsel
this.hasCollar()
) {
ItemStack collar = this.getEquipment(BodyRegionV2.NECK);
- if (collar.getItem() instanceof ItemCollar collarItem) {
+ if (CollarHelper.isCollar(collar)) {
if (
- collarItem.isOwner(collar, player) &&
+ CollarHelper.isOwner(collar, player) &&
player instanceof
net.minecraft.server.level.ServerPlayer serverPlayer
) {
@@ -822,8 +822,8 @@ public class EntityDamsel
public String getTargetRelation(Player player) {
if (hasCollar()) {
ItemStack collar = getEquipment(BodyRegionV2.NECK);
- if (collar.getItem() instanceof ItemCollar collarItem) {
- if (collarItem.isOwner(collar, player)) {
+ if (CollarHelper.isCollar(collar)) {
+ if (CollarHelper.isOwner(collar, player)) {
return "master";
}
}
diff --git a/src/main/java/com/tiedup/remake/entities/EntityKidnapper.java b/src/main/java/com/tiedup/remake/entities/EntityKidnapper.java
index 21d8129..30f0104 100644
--- a/src/main/java/com/tiedup/remake/entities/EntityKidnapper.java
+++ b/src/main/java/com/tiedup/remake/entities/EntityKidnapper.java
@@ -12,7 +12,7 @@ import com.tiedup.remake.entities.kidnapper.components.KidnapperAggressionSystem
import com.tiedup.remake.entities.skins.Gender;
import com.tiedup.remake.entities.skins.KidnapperSkinManager;
import com.tiedup.remake.items.ModItems;
-import com.tiedup.remake.items.base.ItemCollar;
+import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.personality.PersonalityType;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.state.ICaptor;
@@ -1367,10 +1367,7 @@ public class EntityKidnapper
if (!this.hasCollar()) return false;
ItemStack collar = this.getEquipment(BodyRegionV2.NECK);
- if (collar.getItem() instanceof ItemCollar collarItem) {
- return collarItem.isOwner(collar, player);
- }
- return false;
+ return CollarHelper.isOwner(collar, player);
}
/** Damage reduction multiplier against monsters (50% damage taken) */
diff --git a/src/main/java/com/tiedup/remake/entities/EntityKidnapperMerchant.java b/src/main/java/com/tiedup/remake/entities/EntityKidnapperMerchant.java
index edb0e86..b874d21 100644
--- a/src/main/java/com/tiedup/remake/entities/EntityKidnapperMerchant.java
+++ b/src/main/java/com/tiedup/remake/entities/EntityKidnapperMerchant.java
@@ -18,6 +18,7 @@ import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.state.ICaptor;
import com.tiedup.remake.util.MessageDispatcher;
import com.tiedup.remake.v2.BodyRegionV2;
+import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
@@ -615,78 +616,82 @@ public class EntityKidnapperMerchant extends EntityKidnapperElite {
private List collectAllModItems() {
List items = new ArrayList<>();
- // All binds (15)
- // Items with colors get multiple variants (one per color)
+ // All binds — iterate V1 variants, create V2 stacks
for (BindVariant variant : BindVariant.values()) {
if (variant.supportsColor()) {
- // Add one item per color (16 standard colors)
for (ItemColor color : ItemColor.values()) {
if (
color != ItemColor.CAUTION && color != ItemColor.CLEAR
) {
- ItemStack stack = new ItemStack(
- ModItems.getBind(variant)
+ ItemStack stack = DataDrivenBondageItem.createStack(
+ new net.minecraft.resources.ResourceLocation("tiedup", variant.getRegistryName())
);
KidnapperItemSelector.applyColor(stack, color);
items.add(stack);
}
}
} else {
- // No color variants
- items.add(new ItemStack(ModItems.getBind(variant)));
+ items.add(DataDrivenBondageItem.createStack(
+ new net.minecraft.resources.ResourceLocation("tiedup", variant.getRegistryName())
+ ));
}
}
- // All gags (19)
+ // All gags
for (GagVariant variant : GagVariant.values()) {
if (variant.supportsColor()) {
- // Add one item per color
for (ItemColor color : ItemColor.values()) {
- // TAPE_GAG can use caution/clear, others only standard colors
if (
variant == GagVariant.TAPE_GAG ||
(color != ItemColor.CAUTION && color != ItemColor.CLEAR)
) {
- ItemStack stack = new ItemStack(
- ModItems.getGag(variant)
+ ItemStack stack = DataDrivenBondageItem.createStack(
+ new net.minecraft.resources.ResourceLocation("tiedup", variant.getRegistryName())
);
KidnapperItemSelector.applyColor(stack, color);
items.add(stack);
}
}
} else {
- items.add(new ItemStack(ModItems.getGag(variant)));
+ items.add(DataDrivenBondageItem.createStack(
+ new net.minecraft.resources.ResourceLocation("tiedup", variant.getRegistryName())
+ ));
}
}
- // All blindfolds (2) - BOTH support colors
+ // All blindfolds
for (BlindfoldVariant variant : BlindfoldVariant.values()) {
if (variant.supportsColor()) {
- // Add one item per color (16 standard colors)
for (ItemColor color : ItemColor.values()) {
if (
color != ItemColor.CAUTION && color != ItemColor.CLEAR
) {
- ItemStack stack = new ItemStack(
- ModItems.getBlindfold(variant)
+ ItemStack stack = DataDrivenBondageItem.createStack(
+ new net.minecraft.resources.ResourceLocation("tiedup", variant.getRegistryName())
);
KidnapperItemSelector.applyColor(stack, color);
items.add(stack);
}
}
} else {
- items.add(new ItemStack(ModItems.getBlindfold(variant)));
+ items.add(DataDrivenBondageItem.createStack(
+ new net.minecraft.resources.ResourceLocation("tiedup", variant.getRegistryName())
+ ));
}
}
// Earplugs - no color support
for (EarplugsVariant variant : EarplugsVariant.values()) {
- items.add(new ItemStack(ModItems.getEarplugs(variant)));
+ items.add(DataDrivenBondageItem.createStack(
+ new net.minecraft.resources.ResourceLocation("tiedup", variant.getRegistryName())
+ ));
}
// Mittens - no color support
for (MittensVariant variant : MittensVariant.values()) {
- items.add(new ItemStack(ModItems.getMittens(variant)));
+ items.add(DataDrivenBondageItem.createStack(
+ new net.minecraft.resources.ResourceLocation("tiedup", variant.getRegistryName())
+ ));
}
// Knives - no color support
diff --git a/src/main/java/com/tiedup/remake/entities/EntityRopeArrow.java b/src/main/java/com/tiedup/remake/entities/EntityRopeArrow.java
index 5640e32..d9410af 100644
--- a/src/main/java/com/tiedup/remake/entities/EntityRopeArrow.java
+++ b/src/main/java/com/tiedup/remake/entities/EntityRopeArrow.java
@@ -94,10 +94,8 @@ public class EntityRopeArrow extends AbstractArrow {
int roll = this.random.nextInt(100) + 1;
if (roll <= bindChance) {
// Success! Bind the target
- ItemStack ropeItem = new ItemStack(
- ModItems.getBind(
- com.tiedup.remake.items.base.BindVariant.ROPES
- )
+ ItemStack ropeItem = com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem.createStack(
+ new net.minecraft.resources.ResourceLocation("tiedup", "ropes")
);
targetState.equip(BodyRegionV2.ARMS, ropeItem);
diff --git a/src/main/java/com/tiedup/remake/entities/KidnapperCaptureEquipment.java b/src/main/java/com/tiedup/remake/entities/KidnapperCaptureEquipment.java
index 243b53d..dce8890 100644
--- a/src/main/java/com/tiedup/remake/entities/KidnapperCaptureEquipment.java
+++ b/src/main/java/com/tiedup/remake/entities/KidnapperCaptureEquipment.java
@@ -1,7 +1,8 @@
package com.tiedup.remake.entities;
import com.tiedup.remake.items.ModItems;
-import com.tiedup.remake.items.base.BindVariant;
+import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
+import net.minecraft.resources.ResourceLocation;
import com.tiedup.remake.v2.bondage.IV2BondageItem;
import net.minecraft.world.entity.EquipmentSlot;
import net.minecraft.world.item.ItemStack;
@@ -46,7 +47,7 @@ public class KidnapperCaptureEquipment {
) {
return mainHand;
}
- return new ItemStack(ModItems.getBind(BindVariant.ROPES));
+ return DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "ropes"));
}
/**
diff --git a/src/main/java/com/tiedup/remake/entities/KidnapperCollarConfig.java b/src/main/java/com/tiedup/remake/entities/KidnapperCollarConfig.java
index 0dc88a4..06112d6 100644
--- a/src/main/java/com/tiedup/remake/entities/KidnapperCollarConfig.java
+++ b/src/main/java/com/tiedup/remake/entities/KidnapperCollarConfig.java
@@ -1,11 +1,9 @@
package com.tiedup.remake.entities;
-import com.tiedup.remake.items.base.ItemCollar;
-import com.tiedup.remake.util.teleport.Position;
import com.tiedup.remake.v2.BodyRegionV2;
+import com.tiedup.remake.v2.bondage.CollarHelper;
import java.util.List;
import java.util.UUID;
-import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import org.jetbrains.annotations.Nullable;
@@ -31,21 +29,7 @@ public class KidnapperCollarConfig {
this.kidnapper = kidnapper;
}
- // COLLAR ITEM ACCESS
-
- /**
- * Get the collar item if equipped.
- * @return ItemCollar or null if no collar or not an ItemCollar
- */
- @Nullable
- public ItemCollar getCollarItem() {
- ItemStack collar = kidnapper.getEquipment(BodyRegionV2.NECK);
- if (collar.isEmpty()) return null;
- if (collar.getItem() instanceof ItemCollar itemCollar) {
- return itemCollar;
- }
- return null;
- }
+ // COLLAR STACK ACCESS
/**
* Get the collar ItemStack.
@@ -55,30 +39,29 @@ public class KidnapperCollarConfig {
return kidnapper.getEquipment(BodyRegionV2.NECK);
}
+ /**
+ * Check if the kidnapper has a valid collar.
+ */
+ private boolean hasValidCollar() {
+ return kidnapper.hasCollar() && CollarHelper.isCollar(getCollarStack());
+ }
+
// KIDNAPPING MODE
/**
* Check if kidnapping mode is enabled via collar.
*/
public boolean isKidnappingModeEnabled() {
- if (!kidnapper.hasCollar()) return false;
-
- ItemCollar itemCollar = getCollarItem();
- if (itemCollar == null) return false;
-
- return itemCollar.isKidnappingModeEnabled(getCollarStack());
+ if (!hasValidCollar()) return false;
+ return CollarHelper.isKidnappingModeEnabled(getCollarStack());
}
/**
* Check if kidnapping mode is fully ready (enabled + prison set).
*/
public boolean isKidnappingModeReady() {
- if (!kidnapper.hasCollar()) return false;
-
- ItemCollar itemCollar = getCollarItem();
- if (itemCollar == null) return false;
-
- return itemCollar.isKidnappingModeReady(getCollarStack());
+ if (!hasValidCollar()) return false;
+ return CollarHelper.isKidnappingModeReady(getCollarStack());
}
// POSITION GETTERS
@@ -88,20 +71,16 @@ public class KidnapperCollarConfig {
*/
@Nullable
public java.util.UUID getCellId() {
- ItemCollar itemCollar = getCollarItem();
- if (itemCollar == null) return null;
-
- return itemCollar.getCellId(getCollarStack());
+ if (!hasValidCollar()) return null;
+ return CollarHelper.getCellId(getCollarStack());
}
/**
* Check if collar has a cell assigned.
*/
public boolean hasCellAssigned() {
- ItemCollar itemCollar = getCollarItem();
- if (itemCollar == null) return false;
-
- return itemCollar.hasCellAssigned(getCollarStack());
+ if (!hasValidCollar()) return false;
+ return CollarHelper.hasCellAssigned(getCollarStack());
}
// BEHAVIOR FLAGS
@@ -110,20 +89,16 @@ public class KidnapperCollarConfig {
* Check if should warn masters after capturing slave.
*/
public boolean shouldWarnMasters() {
- ItemCollar itemCollar = getCollarItem();
- if (itemCollar == null) return false;
-
- return itemCollar.shouldWarnMasters(getCollarStack());
+ if (!hasValidCollar()) return false;
+ return CollarHelper.shouldWarnMasters(getCollarStack());
}
/**
* Check if should tie slave to pole in prison.
*/
public boolean shouldTieToPole() {
- ItemCollar itemCollar = getCollarItem();
- if (itemCollar == null) return false;
-
- return itemCollar.shouldTieToPole(getCollarStack());
+ if (!hasValidCollar()) return false;
+ return CollarHelper.shouldTieToPole(getCollarStack());
}
// BLACKLIST/WHITELIST
@@ -139,19 +114,18 @@ public class KidnapperCollarConfig {
*
*/
public boolean isValidKidnappingTarget(Player player) {
- ItemCollar itemCollar = getCollarItem();
- if (itemCollar == null) return true; // No collar config = everyone is valid
+ if (!hasValidCollar()) return true; // No collar config = everyone is valid
ItemStack collarStack = getCollarStack();
UUID playerUUID = player.getUUID();
// If whitelist exists and is not empty, player MUST be on it
- List whitelist = itemCollar.getWhitelist(collarStack);
+ List whitelist = CollarHelper.getWhitelist(collarStack);
if (!whitelist.isEmpty()) {
return whitelist.contains(playerUUID);
}
// Otherwise, check blacklist (blacklisted = not a valid target)
- return !itemCollar.isBlacklisted(collarStack, playerUUID);
+ return !CollarHelper.isBlacklisted(collarStack, playerUUID);
}
}
diff --git a/src/main/java/com/tiedup/remake/entities/KidnapperItemSelector.java b/src/main/java/com/tiedup/remake/entities/KidnapperItemSelector.java
index 90b459f..75a655f 100644
--- a/src/main/java/com/tiedup/remake/entities/KidnapperItemSelector.java
+++ b/src/main/java/com/tiedup/remake/entities/KidnapperItemSelector.java
@@ -1,8 +1,9 @@
package com.tiedup.remake.entities;
-import com.tiedup.remake.items.ModItems;
import com.tiedup.remake.items.base.*;
+import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
import java.util.Random;
+import net.minecraft.resources.ResourceLocation;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.item.ItemStack;
import org.jetbrains.annotations.Nullable;
@@ -242,7 +243,9 @@ public class KidnapperItemSelector {
BindVariant variant,
@Nullable ItemColor color
) {
- ItemStack stack = new ItemStack(ModItems.getBind(variant));
+ ItemStack stack = DataDrivenBondageItem.createStack(
+ new ResourceLocation("tiedup", variant.getRegistryName())
+ );
if (color != null && variant.supportsColor()) {
applyColor(stack, color);
}
@@ -257,7 +260,9 @@ public class KidnapperItemSelector {
GagVariant variant,
@Nullable ItemColor color
) {
- ItemStack stack = new ItemStack(ModItems.getGag(variant));
+ ItemStack stack = DataDrivenBondageItem.createStack(
+ new ResourceLocation("tiedup", variant.getRegistryName())
+ );
if (
color != null &&
variant.supportsColor() &&
@@ -276,7 +281,9 @@ public class KidnapperItemSelector {
BlindfoldVariant variant,
@Nullable ItemColor color
) {
- ItemStack stack = new ItemStack(ModItems.getBlindfold(variant));
+ ItemStack stack = DataDrivenBondageItem.createStack(
+ new ResourceLocation("tiedup", variant.getRegistryName())
+ );
if (
color != null &&
variant.supportsColor() &&
@@ -292,7 +299,9 @@ public class KidnapperItemSelector {
* Mittens don't have color variants.
*/
public static ItemStack createMittens() {
- return new ItemStack(ModItems.getMittens(MittensVariant.LEATHER));
+ return DataDrivenBondageItem.createStack(
+ new ResourceLocation("tiedup", "leather_mittens")
+ );
}
/**
@@ -300,7 +309,9 @@ public class KidnapperItemSelector {
* Earplugs don't have color variants.
*/
public static ItemStack createEarplugs() {
- return new ItemStack(ModItems.getEarplugs(EarplugsVariant.CLASSIC));
+ return DataDrivenBondageItem.createStack(
+ new ResourceLocation("tiedup", "classic_earplugs")
+ );
}
// COLOR METHODS
diff --git a/src/main/java/com/tiedup/remake/entities/KidnapperJobManager.java b/src/main/java/com/tiedup/remake/entities/KidnapperJobManager.java
index 7d2499b..1914b32 100644
--- a/src/main/java/com/tiedup/remake/entities/KidnapperJobManager.java
+++ b/src/main/java/com/tiedup/remake/entities/KidnapperJobManager.java
@@ -2,7 +2,7 @@ package com.tiedup.remake.entities;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.ModItems;
-import com.tiedup.remake.items.base.ItemCollar;
+import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.state.CollarRegistry;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.util.tasks.ItemTask;
@@ -162,15 +162,15 @@ public class KidnapperJobManager {
// Put a shock collar on the worker AFTER untie/free
ItemStack shockCollar = new ItemStack(ModItems.SHOCK_COLLAR_AUTO.get());
- if (shockCollar.getItem() instanceof ItemCollar collarItem) {
- // Add kidnapper as owner so the collar is linked
- collarItem.addOwner(
- shockCollar,
- kidnapper.getUUID(),
- kidnapper.getNpcName()
- );
- // Lock the collar so they can't remove it
- shockCollar = collarItem.setLocked(shockCollar, true);
+ // Add kidnapper as owner so the collar is linked
+ CollarHelper.addOwner(
+ shockCollar,
+ kidnapper.getUUID(),
+ kidnapper.getNpcName()
+ );
+ // Lock the collar so they can't remove it
+ if (shockCollar.getItem() instanceof com.tiedup.remake.items.base.ILockable lockable) {
+ shockCollar = lockable.setLocked(shockCollar, true);
}
captive.equip(BodyRegionV2.NECK, shockCollar);
diff --git a/src/main/java/com/tiedup/remake/entities/LeashProxyEntity.java b/src/main/java/com/tiedup/remake/entities/LeashProxyEntity.java
index 9600210..84d53f9 100644
--- a/src/main/java/com/tiedup/remake/entities/LeashProxyEntity.java
+++ b/src/main/java/com/tiedup/remake/entities/LeashProxyEntity.java
@@ -1,7 +1,6 @@
package com.tiedup.remake.entities;
-import com.tiedup.remake.items.ModItems;
-import com.tiedup.remake.items.base.BindVariant;
+import com.tiedup.remake.v2.bondage.PoseTypeHelper;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.v2.BodyRegionV2;
@@ -108,7 +107,7 @@ public final class LeashProxyEntity extends Turtle {
ItemStack bind = state.getEquipment(BodyRegionV2.ARMS);
if (
!bind.isEmpty() &&
- bind.getItem() == ModItems.getBind(BindVariant.DOGBINDER)
+ PoseTypeHelper.getPoseType(bind) == com.tiedup.remake.items.base.PoseType.DOG
) {
yOffset = 0.35D; // Lower for 4-legged dogwalk pose (back/hip level)
}
diff --git a/src/main/java/com/tiedup/remake/entities/ai/kidnapper/KidnapperBringToCellGoal.java b/src/main/java/com/tiedup/remake/entities/ai/kidnapper/KidnapperBringToCellGoal.java
index 43b2550..741d4f9 100644
--- a/src/main/java/com/tiedup/remake/entities/ai/kidnapper/KidnapperBringToCellGoal.java
+++ b/src/main/java/com/tiedup/remake/entities/ai/kidnapper/KidnapperBringToCellGoal.java
@@ -11,7 +11,7 @@ import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.entities.EntityKidnapper.CaptivePriority;
import com.tiedup.remake.entities.ai.StuckDetector;
import com.tiedup.remake.entities.ai.WaypointNavigator;
-import com.tiedup.remake.items.base.ItemCollar;
+import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.prison.PrisonerManager;
import com.tiedup.remake.prison.PrisonerRecord;
import com.tiedup.remake.prison.PrisonerState;
@@ -260,7 +260,7 @@ public class KidnapperBringToCellGoal extends Goal {
);
IRestrainable captive = this.kidnapper.getCaptive();
if (captive != null) {
- ItemCollar.runWithSuppressedAlert(() ->
+ CollarHelper.runWithSuppressedAlert(() ->
captive.free(false)
);
this.kidnapper.removeCaptive(captive, false);
@@ -1079,9 +1079,9 @@ public class KidnapperBringToCellGoal extends Goal {
ItemStack collar = captive.getEquipment(BodyRegionV2.NECK);
if (
!collar.isEmpty() &&
- collar.getItem() instanceof ItemCollar collarItem
+ CollarHelper.isCollar(collar)
) {
- collarItem.setCellId(collar, this.targetCell.getId());
+ CollarHelper.setCellId(collar, this.targetCell.getId());
}
TiedUpMod.LOGGER.info(
diff --git a/src/main/java/com/tiedup/remake/entities/ai/kidnapper/KidnapperCaptureGoal.java b/src/main/java/com/tiedup/remake/entities/ai/kidnapper/KidnapperCaptureGoal.java
index ff9437b..8725141 100644
--- a/src/main/java/com/tiedup/remake/entities/ai/kidnapper/KidnapperCaptureGoal.java
+++ b/src/main/java/com/tiedup/remake/entities/ai/kidnapper/KidnapperCaptureGoal.java
@@ -8,7 +8,7 @@ import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.entities.ai.StuckDetector;
import com.tiedup.remake.items.ItemKey;
import com.tiedup.remake.items.ModItems;
-import com.tiedup.remake.items.base.ItemCollar;
+import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.network.action.PacketTying;
import com.tiedup.remake.prison.PrisonerManager;
@@ -849,11 +849,8 @@ public class KidnapperCaptureGoal extends Goal {
// If already has collar, check ownership
if (state.hasCollar()) {
ItemStack existingCollar = state.getEquipment(BodyRegionV2.NECK);
- if (
- existingCollar.getItem() instanceof
- com.tiedup.remake.items.base.ItemCollar collarItem
- ) {
- java.util.List owners = collarItem.getOwners(
+ if (CollarHelper.isCollar(existingCollar)) {
+ java.util.List owners = CollarHelper.getOwners(
existingCollar
);
if (!owners.contains(this.kidnapper.getUUID())) {
@@ -885,9 +882,9 @@ public class KidnapperCaptureGoal extends Goal {
for (java.util.UUID oldOwner : new java.util.ArrayList<>(
owners
)) {
- collarItem.removeOwner(existingCollar, oldOwner);
+ CollarHelper.removeOwner(existingCollar, oldOwner);
}
- collarItem.addOwner(
+ CollarHelper.addOwner(
existingCollar,
this.kidnapper.getUUID(),
this.kidnapper.getNpcName()
@@ -929,9 +926,9 @@ public class KidnapperCaptureGoal extends Goal {
if (this.captureProgress >= COLLAR_APPLY_TIME) {
// Create a copy of the collar and configure it
ItemStack collarCopy = collar.copy();
- if (collarCopy.getItem() instanceof ItemCollar collarItem) {
+ if (CollarHelper.isCollar(collarCopy)) {
// Add kidnapper as owner
- collarItem.addOwner(
+ CollarHelper.addOwner(
collarCopy,
this.kidnapper.getUUID(),
this.kidnapper.getNpcName()
@@ -941,12 +938,17 @@ public class KidnapperCaptureGoal extends Goal {
ItemStack keyStack = new ItemStack(ModItems.COLLAR_KEY.get());
if (keyStack.getItem() instanceof ItemKey keyItem) {
UUID keyUUID = keyItem.getKeyUUID(keyStack);
- collarItem.setLockedByKeyUUID(collarCopy, keyUUID);
+ // Lock via ILockable interface (collar must implement it)
+ if (collarCopy.getItem() instanceof com.tiedup.remake.items.base.ILockable lockable) {
+ lockable.setLockedByKeyUUID(collarCopy, keyUUID);
+ }
// Store the key on the kidnapper for potential drop on death
this.kidnapper.addCollarKey(keyStack);
} else {
// Fallback: just lock without a key
- collarItem.setLocked(collarCopy, true);
+ if (collarCopy.getItem() instanceof com.tiedup.remake.items.base.ILockable lockable) {
+ lockable.setLocked(collarCopy, true);
+ }
}
}
diff --git a/src/main/java/com/tiedup/remake/entities/ai/kidnapper/KidnapperWalkPrisonerGoal.java b/src/main/java/com/tiedup/remake/entities/ai/kidnapper/KidnapperWalkPrisonerGoal.java
index a0c5534..d035b58 100644
--- a/src/main/java/com/tiedup/remake/entities/ai/kidnapper/KidnapperWalkPrisonerGoal.java
+++ b/src/main/java/com/tiedup/remake/entities/ai/kidnapper/KidnapperWalkPrisonerGoal.java
@@ -8,8 +8,8 @@ import com.tiedup.remake.entities.AbstractTiedUpNpc;
import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.entities.ai.StuckDetector;
import com.tiedup.remake.entities.ai.WaypointNavigator;
-import com.tiedup.remake.items.ModItems;
-import com.tiedup.remake.items.base.BindVariant;
+import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
+import net.minecraft.resources.ResourceLocation;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.util.KidnapperAIHelper;
@@ -547,9 +547,7 @@ public class KidnapperWalkPrisonerGoal extends Goal {
);
// 3. Change bind to DOGBINDER
- ItemStack dogBinder = new ItemStack(
- ModItems.getBind(BindVariant.DOGBINDER)
- );
+ ItemStack dogBinder = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "dogbinder"));
this.walkedPrisoner.equip(BodyRegionV2.ARMS, dogBinder);
TiedUpMod.LOGGER.debug(
@@ -738,7 +736,7 @@ public class KidnapperWalkPrisonerGoal extends Goal {
if (currentBind.isEmpty() || !prisoner.isTiedUp()) {
// They freed themselves - put dogbinder back on
ItemStack dogBinder = new ItemStack(
- ModItems.getBind(BindVariant.DOGBINDER)
+ DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "dogbinder")).getItem()
);
prisoner.equip(BodyRegionV2.ARMS, dogBinder);
}
diff --git a/src/main/java/com/tiedup/remake/entities/ai/maid/goals/MaidReturnGoal.java b/src/main/java/com/tiedup/remake/entities/ai/maid/goals/MaidReturnGoal.java
index 5d31ded..6128d5e 100644
--- a/src/main/java/com/tiedup/remake/entities/ai/maid/goals/MaidReturnGoal.java
+++ b/src/main/java/com/tiedup/remake/entities/ai/maid/goals/MaidReturnGoal.java
@@ -338,10 +338,8 @@ public class MaidReturnGoal extends Goal {
// Fallback: use basic rope if no snapshot
cap.equip(
BodyRegionV2.ARMS,
- new ItemStack(
- com.tiedup.remake.items.ModItems.getBind(
- com.tiedup.remake.items.base.BindVariant.ROPES
- )
+ com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem.createStack(
+ new net.minecraft.resources.ResourceLocation("tiedup", "ropes")
)
);
}
diff --git a/src/main/java/com/tiedup/remake/entities/ai/master/MasterHumanChairGoal.java b/src/main/java/com/tiedup/remake/entities/ai/master/MasterHumanChairGoal.java
index 37f0716..9fb0909 100644
--- a/src/main/java/com/tiedup/remake/entities/ai/master/MasterHumanChairGoal.java
+++ b/src/main/java/com/tiedup/remake/entities/ai/master/MasterHumanChairGoal.java
@@ -3,8 +3,8 @@ package com.tiedup.remake.entities.ai.master;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.dialogue.DialogueBridge;
import com.tiedup.remake.entities.EntityMaster;
-import com.tiedup.remake.items.ModItems;
-import com.tiedup.remake.items.base.BindVariant;
+import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
+import net.minecraft.resources.ResourceLocation;
import com.tiedup.remake.state.HumanChairHelper;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.v2.BodyRegionV2;
@@ -394,8 +394,8 @@ public class MasterHumanChairGoal extends Goal {
// Apply invisible dogbind for the pose animation
if (!bindState.isTiedUp()) {
- ItemStack dogbind = new ItemStack(
- ModItems.getBind(BindVariant.DOGBINDER)
+ ItemStack dogbind = DataDrivenBondageItem.createStack(
+ new ResourceLocation("tiedup", "dogbinder")
);
CompoundTag tag = dogbind.getOrCreateTag();
tag.putBoolean(NBT_HUMAN_CHAIR_BIND, true);
diff --git a/src/main/java/com/tiedup/remake/entities/ai/master/MasterPunishGoal.java b/src/main/java/com/tiedup/remake/entities/ai/master/MasterPunishGoal.java
index 28c97db..d985c62 100644
--- a/src/main/java/com/tiedup/remake/entities/ai/master/MasterPunishGoal.java
+++ b/src/main/java/com/tiedup/remake/entities/ai/master/MasterPunishGoal.java
@@ -3,12 +3,9 @@ package com.tiedup.remake.entities.ai.master;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.dialogue.DialogueBridge;
import com.tiedup.remake.entities.EntityMaster;
-import com.tiedup.remake.items.ItemChokeCollar;
-import com.tiedup.remake.items.ModItems;
-import com.tiedup.remake.items.base.BindVariant;
-import com.tiedup.remake.items.base.BlindfoldVariant;
-import com.tiedup.remake.items.base.GagVariant;
-import com.tiedup.remake.items.base.MittensVariant;
+import com.tiedup.remake.v2.bondage.CollarHelper;
+import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
+import net.minecraft.resources.ResourceLocation;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.IV2BondageEquipment;
@@ -261,7 +258,7 @@ public class MasterPunishGoal extends Goal {
// CHOKE: only if pet has choke collar
if (bindState != null && bindState.hasCollar()) {
ItemStack collar = bindState.getEquipment(BodyRegionV2.NECK);
- if (collar.getItem() instanceof ItemChokeCollar) {
+ if (CollarHelper.isChokeCollar(collar)) {
available.add(PunishmentType.CHOKE_COLLAR);
}
}
@@ -393,8 +390,8 @@ public class MasterPunishGoal extends Goal {
PlayerBindState bindState = PlayerBindState.getInstance(pet);
if (bindState != null && bindState.hasCollar()) {
ItemStack collar = bindState.getEquipment(BodyRegionV2.NECK);
- if (collar.getItem() instanceof ItemChokeCollar chokeCollar) {
- chokeCollar.setChoking(collar, true);
+ if (CollarHelper.isChokeCollar(collar)) {
+ CollarHelper.setChoking(collar, true);
this.activeChokeCollar = collar;
this.chokeActiveTimer = 1;
@@ -457,12 +454,14 @@ public class MasterPunishGoal extends Goal {
*/
private ItemStack createAccessory(BodyRegionV2 region) {
return switch (region) {
- case EYES -> new ItemStack(
- ModItems.getBlindfold(BlindfoldVariant.CLASSIC)
+ case EYES -> DataDrivenBondageItem.createStack(
+ new ResourceLocation("tiedup", "classic_blindfold")
);
- case MOUTH -> new ItemStack(ModItems.getGag(GagVariant.BALL_GAG));
- case HANDS -> new ItemStack(
- ModItems.getMittens(MittensVariant.LEATHER)
+ case MOUTH -> DataDrivenBondageItem.createStack(
+ new ResourceLocation("tiedup", "ball_gag")
+ );
+ case HANDS -> DataDrivenBondageItem.createStack(
+ new ResourceLocation("tiedup", "leather_mittens")
);
default -> ItemStack.EMPTY;
};
@@ -472,8 +471,8 @@ public class MasterPunishGoal extends Goal {
* Apply armbinder as punishment.
*/
private void applyTighten(ServerPlayer pet) {
- ItemStack armbinder = new ItemStack(
- ModItems.getBind(BindVariant.ARMBINDER)
+ ItemStack armbinder = DataDrivenBondageItem.createStack(
+ new ResourceLocation("tiedup", "armbinder")
);
// Mark as temporary
@@ -545,9 +544,9 @@ public class MasterPunishGoal extends Goal {
private void deactivateChoke() {
if (
!activeChokeCollar.isEmpty() &&
- activeChokeCollar.getItem() instanceof ItemChokeCollar chokeCollar
+ CollarHelper.isChokeCollar(activeChokeCollar)
) {
- chokeCollar.setChoking(activeChokeCollar, false);
+ CollarHelper.setChoking(activeChokeCollar, false);
}
this.activeChokeCollar = ItemStack.EMPTY;
this.chokeActiveTimer = 0;
diff --git a/src/main/java/com/tiedup/remake/entities/ai/master/MasterRandomEventGoal.java b/src/main/java/com/tiedup/remake/entities/ai/master/MasterRandomEventGoal.java
index bec353d..3ca03b7 100644
--- a/src/main/java/com/tiedup/remake/entities/ai/master/MasterRandomEventGoal.java
+++ b/src/main/java/com/tiedup/remake/entities/ai/master/MasterRandomEventGoal.java
@@ -3,11 +3,8 @@ package com.tiedup.remake.entities.ai.master;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.dialogue.DialogueBridge;
import com.tiedup.remake.entities.EntityMaster;
-import com.tiedup.remake.items.ModItems;
-import com.tiedup.remake.items.base.BindVariant;
-import com.tiedup.remake.items.base.BlindfoldVariant;
-import com.tiedup.remake.items.base.GagVariant;
-import com.tiedup.remake.items.base.MittensVariant;
+import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
+import net.minecraft.resources.ResourceLocation;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.IV2BondageEquipment;
@@ -17,7 +14,6 @@ import java.util.List;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.ai.goal.Goal;
-import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
/**
@@ -249,15 +245,18 @@ public class MasterRandomEventGoal extends Goal {
* Create a random accessory item for the given body region.
*/
private ItemStack createRandomAccessory(BodyRegionV2 region) {
- Item item = switch (region) {
- case EYES -> ModItems.getBlindfold(BlindfoldVariant.CLASSIC);
- case MOUTH -> ModItems.getGag(GagVariant.BALL_GAG);
- case HANDS -> ModItems.getMittens(MittensVariant.LEATHER);
- default -> null;
+ return switch (region) {
+ case EYES -> DataDrivenBondageItem.createStack(
+ new ResourceLocation("tiedup", "classic_blindfold")
+ );
+ case MOUTH -> DataDrivenBondageItem.createStack(
+ new ResourceLocation("tiedup", "ball_gag")
+ );
+ case HANDS -> DataDrivenBondageItem.createStack(
+ new ResourceLocation("tiedup", "leather_mittens")
+ );
+ default -> ItemStack.EMPTY;
};
-
- if (item == null) return ItemStack.EMPTY;
- return new ItemStack(item);
}
/**
@@ -304,8 +303,8 @@ public class MasterRandomEventGoal extends Goal {
// Put pet in dogbind if not already tied
PlayerBindState bindState = PlayerBindState.getInstance(pet);
if (bindState != null && !bindState.isTiedUp()) {
- ItemStack dogbind = new ItemStack(
- ModItems.getBind(BindVariant.DOGBINDER)
+ ItemStack dogbind = DataDrivenBondageItem.createStack(
+ new ResourceLocation("tiedup", "dogbinder")
);
bindState.equip(BodyRegionV2.ARMS, dogbind);
}
diff --git a/src/main/java/com/tiedup/remake/entities/ai/master/MasterTaskWatchGoal.java b/src/main/java/com/tiedup/remake/entities/ai/master/MasterTaskWatchGoal.java
index 444d754..9931feb 100644
--- a/src/main/java/com/tiedup/remake/entities/ai/master/MasterTaskWatchGoal.java
+++ b/src/main/java/com/tiedup/remake/entities/ai/master/MasterTaskWatchGoal.java
@@ -3,7 +3,7 @@ package com.tiedup.remake.entities.ai.master;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.dialogue.DialogueBridge;
import com.tiedup.remake.entities.EntityMaster;
-import com.tiedup.remake.items.ItemChokeCollar;
+import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.util.MessageDispatcher;
import com.tiedup.remake.v2.BodyRegionV2;
@@ -441,8 +441,8 @@ public class MasterTaskWatchGoal extends Goal {
if (bindState != null && bindState.hasCollar()) {
ItemStack collar = bindState.getEquipment(BodyRegionV2.NECK);
- if (collar.getItem() instanceof ItemChokeCollar chokeCollar) {
- chokeCollar.setChoking(collar, true);
+ if (CollarHelper.isChokeCollar(collar)) {
+ CollarHelper.setChoking(collar, true);
this.activeChokeCollar = collar;
this.chokeTimer = CHOKE_DURATION;
@@ -475,9 +475,9 @@ public class MasterTaskWatchGoal extends Goal {
private void deactivateChoke() {
if (
!activeChokeCollar.isEmpty() &&
- activeChokeCollar.getItem() instanceof ItemChokeCollar chokeCollar
+ CollarHelper.isChokeCollar(activeChokeCollar)
) {
- chokeCollar.setChoking(activeChokeCollar, false);
+ CollarHelper.setChoking(activeChokeCollar, false);
TiedUpMod.LOGGER.debug(
"[MasterTaskWatchGoal] {} deactivated choke",
master.getNpcName()
diff --git a/src/main/java/com/tiedup/remake/entities/ai/personality/NpcFollowCommandGoal.java b/src/main/java/com/tiedup/remake/entities/ai/personality/NpcFollowCommandGoal.java
index 33a29f3..a6557b4 100644
--- a/src/main/java/com/tiedup/remake/entities/ai/personality/NpcFollowCommandGoal.java
+++ b/src/main/java/com/tiedup/remake/entities/ai/personality/NpcFollowCommandGoal.java
@@ -1,7 +1,7 @@
package com.tiedup.remake.entities.ai.personality;
import com.tiedup.remake.entities.EntityDamsel;
-import com.tiedup.remake.items.base.ItemBind;
+import com.tiedup.remake.v2.bondage.BindModeHelper;
import com.tiedup.remake.personality.NpcCommand;
import com.tiedup.remake.personality.ToolMode;
import com.tiedup.remake.v2.BodyRegionV2;
@@ -506,7 +506,7 @@ public class NpcFollowCommandGoal extends Goal {
if (dist <= ATTACK_RANGE) {
// Try to capture using bind item
ItemStack bindItem = npc.getMainHandItem();
- if (bindItem.getItem() instanceof ItemBind) {
+ if (BindModeHelper.isBindItem(bindItem)) {
// Apply bind to target
captureTarget.equip(BodyRegionV2.ARMS, bindItem.copy());
diff --git a/src/main/java/com/tiedup/remake/entities/ai/personality/NpcGuardCommandGoal.java b/src/main/java/com/tiedup/remake/entities/ai/personality/NpcGuardCommandGoal.java
index 0f44bb5..1f1f632 100644
--- a/src/main/java/com/tiedup/remake/entities/ai/personality/NpcGuardCommandGoal.java
+++ b/src/main/java/com/tiedup/remake/entities/ai/personality/NpcGuardCommandGoal.java
@@ -1,8 +1,8 @@
package com.tiedup.remake.entities.ai.personality;
import com.tiedup.remake.entities.EntityDamsel;
-import com.tiedup.remake.items.base.ItemBind;
-import com.tiedup.remake.items.base.ItemCollar;
+import com.tiedup.remake.v2.bondage.BindModeHelper;
+import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.personality.NpcCommand;
import com.tiedup.remake.personality.PersonalityState;
import com.tiedup.remake.v2.BodyRegionV2;
@@ -426,7 +426,7 @@ public class NpcGuardCommandGoal extends Goal {
NonNullList inventory = npc.getNpcInventory();
for (int i = 0; i < inventory.size(); i++) {
ItemStack stack = inventory.get(i);
- if (stack.getItem() instanceof ItemBind) {
+ if (BindModeHelper.isBindItem(stack)) {
// Apply bind to slave
slave.equip(BodyRegionV2.ARMS, stack.copy());
stack.shrink(1);
@@ -486,8 +486,8 @@ public class NpcGuardCommandGoal extends Goal {
private UUID getCollarOwnerUUID(EntityDamsel slave) {
if (!slave.hasCollar()) return null;
ItemStack collar = slave.getEquipment(BodyRegionV2.NECK);
- if (collar.getItem() instanceof ItemCollar collarItem) {
- java.util.List owners = collarItem.getOwners(collar);
+ if (CollarHelper.isCollar(collar)) {
+ java.util.List owners = CollarHelper.getOwners(collar);
if (!owners.isEmpty()) {
return owners.get(0);
}
diff --git a/src/main/java/com/tiedup/remake/entities/damsel/components/DamselBondageManager.java b/src/main/java/com/tiedup/remake/entities/damsel/components/DamselBondageManager.java
index 351c61e..073bc11 100644
--- a/src/main/java/com/tiedup/remake/entities/damsel/components/DamselBondageManager.java
+++ b/src/main/java/com/tiedup/remake/entities/damsel/components/DamselBondageManager.java
@@ -3,7 +3,7 @@ package com.tiedup.remake.entities.damsel.components;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.AbstractTiedUpNpc;
import com.tiedup.remake.entities.BondageServiceHandler;
-import com.tiedup.remake.items.base.ItemCollar;
+import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.state.ICaptor;
import com.tiedup.remake.state.IRestrainable;
import com.tiedup.remake.state.IRestrainableEntity;
@@ -565,9 +565,9 @@ public class DamselBondageManager implements IRestrainable {
ItemStack collar = getEquipment(BodyRegionV2.NECK);
if (collar.isEmpty()) return false;
- if (!(collar.getItem() instanceof ItemCollar itemCollar)) return false;
+ if (!CollarHelper.isCollar(collar)) return false;
- UUID cellId = itemCollar.getCellId(collar);
+ UUID cellId = CollarHelper.getCellId(collar);
if (cellId == null) return false;
// Get cell position from registry
diff --git a/src/main/java/com/tiedup/remake/entities/damsel/components/DamselPersonalitySystem.java b/src/main/java/com/tiedup/remake/entities/damsel/components/DamselPersonalitySystem.java
index aeff9f9..52d1eee 100644
--- a/src/main/java/com/tiedup/remake/entities/damsel/components/DamselPersonalitySystem.java
+++ b/src/main/java/com/tiedup/remake/entities/damsel/components/DamselPersonalitySystem.java
@@ -2,7 +2,7 @@ package com.tiedup.remake.entities.damsel.components;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.dialogue.EntityDialogueManager;
-import com.tiedup.remake.items.base.ItemCollar;
+import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.personality.*;
import com.tiedup.remake.v2.BodyRegionV2;
import java.util.*;
@@ -180,8 +180,8 @@ public class DamselPersonalitySystem {
UUID masterUUID = null;
if (context.hasCollar()) {
ItemStack collar = context.getEquipment(BodyRegionV2.NECK);
- if (collar.getItem() instanceof ItemCollar collarItem) {
- List owners = collarItem.getOwners(collar);
+ if (CollarHelper.isCollar(collar)) {
+ List owners = CollarHelper.getOwners(collar);
if (!owners.isEmpty()) {
masterUUID = owners.get(0);
}
diff --git a/src/main/java/com/tiedup/remake/entities/damsel/components/NpcEquipmentManager.java b/src/main/java/com/tiedup/remake/entities/damsel/components/NpcEquipmentManager.java
index e13007d..2d4d1c5 100644
--- a/src/main/java/com/tiedup/remake/entities/damsel/components/NpcEquipmentManager.java
+++ b/src/main/java/com/tiedup/remake/entities/damsel/components/NpcEquipmentManager.java
@@ -4,7 +4,11 @@ import com.tiedup.remake.core.ModConfig;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.AbstractTiedUpNpc;
import com.tiedup.remake.items.base.ILockable;
-import com.tiedup.remake.items.base.ItemCollar;
+import com.tiedup.remake.v2.bondage.CollarHelper;
+import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
+import com.tiedup.remake.v2.bondage.component.ComponentType;
+import com.tiedup.remake.v2.bondage.component.GaggingComponent;
+import com.tiedup.remake.v2.bondage.component.BlindingComponent;
import com.tiedup.remake.state.IRestrainableEntity;
import com.tiedup.remake.util.RestraintEffectUtils;
import com.tiedup.remake.util.TiedUpSounds;
@@ -112,19 +116,15 @@ public class NpcEquipmentManager {
public boolean hasGaggingEffect() {
ItemStack gag = getCurrentGag();
if (gag.isEmpty()) return false;
- return (
- gag.getItem() instanceof
- com.tiedup.remake.items.base.IHasGaggingEffect
- );
+ if (DataDrivenBondageItem.getComponent(gag, ComponentType.GAGGING, GaggingComponent.class) != null) return true;
+ return gag.getItem() instanceof com.tiedup.remake.items.base.IHasGaggingEffect;
}
public boolean hasBlindingEffect() {
ItemStack blindfold = getCurrentBlindfold();
if (blindfold.isEmpty()) return false;
- return (
- blindfold.getItem() instanceof
- com.tiedup.remake.items.base.IHasBlindingEffect
- );
+ if (DataDrivenBondageItem.getComponent(blindfold, ComponentType.BLINDING, BlindingComponent.class) != null) return true;
+ return blindfold.getItem() instanceof com.tiedup.remake.items.base.IHasBlindingEffect;
}
public boolean hasKnives() {
@@ -768,8 +768,8 @@ public class NpcEquipmentManager {
public boolean isCollarOwner(Player player) {
if (!hasCollar()) return true;
ItemStack collar = getCurrentCollar();
- if (!(collar.getItem() instanceof ItemCollar collarItem)) return true;
- return collarItem.isOwner(collar, player);
+ if (!CollarHelper.isCollar(collar)) return true;
+ return CollarHelper.isOwner(collar, player);
}
// COERCION
diff --git a/src/main/java/com/tiedup/remake/entities/kidnapper/components/KidnapperCaptiveManager.java b/src/main/java/com/tiedup/remake/entities/kidnapper/components/KidnapperCaptiveManager.java
index 0143e9e..925e247 100644
--- a/src/main/java/com/tiedup/remake/entities/kidnapper/components/KidnapperCaptiveManager.java
+++ b/src/main/java/com/tiedup/remake/entities/kidnapper/components/KidnapperCaptiveManager.java
@@ -6,7 +6,7 @@ import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.dialogue.EntityDialogueManager;
import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.entities.ai.kidnapper.KidnapperState;
-import com.tiedup.remake.items.base.ItemCollar;
+import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.prison.PrisonerManager;
import com.tiedup.remake.prison.PrisonerRecord;
import com.tiedup.remake.prison.PrisonerState;
@@ -189,9 +189,9 @@ public class KidnapperCaptiveManager {
// If target has collar, verify we own it
if (target.hasCollar()) {
ItemStack collar = target.getEquipment(BodyRegionV2.NECK);
- if (collar.getItem() instanceof ItemCollar collarItem) {
+ if (CollarHelper.isCollar(collar)) {
// Check if THIS kidnapper is owner
- if (!collarItem.getOwners(collar).contains(host.getUUID())) {
+ if (!CollarHelper.getOwners(collar).contains(host.getUUID())) {
TiedUpMod.LOGGER.debug(
"[KidnapperCaptiveManager] {} can't capture {} - collar owned by someone else",
host.getNpcName(),
diff --git a/src/main/java/com/tiedup/remake/entities/kidnapper/components/KidnapperTargetSelector.java b/src/main/java/com/tiedup/remake/entities/kidnapper/components/KidnapperTargetSelector.java
index 4f9f659..2d60782 100644
--- a/src/main/java/com/tiedup/remake/entities/kidnapper/components/KidnapperTargetSelector.java
+++ b/src/main/java/com/tiedup/remake/entities/kidnapper/components/KidnapperTargetSelector.java
@@ -7,7 +7,7 @@ import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.entities.EntityKidnapperElite;
import com.tiedup.remake.entities.ai.kidnapper.KidnapperState;
import com.tiedup.remake.items.ModItems;
-import com.tiedup.remake.items.base.ItemCollar;
+import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.prison.PrisonerManager;
import com.tiedup.remake.prison.PrisonerRecord;
import com.tiedup.remake.state.IBondageState;
@@ -206,8 +206,8 @@ public class KidnapperTargetSelector {
// Self-collared entities (exploit) are treated as uncolllared — kidnappers ignore self-collars
if (state.hasCollar()) {
ItemStack collar = state.getEquipment(BodyRegionV2.NECK);
- if (collar.getItem() instanceof ItemCollar collarItem) {
- java.util.List owners = collarItem.getOwners(collar);
+ if (CollarHelper.isCollar(collar)) {
+ java.util.List owners = CollarHelper.getOwners(collar);
// Filter out self-collar (owner == wearer = exploit)
java.util.List realOwners = owners
.stream()
@@ -312,8 +312,8 @@ public class KidnapperTargetSelector {
// Other kidnappers' collars are fair game — kidnapper can steal from kidnapper
if (state != null && state.hasCollar()) {
ItemStack collar = state.getEquipment(BodyRegionV2.NECK);
- if (collar.getItem() instanceof ItemCollar collarItem) {
- java.util.List owners = collarItem.getOwners(collar);
+ if (CollarHelper.isCollar(collar)) {
+ java.util.List owners = CollarHelper.getOwners(collar);
if (!owners.isEmpty() && !owners.contains(host.getUUID())) {
// Check if any owner is a DIFFERENT player (not self-collared, not a kidnapper)
if (host.level() instanceof ServerLevel sl) {
diff --git a/src/main/java/com/tiedup/remake/entities/master/components/MasterPetManager.java b/src/main/java/com/tiedup/remake/entities/master/components/MasterPetManager.java
index 61d4541..ddd5b01 100644
--- a/src/main/java/com/tiedup/remake/entities/master/components/MasterPetManager.java
+++ b/src/main/java/com/tiedup/remake/entities/master/components/MasterPetManager.java
@@ -4,7 +4,7 @@ import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.EntityMaster;
import com.tiedup.remake.entities.ai.master.MasterRandomEventGoal;
import com.tiedup.remake.entities.ai.master.MasterState;
-import com.tiedup.remake.items.base.ItemCollar;
+import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.minigame.StruggleSessionManager;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.util.KidnappedHelper;
@@ -126,22 +126,27 @@ public class MasterPetManager {
// Configure for pet play BEFORE equipping
if (
chokeCollar.getItem() instanceof
- com.tiedup.remake.items.ItemChokeCollar collar
+ com.tiedup.remake.items.base.IHasResistance resistable
) {
- collar.setCurrentResistance(chokeCollar, PET_COLLAR_RESISTANCE);
- collar.setLocked(chokeCollar, true);
- collar.setLockable(chokeCollar, false); // Cannot be lockpicked
- collar.setCanBeStruggledOut(chokeCollar, true); // Can struggle, but very hard
-
- // Add this Master as owner
- collar.addOwner(chokeCollar, master.getUUID(), master.getNpcName());
-
- // Set NBT flag for pet play mode
- chokeCollar.getOrCreateTag().putBoolean("petPlayMode", true);
- chokeCollar
- .getOrCreateTag()
- .putUUID("masterUUID", master.getUUID());
+ resistable.setCurrentResistance(chokeCollar, PET_COLLAR_RESISTANCE);
+ resistable.setCanBeStruggledOut(chokeCollar, true); // Can struggle, but very hard
}
+ if (
+ chokeCollar.getItem() instanceof
+ com.tiedup.remake.items.base.ILockable lockable
+ ) {
+ lockable.setLocked(chokeCollar, true);
+ lockable.setLockable(chokeCollar, false); // Cannot be lockpicked
+ }
+
+ // Add this Master as owner
+ CollarHelper.addOwner(chokeCollar, master.getUUID(), master.getNpcName());
+
+ // Set NBT flag for pet play mode
+ CollarHelper.setPetPlayMode(chokeCollar, true);
+ chokeCollar
+ .getOrCreateTag()
+ .putUUID("masterUUID", master.getUUID());
// Replace any existing collar (force removal) with the choke collar
state.replaceEquipment(BodyRegionV2.NECK, chokeCollar, true);
@@ -186,9 +191,9 @@ public class MasterPetManager {
}
// Unlock the collar
- if (collarStack.getItem() instanceof ItemCollar collar) {
- collar.setLocked(collarStack, false);
- collar.setLockable(collarStack, true);
+ if (collarStack.getItem() instanceof com.tiedup.remake.items.base.ILockable lockable) {
+ lockable.setLocked(collarStack, false);
+ lockable.setLockable(collarStack, true);
}
}
@@ -242,8 +247,8 @@ public class MasterPetManager {
);
if (collarStack.isEmpty()) return;
- if (collarStack.getItem() instanceof ItemCollar collar) {
- collar.setCurrentResistance(collarStack, PET_COLLAR_RESISTANCE);
+ if (collarStack.getItem() instanceof com.tiedup.remake.items.base.IHasResistance resistable) {
+ resistable.setCurrentResistance(collarStack, PET_COLLAR_RESISTANCE);
TiedUpMod.LOGGER.debug(
"[MasterPetManager] Reset collar resistance for {}",
pet.getName().getString()
diff --git a/src/main/java/com/tiedup/remake/events/camp/CampNpcProtectionHandler.java b/src/main/java/com/tiedup/remake/events/camp/CampNpcProtectionHandler.java
index adee72d..9162606 100644
--- a/src/main/java/com/tiedup/remake/events/camp/CampNpcProtectionHandler.java
+++ b/src/main/java/com/tiedup/remake/events/camp/CampNpcProtectionHandler.java
@@ -4,7 +4,7 @@ import com.tiedup.remake.cells.CampLifecycleManager;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.EntityMaid;
import com.tiedup.remake.entities.EntitySlaveTrader;
-import com.tiedup.remake.items.base.ItemBind;
+import com.tiedup.remake.v2.bondage.BindModeHelper;
import java.util.UUID;
import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.Component;
@@ -40,7 +40,7 @@ public class CampNpcProtectionHandler {
// Check if player is holding restraint item
ItemStack heldItem = player.getItemInHand(event.getHand());
- if (!(heldItem.getItem() instanceof ItemBind)) return;
+ if (!BindModeHelper.isBindItem(heldItem)) return;
// Check if target is trader or maid with active camp
UUID campId = null;
diff --git a/src/main/java/com/tiedup/remake/events/captivity/PlayerEnslavementHandler.java b/src/main/java/com/tiedup/remake/events/captivity/PlayerEnslavementHandler.java
index 4078620..db1547e 100644
--- a/src/main/java/com/tiedup/remake/events/captivity/PlayerEnslavementHandler.java
+++ b/src/main/java/com/tiedup/remake/events/captivity/PlayerEnslavementHandler.java
@@ -3,8 +3,8 @@ package com.tiedup.remake.events.captivity;
import com.tiedup.remake.core.SettingsAccessor;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.LeashProxyEntity;
-import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.state.IBondageState;
+import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.state.IPlayerLeashAccess;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.util.KidnappedHelper;
@@ -146,9 +146,9 @@ public class PlayerEnslavementHandler {
ItemStack collar = slaveKidnappedState.getEquipment(
BodyRegionV2.NECK
);
- if (collar.getItem() instanceof ItemCollar collarItem) {
+ if (CollarHelper.isCollar(collar)) {
if (
- collarItem
+ CollarHelper
.getOwners(collar)
.contains(master.getUUID())
) {
diff --git a/src/main/java/com/tiedup/remake/events/combat/LaborAttackPunishmentHandler.java b/src/main/java/com/tiedup/remake/events/combat/LaborAttackPunishmentHandler.java
index 901c57d..9363db1 100644
--- a/src/main/java/com/tiedup/remake/events/combat/LaborAttackPunishmentHandler.java
+++ b/src/main/java/com/tiedup/remake/events/combat/LaborAttackPunishmentHandler.java
@@ -4,8 +4,8 @@ import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.entities.EntityMaid;
import com.tiedup.remake.entities.EntitySlaveTrader;
-import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.prison.LaborRecord;
+import com.tiedup.remake.v2.bondage.BindModeHelper;
import com.tiedup.remake.prison.PrisonerManager;
import com.tiedup.remake.prison.PrisonerRecord;
import com.tiedup.remake.prison.PrisonerState;
@@ -145,7 +145,7 @@ public class LaborAttackPunishmentHandler {
// Check if player is holding a restraint item (rope, chain, etc.)
ItemStack heldItem = serverPlayer.getItemInHand(hand);
- if (!(heldItem.getItem() instanceof ItemBind)) {
+ if (!BindModeHelper.isBindItem(heldItem)) {
return; // Not a restraint item
}
diff --git a/src/main/java/com/tiedup/remake/events/restriction/PetPlayRestrictionHandler.java b/src/main/java/com/tiedup/remake/events/restriction/PetPlayRestrictionHandler.java
index 46dd127..6d38333 100644
--- a/src/main/java/com/tiedup/remake/events/restriction/PetPlayRestrictionHandler.java
+++ b/src/main/java/com/tiedup/remake/events/restriction/PetPlayRestrictionHandler.java
@@ -2,8 +2,8 @@ package com.tiedup.remake.events.restriction;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.EntityMaster;
-import com.tiedup.remake.items.ItemChokeCollar;
import com.tiedup.remake.state.PlayerBindState;
+import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.blocks.PetBedManager;
import net.minecraft.core.BlockPos;
@@ -284,8 +284,8 @@ public class PetPlayRestrictionHandler {
if (bindState == null || !bindState.hasCollar()) return;
ItemStack collar = bindState.getEquipment(BodyRegionV2.NECK);
- if (collar.getItem() instanceof ItemChokeCollar chokeCollar) {
- if (chokeCollar.isChoking(collar)) {
+ if (CollarHelper.isChokeCollar(collar)) {
+ if (CollarHelper.isChoking(collar)) {
// Apply ChokeEffect (short duration, re-applied each active tick)
if (
!player.hasEffect(
diff --git a/src/main/java/com/tiedup/remake/events/restriction/RestraintTaskTickHandler.java b/src/main/java/com/tiedup/remake/events/restriction/RestraintTaskTickHandler.java
index b65339b..fe38d36 100644
--- a/src/main/java/com/tiedup/remake/events/restriction/RestraintTaskTickHandler.java
+++ b/src/main/java/com/tiedup/remake/events/restriction/RestraintTaskTickHandler.java
@@ -3,8 +3,8 @@ package com.tiedup.remake.events.restriction;
import com.tiedup.remake.core.SettingsAccessor;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.EntityKidnapper;
-import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.minigame.StruggleSessionManager;
+import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.network.personality.PacketSlaveBeingFreed;
import com.tiedup.remake.state.IBondageState;
@@ -642,14 +642,11 @@ public class RestraintTaskTickHandler {
if (!(slave.level() instanceof ServerLevel serverLevel)) return;
ItemStack collar = slave.getEquipment(BodyRegionV2.NECK);
- if (
- collar.isEmpty() ||
- !(collar.getItem() instanceof ItemCollar collarItem)
- ) {
+ if (collar.isEmpty() || !CollarHelper.isCollar(collar)) {
return;
}
- List owners = collarItem.getOwners(collar);
+ List owners = CollarHelper.getOwners(collar);
if (owners.isEmpty()) return;
// Create alert packet
diff --git a/src/main/java/com/tiedup/remake/events/system/ChatEventHandler.java b/src/main/java/com/tiedup/remake/events/system/ChatEventHandler.java
index 8420334..96ff8d1 100644
--- a/src/main/java/com/tiedup/remake/events/system/ChatEventHandler.java
+++ b/src/main/java/com/tiedup/remake/events/system/ChatEventHandler.java
@@ -7,6 +7,9 @@ import com.tiedup.remake.dialogue.GagTalkManager;
import com.tiedup.remake.items.base.ItemGag;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.util.GagMaterial;
+import com.tiedup.remake.v2.bondage.component.ComponentType;
+import com.tiedup.remake.v2.bondage.component.GaggingComponent;
+import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.util.TiedUpUtils;
import com.tiedup.remake.v2.BodyRegionV2;
@@ -54,12 +57,22 @@ public class ChatEventHandler {
BodyRegionV2.MOUTH
);
- if (
- !gagStack.isEmpty() &&
- gagStack.getItem() instanceof ItemGag gagItem
- ) {
+ // V2: check gagging component, V1 fallback: instanceof ItemGag
+ GaggingComponent gaggingComp = DataDrivenBondageItem.getComponent(
+ gagStack, ComponentType.GAGGING, GaggingComponent.class);
+ boolean isGagItem = gaggingComp != null
+ || gagStack.getItem() instanceof ItemGag;
+
+ if (!gagStack.isEmpty() && isGagItem) {
String originalMessage = event.getRawText();
- GagMaterial material = gagItem.getGagMaterial();
+ // V2: get material from component, V1 fallback: from ItemGag
+ GagMaterial material = null;
+ if (gaggingComp != null) {
+ material = gaggingComp.getMaterial();
+ }
+ if (material == null && gagStack.getItem() instanceof ItemGag gagItem) {
+ material = gagItem.getGagMaterial();
+ }
// 1. Process the message through our GagTalkManager V2
Component muffledMessage = GagTalkManager.processGagMessage(
@@ -83,7 +96,9 @@ public class ChatEventHandler {
.append("> ")
.append(muffledMessage);
- double range = material.getTalkRange();
+ double range = material != null
+ ? material.getTalkRange()
+ : (gaggingComp != null ? gaggingComp.getRange() : 10.0);
List nearbyPlayers =
TiedUpUtils.getPlayersAround(
diff --git a/src/main/java/com/tiedup/remake/minigame/StruggleSessionManager.java b/src/main/java/com/tiedup/remake/minigame/StruggleSessionManager.java
index c628c41..eaaae08 100644
--- a/src/main/java/com/tiedup/remake/minigame/StruggleSessionManager.java
+++ b/src/main/java/com/tiedup/remake/minigame/StruggleSessionManager.java
@@ -1,9 +1,8 @@
package com.tiedup.remake.minigame;
import com.tiedup.remake.core.TiedUpMod;
-import com.tiedup.remake.items.ItemShockCollar;
-import com.tiedup.remake.items.base.ItemBind;
-import com.tiedup.remake.items.base.ItemCollar;
+import com.tiedup.remake.items.base.IHasResistance;
+import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.minigame.ContinuousStruggleMiniGameState.TickResult;
import com.tiedup.remake.minigame.ContinuousStruggleMiniGameState.UpdateType;
import com.tiedup.remake.network.ModNetwork;
@@ -544,11 +543,11 @@ public class StruggleSessionManager {
if (collar.isEmpty()) return false;
// Only shock collars can trigger during struggle
- if (!(collar.getItem() instanceof ItemShockCollar)) return false;
+ if (!CollarHelper.canShock(collar)) return false;
// Must be locked
- if (collar.getItem() instanceof ItemCollar collarItem) {
- return collarItem.isLocked(collar);
+ if (collar.getItem() instanceof com.tiedup.remake.items.base.ILockable lockable) {
+ return lockable.isLocked(collar);
}
return false;
}
@@ -643,14 +642,12 @@ public class StruggleSessionManager {
player,
BodyRegionV2.ARMS
);
- if (
- bindStack.isEmpty() ||
- !(bindStack.getItem() instanceof ItemBind bind)
- ) {
+ if (bindStack.isEmpty()) {
return;
}
-
- bind.setCurrentResistance(bindStack, session.getCurrentResistance());
+ if (bindStack.getItem() instanceof IHasResistance resistanceItem) {
+ resistanceItem.setCurrentResistance(bindStack, session.getCurrentResistance());
+ }
}
/**
diff --git a/src/main/java/com/tiedup/remake/mixin/MixinMCAVillagerInteraction.java b/src/main/java/com/tiedup/remake/mixin/MixinMCAVillagerInteraction.java
index b25ef92..5552eab 100644
--- a/src/main/java/com/tiedup/remake/mixin/MixinMCAVillagerInteraction.java
+++ b/src/main/java/com/tiedup/remake/mixin/MixinMCAVillagerInteraction.java
@@ -4,7 +4,7 @@ import com.tiedup.remake.compat.mca.MCACompat;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.ItemKey;
import com.tiedup.remake.items.ItemMasterKey;
-import com.tiedup.remake.items.base.ItemCollar;
+import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.IV2BondageItem;
@@ -182,8 +182,8 @@ public abstract class MixinMCAVillagerInteraction {
// Can leash if player is a collar owner
if (state.hasCollar()) {
ItemStack collar = state.getEquipment(BodyRegionV2.NECK);
- if (collar.getItem() instanceof ItemCollar collarItem) {
- return collarItem.getOwners(collar).contains(player.getUUID());
+ if (CollarHelper.isCollar(collar)) {
+ return CollarHelper.isOwner(collar, player);
}
}
diff --git a/src/main/java/com/tiedup/remake/mixin/MixinMCAVillagerLeash.java b/src/main/java/com/tiedup/remake/mixin/MixinMCAVillagerLeash.java
index a6d11d6..c59c150 100644
--- a/src/main/java/com/tiedup/remake/mixin/MixinMCAVillagerLeash.java
+++ b/src/main/java/com/tiedup/remake/mixin/MixinMCAVillagerLeash.java
@@ -1,7 +1,7 @@
package com.tiedup.remake.mixin;
import com.tiedup.remake.compat.mca.MCACompat;
-import com.tiedup.remake.items.base.ItemCollar;
+import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.v2.BodyRegionV2;
import net.minecraft.world.entity.LivingEntity;
@@ -71,8 +71,8 @@ public class MixinMCAVillagerLeash {
// Can be leashed if player is collar owner
if (state.hasCollar()) {
ItemStack collar = state.getEquipment(BodyRegionV2.NECK);
- if (collar.getItem() instanceof ItemCollar collarItem) {
- if (collarItem.getOwners(collar).contains(player.getUUID())) {
+ if (CollarHelper.isCollar(collar)) {
+ if (CollarHelper.isOwner(collar, player)) {
cir.setReturnValue(true);
return;
}
diff --git a/src/main/java/com/tiedup/remake/mixin/client/MixinCamera.java b/src/main/java/com/tiedup/remake/mixin/client/MixinCamera.java
index c7a94a9..814cf5c 100644
--- a/src/main/java/com/tiedup/remake/mixin/client/MixinCamera.java
+++ b/src/main/java/com/tiedup/remake/mixin/client/MixinCamera.java
@@ -1,7 +1,7 @@
package com.tiedup.remake.mixin.client;
-import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.items.base.PoseType;
+import com.tiedup.remake.v2.bondage.PoseTypeHelper;
import com.tiedup.remake.state.HumanChairHelper;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.v2.BodyRegionV2;
@@ -57,11 +57,11 @@ public abstract class MixinCamera {
}
ItemStack bind = state.getEquipment(BodyRegionV2.ARMS);
- if (bind.isEmpty() || !(bind.getItem() instanceof ItemBind itemBind)) {
+ if (bind.isEmpty()) {
return;
}
- if (itemBind.getPoseType() != PoseType.DOG) {
+ if (PoseTypeHelper.getPoseType(bind) != PoseType.DOG) {
return;
}
diff --git a/src/main/java/com/tiedup/remake/mixin/client/MixinPlayerModel.java b/src/main/java/com/tiedup/remake/mixin/client/MixinPlayerModel.java
index 9e3153a..0a37cbc 100644
--- a/src/main/java/com/tiedup/remake/mixin/client/MixinPlayerModel.java
+++ b/src/main/java/com/tiedup/remake/mixin/client/MixinPlayerModel.java
@@ -2,8 +2,8 @@ package com.tiedup.remake.mixin.client;
import com.tiedup.remake.client.animation.render.DogPoseRenderHandler;
import com.tiedup.remake.client.animation.util.DogPoseHelper;
-import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.items.base.PoseType;
+import com.tiedup.remake.v2.bondage.PoseTypeHelper;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.v2.BodyRegionV2;
import net.minecraft.client.model.PlayerModel;
@@ -45,11 +45,11 @@ public class MixinPlayerModel {
}
ItemStack bind = state.getEquipment(BodyRegionV2.ARMS);
- if (bind.isEmpty() || !(bind.getItem() instanceof ItemBind itemBind)) {
+ if (bind.isEmpty()) {
return;
}
- if (itemBind.getPoseType() != PoseType.DOG) {
+ if (PoseTypeHelper.getPoseType(bind) != PoseType.DOG) {
return;
}
diff --git a/src/main/java/com/tiedup/remake/mixin/client/MixinVillagerEntityBaseModelMCA.java b/src/main/java/com/tiedup/remake/mixin/client/MixinVillagerEntityBaseModelMCA.java
index 044abe1..b86c14a 100644
--- a/src/main/java/com/tiedup/remake/mixin/client/MixinVillagerEntityBaseModelMCA.java
+++ b/src/main/java/com/tiedup/remake/mixin/client/MixinVillagerEntityBaseModelMCA.java
@@ -4,8 +4,9 @@ import com.tiedup.remake.client.animation.BondageAnimationManager;
import com.tiedup.remake.client.animation.StaticPoseApplier;
import com.tiedup.remake.client.animation.util.AnimationIdBuilder;
import com.tiedup.remake.compat.mca.MCACompat;
-import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.items.base.PoseType;
+import com.tiedup.remake.v2.bondage.BindModeHelper;
+import com.tiedup.remake.v2.bondage.PoseTypeHelper;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
@@ -78,11 +79,7 @@ public class MixinVillagerEntityBaseModelMCA {
// Get pose info from bind item
ItemStack bind = state.getEquipment(BodyRegionV2.ARMS);
- PoseType poseType = PoseType.STANDARD;
-
- if (bind.getItem() instanceof ItemBind itemBind) {
- poseType = itemBind.getPoseType();
- }
+ PoseType poseType = PoseTypeHelper.getPoseType(bind);
// Derive bound state from V2 regions, fallback to V1 bind mode NBT
boolean armsBound = V2EquipmentHelper.isRegionOccupied(
@@ -94,9 +91,9 @@ public class MixinVillagerEntityBaseModelMCA {
BodyRegionV2.LEGS
);
- if (!armsBound && !legsBound && bind.getItem() instanceof ItemBind) {
- armsBound = ItemBind.hasArmsBound(bind);
- legsBound = ItemBind.hasLegsBound(bind);
+ if (!armsBound && !legsBound && BindModeHelper.isBindItem(bind)) {
+ armsBound = BindModeHelper.hasArmsBound(bind);
+ legsBound = BindModeHelper.hasLegsBound(bind);
}
// MCA doesn't track struggling state - use false for now
diff --git a/src/main/java/com/tiedup/remake/network/cell/PacketAssignCellToCollar.java b/src/main/java/com/tiedup/remake/network/cell/PacketAssignCellToCollar.java
index 1094b8a..5291f01 100644
--- a/src/main/java/com/tiedup/remake/network/cell/PacketAssignCellToCollar.java
+++ b/src/main/java/com/tiedup/remake/network/cell/PacketAssignCellToCollar.java
@@ -5,8 +5,8 @@ import com.tiedup.remake.cells.CellRegistryV2;
import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.EntityDamsel;
-import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.network.PacketRateLimiter;
+import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.network.sync.SyncManager;
import com.tiedup.remake.personality.PersonalityState;
import com.tiedup.remake.state.IBondageState;
@@ -14,9 +14,9 @@ import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.v2.BodyRegionV2;
import java.util.UUID;
import java.util.function.Supplier;
+import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer;
-import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
@@ -96,7 +96,7 @@ public class PacketAssignCellToCollar {
}
ItemStack collarStack = state.getEquipment(BodyRegionV2.NECK);
- if (!(collarStack.getItem() instanceof ItemCollar collar)) {
+ if (!CollarHelper.isCollar(collarStack)) {
TiedUpMod.LOGGER.debug(
"[PacketAssignCellToCollar] Invalid collar item"
);
@@ -105,7 +105,7 @@ public class PacketAssignCellToCollar {
// Security: Verify sender owns the collar (or is admin)
if (
- !collar.isOwner(collarStack, sender) &&
+ !CollarHelper.isOwner(collarStack, sender) &&
!sender.hasPermissions(2)
) {
TiedUpMod.LOGGER.debug(
@@ -139,7 +139,15 @@ public class PacketAssignCellToCollar {
}
// Set the cell ID on the collar
- collar.setCellId(collarStack, msg.cellId);
+ if (msg.cellId != null) {
+ CollarHelper.setCellId(collarStack, msg.cellId);
+ } else {
+ // Clear cell assignment
+ CompoundTag collarTag = collarStack.getTag();
+ if (collarTag != null) {
+ collarTag.remove("cellId");
+ }
+ }
// Sync PersonalityState for damsels
if (target instanceof EntityDamsel damsel) {
diff --git a/src/main/java/com/tiedup/remake/network/personality/PacketNpcCommand.java b/src/main/java/com/tiedup/remake/network/personality/PacketNpcCommand.java
index 72adc86..9fe2ef9 100644
--- a/src/main/java/com/tiedup/remake/network/personality/PacketNpcCommand.java
+++ b/src/main/java/com/tiedup/remake/network/personality/PacketNpcCommand.java
@@ -7,8 +7,8 @@ import com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory;
import com.tiedup.remake.entities.EntityDamsel;
import com.tiedup.remake.items.ItemCommandWand;
import com.tiedup.remake.items.ModItems;
-import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.network.ModNetwork;
+import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.network.PacketRateLimiter;
import com.tiedup.remake.personality.JobExperience;
import com.tiedup.remake.personality.NpcCommand;
@@ -390,11 +390,11 @@ public class PacketNpcCommand {
}
ItemStack collar = damsel.getEquipment(BodyRegionV2.NECK);
- if (!(collar.getItem() instanceof ItemCollar collarItem)) {
+ if (!CollarHelper.isCollar(collar)) {
return false;
}
- if (!collarItem.getOwners(collar).contains(sender.getUUID())) {
+ if (!CollarHelper.isOwner(collar, sender)) {
SystemMessageManager.sendToPlayer(
sender,
SystemMessageManager.MessageCategory.ERROR,
diff --git a/src/main/java/com/tiedup/remake/network/slave/PacketMasterEquip.java b/src/main/java/com/tiedup/remake/network/slave/PacketMasterEquip.java
index daccf5f..5b656ce 100644
--- a/src/main/java/com/tiedup/remake/network/slave/PacketMasterEquip.java
+++ b/src/main/java/com/tiedup/remake/network/slave/PacketMasterEquip.java
@@ -1,7 +1,7 @@
package com.tiedup.remake.network.slave;
-import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.network.PacketRateLimiter;
+import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.v2.BodyRegionV2;
@@ -75,9 +75,9 @@ public class PacketMasterEquip {
);
if (targetState == null || !targetState.hasCollar()) return;
ItemStack collarStack = targetState.getEquipment(BodyRegionV2.NECK);
- if (collarStack.getItem() instanceof ItemCollar collar) {
+ if (CollarHelper.isCollar(collarStack)) {
if (
- !collar.isOwner(collarStack, sender) &&
+ !CollarHelper.isOwner(collarStack, sender) &&
!sender.hasPermissions(2)
) return;
}
diff --git a/src/main/java/com/tiedup/remake/network/slave/PacketSlaveAction.java b/src/main/java/com/tiedup/remake/network/slave/PacketSlaveAction.java
index 25bfbc8..652beba 100644
--- a/src/main/java/com/tiedup/remake/network/slave/PacketSlaveAction.java
+++ b/src/main/java/com/tiedup/remake/network/slave/PacketSlaveAction.java
@@ -2,10 +2,8 @@ package com.tiedup.remake.network.slave;
import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.core.TiedUpMod;
-import com.tiedup.remake.items.ItemGpsCollar;
-import com.tiedup.remake.items.ItemShockCollar;
-import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.state.IBondageState;
+import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.state.IRestrainable;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.state.PlayerCaptorManager;
@@ -128,8 +126,8 @@ public class PacketSlaveAction {
ItemStack collarStack = kidnapped.getEquipment(
BodyRegionV2.NECK
);
- if (collarStack.getItem() instanceof ItemCollar collar) {
- if (collar.isOwner(collarStack, sender)) {
+ if (CollarHelper.isCollar(collarStack)) {
+ if (CollarHelper.isOwner(collarStack, sender)) {
targetCaptive = kidnapped;
break;
}
@@ -219,10 +217,7 @@ public class PacketSlaveAction {
}
ItemStack collarStack = target.getEquipment(BodyRegionV2.NECK);
- if (
- !(collarStack.getItem() instanceof ItemCollar collar) ||
- !collar.canShock()
- ) {
+ if (!CollarHelper.canShock(collarStack)) {
SystemMessageManager.sendToPlayer(
sender,
SystemMessageManager.MessageCategory.ERROR,
@@ -232,14 +227,8 @@ public class PacketSlaveAction {
}
// Check if sender is owner of the collar or collar is public
- // FIX: Always check permissions for ANY collar that can shock, not just ItemShockCollar
- boolean isOwner = collar.isOwner(collarStack, sender);
- boolean isPublic = false;
-
- // ItemShockCollar has additional "public mode" that allows anyone to shock
- if (collarStack.getItem() instanceof ItemShockCollar shockCollar) {
- isPublic = shockCollar.isPublic(collarStack);
- }
+ boolean isOwner = CollarHelper.isOwner(collarStack, sender);
+ boolean isPublic = CollarHelper.isPublicShock(collarStack);
if (!isOwner && !isPublic) {
SystemMessageManager.sendToPlayer(
@@ -285,10 +274,7 @@ public class PacketSlaveAction {
}
ItemStack collarStack = target.getEquipment(BodyRegionV2.NECK);
- if (
- !(collarStack.getItem() instanceof ItemCollar collar) ||
- !collar.hasGPS()
- ) {
+ if (!CollarHelper.hasGPS(collarStack)) {
SystemMessageManager.sendToPlayer(
sender,
SystemMessageManager.MessageCategory.ERROR,
@@ -298,18 +284,16 @@ public class PacketSlaveAction {
}
// Check permissions
- if (collarStack.getItem() instanceof ItemGpsCollar gpsCollar) {
- boolean isOwner = collar.isOwner(collarStack, sender);
- boolean isPublic = gpsCollar.hasPublicTracking(collarStack);
+ boolean isOwner = CollarHelper.isOwner(collarStack, sender);
+ boolean isPublic = CollarHelper.hasPublicTracking(collarStack);
- if (!isOwner && !isPublic) {
- SystemMessageManager.sendToPlayer(
- sender,
- SystemMessageManager.MessageCategory.ERROR,
- "You don't have permission to track " + name + "!"
- );
- return;
- }
+ if (!isOwner && !isPublic) {
+ SystemMessageManager.sendToPlayer(
+ sender,
+ SystemMessageManager.MessageCategory.ERROR,
+ "You don't have permission to track " + name + "!"
+ );
+ return;
}
// Check same dimension
@@ -367,8 +351,8 @@ public class PacketSlaveAction {
} else {
// For collar-owned entities, just remove collar ownership
ItemStack collarStack = target.getEquipment(BodyRegionV2.NECK);
- if (collarStack.getItem() instanceof ItemCollar collar) {
- collar.removeOwner(collarStack, sender.getUUID());
+ if (CollarHelper.isCollar(collarStack)) {
+ CollarHelper.removeOwner(collarStack, sender.getUUID());
SystemMessageManager.sendToPlayer(
sender,
"Released collar control of " + name + "!",
diff --git a/src/main/java/com/tiedup/remake/personality/ToolMode.java b/src/main/java/com/tiedup/remake/personality/ToolMode.java
index 11e230a..f16e739 100644
--- a/src/main/java/com/tiedup/remake/personality/ToolMode.java
+++ b/src/main/java/com/tiedup/remake/personality/ToolMode.java
@@ -1,6 +1,6 @@
package com.tiedup.remake.personality;
-import com.tiedup.remake.items.base.ItemBind;
+import com.tiedup.remake.v2.bondage.BindModeHelper;
import net.minecraft.world.item.AxeItem;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.PickaxeItem;
@@ -48,7 +48,7 @@ public enum ToolMode {
if (item instanceof AxeItem) {
return WOODCUTTING;
}
- if (item instanceof ItemBind) {
+ if (BindModeHelper.isBindItem(mainHand)) {
return CAPTURE;
}
diff --git a/src/main/java/com/tiedup/remake/state/IBondageState.java b/src/main/java/com/tiedup/remake/state/IBondageState.java
index 6245034..73e430b 100644
--- a/src/main/java/com/tiedup/remake/state/IBondageState.java
+++ b/src/main/java/com/tiedup/remake/state/IBondageState.java
@@ -1,8 +1,8 @@
package com.tiedup.remake.state;
import com.tiedup.remake.items.base.ILockable;
-import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.v2.BodyRegionV2;
+import com.tiedup.remake.v2.bondage.BindModeHelper;
import java.util.function.Supplier;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
@@ -97,7 +97,7 @@ public interface IBondageState extends ICapturable {
if (!isTiedUp()) return false;
ItemStack bind = getEquipment(BodyRegionV2.ARMS);
if (bind.isEmpty()) return false;
- return ItemBind.hasArmsBound(bind);
+ return BindModeHelper.hasArmsBound(bind);
}
/**
@@ -111,7 +111,7 @@ public interface IBondageState extends ICapturable {
if (!isTiedUp()) return false;
ItemStack bind = getEquipment(BodyRegionV2.ARMS);
if (bind.isEmpty()) return false;
- return ItemBind.hasLegsBound(bind);
+ return BindModeHelper.hasLegsBound(bind);
}
/**
@@ -121,10 +121,10 @@ public interface IBondageState extends ICapturable {
* @return "full", "arms", or "legs"
*/
default String getBindModeId() {
- if (!isTiedUp()) return ItemBind.BIND_MODE_FULL;
+ if (!isTiedUp()) return BindModeHelper.MODE_FULL;
ItemStack bind = getEquipment(BodyRegionV2.ARMS);
- if (bind.isEmpty()) return ItemBind.BIND_MODE_FULL;
- return ItemBind.getBindModeId(bind);
+ if (bind.isEmpty()) return BindModeHelper.MODE_FULL;
+ return BindModeHelper.getBindModeId(bind);
}
/**
diff --git a/src/main/java/com/tiedup/remake/state/PlayerBindState.java b/src/main/java/com/tiedup/remake/state/PlayerBindState.java
index 77bb59f..a571d9a 100644
--- a/src/main/java/com/tiedup/remake/state/PlayerBindState.java
+++ b/src/main/java/com/tiedup/remake/state/PlayerBindState.java
@@ -3,8 +3,8 @@ package com.tiedup.remake.state;
import com.tiedup.remake.core.ModSounds;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.LeashProxyEntity;
-import com.tiedup.remake.items.base.ItemBind;
-import com.tiedup.remake.items.base.ItemCollar;
+import com.tiedup.remake.items.base.ILockable;
+import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.network.sync.SyncManager;
import com.tiedup.remake.state.components.PlayerCaptivity;
import com.tiedup.remake.state.components.PlayerClothesPermission;
@@ -737,8 +737,8 @@ public class PlayerBindState implements IRestrainable, IPlayerBindStateHost {
ItemStack collar = getCurrentCollar();
return (
!collar.isEmpty() &&
- collar.getItem() instanceof ItemCollar collarItem &&
- collarItem.isLocked(collar)
+ collar.getItem() instanceof ILockable lockable &&
+ lockable.isLocked(collar)
);
}
@@ -1211,9 +1211,9 @@ public class PlayerBindState implements IRestrainable, IPlayerBindStateHost {
ItemStack collar = getEquipment(BodyRegionV2.NECK);
if (
!collar.isEmpty() &&
- collar.getItem() instanceof ItemCollar collarItem
+ collar.getItem() instanceof ILockable lockable
) {
- collarItem.setLocked(collar, false);
+ lockable.setLocked(collar, false);
}
// Drop all items
diff --git a/src/main/java/com/tiedup/remake/state/PlayerCaptorManager.java b/src/main/java/com/tiedup/remake/state/PlayerCaptorManager.java
index d247c63..cfc970c 100644
--- a/src/main/java/com/tiedup/remake/state/PlayerCaptorManager.java
+++ b/src/main/java/com/tiedup/remake/state/PlayerCaptorManager.java
@@ -1,8 +1,8 @@
package com.tiedup.remake.state;
import com.tiedup.remake.core.TiedUpMod;
-import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.v2.BodyRegionV2;
+import com.tiedup.remake.v2.bondage.CollarHelper;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
@@ -144,9 +144,9 @@ public class PlayerCaptorManager implements ICaptor {
// Check if target has collar with this captor as owner
if (target.hasCollar()) {
ItemStack collar = target.getEquipment(BodyRegionV2.NECK);
- if (collar.getItem() instanceof ItemCollar collarItem) {
+ if (CollarHelper.isCollar(collar)) {
if (
- collarItem.getOwners(collar).contains(this.captor.getUUID())
+ CollarHelper.getOwners(collar).contains(this.captor.getUUID())
) {
return true;
}
diff --git a/src/main/java/com/tiedup/remake/state/components/PlayerDataRetrieval.java b/src/main/java/com/tiedup/remake/state/components/PlayerDataRetrieval.java
index 73d9db0..21fe85e 100644
--- a/src/main/java/com/tiedup/remake/state/components/PlayerDataRetrieval.java
+++ b/src/main/java/com/tiedup/remake/state/components/PlayerDataRetrieval.java
@@ -1,7 +1,7 @@
package com.tiedup.remake.state.components;
-import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.state.hosts.IPlayerBindStateHost;
+import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
import net.minecraft.world.entity.LivingEntity;
@@ -87,12 +87,9 @@ public class PlayerDataRetrieval {
Player player = host.getPlayer();
ItemStack collar = getCurrentCollar();
- if (
- !collar.isEmpty() &&
- collar.getItem() instanceof ItemCollar collarItem
- ) {
+ if (!collar.isEmpty() && CollarHelper.isCollar(collar)) {
// Try to get nickname from collar NBT
- String nickname = collarItem.getNickname(collar);
+ String nickname = CollarHelper.getNickname(collar);
if (nickname != null && !nickname.isEmpty()) {
return nickname;
}
@@ -109,8 +106,8 @@ public class PlayerDataRetrieval {
ItemStack collar = getCurrentCollar();
if (collar.isEmpty()) return false;
- if (collar.getItem() instanceof ItemCollar collarItem) {
- String nickname = collarItem.getNickname(collar);
+ if (CollarHelper.isCollar(collar)) {
+ String nickname = CollarHelper.getNickname(collar);
return nickname != null && !nickname.isEmpty();
}
return false;
diff --git a/src/main/java/com/tiedup/remake/state/components/PlayerEquipment.java b/src/main/java/com/tiedup/remake/state/components/PlayerEquipment.java
index 8294d3a..b19dae4 100644
--- a/src/main/java/com/tiedup/remake/state/components/PlayerEquipment.java
+++ b/src/main/java/com/tiedup/remake/state/components/PlayerEquipment.java
@@ -1,8 +1,6 @@
package com.tiedup.remake.state.components;
import com.tiedup.remake.items.base.ILockable;
-import com.tiedup.remake.items.base.ItemBind;
-import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.state.hosts.IPlayerBindStateHost;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.IV2BondageEquipment;
@@ -154,9 +152,9 @@ public class PlayerEquipment {
);
if (collar.isEmpty()) return ItemStack.EMPTY;
- if (collar.getItem() instanceof ItemCollar collarItem) {
- if (!force && collarItem.isLocked(collar)) return ItemStack.EMPTY;
- collarItem.setLocked(collar, false);
+ if (collar.getItem() instanceof ILockable lockable) {
+ if (!force && lockable.isLocked(collar)) return ItemStack.EMPTY;
+ lockable.setLocked(collar, false);
}
return V2EquipmentHelper.unequipFromRegion(player, BodyRegionV2.NECK);
}
diff --git a/src/main/java/com/tiedup/remake/state/components/PlayerLifecycle.java b/src/main/java/com/tiedup/remake/state/components/PlayerLifecycle.java
index 55d1cba..180d822 100644
--- a/src/main/java/com/tiedup/remake/state/components/PlayerLifecycle.java
+++ b/src/main/java/com/tiedup/remake/state/components/PlayerLifecycle.java
@@ -3,8 +3,8 @@ package com.tiedup.remake.state.components;
import com.tiedup.remake.cells.CampOwnership;
import com.tiedup.remake.cells.CellRegistryV2;
import com.tiedup.remake.core.TiedUpMod;
-import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.state.hosts.IPlayerBindStateHost;
+import com.tiedup.remake.v2.bondage.BindModeHelper;
import com.tiedup.remake.v2.BodyRegionV2;
import java.util.UUID;
import net.minecraft.server.level.ServerLevel;
@@ -148,7 +148,7 @@ public class PlayerLifecycle {
*/
private boolean hasLegsBound(ItemStack bind) {
if (bind.isEmpty()) return false;
- if (!(bind.getItem() instanceof ItemBind)) return false;
- return ItemBind.hasLegsBound(bind);
+ if (!BindModeHelper.isBindItem(bind)) return false;
+ return BindModeHelper.hasLegsBound(bind);
}
}
diff --git a/src/main/java/com/tiedup/remake/util/BondageItemLoaderUtility.java b/src/main/java/com/tiedup/remake/util/BondageItemLoaderUtility.java
index b6c8d8f..b25395b 100644
--- a/src/main/java/com/tiedup/remake/util/BondageItemLoaderUtility.java
+++ b/src/main/java/com/tiedup/remake/util/BondageItemLoaderUtility.java
@@ -1,7 +1,19 @@
package com.tiedup.remake.util;
import com.tiedup.remake.blocks.entity.IBondageItemHolder;
-import com.tiedup.remake.items.base.*;
+import com.tiedup.remake.items.base.ItemBind;
+import com.tiedup.remake.items.base.ItemBlindfold;
+import com.tiedup.remake.items.base.ItemCollar;
+import com.tiedup.remake.items.base.ItemEarplugs;
+import com.tiedup.remake.items.base.ItemGag;
+import com.tiedup.remake.v2.BodyRegionV2;
+import com.tiedup.remake.v2.bondage.BindModeHelper;
+import com.tiedup.remake.v2.bondage.CollarHelper;
+import com.tiedup.remake.v2.bondage.component.ComponentType;
+import com.tiedup.remake.v2.bondage.component.GaggingComponent;
+import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
+import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemDefinition;
+import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemRegistry;
import java.util.List;
import net.minecraft.ChatFormatting;
import net.minecraft.nbt.CompoundTag;
@@ -41,36 +53,32 @@ public final class BondageItemLoaderUtility {
ItemStack stack,
Player player
) {
- if (stack.getItem() instanceof ItemBind && holder.getBind().isEmpty()) {
+ if ((stack.getItem() instanceof ItemBind || BindModeHelper.isBindItem(stack)) && holder.getBind().isEmpty()) {
holder.setBind(stack.copyWithCount(1));
if (!player.isCreative()) stack.shrink(1);
return true;
}
- if (stack.getItem() instanceof ItemGag && holder.getGag().isEmpty()) {
+ if ((stack.getItem() instanceof ItemGag
+ || DataDrivenBondageItem.getComponent(stack, ComponentType.GAGGING, GaggingComponent.class) != null)
+ && holder.getGag().isEmpty()) {
holder.setGag(stack.copyWithCount(1));
if (!player.isCreative()) stack.shrink(1);
return true;
}
- if (
- stack.getItem() instanceof ItemBlindfold &&
- holder.getBlindfold().isEmpty()
- ) {
+ if ((stack.getItem() instanceof ItemBlindfold || isDataDrivenForRegion(stack, BodyRegionV2.EYES))
+ && holder.getBlindfold().isEmpty()) {
holder.setBlindfold(stack.copyWithCount(1));
if (!player.isCreative()) stack.shrink(1);
return true;
}
- if (
- stack.getItem() instanceof ItemEarplugs &&
- holder.getEarplugs().isEmpty()
- ) {
+ if ((stack.getItem() instanceof ItemEarplugs || isDataDrivenForRegion(stack, BodyRegionV2.EARS))
+ && holder.getEarplugs().isEmpty()) {
holder.setEarplugs(stack.copyWithCount(1));
if (!player.isCreative()) stack.shrink(1);
return true;
}
- if (
- stack.getItem() instanceof ItemCollar &&
- holder.getCollar().isEmpty()
- ) {
+ if ((stack.getItem() instanceof ItemCollar || CollarHelper.isCollar(stack))
+ && holder.getCollar().isEmpty()) {
holder.setCollar(stack.copyWithCount(1));
if (!player.isCreative()) stack.shrink(1);
return true;
@@ -87,13 +95,28 @@ public final class BondageItemLoaderUtility {
* @return true if the item can be loaded into a bondage item holder
*/
public static boolean isLoadableBondageItem(ItemStack stack) {
- return (
- (stack.getItem() instanceof ItemBind) ||
- (stack.getItem() instanceof ItemGag) ||
- (stack.getItem() instanceof ItemBlindfold) ||
- (stack.getItem() instanceof ItemEarplugs) ||
- (stack.getItem() instanceof ItemCollar)
- );
+ if (stack.isEmpty()) return false;
+ // V1 item types
+ if (stack.getItem() instanceof ItemBind
+ || stack.getItem() instanceof ItemGag
+ || stack.getItem() instanceof ItemBlindfold
+ || stack.getItem() instanceof ItemEarplugs
+ || stack.getItem() instanceof ItemCollar) {
+ return true;
+ }
+ // V2 data-driven items: bind, gag, blindfold, earplugs, collar
+ if (BindModeHelper.isBindItem(stack)) return true;
+ if (DataDrivenBondageItem.getComponent(stack, ComponentType.GAGGING, GaggingComponent.class) != null) return true;
+ if (isDataDrivenForRegion(stack, BodyRegionV2.EYES)) return true;
+ if (isDataDrivenForRegion(stack, BodyRegionV2.EARS)) return true;
+ if (CollarHelper.isCollar(stack)) return true;
+ return false;
+ }
+
+ /** Check if a stack is a data-driven item occupying the given body region. */
+ private static boolean isDataDrivenForRegion(ItemStack stack, BodyRegionV2 region) {
+ DataDrivenItemDefinition def = DataDrivenItemRegistry.get(stack);
+ return def != null && def.occupiedRegions().contains(region);
}
/**
diff --git a/src/main/java/com/tiedup/remake/util/RestraintApplicator.java b/src/main/java/com/tiedup/remake/util/RestraintApplicator.java
index 28ed1a0..b9f8493 100644
--- a/src/main/java/com/tiedup/remake/util/RestraintApplicator.java
+++ b/src/main/java/com/tiedup/remake/util/RestraintApplicator.java
@@ -2,9 +2,8 @@ package com.tiedup.remake.util;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.base.IHasResistance;
-import com.tiedup.remake.items.base.ItemBind;
-import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.state.IBondageState;
+import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.v2.BodyRegionV2;
import java.util.UUID;
import net.minecraft.ChatFormatting;
@@ -60,7 +59,7 @@ public final class RestraintApplicator {
}
// Tighten existing bind - reset resistance to max
- if (currentBind.getItem() instanceof ItemBind bindItem) {
+ if (currentBind.getItem() instanceof IHasResistance bindItem) {
int maxResistance = bindItem.getBaseResistance(target);
state.setCurrentBindResistance(maxResistance);
return true;
@@ -286,11 +285,8 @@ public final class RestraintApplicator {
ItemStack collarCopy = collar.copy();
// Add owner if provided
- if (
- ownerUUID != null &&
- collarCopy.getItem() instanceof ItemCollar collarItem
- ) {
- collarItem.addOwner(
+ if (ownerUUID != null && CollarHelper.isCollar(collarCopy)) {
+ CollarHelper.addOwner(
collarCopy,
ownerUUID,
ownerName != null ? ownerName : "Unknown"
diff --git a/src/main/java/com/tiedup/remake/v2/bondage/movement/MovementStyleResolver.java b/src/main/java/com/tiedup/remake/v2/bondage/movement/MovementStyleResolver.java
index d58482e..1c1e2f4 100644
--- a/src/main/java/com/tiedup/remake/v2/bondage/movement/MovementStyleResolver.java
+++ b/src/main/java/com/tiedup/remake/v2/bondage/movement/MovementStyleResolver.java
@@ -1,7 +1,8 @@
package com.tiedup.remake.v2.bondage.movement;
-import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.items.base.PoseType;
+import com.tiedup.remake.v2.bondage.BindModeHelper;
+import com.tiedup.remake.v2.bondage.PoseTypeHelper;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemDefinition;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemRegistry;
@@ -19,7 +20,7 @@ import org.jetbrains.annotations.Nullable;
* default speed/jump values. Modifiers from lower-severity items are ignored.
*
* V1 Compatibility (H6 fix)
- * V1 items ({@link ItemBind}) stored in V2 capability
+ *
V1 items (ItemBind) stored in V2 capability
* do not have data-driven definitions. This resolver provides a fallback that
* maps V1 bind mode + pose type to a {@link MovementStyle} with speed values matching
* the original V1 behavior, preventing double stacking between the legacy
@@ -122,7 +123,7 @@ public final class MovementStyleResolver {
// ==================== V1 Fallback ====================
/**
- * Attempt to derive a movement style from a V1 {@link ItemBind} item.
+ * Attempt to derive a movement style from a V1 bind item.
*
*
Only items with legs bound produce a movement style. The mapping preserves
* the original V1 speed values:
@@ -137,15 +138,15 @@ public final class MovementStyleResolver {
*/
@Nullable
private static V1Fallback resolveV1Fallback(ItemStack stack) {
- if (!(stack.getItem() instanceof ItemBind bindItem)) {
+ if (!BindModeHelper.isBindItem(stack)) {
return null;
}
- if (!ItemBind.hasLegsBound(stack)) {
+ if (!BindModeHelper.hasLegsBound(stack)) {
return null;
}
- PoseType poseType = bindItem.getPoseType();
+ PoseType poseType = PoseTypeHelper.getPoseType(stack);
return switch (poseType) {
case WRAP, LATEX_SACK -> new V1Fallback(
diff --git a/src/main/java/com/tiedup/remake/v2/furniture/network/PacketFurnitureForcemount.java b/src/main/java/com/tiedup/remake/v2/furniture/network/PacketFurnitureForcemount.java
index 39eef2f..1a09c32 100644
--- a/src/main/java/com/tiedup/remake/v2/furniture/network/PacketFurnitureForcemount.java
+++ b/src/main/java/com/tiedup/remake/v2/furniture/network/PacketFurnitureForcemount.java
@@ -1,8 +1,8 @@
package com.tiedup.remake.v2.furniture.network;
import com.tiedup.remake.core.TiedUpMod;
-import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.network.PacketRateLimiter;
+import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.v2.BodyRegionV2;
@@ -28,7 +28,7 @@ import net.minecraftforge.network.NetworkEvent;
* Client-to-server packet: master forces a captive onto a furniture seat.
*
* The sender must own the captive's collar (verified via
- * {@link ItemCollar#isOwner(ItemStack, net.minecraft.world.entity.player.Player)}),
+ * {@link CollarHelper#isOwner(ItemStack, net.minecraft.world.entity.player.Player)}),
* the captive must be alive and within 5 blocks of both sender and furniture,
* and the furniture must have an available seat.
*
@@ -132,10 +132,7 @@ public class PacketFurnitureForcemount {
}
ItemStack collarStack = captiveState.getEquipment(BodyRegionV2.NECK);
- if (
- collarStack.isEmpty() ||
- !(collarStack.getItem() instanceof ItemCollar collar)
- ) {
+ if (!CollarHelper.isCollar(collarStack)) {
TiedUpMod.LOGGER.debug(
"[PacketFurnitureForcemount] Invalid collar item on captive"
);
@@ -143,7 +140,7 @@ public class PacketFurnitureForcemount {
}
// Collar must be owned by sender (or sender has admin permission)
- if (!collar.isOwner(collarStack, sender) && !sender.hasPermissions(2)) {
+ if (!CollarHelper.isOwner(collarStack, sender) && !sender.hasPermissions(2)) {
TiedUpMod.LOGGER.debug(
"[PacketFurnitureForcemount] {} is not the collar owner of {}",
sender.getName().getString(),
diff --git a/src/main/java/com/tiedup/remake/worldgen/HangingCagePiece.java b/src/main/java/com/tiedup/remake/worldgen/HangingCagePiece.java
index d5c333b..f09e2ad 100644
--- a/src/main/java/com/tiedup/remake/worldgen/HangingCagePiece.java
+++ b/src/main/java/com/tiedup/remake/worldgen/HangingCagePiece.java
@@ -3,13 +3,15 @@ package com.tiedup.remake.worldgen;
import com.tiedup.remake.blocks.ModBlocks;
import com.tiedup.remake.blocks.entity.TrappedChestBlockEntity;
import com.tiedup.remake.core.TiedUpMod;
-import com.tiedup.remake.items.ModItems;
-import com.tiedup.remake.items.base.BindVariant;
-import com.tiedup.remake.items.base.BlindfoldVariant;
-import com.tiedup.remake.items.base.GagVariant;
import com.tiedup.remake.v2.V2Blocks;
+import com.tiedup.remake.v2.BodyRegionV2;
+import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
+import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemDefinition;
+import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemRegistry;
import com.tiedup.remake.v2.blocks.PetCageBlock;
import com.tiedup.remake.v2.blocks.PetCagePartBlock;
+import java.util.List;
+import java.util.stream.Collectors;
import javax.annotation.Nullable;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
@@ -540,34 +542,35 @@ public class HangingCagePiece extends StructurePiece {
BlockEntity be = level.getBlockEntity(chestPos);
if (be instanceof TrappedChestBlockEntity trappedChest) {
- // Random bind
- BindVariant[] bindVariants = BindVariant.values();
- BindVariant chosenBind = bindVariants[random.nextInt(
- bindVariants.length
- )];
- ItemStack bindStack = new ItemStack(ModItems.getBind(chosenBind));
- trappedChest.setBind(bindStack);
-
- // Random gag (50% chance)
- if (random.nextFloat() < 0.50f) {
- GagVariant[] gagVariants = GagVariant.values();
- GagVariant chosenGag = gagVariants[random.nextInt(
- gagVariants.length
- )];
- ItemStack gagStack = new ItemStack(ModItems.getGag(chosenGag));
- trappedChest.setGag(gagStack);
+ // Random bind from data-driven ARMS items
+ List binds = DataDrivenItemRegistry.getAll().stream()
+ .filter(d -> d.occupiedRegions().contains(BodyRegionV2.ARMS))
+ .collect(Collectors.toList());
+ if (!binds.isEmpty()) {
+ DataDrivenItemDefinition chosenBind = binds.get(random.nextInt(binds.size()));
+ trappedChest.setBind(DataDrivenBondageItem.createStack(chosenBind.id()));
}
- // Random blindfold (30% chance)
+ // Random gag from data-driven MOUTH items (50% chance)
+ if (random.nextFloat() < 0.50f) {
+ List gags = DataDrivenItemRegistry.getAll().stream()
+ .filter(d -> d.occupiedRegions().contains(BodyRegionV2.MOUTH))
+ .collect(Collectors.toList());
+ if (!gags.isEmpty()) {
+ DataDrivenItemDefinition chosenGag = gags.get(random.nextInt(gags.size()));
+ trappedChest.setGag(DataDrivenBondageItem.createStack(chosenGag.id()));
+ }
+ }
+
+ // Random blindfold from data-driven EYES items (30% chance)
if (random.nextFloat() < 0.30f) {
- BlindfoldVariant[] bfVariants = BlindfoldVariant.values();
- BlindfoldVariant chosenBf = bfVariants[random.nextInt(
- bfVariants.length
- )];
- ItemStack bfStack = new ItemStack(
- ModItems.getBlindfold(chosenBf)
- );
- trappedChest.setBlindfold(bfStack);
+ List blindfolds = DataDrivenItemRegistry.getAll().stream()
+ .filter(d -> d.occupiedRegions().contains(BodyRegionV2.EYES))
+ .collect(Collectors.toList());
+ if (!blindfolds.isEmpty()) {
+ DataDrivenItemDefinition chosenBf = blindfolds.get(random.nextInt(blindfolds.size()));
+ trappedChest.setBlindfold(DataDrivenBondageItem.createStack(chosenBf.id()));
+ }
}
}
}
@@ -629,22 +632,26 @@ public class HangingCagePiece extends StructurePiece {
entityTag.putUUID("UUID", java.util.UUID.randomUUID());
// Random bind item — the damsel spawns already restrained
- BindVariant[] variants = BindVariant.values();
- BindVariant chosenBind = variants[random.nextInt(variants.length)];
- ItemStack bindStack = new ItemStack(ModItems.getBind(chosenBind));
- bindStack.getOrCreateTag().putString("bindMode", "full");
- entityTag.put("Bind", bindStack.save(new CompoundTag()));
+ List bindDefs = DataDrivenItemRegistry.getAll().stream()
+ .filter(d -> d.occupiedRegions().contains(BodyRegionV2.ARMS))
+ .collect(Collectors.toList());
+ if (!bindDefs.isEmpty()) {
+ DataDrivenItemDefinition chosenBind = bindDefs.get(random.nextInt(bindDefs.size()));
+ ItemStack bindStack = DataDrivenBondageItem.createStack(chosenBind.id());
+ bindStack.getOrCreateTag().putString("bindMode", "full");
+ entityTag.put("Bind", bindStack.save(new CompoundTag()));
- // Add directly to chunk's pending entity list
- ChunkAccess chunk = level.getChunk(masterPos);
- if (chunk instanceof ProtoChunk protoChunk) {
- protoChunk.addEntity(entityTag);
- TiedUpMod.LOGGER.info(
- "[HangingCage] Scheduled {} damsel with {} at {}",
- shiny ? "shiny" : "regular",
- chosenBind.getRegistryName(),
- masterPos.toShortString()
- );
+ // Add directly to chunk's pending entity list
+ ChunkAccess chunk = level.getChunk(masterPos);
+ if (chunk instanceof ProtoChunk protoChunk) {
+ protoChunk.addEntity(entityTag);
+ TiedUpMod.LOGGER.info(
+ "[HangingCage] Scheduled {} damsel with {} at {}",
+ shiny ? "shiny" : "regular",
+ chosenBind.id(),
+ masterPos.toShortString()
+ );
+ }
}
}
}
--
2.49.1
From 4246d1c663ed36d5399ce836ce9b7a4a8c58231e Mon Sep 17 00:00:00 2001
From: NotEvil
Date: Wed, 15 Apr 2026 00:26:07 +0200
Subject: [PATCH 2/3] fix(D-01/C): missing sync + worldgen empty registry race
(review)
StruggleSessionManager: add V2EquipmentHelper.sync(player) after bind
resistance update to prevent data loss on server restart during struggle
HangingCagePiece: add fallback ResourceLocation arrays for worldgen when
DataDrivenItemRegistry is empty (race with reload listener on initial
world creation). Registry-first with hardcoded fallbacks.
---
.../minigame/StruggleSessionManager.java | 1 +
.../remake/worldgen/HangingCagePiece.java | 85 ++++++++++---------
2 files changed, 44 insertions(+), 42 deletions(-)
diff --git a/src/main/java/com/tiedup/remake/minigame/StruggleSessionManager.java b/src/main/java/com/tiedup/remake/minigame/StruggleSessionManager.java
index eaaae08..258721f 100644
--- a/src/main/java/com/tiedup/remake/minigame/StruggleSessionManager.java
+++ b/src/main/java/com/tiedup/remake/minigame/StruggleSessionManager.java
@@ -647,6 +647,7 @@ public class StruggleSessionManager {
}
if (bindStack.getItem() instanceof IHasResistance resistanceItem) {
resistanceItem.setCurrentResistance(bindStack, session.getCurrentResistance());
+ V2EquipmentHelper.sync(player);
}
}
diff --git a/src/main/java/com/tiedup/remake/worldgen/HangingCagePiece.java b/src/main/java/com/tiedup/remake/worldgen/HangingCagePiece.java
index f09e2ad..06c8256 100644
--- a/src/main/java/com/tiedup/remake/worldgen/HangingCagePiece.java
+++ b/src/main/java/com/tiedup/remake/worldgen/HangingCagePiece.java
@@ -16,6 +16,7 @@ import javax.annotation.Nullable;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.nbt.CompoundTag;
+import net.minecraft.resources.ResourceLocation;
import net.minecraft.nbt.DoubleTag;
import net.minecraft.nbt.FloatTag;
import net.minecraft.nbt.ListTag;
@@ -543,38 +544,45 @@ public class HangingCagePiece extends StructurePiece {
BlockEntity be = level.getBlockEntity(chestPos);
if (be instanceof TrappedChestBlockEntity trappedChest) {
// Random bind from data-driven ARMS items
- List binds = DataDrivenItemRegistry.getAll().stream()
- .filter(d -> d.occupiedRegions().contains(BodyRegionV2.ARMS))
- .collect(Collectors.toList());
- if (!binds.isEmpty()) {
- DataDrivenItemDefinition chosenBind = binds.get(random.nextInt(binds.size()));
- trappedChest.setBind(DataDrivenBondageItem.createStack(chosenBind.id()));
- }
+ trappedChest.setBind(randomItemForRegion(random, BodyRegionV2.ARMS, FALLBACK_BINDS));
- // Random gag from data-driven MOUTH items (50% chance)
+ // Random gag (50% chance)
if (random.nextFloat() < 0.50f) {
- List gags = DataDrivenItemRegistry.getAll().stream()
- .filter(d -> d.occupiedRegions().contains(BodyRegionV2.MOUTH))
- .collect(Collectors.toList());
- if (!gags.isEmpty()) {
- DataDrivenItemDefinition chosenGag = gags.get(random.nextInt(gags.size()));
- trappedChest.setGag(DataDrivenBondageItem.createStack(chosenGag.id()));
- }
+ trappedChest.setGag(randomItemForRegion(random, BodyRegionV2.MOUTH, FALLBACK_GAGS));
}
- // Random blindfold from data-driven EYES items (30% chance)
+ // Random blindfold (30% chance)
if (random.nextFloat() < 0.30f) {
- List blindfolds = DataDrivenItemRegistry.getAll().stream()
- .filter(d -> d.occupiedRegions().contains(BodyRegionV2.EYES))
- .collect(Collectors.toList());
- if (!blindfolds.isEmpty()) {
- DataDrivenItemDefinition chosenBf = blindfolds.get(random.nextInt(blindfolds.size()));
- trappedChest.setBlindfold(DataDrivenBondageItem.createStack(chosenBf.id()));
- }
+ trappedChest.setBlindfold(randomItemForRegion(random, BodyRegionV2.EYES, FALLBACK_BLINDFOLDS));
}
}
}
+ // Fallback item IDs for worldgen when DataDrivenItemRegistry is empty (race with reload)
+ private static final ResourceLocation[] FALLBACK_BINDS = {
+ new ResourceLocation("tiedup", "ropes"),
+ new ResourceLocation("tiedup", "chain"),
+ new ResourceLocation("tiedup", "armbinder")
+ };
+ private static final ResourceLocation[] FALLBACK_GAGS = {
+ new ResourceLocation("tiedup", "cloth_gag"),
+ new ResourceLocation("tiedup", "ball_gag")
+ };
+ private static final ResourceLocation[] FALLBACK_BLINDFOLDS = {
+ new ResourceLocation("tiedup", "classic_blindfold")
+ };
+
+ private static ItemStack randomItemForRegion(RandomSource random, BodyRegionV2 region, ResourceLocation[] fallbacks) {
+ List defs = DataDrivenItemRegistry.getAll().stream()
+ .filter(d -> d.occupiedRegions().contains(region))
+ .collect(Collectors.toList());
+ if (!defs.isEmpty()) {
+ return DataDrivenBondageItem.createStack(defs.get(random.nextInt(defs.size())).id());
+ }
+ // Fallback for worldgen race condition (registry not loaded yet)
+ return DataDrivenBondageItem.createStack(fallbacks[random.nextInt(fallbacks.length)]);
+ }
+
static void safeSetBlock(
WorldGenLevel level,
BlockPos pos,
@@ -632,26 +640,19 @@ public class HangingCagePiece extends StructurePiece {
entityTag.putUUID("UUID", java.util.UUID.randomUUID());
// Random bind item — the damsel spawns already restrained
- List bindDefs = DataDrivenItemRegistry.getAll().stream()
- .filter(d -> d.occupiedRegions().contains(BodyRegionV2.ARMS))
- .collect(Collectors.toList());
- if (!bindDefs.isEmpty()) {
- DataDrivenItemDefinition chosenBind = bindDefs.get(random.nextInt(bindDefs.size()));
- ItemStack bindStack = DataDrivenBondageItem.createStack(chosenBind.id());
- bindStack.getOrCreateTag().putString("bindMode", "full");
- entityTag.put("Bind", bindStack.save(new CompoundTag()));
+ ItemStack bindStack = randomItemForRegion(random, BodyRegionV2.ARMS, FALLBACK_BINDS);
+ bindStack.getOrCreateTag().putString("bindMode", "full");
+ entityTag.put("Bind", bindStack.save(new CompoundTag()));
- // Add directly to chunk's pending entity list
- ChunkAccess chunk = level.getChunk(masterPos);
- if (chunk instanceof ProtoChunk protoChunk) {
- protoChunk.addEntity(entityTag);
- TiedUpMod.LOGGER.info(
- "[HangingCage] Scheduled {} damsel with {} at {}",
- shiny ? "shiny" : "regular",
- chosenBind.id(),
- masterPos.toShortString()
- );
- }
+ // Add directly to chunk's pending entity list
+ ChunkAccess chunk = level.getChunk(masterPos);
+ if (chunk instanceof ProtoChunk protoChunk) {
+ protoChunk.addEntity(entityTag);
+ TiedUpMod.LOGGER.info(
+ "[HangingCage] Scheduled {} damsel at {}",
+ shiny ? "shiny" : "regular",
+ masterPos.toShortString()
+ );
}
}
}
--
2.49.1
From 9e73434a4a89d37a01ba4bad6a5ba161a5c80826 Mon Sep 17 00:00:00 2001
From: NotEvil
Date: Wed, 15 Apr 2026 00:55:02 +0200
Subject: [PATCH 3/3] chore: remove docs from branch C (should not be tracked
here)
---
docs/AUDIT.md | 413 ---------
docs/plans/D01-branch-A-bridge-utilities.md | 284 ------
docs/plans/D01-branch-B-json-definitions.md | 128 ---
docs/plans/D01-branch-C-consumer-migration.md | 107 ---
docs/plans/D01-branch-D-v1-cleanup.md | 188 ----
docs/plans/D01-branch-E-resistance-rework.md | 155 ----
.../2026-04-13-d01-phase1-component-system.md | 869 ------------------
7 files changed, 2144 deletions(-)
delete mode 100644 docs/AUDIT.md
delete mode 100644 docs/plans/D01-branch-A-bridge-utilities.md
delete mode 100644 docs/plans/D01-branch-B-json-definitions.md
delete mode 100644 docs/plans/D01-branch-C-consumer-migration.md
delete mode 100644 docs/plans/D01-branch-D-v1-cleanup.md
delete mode 100644 docs/plans/D01-branch-E-resistance-rework.md
delete mode 100644 docs/superpowers/plans/2026-04-13-d01-phase1-component-system.md
diff --git a/docs/AUDIT.md b/docs/AUDIT.md
deleted file mode 100644
index 54a3b77..0000000
--- a/docs/AUDIT.md
+++ /dev/null
@@ -1,413 +0,0 @@
-# TiedUp! — Codebase Audit
-
-> Audit complet du mod, systeme par systeme, pour consolider la base et la coherence du code.
-
----
-
-## Objectif
-
-Passer en revue **chaque systeme** du mod en profondeur pour :
-1. **Consolider** — identifier et corriger les incohérences, le code mort, la duplication
-2. **Améliorer** — proposer des refactors ciblés là où l'architecture freine le développement
-3. **Documenter** — laisser une trace des décisions et de l'état de chaque système
-
-Ce n'est pas un rewrite. On stabilise ce qui existe.
-
----
-
-## Règles de l'audit
-
-- **Système par système**, dans l'ordre de dépendance (fondations d'abord)
-- **Deep dive** — on remonte les sources, les dépendants, les call chains via le MCP
-- **Pas de changement sans discussion** — on constate, on discute, puis on corrige
-- **Pas de sur-ingénierie** — on fixe les vrais problèmes, on ne refactor pas pour le plaisir
-- **Reindex MCP** après chaque batch de corrections significatives
-
-### Processus par système
-
-Chaque audit suit ce cycle :
-
-```
-1. EXPLORATION — Lire le code, tracer les dépendances (MCP)
-2. CONSTATS — Documenter les problèmes trouvés (dans ce fichier)
-3. VÉRIFICATION — Relire, confirmer, pas de faux positifs
-4. PROPOSITIONS — Pour chaque constat :
- → Fix direct (bug, incohérence simple)
- → Amélioration architecturale (refactor ciblé, avec justification)
-5. DISCUSSION — Valider avec l'utilisateur avant d'implémenter
-6. CORRECTION — Appliquer les changements validés
-7. REINDEX — MCP reindex après corrections
-```
-
-On ne passe au système suivant qu'une fois le cycle terminé.
-
----
-
-## Ordre d'audit
-
-| # | Système | Packages | Status |
-|---|---------|----------|--------|
-| 1 | Core + Registries | `core`, `ModItems`, `ModEntities`, `ModNetwork`, `ModGameRules` | Done |
-| 2 | State | `state`, `state/components`, `state/hosts`, `state/struggle` | Done |
-| 3 | Items (v1) + V2 Bondage | `items/**`, `v2/bondage/**` | Done |
-| 4 | Network | `network/**` | Done |
-| 5 | Client Animation + GLTF | `client/animation/**`, `client/gltf` | Done |
-| 6 | Furniture | `v2/furniture/**` | Done |
-| 7 | Entities + AI | `entities/**` | Done |
-| 8 | Events | `events/**` | Done |
-| 9 | Dialogue + Personality | `dialogue/**`, `personality` | Done |
-| 10 | Cells / Prison / Blocks | `cells`, `prison/**`, `blocks/**` | Done |
-| 11 | Compat | `compat/mca/**`, `compat/wildfire/**` | Done |
-| 12 | Util + Commands + Worldgen + Resources | `util/**`, `commands/**`, `worldgen`, resources | Done |
-
----
-
-## Échelle de sévérité
-
-| Niveau | Signification | Action |
-|--------|--------------|--------|
-| **Haute** | Bug actif, bloquant, ou dette qui empêche une feature majeure | Corriger avant de continuer |
-| **Moyenne** | Incohérence ou fragilité qui va poser problème à terme | Planifier la correction |
-| **Basse** | Code smell, naming, organisation — pas urgent | Corriger si on touche le fichier |
-| **Cosmétique** | Style, formatting, commentaires | Optionnel |
-
----
-
-## Constats
-
-### #1 — Core + Registries
-
-**Positif :**
-- DeferredRegister correct partout
-- Factory pattern propre pour les variant items
-- SettingsAccessor bridge solide (safeGet, BUG-003 fix)
-- Séparation client/server correcte
-- Data-driven reload listeners bien câblés
-
-**Problèmes :** (tous vérifiés ✓)
-
-| ID | Sévérité | Constat | Fichier(s) | Proposition |
-|----|----------|---------|------------|-------------|
-| C-01 | Haute | SystemMessageManager : 80+ messages hardcodés en anglais, zero `Component.translatable()` | `core/SystemMessageManager.java` | **Fix** : Remplacer `Component.literal()` par `Component.translatable()` avec clés dans `en_us.json`. Garder l'enum + les couleurs, changer uniquement le transport du texte. |
-| C-02 | Moyenne | 25 settings triplés entre ModConfig + ModGameRules + SettingsAccessor. Historique de bug (BUG-001 : defaults désyncés) | `core/ModConfig.java`, `core/ModGameRules.java`, `core/SettingsAccessor.java` | **Archi** : Évaluer si les GameRules sont vraiment utiles (ils dupliquent la config serveur). Si oui, centraliser les defaults dans une seule source. Si non, supprimer les GameRules doublons et garder ModConfig + SettingsAccessor. |
-| C-03 | Moyenne | ModNetwork : 74 IDs séquentiels. Pattern standard Forge mais fragile à l'insertion. `PROTOCOL_VERSION` protège partiellement. | `network/ModNetwork.java` | **Pas d'action immédiate** : pattern idiomatique Forge 1.20.1. Bumper `PROTOCOL_VERSION` à chaque ajout/suppression de packet. Documenter cette règle. |
-| C-04 | Basse | ChokeEffect importe EntityMaster (core → entities) pour check "non-lethal when master-owned" | `core/ChokeEffect.java` | **Fix** : Extraire le check via un tag NBT ou capability sur le Player, consultable sans dépendre d'EntityMaster. |
-| C-05 | Basse | 10 types d'entités NPC utilisent DamselRenderer — nom spécifique pour un usage générique | `core/TiedUpMod.java`, `client/renderer/DamselRenderer.java` | **Fix** : Renommer `DamselRenderer` → `NpcRenderer` ou `HumanoidNpcRenderer`. |
-| C-06 | Cosmétique | 47+ FQCNs dans le corps de TiedUpMod au lieu d'imports | `core/TiedUpMod.java` | **Fix** : Remplacer par des imports. Faire quand on touche le fichier. |
-| C-07 | Basse | ModConfig.ServerConfig : 127 valeurs configurables, 628 lignes, 20+ catégories dans une classe | `core/ModConfig.java` | **Archi** : Découper en sous-classes par domaine (StruggleConfig, NpcConfig, EconomyConfig, etc.) au prochain refactor config. Pas urgent. |
-
-### #2 — State
-
-**Positif :**
-- Hiérarchie d'interfaces bien conçue (ISP) : `IRestrainableEntity` → `ICapturable` → `IBondageState` → `IRestrainable` (union)
-- Décomposition en 11 components (PlayerEquipment, PlayerStateQuery, etc.) — bonne intention
-- `IBondageState` n'expose que l'API V2 region-based — l'interface publique est propre
-- `CollarRegistry` et `SocialData` : implémentations `SavedData` propres avec persistence correcte
-- `IPlayerLeashAccess` : séparation mixin clean pour le système de laisse
-- `PlayerCaptorManager` : thread-safe avec `CopyOnWriteArrayList` et `synchronized`
-
-**Problèmes :** (tous vérifiés ✓)
-
-| ID | Sévérité | Constat | Fichier(s) | Proposition |
-|----|----------|---------|------------|-------------|
-| S-01 | Moyenne | PlayerBindState : god class de façade, 1237 lignes, ~80 méthodes de pure délégation vers 11 components | `state/PlayerBindState.java` | **Archi** : Évaluer si les consommateurs peuvent utiliser les components directement via des accesseurs typés (`state.equipment().putBindOn()`) au lieu de passer par la façade. Réduirait la surface de ~80 méthodes de boilerplate. Risque : gros refactor, beaucoup de call sites. |
-| S-02 | Moyenne | 8 champs de mouvement publics (`hopCooldown`, `lastX`, etc.) directement mutés par MovementStyleManager (33 accès directs) | `state/PlayerBindState.java`, `v2/bondage/movement/MovementStyleManager.java` | **Fix** : Extraire dans un `MovementState` component avec getters/setters. MovementStyleManager opère via ce component. |
-| S-03 | Basse | API V1 slot-based (`putBindOn`, `takeGagOff`) coexiste avec V2 region-based sur la classe concrète PlayerBindState. L'interface `IBondageState` est propre (V2 only). | `state/PlayerBindState.java` | **Archi** : Marquer les méthodes V1 `@Deprecated` pour guider la migration. Les call sites (commands, etc.) devraient migrer vers `equip(BodyRegionV2)`. Pas urgent car l'interface est déjà propre. |
-| S-04 | Basse | `hasLegsBound()` lit le slot ARMS (pas LEGS) — design V1 intentionnel : un seul item "bind" couvre bras+jambes via NBT mode. Pas un bug. | `state/IBondageState.java` | **Pas d'action immédiate** : cohérent avec le système actuel. Documenter le design dans un commentaire. Deviendra un vrai problème quand des items LEGS dédiés seront ajoutés en V2. |
-| S-05 | Moyenne | Thread safety incohérente : `volatile` (3 champs), `synchronized` (5 méthodes), rien (le reste). La paire `isStruggling`/`struggleStartTick` peut être observée dans un état inconsistant. | `state/PlayerBindState.java` | **Fix** : Définir une stratégie claire. Les champs accédés cross-thread (mouvement, struggle, captor) doivent être soit volatile soit synchronized. Auditer chaque champ. |
-| S-06 | Basse | `HumanChairHelper` dans `state/` mais c'est un utilitaire pur sans lien avec le state. Utilisé par AI, animation, mixins. | `state/HumanChairHelper.java` | **Fix** : Déplacer dans `items/base/` (à côté de `PoseType` dont il dépend) ou `util/`. Faire quand on touche le fichier. |
-
-### #3 — Items (V1) + V2 Bondage
-
-**Positif :**
-- `IV2BondageItem` bien conçu : multi-region, stack-aware, pose priority, blocked regions
-- `V2EquipmentManager` : conflict resolution solide (swap single, supersede global)
-- `V2EquipmentHelper` : facade propre pour read/write/sync
-- `DataDrivenBondageItem` : singleton + NBT registry pattern intelligent pour items data-driven
-- `ILockable` : système lock/jam/key complet et cohérent
-- `IHasResistance` : résistance NBT avec migration legacy, bien documentée
-- `BodyRegionV2` enum complet (15 régions, global flag)
-- Variant enums + factory pattern (BindVariant, GagVariant, etc.) propres
-
-**Problèmes :** (tous vérifiés ✓)
-
-| ID | Sévérité | Constat | Fichier(s) | Proposition |
-|----|----------|---------|------------|-------------|
-| I-01 | ~~Haute~~ | ~~Deux hiérarchies d'interfaces parallèles~~ | | **RÉSOLU** : suppression V1 (voir Décision D-01) |
-| I-02 | ~~Haute~~ | ~~V1 items bypassent la conflict resolution V2~~ | | **RÉSOLU** : suppression V1 (voir Décision D-01) |
-| I-03 | Moyenne | `DataDrivenBondageItem.getBaseResistance()` scanne tous items équipés et retourne MAX difficulty car `IHasResistance` n'a pas de paramètre ItemStack. Workaround documenté mais approximatif — peut surestimer la résistance. | `v2/bondage/datadriven/DataDrivenBondageItem.java` | **Archi** : Ajouter `getBaseResistance(ItemStack, LivingEntity)` à `IHasResistance` avec default qui délègue à l'ancienne méthode. DataDrivenBondageItem override la version stack-aware. |
-| I-04 | ~~Basse~~ | ~~IBondageItem.getBodyRegion() single-region~~ | | **RÉSOLU** : suppression V1 (voir Décision D-01) |
-| I-05 | ~~Moyenne~~ | ~~V1 items pas @Deprecated~~ | | **RÉSOLU** : suppression V1 (voir Décision D-01) |
-
-### Décision D-01 — Suppression totale du système V1 + Composants data-driven
-
-**Décision :** Le système V1 items est supprimé entièrement. Tous les items deviennent data-driven V2. La logique complexe (shock, GPS, lock, gagging, blinding, resistance, etc.) est extraite en **composants réutilisables** déclarables dans le JSON.
-
-**Périmètre de suppression :**
-- `IBondageItem` (interface)
-- `ItemBind`, `ItemGag`, `ItemBlindfold`, `ItemCollar`, `ItemEarplugs`, `ItemMittens` (abstracts)
-- `GenericBind`, `GenericGag`, `GenericBlindfold`, `GenericEarplugs`, `GenericMittens` (concrets)
-- `BindVariant`, `GagVariant`, `BlindfoldVariant`, `EarplugsVariant`, `MittensVariant` (enums)
-- `ItemClassicCollar`, `ItemShockCollar`, `ItemShockCollarAuto`, `ItemGpsCollar`, `ItemChokeCollar` (collars)
-- `ItemHood`, `ItemMedicalGag`, `ItemBallGag3D` (combos/special)
-- Registrations V1 dans `ModItems`
-- `PlayerEquipment.equipInRegion()` → remplacé par `V2EquipmentManager.tryEquip()`
-
-**Interfaces à conserver / migrer :**
-- `ILockable` — conservé, utilisé par V2 items
-- `IHasResistance` — conservé, refactoré avec paramètre ItemStack (I-03)
-- `IKnife` — conservé (outils, pas des bondage items)
-- `IAdjustable` — à évaluer (potentiellement composant)
-- `IHasBlindingEffect`, `IHasGaggingEffect` — deviennent des composants
-
-**Système de composants envisagé :**
-
-Chaque composant est une logique serveur réutilisable qu'un item data-driven peut déclarer :
-
-```json
-{
- "type": "tiedup:bondage_item",
- "display_name": "Shock Collar",
- "model": "tiedup:models/gltf/shock_collar.glb",
- "regions": ["NECK"],
- "components": {
- "lockable": true,
- "shock": { "auto_interval": 200, "damage": 2.0 },
- "gps": { "safe_zone_radius": 50 },
- "gagging": { "comprehension": 0.2, "range": 10.0 },
- "blinding": { "overlay": "tiedup:textures/overlay/blindfold.png" },
- "resistance": { "base": 150 }
- },
- "escape_difficulty": 5,
- "pose_priority": 10
-}
-```
-
-Exemples de composants à extraire de la logique V1 existante :
-| Composant | Source V1 | Comportement |
-|-----------|-----------|-------------|
-| `lockable` | `ILockable` | Lock/unlock, padlock, key matching, jam, lock resistance |
-| `resistance` | `IHasResistance` | Struggle resistance, configurable base value |
-| `shock` | `ItemShockCollar` | Auto-shock intervals, manual shock, damage |
-| `gps` | `ItemGpsCollar` | Safe zone, zone violation detection, owner alerts |
-| `gagging` | `IHasGaggingEffect` | Muffled speech, comprehension %, range limit |
-| `blinding` | `IHasBlindingEffect` | Blindfold overlay, hardcore mode |
-| `choking` | `ItemChokeCollar` | Air drain, darkness, slowness, non-lethal master mode |
-| `adjustable` | `IAdjustable` | Tightness level, visual adjustment |
-
-**Ce refactor est le plus gros chantier identifié par l'audit.** Il fera l'objet d'un plan d'implémentation dédié après la fin de l'audit.
-
-### #4 — Network
-
-**Positif :**
-- `AbstractClientPacket` / `AbstractPlayerSyncPacket` — bon pattern de base, handle enqueue sur main thread, retry queue pour les players pas encore loaded
-- `PacketRateLimiter` — token bucket complet avec catégories (struggle, minigame, action, selfbondage, ui). Thread-safe. Bon anti-spam.
-- `SyncManager` — facade centralisée pour sync inventory/state/enslavement/struggle/clothes. Pattern `sendSync()` générique propre.
-- `NetworkEventHandler` — gère correctement login sync, start-tracking sync, furniture reconnection, et fix MC-262715 (stale riding state)
-- `PacketSlaveAction` — bonnes validations serveur : dimension check, distance check, collar ownership check, GPS permission check
-- `PacketSelfBondage` — rate limited, route correctement V2 via `handleV2SelfBondage()` avec conflict check (`isRegionOccupied` + `isRegionBlocked`)
-
-**Problèmes :**
-
-| ID | Sévérité | Constat | Fichier(s) | Proposition |
-|----|----------|---------|------------|-------------|
-| N-01 | Moyenne | `PacketSelfBondage.handle()` contient 5 branches V1 (`instanceof ItemBind/ItemGag/ItemBlindfold/ItemMittens/ItemEarplugs`) qui devront être supprimées avec D-01. La branche V2 (`instanceof IV2BondageItem`) restera seule. | `network/selfbondage/PacketSelfBondage.java` | **Fix D-01** : Supprimer les branches V1, ne garder que la route V2. Simplifie massivement le fichier. |
-| N-02 | Moyenne | 4 packets dépendent de `ItemCollar` (V1 class) : `PacketSlaveAction`, `PacketMasterEquip`, `PacketAssignCellToCollar`, `PacketNpcCommand`. La logique collar (ownership, canShock, hasGPS) est couplée à la classe Java. | `network/slave/PacketSlaveAction.java`, `network/slave/PacketMasterEquip.java`, `network/cell/PacketAssignCellToCollar.java`, `network/personality/PacketNpcCommand.java` | **Fix D-01** : Quand ItemCollar migre vers le système composants, ces packets devront checker les composants (ex: `hasComponent("shock")`) au lieu de `instanceof ItemCollar`. |
-| N-03 | Basse | `PacketSyncBindState` sync des flags d'état V1 (isTiedUp, isGagged, isBlindfolded, etc.) séparément de `PacketSyncV2Equipment` qui sync le V2 capability. Potentiellement redondant post-suppression V1 — l'état peut être dérivé du V2 equipment. | `network/sync/PacketSyncBindState.java`, `v2/bondage/network/PacketSyncV2Equipment.java` | **Archi post-D-01** : Évaluer si `PacketSyncBindState` peut être supprimé et ses flags dérivés côté client depuis V2 equipment. Réduirait le nombre de packets sync. |
-| N-04 | Basse | `SyncManager.syncAllPlayersTo()` envoie 4 packets distincts par joueur (V2Equipment, BindState, Enslavement, Struggle, + Clothes si applicable). Pour un serveur avec N joueurs, un login génère ~4N packets. | `network/sync/SyncManager.java` | **Archi** : Considérer un packet bulk `PacketSyncFullState` qui combine tout en un seul envoi. Pas urgent — 4N packets est acceptable pour les tailles de serveur visées. |
-| N-05 | Cosmétique | Pas de `MCABondageManager` dans le package network, mais `PacketSyncMCABondage` existe — la logique MCA bondage sync est split entre `network/sync/` et `compat/mca/`. | `network/sync/PacketSyncMCABondage.java`, `compat/mca/` | **Pas d'action** : Acceptable pour un module de compatibilité. |
-
-### #5 — Client Animation + GLTF
-
-**Positif :**
-- **Architecture 3 couches propre** : Context layer (pri 40) → Item layer (pri 42) → Furniture layer (pri 43). Priorités claires, bon découplage.
-- **BondageAnimationManager** : API unifiée `playAnimation/playDirect/playContext/playFurniture` pour players et NPCs. Gestion des remote players (fallback stack), pending queue pour retry, furniture grace ticks pour éviter les stuck poses.
-- **GlbAnimationResolver** : Fallback chain fidèle au ARTIST_GUIDE (FullSitStruggle → SitStruggle → FullStruggle → Struggle → FullIdle → Idle). Support variants (.1, .2) avec random selection.
-- **GltfAnimationApplier** : Multi-item composite animation propre. Cache par state key, skip si unchanged. `applyMultiItemV2Animation()` merge les bones de plusieurs items dans un seul AnimationBuilder.
-- **ContextGlbRegistry** : Hot-reload des GLB de contexte depuis resource packs. Atomic swap pour thread safety render.
-- **AnimationContextResolver** : Résolution claire de contexte (sitting → struggling → movement style → sneaking → walking → idle). Version NPC séparée.
-- **GLTF pipeline (12 fichiers)** : Zéro dépendance V1. Parser, cache, skinning engine, mesh renderer, bone mapper, pose converter — tout est V2 natif.
-
-**Problèmes :**
-
-| ID | Sévérité | Constat | Fichier(s) | Proposition |
-|----|----------|---------|------------|-------------|
-| A-01 | Moyenne | 6 fichiers animation dépendent de `ItemBind` et `PoseType` (V1) pour déterminer le type de pose (STANDARD, DOG, HUMAN_CHAIR) et le bind mode (arms/legs/full). | `tick/AnimationTickHandler.java`, `tick/NpcAnimationTickHandler.java`, `render/PlayerArmHideEventHandler.java`, `render/PetBedRenderHandler.java`, `render/DogPoseRenderHandler.java`, `util/AnimationIdBuilder.java` | **Fix D-01** : Quand les items V1 sont supprimés, la pose type et le bind mode doivent venir du système V2 (data-driven definition ou composant). `PoseType` peut être conservé comme enum mais lu depuis `DataDrivenItemDefinition` au lieu de `ItemBind.getPoseType()`. |
-| A-02 | Basse | `StaticPoseApplier` dépend de `PoseType` — applique des rotations hardcodées par pose type (V1 fallback pour quand le GLTF n'est pas disponible). | `animation/StaticPoseApplier.java` | **Évaluer D-01** : Si tous les items ont un GLB, le static pose applier devient un fallback pur. Peut être conservé comme sécurité ou supprimé. |
-| A-03 | Basse | `GltfAnimationApplier` a un toggle debug F9 hardcodé qui charge un GLB spécifique (`cuffs_prototype.glb`). | `client/gltf/GltfAnimationApplier.java` (l.~350) | **Fix** : Supprimer ou mettre derrière un flag dev. Mineur. |
-| A-04 | Cosmétique | Le fallback animation dans `BondageAnimationManager.tryFallbackAnimation()` contient des patterns V1 spécifiques (`_arms_`, `sit_dog_`, `kneel_dog_`). Post-D-01, ces patterns n'existeront plus. | `animation/BondageAnimationManager.java` | **Fix D-01** : Nettoyer les fallbacks V1 obsolètes. Le système GLB a sa propre fallback chain (GlbAnimationResolver). |
-
-### #6 — Furniture
-
-**Positif :**
-- **Architecture data-driven exemplaire** : `FurnitureDefinition` (record immuable) + `FurnitureRegistry` (volatile atomic swap) + `FurnitureParser` + `FurnitureServerReloadListener`. Exactement le même pattern que les bondage items V2 data-driven.
-- **`ISeatProvider`** : interface propre et générique — conçue pour être implémentée par des monstres aussi (ARTIST_GUIDE: "monster seat system"). Bonne anticipation.
-- **`SeatDefinition`** : record immuable avec tous les champs du guide artiste (blocked regions, lockable, locked difficulty, item difficulty bonus).
-- **`EntityFurniture`** : Entity simple (pas LivingEntity), synced via `IEntityAdditionalSpawnData`. Dimensions variables depuis la definition. Animation state machine (IDLE → OCCUPIED → LOCKING → STRUGGLE → UNLOCKING). Seat assignments persistés en NBT.
-- **`FurniturePlacerItem`** : singleton item avec NBT ID, snap-to-wall, floor-only. Même pattern que `DataDrivenBondageItem`.
-- **`FurnitureAnimationContext`** : Conversion GLB → KeyframeAnimation avec bones sélectifs (blocked regions only). S'intègre proprement avec la furniture layer (pri 43) de BondageAnimationManager.
-- **`FurnitureGltfData`** : Parsing dédié qui sépare furniture armature des Player_* seat skeletons dans un seul GLB. Fidèle à l'ARTIST_GUIDE.
-- **Packets** : Rate limited, distance checks, permission checks (collar ownership pour forcemount).
-- **Reconnection robuste** : `NetworkEventHandler.handleFurnitureReconnection()` restaure les joueurs locked dans un seat après déconnexion, avec teleport si le meuble n'existe plus.
-
-**Problèmes :**
-
-| ID | Sévérité | Constat | Fichier(s) | Proposition |
-|----|----------|---------|------------|-------------|
-| F-01 | Moyenne | `EntityFurniture` et `PacketFurnitureForcemount` dépendent de `ItemCollar` (V1) pour vérifier collar ownership avant forcemount. | `v2/furniture/EntityFurniture.java`, `v2/furniture/network/PacketFurnitureForcemount.java` | **Fix D-01** : Quand ItemCollar migre vers composants, le check ownership doit utiliser le composant `lockable` ou `collar` au lieu de `instanceof ItemCollar`. |
-| F-02 | Basse | `FurnitureAnimationContext.create()` log "V1: skeleton parsing not yet implemented" quand `seatSkeleton` est null. Si le GLB n'a pas de skeleton data parsé, l'animation silencieusement ne se joue pas. | `v2/furniture/client/FurnitureAnimationContext.java` | **Évaluer** : Vérifier que le parser GLB furniture extrait toujours le skeleton. Si oui, le fallback est juste un safety net. Sinon, c'est un bug silencieux. |
-
-**Verdict : Le système furniture est le plus propre du mod.** Zéro dette architecturale, fidèle au guide artiste, extensible (monster seats prêts). Les deux constats sont mineurs — un couplage V1 qui part avec D-01 et un fallback debug à vérifier.
-
-### #7 — Entities + AI
-
-**Hiérarchie d'héritage :**
-
-```
-PathfinderMob
- └─ AbstractTiedUpNpc (1281 lignes, ~100 méthodes) — implements IRestrainable, IAnimatedPlayer, IV2EquipmentHolder
- ├─ EntityDamsel (834 lignes) — capturable NPC, personality, dialogue, inventory
- │ ├─ EntityDamselShiny — variante rare
- │ └─ EntityLaborGuard — garde de prison
- └─ EntityKidnapper (2039 lignes, ~170 méthodes) — implements ICaptor, IDialogueSpeaker
- └─ EntityKidnapperElite
- ├─ EntityKidnapperMerchant — marchand neutre/hostile
- ├─ EntityKidnapperArcher — attaque à distance
- ├─ EntitySlaveTrader — boss de camp
- ├─ EntityMaid — servante du trader
- └─ EntityMaster (1192 lignes) — pet play system
-```
-
-**Positif :**
-- **Composant-based decomposition pour Damsel** : `DamselBondageManager`, `DamselPersonalitySystem`, `DamselInventoryManager`, `DamselAIController`, `DamselAnimationController`, `DamselAppearance`, `NpcEquipmentManager`, `NpcCaptivityManager` — 8 components avec interfaces host (`IBondageHost`, `IAIHost`, `IAnimationHost`, etc.). Bonne intention.
-- **Composant-based pour Kidnapper** : `KidnapperAggressionSystem`, `KidnapperAlertManager`, `KidnapperAppearance`, `KidnapperCaptiveManager`, `KidnapperCellManager`, `KidnapperCampManager`, `KidnapperStateManager`, `KidnapperSaleManager`, `KidnapperTargetSelector`, `KidnapperDataSerializer` — 10 components avec interfaces host. Très granulaire.
-- **AI goals bien séparés** : 80+ goals dédiés par type de NPC. Chaque goal est une classe autonome avec une seule responsabilité (KidnapperCaptureGoal, MasterDogwalkGoal, NpcFarmCommandGoal, etc.).
-- **V2 equipment intégré** : `AbstractTiedUpNpc` implémente `IV2EquipmentHolder`, utilise `V2BondageEquipment` directement. Les NPCs sont déjà sur le système V2.
-- **State machines Kidnapper** : `KidnapperState` enum avec états clairs (IDLE, HUNTING, CAPTURING, FLEEING, etc.).
-- **Master NPC complet** : pet play system avec task manager, state machine, punishment, dogwalk, furniture interaction — complexe mais fonctionnel.
-
-**Problèmes :**
-
-| ID | Sévérité | Constat | Fichier(s) | Proposition |
-|----|----------|---------|------------|-------------|
-| E-01 | Haute | **EntityKidnapper = 2039 lignes**, la plus grosse classe du mod. Malgré la décomposition en 10 components, la classe reste un god class. Elle mélange : ICaptor impl, targeting, capture equipment, sale system, job system, camp system, cell integration, alert system, NBT serialization, display name, dialogue, et des dizaines de getters/helpers. | `entities/EntityKidnapper.java` | **Archi** : Continuer la décomposition. Candidates : extraire le système de vente (`startSale`/`completeSale`/`cancelSale`/`abandonCaptive`) dans un component dédié, extraire le dialogue, extraire le ciblage. Objectif : ramener la classe sous 800 lignes. |
-| E-02 | Haute | **AbstractTiedUpNpc = 1281 lignes** avec ~100 méthodes. Même pattern que PlayerBindState (S-01) — c'est une façade de délégation vers les components, mais doit aussi implémenter IRestrainable (30+ méthodes) directement. | `entities/AbstractTiedUpNpc.java` | **Archi** : La taille vient surtout de l'implémentation de IRestrainable. Évaluer si les méthodes bondage peuvent être déléguées à `DamselBondageManager` via un pattern `default` sur IRestrainable (mais IRestrainable est une interface, pas une classe — limité). Ou accepter la taille comme coût de l'implémentation multi-interface. |
-| E-03 | Moyenne | **24 fichiers entities** dépendent de V1 item classes (`ItemBind`, `ItemCollar`, `PoseType`, `BindVariant`, etc.). C'est le package le plus impacté par D-01. | 24 fichiers (voir liste grep) | **Fix D-01** : Migration bulk. Les `instanceof ItemBind` deviennent `instanceof IV2BondageItem`, les `ItemCollar` checks deviennent des component checks. `PoseType` et `BindVariant` sont remplacés par des propriétés data-driven. |
-| E-04 | Moyenne | **Héritage profond** : EntityMaid → EntityKidnapperElite → EntityKidnapper → AbstractTiedUpNpc → PathfinderMob. 5 niveaux. EntityMaid et EntitySlaveTrader héritent de toute la logique kidnapper (capture, targeting, sale) alors qu'ils n'utilisent pas tout. | `entities/EntityMaid.java`, `entities/EntitySlaveTrader.java` | **Archi** : Envisager une refactorisation vers composition plutôt qu'héritage. La Maid n'est PAS un kidnapper — elle ne devrait pas hériter de `canCapture()`, `getCaptureBindTime()`, etc. Long terme : AbstractTiedUpNpc → EntityDamsel (passive) / EntityKidnapper (hostile), et les autres types composent leurs comportements. Pas urgent mais dette croissante. |
-| E-05 | Basse | `EntityDamsel` et `EntityKidnapper` ont des hiérarchies de host interfaces parallèles : `damsel/components/IBondageHost`, `damsel/components/IAIHost` vs `kidnapper/components/IAIHost`, `kidnapper/components/ICaptiveHost`, etc. Certaines pourraient être unifiées. | `entities/damsel/components/*.java`, `entities/kidnapper/components/*.java` | **Pas d'action immédiate** : Les interfaces host sont des contrats internes de chaque sous-arbre. Les unifier créerait un couplage horizontal. Acceptable tel quel. |
-| E-06 | Basse | `EntityMaster` (1192 lignes) contient le pet play system complet. Components `MasterPetManager`, `MasterTaskManager`, `MasterStateManager` existent mais la classe orchestre encore beaucoup de logique. | `entities/EntityMaster.java` | **Archi** : Même recommandation que E-01 — continuer la décomposition. Moins urgent car le système est plus cohérent (une seule responsabilité : pet play). |
-
-### #8 — Events
-
-**27 handlers, 5722 lignes, 8 domaines** (camp, captivity, combat, interaction, lifecycle, restriction, system).
-
-**Positif :** Bonne séparation par domaine. La plupart des handlers sont focalisés (85-200 lignes).
-
-| ID | Sévérité | Constat | Proposition |
-|----|----------|---------|-------------|
-| EV-01 | Moyenne | `RestraintTaskTickHandler` (675 lignes, 12 @SubscribeEvent) — consolide tous les ticks restraint. | Découper par type de tâche (tying, untying, force-feeding, shock checks). |
-| EV-02 | Moyenne | `BondageItemRestrictionHandler` (544 lignes, 12 @SubscribeEvent) — consolide toutes les restrictions. | Découper par type de restriction (legs, arms, gags, etc.). |
-| EV-03 | Moyenne | 7/27 handlers importent des classes V1 (`ItemBind`, `ItemCollar`, `ItemGag`, `IKnife`, `ILockable`). | **Fix D-01** : Migrer vers V2 checks (composants). |
-
-### #9 — Dialogue + Personality
-
-**31 fichiers, 7625 lignes.** Dialogue 100% data-driven (JSON par personality × speaker type). Personality enum-based (11 types) avec state machine (needs, mood, commands).
-
-**Positif :** Pipeline de chargement JSON propre (default → personality override → speaker-type). 18 catégories de dialogue. Dépendance unidirectionnelle (dialogue → personality, pas l'inverse). Seulement 3 fichiers importent du V1.
-
-| ID | Sévérité | Constat | Proposition |
-|----|----------|---------|-------------|
-| DI-01 | Moyenne | 6 god classes dans dialogue/ (EntityDialogueManager 622l, ConversationManager 564l, GagTalkManager 557l, DialogueLoader 469l, DialogueBridge 463l, DialogueManager 428l). | Acceptable pour la complexité du système. `DialogueBridge` (mapping legacy → new) peut être supprimé après D-01. |
-| DI-02 | Basse | `PersonalityState` (709 lignes) — god class conteneur d'état NPC (needs, mood, commands, jobs, home). | Continuer la décomposition si ça grossit. OK pour l'instant. |
-| DI-03 | Basse | 3 fichiers importent V1 (`GagTalkManager` → ItemGag, `ToolMode` → ItemBind, `PetRequestManager` → BindVariant). | **Fix D-01** : Migrer. |
-
-### #10 — Cells / Prison / Blocks
-
-**41 fichiers, 13329 lignes.** Système SavedData massif (CellRegistryV2, PrisonerManager, CampOwnership, ConfiscatedInventoryRegistry).
-
-**Positif :** Architecture SavedData correcte. Spatial indexing dans CellRegistryV2. Séparation services (PrisonerService, ItemService, BondageService).
-
-| ID | Sévérité | Constat | Proposition |
-|----|----------|---------|-------------|
-| CP-01 | Haute | `PrisonerService` (1058 lignes) — plus grosse classe du package, gère tout le lifecycle prisonnier. | Décomposer : labor, ransom, confiscation pourraient être des services séparés. |
-| CP-02 | Moyenne | `MarkerBlockEntity` (1146 lignes) — god class block entity, gère spawning + teleportation + cell deletion. | Extraire la logique spawning et teleportation dans des helpers. |
-| CP-03 | Moyenne | `BondageItemBlockEntity` utilise 6 imports V1 (ItemBind, ItemGag, ItemBlindfold, ItemCollar, ItemEarplugs, GenericClothes) pour valider les items dans les trap blocks. | **Fix D-01** : Remplacer par `instanceof IV2BondageItem` + component checks. |
-| CP-04 | Basse | `CellRegistryV2` (903 lignes) — gros mais justifié par les index spatiaux. | Acceptable. |
-
-### #11 — Compat (MCA + Wildfire)
-
-**24 fichiers, 6536 lignes.** MCA très couplé (21 fichiers, 5 sous-systèmes). Wildfire léger (3 fichiers, rendu only).
-
-| ID | Sévérité | Constat | Proposition |
-|----|----------|---------|-------------|
-| CO-01 | Haute | `MCAKidnappedAdapter` (907 lignes) — implémente IRestrainable complet pour les villagers MCA. God class + dépend de V1 items. | **Fix D-01** : Migrer V1 → V2. Décomposer en components comme AbstractTiedUpNpc. |
-| CO-02 | Moyenne | `WildfireDamselLayer` (988 lignes) — rendu physique très complexe. | Acceptable pour un système de rendu physique. Pas urgent. |
-| CO-03 | Basse | MCA compat utilise WeakHashMap et reflection pour détection — bon pattern de découplage. | Pas d'action. |
-
-### #12 — Util + Commands + Worldgen
-
-**55 fichiers, 15475 lignes.**
-
-| ID | Sévérité | Constat | Proposition |
-|----|----------|---------|-------------|
-| UC-01 | Haute | `BondageSubCommand` (1232 lignes) — monolithique, contient 16 sous-commandes (tie, untie, gag, collar, etc.) dans un seul fichier. | Découper : TieCommands, GagCommands, CollarCommands, etc. |
-| UC-02 | Haute | `RoomTheme` (1368 lignes) — config hardcodée de palettes de blocs pour worldgen. | **Archi** : Externaliser en data-driven (JSON). C'est exactement le type de contenu qui devrait être configurable. |
-| UC-03 | Moyenne | 7 fichiers importent V1 items (commands + RestraintApplicator + MCA adapter + HangingCagePiece). | **Fix D-01** : Migrer. |
-| UC-04 | Basse | `NPCCommand` (764 lignes) — gros mais focalisé sur le spawning/state NPC. | Acceptable, pourrait split par entity type. |
-
----
-
-## Bilan Final
-
-### Statistiques
-
-- **12/12 systèmes audités**
-- **744 classes, 233k lignes** analysées
-- **38 constats** documentés (+ 4 résolus par D-01)
-- **1 décision architecturale majeure** (D-01 : suppression V1 + composants data-driven)
-
-### Classement par santé
-
-| Rang | Système | Verdict | Problèmes |
-|------|---------|---------|-----------|
-| 1 | Furniture | Exemplaire | 2 mineurs |
-| 2 | Animation + GLTF | Excellent | Résidus V1 seulement |
-| 3 | Network | Solide | Résidus V1 seulement |
-| 4 | Dialogue + Personality | Bon | God classes acceptables |
-| 5 | Events | Bon | 2 handlers trop gros |
-| 6 | Core | Dette technique | i18n, config triple |
-| 7 | State | Croissance organique | God class, thread safety |
-| 8 | Cells / Prison | Fonctionnel mais lourd | 11 god classes |
-| 9 | Compat | Fonctionnel mais couplé | MCA adapter 907l |
-| 10 | Util / Commands / Worldgen | Fonctionnel | BondageSubCmd 1232l, RoomTheme 1368l |
-| 11 | Entities + AI | Riche mais massif | 2039l god class, héritage 5 niveaux |
-| 12 | Items V1/V2 | **Point critique** | **D-01 : suppression totale V1** |
-
-### Priorités de correction
-
-| Priorité | Chantier | Impact |
-|----------|----------|--------|
-| **P0** | **D-01 : Suppression V1 + composants data-driven** | Élimine la dette #1 du mod. Impacte ~60 fichiers. Requiert un plan dédié. |
-| **P1** | C-01 : i18n SystemMessageManager | Requis pour toute traduction du mod. |
-| **P1** | UC-02 : RoomTheme → data-driven | 1368 lignes hardcodées de config worldgen. |
-| **P2** | E-01/E-02 : Décomposition EntityKidnapper/AbstractTiedUpNpc | 2039 + 1281 lignes. Améliore la maintenabilité entities. |
-| **P2** | CP-01 : Décomposition PrisonerService | 1058 lignes. |
-| **P2** | UC-01 : Split BondageSubCommand | 1232 lignes en 1 fichier. |
-| **P3** | S-02 : Encapsuler MovementState | 8 champs publics mutés directement. |
-| **P3** | S-05 : Thread safety cohérente | 3 stratégies sans cohérence dans PlayerBindState. |
-| **P3** | C-02 : Unifier Config/GameRules | 25 settings triplés. |
-| **P4** | Renommages (C-05 DamselRenderer, C-06 FQCNs) et cleanups cosmétiques. |
-
-### D-01 Phase 1 — Suivi implémentation
-
-**Branche :** `feature/d01-component-system` (17 commits, build clean)
-
-**Review adversariale :** 3 critiques + 5 hauts trouvés et corrigés.
-
-**Problèmes notés (non bloquants, à traiter lors de la migration Phase 2) :**
-
-| ID | Constat | Action |
-|----|---------|--------|
-| SMELL-002 | `GaggingComponent` n'a aucun consommateur — `GagTalkManager` lit `GagMaterial.getComprehension()`, pas le composant. | Lors de la migration Phase 2, faire pointer `GagTalkManager` vers le composant pour les items data-driven. |
-| SMELL-003 | Duplication sémantique entre le champ top-level `lockable` (boolean) et le composant `LockableComponent`. Un item doit configurer les deux pour un lock complet. | Lors de la migration Phase 2, le champ `lockable` devrait être dérivé de la présence du composant `lockable`. |
-| NOTE-003 | `test_component_gag.json` est dans les resources de production — visible par les joueurs. | Supprimer ou déplacer avant release. OK pour le dev. |
\ No newline at end of file
diff --git a/docs/plans/D01-branch-A-bridge-utilities.md b/docs/plans/D01-branch-A-bridge-utilities.md
deleted file mode 100644
index 12dd9b1..0000000
--- a/docs/plans/D01-branch-A-bridge-utilities.md
+++ /dev/null
@@ -1,284 +0,0 @@
-# D-01 Branch A : Bridge Utilities
-
-> **Prérequis :** Phase 1 (component system) mergée dans develop.
-> **Branche :** `feature/d01-branch-a-bridge`
-> **Objectif :** Créer les utilitaires V2 qui remplacent la logique V1, SANS supprimer de code V1. À la fin de cette branche, le mod compile, les items V1 et V2 coexistent, le struggle fonctionne pour les deux, et les nouveaux helpers sont prêts pour la migration des consommateurs.
-
----
-
-## Décisions actées
-
-- **Stack size** : Stacks de 1 pour tout. Régression acceptée.
-- **Save compat** : Breaking change. Pas de migration. Mod en alpha.
-- **Résistance** : Config-driven via `resistanceId`, pas hardcodé en JSON.
-- **Comprehension/range gags** : Config-driven via `gagMaterial`, délègue à ModConfig au runtime.
-- **IHasGaggingEffect/IHasBlindingEffect** : DataDrivenBondageItem les implémente en checkant les composants.
-
----
-
-## Tâches
-
-### A1. Modifier ResistanceComponent — config-driven
-
-**Fichier :** `src/main/java/com/tiedup/remake/v2/bondage/component/ResistanceComponent.java`
-
-Actuellement stocke un `int baseResistance` hardcodé. Doit stocker un `String resistanceId` et déléguer à `SettingsAccessor.getBindResistance(resistanceId)` au runtime.
-
-- Remplacer le champ `baseResistance` par `resistanceId` (String)
-- `fromJson()` : parse `"id"` au lieu de `"base"` — `"resistance": {"id": "rope"}`
-- `getBaseResistance()` : `return SettingsAccessor.getBindResistance(resistanceId);`
-- Garder un fallback `"base"` pour backward compat avec test_component_gag.json (ou le mettre à jour)
-
-**Référence :** `SettingsAccessor.getBindResistance(String)` dans `core/SettingsAccessor.java` — normalise les clés et lit depuis ModConfig.
-
----
-
-### A2. Modifier GaggingComponent — config-driven + GagMaterial
-
-**Fichier :** `src/main/java/com/tiedup/remake/v2/bondage/component/GaggingComponent.java`
-
-Actuellement stocke `comprehension` et `range` hardcodés. Doit stocker un `String material` et déléguer à `GagMaterial`/ModConfig au runtime.
-
-- Ajouter champ `@Nullable String material`
-- `fromJson()` : parse `"material"` — `"gagging": {"material": "ball"}`
-- `getComprehension()` : si material != null → `GagMaterial.valueOf(material).getComprehension()` (lit ModConfig). Sinon → fallback au champ hardcodé (compat).
-- `getRange()` : idem via `GagMaterial.valueOf(material).getTalkRange()`
-- `getMaterial()` : expose le `GagMaterial` enum pour `GagTalkManager`
-- Garder les champs `comprehension`/`range` comme overrides optionnels (si présents dans JSON, ils prennent priorité sur le material)
-
-**Référence :** `GagMaterial` enum dans `items/base/GagVariant.java` ou `util/GagMaterial.java`.
-
----
-
-### A3. Ajouter `appendTooltip()` à IItemComponent + ComponentHolder
-
-**Fichiers :**
-- `src/main/java/com/tiedup/remake/v2/bondage/component/IItemComponent.java`
-- `src/main/java/com/tiedup/remake/v2/bondage/component/ComponentHolder.java`
-- `src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenBondageItem.java`
-
-Ajouter hook tooltip pour que chaque composant contribue des lignes :
-
-```java
-// IItemComponent
-default void appendTooltip(ItemStack stack, @Nullable Level level, List tooltip, TooltipFlag flag) {}
-
-// ComponentHolder
-public void appendTooltip(ItemStack stack, @Nullable Level level, List tooltip, TooltipFlag flag) {
- for (IItemComponent c : components.values()) c.appendTooltip(stack, level, tooltip, flag);
-}
-```
-
-Dans `DataDrivenBondageItem.appendHoverText()` : appeler `holder.appendTooltip(...)`.
-
-Implémenter `appendTooltip` dans chaque composant existant :
-- `LockableComponent` : affiche "Locked" / "Lockable"
-- `ResistanceComponent` : affiche la résistance en advanced mode (F3+H)
-- `GaggingComponent` : affiche le type de gag
-- `BlindingComponent` : rien (pas d'info utile)
-- `ShockComponent` : affiche "Shock: Manual" ou "Shock: Auto (Xs)"
-- `GpsComponent` : affiche "GPS Tracking" + zone radius
-- `ChokingComponent` : affiche "Choking Effect"
-- `AdjustableComponent` : rien (ajustement est visuel)
-
----
-
-### A4. Champ `pose_type` + PoseTypeHelper
-
-**Fichiers :**
-- `src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemDefinition.java` — ajouter `@Nullable String poseType`
-- `src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemParser.java` — parser `"pose_type"`
-- Créer `src/main/java/com/tiedup/remake/v2/bondage/PoseTypeHelper.java`
-
-```java
-public static PoseType getPoseType(ItemStack stack) {
- // V2: read from data-driven definition
- DataDrivenItemDefinition def = DataDrivenItemRegistry.get(stack);
- if (def != null && def.poseType() != null) {
- try { return PoseType.valueOf(def.poseType()); }
- catch (IllegalArgumentException e) { return PoseType.STANDARD; }
- }
- // V1 fallback: instanceof ItemBind
- if (stack.getItem() instanceof ItemBind bind) {
- return bind.getPoseType();
- }
- return PoseType.STANDARD;
-}
-```
-
-**Note mixin :** Les mixins (`MixinPlayerModel`, `MixinCamera`, etc.) appellent `itemBind.getPoseType()`. Ils devront migrer vers `PoseTypeHelper.getPoseType(stack)` en Branch C. Le helper doit être dans un package chargé tôt — `v2/bondage/` est OK car le mod est chargé avant les mixins client.
-
----
-
-### A5. BindModeHelper
-
-**Fichier :** Créer `src/main/java/com/tiedup/remake/v2/bondage/BindModeHelper.java`
-
-Méthodes statiques pures NBT (clé `"bindMode"`) :
-- `hasArmsBound(ItemStack)` → true si mode "arms" ou "full"
-- `hasLegsBound(ItemStack)` → true si mode "legs" ou "full"
-- `getBindModeId(ItemStack)` → "full", "arms", ou "legs"
-- `cycleBindModeId(ItemStack)` → full→arms→legs→full, retourne le nouveau mode
-- `getBindModeTranslationKey(ItemStack)` → clé i18n
-- `isBindItem(ItemStack)` → true si l'item a la région ARMS dans sa definition V2, OU est `instanceof ItemBind`
-
-**Référence :** `ItemBind.java` lignes 64-160 pour les méthodes statiques existantes.
-
----
-
-### A6. CollarHelper (complet)
-
-**Fichier :** Créer `src/main/java/com/tiedup/remake/v2/bondage/CollarHelper.java`
-
-Extraire TOUTES les méthodes NBT de `ItemCollar` (1407 lignes) + 5 sous-classes en méthodes statiques. Sections :
-
-**Ownership :**
-- `isOwner(ItemStack, Player)`, `isOwner(ItemStack, UUID)`
-- `getOwners(ItemStack)` → Set
-- `addOwner(ItemStack, UUID, String name)`, `removeOwner(ItemStack, UUID)`
-- `getBlacklist(ItemStack)`, `addToBlacklist(ItemStack, UUID)`, `removeFromBlacklist(ItemStack, UUID)`
-- `getWhitelist(ItemStack)`, `addToWhitelist(ItemStack, UUID)`, `removeFromWhitelist(ItemStack, UUID)`
-
-**Collar features :**
-- `isCollar(ItemStack)` → check OwnershipComponent presence OR instanceof ItemCollar
-- `getNickname(ItemStack)`, `setNickname(ItemStack, String)`
-- `isKidnappingModeEnabled(ItemStack)`, `setKidnappingModeEnabled(ItemStack, boolean)`
-- `getCellId(ItemStack)`, `setCellId(ItemStack, UUID)`
-- `shouldTieToPole(ItemStack)`, `setShouldTieToPole(ItemStack, boolean)`
-- `shouldWarnMasters(ItemStack)`, `setShouldWarnMasters(ItemStack, boolean)`
-- `isBondageServiceEnabled(ItemStack)`, `setBondageServiceEnabled(ItemStack, boolean)`
-- `getServiceSentence(ItemStack)`, `setServiceSentence(ItemStack, String)`
-
-**Shock :**
-- `canShock(ItemStack)` → check ShockComponent presence OR instanceof ItemShockCollar
-- `isPublic(ItemStack)`, `setPublic(ItemStack, boolean)`
-- `getShockInterval(ItemStack)` → depuis ShockComponent ou ItemShockCollarAuto
-
-**GPS :**
-- `hasGPS(ItemStack)` → check GpsComponent presence OR instanceof ItemGpsCollar
-- `hasPublicTracking(ItemStack)`, `setPublicTracking(ItemStack, boolean)`
-- `getSafeSpots(ItemStack)`, `addSafeSpot(ItemStack, ...)`, `removeSafeSpot(ItemStack, int)`
-- `isActive(ItemStack)`, `setActive(ItemStack, boolean)`
-
-**Choke :**
-- `isChokeCollar(ItemStack)` → check ChokingComponent presence OR instanceof ItemChokeCollar
-- `isChoking(ItemStack)`, `setChoking(ItemStack, boolean)`
-- `isPetPlayMode(ItemStack)`, `setPetPlayMode(ItemStack, boolean)`
-
-**Alert suppression :**
-- `runWithSuppressedAlert(Runnable)` — ThreadLocal, délègue à `ItemCollar` pendant la coexistence
-- `isRemovalAlertSuppressed()` → lit le ThreadLocal
-
-**Référence :** `ItemCollar.java` (1407 lignes), `ItemShockCollar.java` (133 lignes), `ItemGpsCollar.java` (369 lignes), `ItemChokeCollar.java` (154 lignes), `ItemShockCollarAuto.java` (58 lignes).
-
----
-
-### A7. OwnershipComponent (complet)
-
-**Fichier :** Créer `src/main/java/com/tiedup/remake/v2/bondage/component/OwnershipComponent.java`
-**Modifier :** `ComponentType.java` — ajouter `OWNERSHIP`
-
-JSON : `"ownership": {}`
-
-Lifecycle hooks :
-- `onEquipped(stack, entity)` : enregistrer dans CollarRegistry (extraire de `ItemCollar.registerCollarInRegistry`). **Note :** le owner initial (le player qui equip) n'est pas dans la signature `onEquipped(stack, entity)`. Options : lire le owner depuis le NBT du stack (déjà écrit par l'interaction flow), ou passer par un tag temporaire.
-- `onUnequipped(stack, entity)` : alerter les owners (si pas supprimé via ThreadLocal), désenregistrer du CollarRegistry, reset auto-shock timer.
-- `appendTooltip` : nickname, lock status, kidnapping mode, cell ID, bondage service, shock status, GPS status.
-- `blocksUnequip` : si locked via ILockable.
-- **Override `dropLockOnUnlock()`** : retourner false pour les collars (pas de padlock drop). Note : ceci doit être sur DataDrivenBondageItem, pas sur le composant (ILockable est sur l'Item, pas sur le composant). → DataDrivenBondageItem override `dropLockOnUnlock()` quand OwnershipComponent est présent.
-
----
-
-### A8. TyingInteractionHelper + DataDrivenBondageItem extensions
-
-**Fichier :** Créer `src/main/java/com/tiedup/remake/v2/bondage/TyingInteractionHelper.java`
-**Modifier :** `src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenBondageItem.java`
-
-Extraire le flow tying de `ItemBind.interactLivingEntity()` dans `TyingInteractionHelper` :
-- Accepte `(ServerPlayer player, LivingEntity target, ItemStack stack, InteractionHand hand)`
-- Crée TyingPlayerTask, gère la progress bar, consume l'item, equipe via V2
-
-Dans `DataDrivenBondageItem` :
-- `use()` : si regions contient ARMS → shift+click cycle bind mode (son + message action bar). Server-side only.
-- `interactLivingEntity()` : routing par région :
- - ARMS → `TyingInteractionHelper` (tying task flow)
- - NECK → collar equip flow (add owner, register CollarRegistry, play sound) — extraire de `ItemCollar.interactLivingEntity()`
- - MOUTH/EYES/EARS/HANDS → instant equip (existant via AbstractV2BondageItem)
-
----
-
-### A9. Réécrire StruggleBinds/StruggleCollar pour V2
-
-**Fichiers :**
-- `src/main/java/com/tiedup/remake/state/struggle/StruggleBinds.java`
-- `src/main/java/com/tiedup/remake/state/struggle/StruggleCollar.java`
-
-`StruggleBinds.canStruggle()` :
-- Actuellement : `instanceof ItemBind` → rejette V2
-- Fix : accepter `instanceof IV2BondageItem` avec region ARMS, OU `instanceof ItemBind`
-- Résistance : si V2 → `ResistanceComponent.getBaseResistance()`. Si V1 → existant via `IHasResistance`.
-
-`StruggleCollar` :
-- Actuellement : `instanceof ItemCollar` → rejette V2
-- Fix : accepter items avec `OwnershipComponent` (via `CollarHelper.isCollar(stack)`)
-- Résistance collar : via `ResistanceComponent` ou `IHasResistance`
-
----
-
-### A10. DataDrivenBondageItem implémente IHasGaggingEffect/IHasBlindingEffect
-
-**Fichier :** `src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenBondageItem.java`
-
-Ajouter `implements IHasGaggingEffect, IHasBlindingEffect` sur la classe. Les 7+ call sites qui font `instanceof IHasGaggingEffect` continueront de fonctionner.
-
-**Problème :** Ces interfaces sont des markers (pas de méthodes). La simple présence de l'interface signifie "a l'effet". Mais `DataDrivenBondageItem` est un singleton — TOUS les items data-driven auront ces interfaces.
-
-**Solution :** Ne PAS implémenter les interfaces marker sur la classe. À la place, lors de la migration Branch C, convertir les call sites vers des component checks. C'est plus propre.
-
-→ **A10 annulé.** Les call sites migreront en Branch C vers `DataDrivenBondageItem.getComponent(stack, GAGGING, ...) != null`.
-
----
-
-### A11. Fix PacketSelfBondage — routing par région
-
-**Fichier :** `src/main/java/com/tiedup/remake/network/selfbondage/PacketSelfBondage.java`
-
-Dans `handleV2SelfBondage()`, ajouter du routing par région :
-
-```java
-Set regions = v2Item.getOccupiedRegions(stack);
-if (regions.contains(BodyRegionV2.NECK)) {
- // Cannot self-collar
- return;
-}
-if (regions.contains(BodyRegionV2.ARMS)) {
- // Tying task (existing flow)
- handleV2SelfBind(player, stack, v2Item, state);
-} else {
- // Accessories: instant equip (no tying delay)
- handleV2SelfAccessory(player, stack, v2Item, state);
-}
-```
-
-Créer `handleV2SelfAccessory()` basé sur le pattern V1 `handleSelfAccessory()` (instant equip, swap si déjà équipé, locked check).
-
-### A12. NPC speed reduction
-
-**Fichier :** À déterminer (composant ou event handler)
-
-`ItemBind.onEquipped()` appelle `RestraintEffectUtils.applyBindSpeedReduction(entity)` pour les NPCs. Cette logique doit survivre à la migration.
-
-**Option :** Ajouter dans `DataDrivenBondageItem.onEquipped()` (après le component dispatch) un check : si entity n'est PAS un Player ET l'item a la région ARMS → appeler `RestraintEffectUtils.applyBindSpeedReduction(entity)`.
-
----
-
-## Vérification
-
-- [ ] `make build` — compilation clean
-- [ ] Struggle fonctionne avec V1 items (ropes, chain, collar)
-- [ ] Struggle fonctionne avec V2 items (test_component_gag.json ou nouveau test item)
-- [ ] Self-bondage V2 : ARMS → tying delay, MOUTH → instant, NECK → rejeté
-- [ ] Tooltips : composants contribuent des lignes
-- [ ] PoseTypeHelper résout V1 (ItemBind) et V2 (definition.poseType)
-- [ ] CollarHelper.isOwner() fonctionne sur V1 ET V2 collars
-- [ ] MCP reindex après la branche
diff --git a/docs/plans/D01-branch-B-json-definitions.md b/docs/plans/D01-branch-B-json-definitions.md
deleted file mode 100644
index 8436bcf..0000000
--- a/docs/plans/D01-branch-B-json-definitions.md
+++ /dev/null
@@ -1,128 +0,0 @@
-# D-01 Branch B : 46 JSON Item Definitions
-
-> **Prérequis :** Branch A (bridge utilities) mergée.
-> **Branche :** `feature/d01-branch-b-definitions`
-> **Objectif :** Créer les définitions JSON data-driven pour les 46 items V1. Chaque item V1 a un équivalent JSON. Les valeurs sont config-driven (pas hardcodées). À la fin, les 46 items V2 existent en parallèle des V1.
-
----
-
-## Extensions parser nécessaires
-
-Avant de créer les JSON, vérifier que le parser supporte :
-- `pose_type` (string, optionnel) — ajouté en Branch A4
-- `can_attach_padlock` (boolean, default true) — **à ajouter si pas fait en A**
-- GaggingComponent `material` — ajouté en Branch A2
-- ResistanceComponent `id` — ajouté en Branch A1
-
-**Fichiers :** `DataDrivenItemParser.java`, `DataDrivenItemDefinition.java`
-
----
-
-## Items à créer
-
-Tous dans `src/main/resources/data/tiedup/tiedup_items/`.
-
-### Binds (16 fichiers)
-
-| Fichier | display_name | resistance id | pose_type | Notes |
-|---------|-------------|---------------|-----------|-------|
-| `ropes.json` | Ropes | rope | STANDARD | supports_color: true |
-| `armbinder.json` | Armbinder | armbinder | STANDARD | |
-| `dogbinder.json` | Dogbinder | armbinder | DOG | movement_style: crawl |
-| `chain.json` | Chains | chain | STANDARD | |
-| `ribbon.json` | Ribbon | ribbon | STANDARD | supports_color: true |
-| `slime.json` | Slime Bind | slime | STANDARD | can_attach_padlock: false |
-| `vine_seed.json` | Vine Bind | vine | STANDARD | can_attach_padlock: false |
-| `web_bind.json` | Web Bind | web | STANDARD | can_attach_padlock: false |
-| `shibari.json` | Shibari | rope | STANDARD | supports_color: true |
-| `leather_straps.json` | Leather Straps | armbinder | STANDARD | |
-| `medical_straps.json` | Medical Straps | armbinder | STANDARD | |
-| `beam_cuffs.json` | Beam Cuffs | chain | STANDARD | |
-| `duct_tape.json` | Duct Tape | tape | STANDARD | can_attach_padlock: false |
-| `straitjacket.json` | Straitjacket | straitjacket | STRAITJACKET | |
-| `wrap.json` | Wrap | wrap | WRAP | |
-| `latex_sack.json` | Latex Sack | latex_sack | LATEX_SACK | |
-
-Tous ont : regions `["ARMS"]`, lockable component (sauf organiques), resistance component avec `id`.
-
-**Référence :** `BindVariant.java` pour les registry names, pose types, supports_color. `ModConfig.ServerConfig` pour les resistance IDs (lignes 413-428).
-
-### Gags (19 fichiers)
-
-| Fichier | display_name | gag material | Notes |
-|---------|-------------|-------------|-------|
-| `cloth_gag.json` | Cloth Gag | cloth | |
-| `ropes_gag.json` | Rope Gag | cloth | |
-| `cleave_gag.json` | Cleave Gag | cloth | |
-| `ribbon_gag.json` | Ribbon Gag | cloth | supports_color: true |
-| `ball_gag.json` | Ball Gag | ball | supports_color: true |
-| `ball_gag_strap.json` | Ball Gag (Strap) | ball | |
-| `tape_gag.json` | Tape Gag | tape | can_attach_padlock: false |
-| `wrap_gag.json` | Wrap Gag | tape | |
-| `slime_gag.json` | Slime Gag | tape | can_attach_padlock: false |
-| `vine_gag.json` | Vine Gag | tape | can_attach_padlock: false |
-| `web_gag.json` | Web Gag | tape | can_attach_padlock: false |
-| `panel_gag.json` | Panel Gag | panel | |
-| `beam_panel_gag.json` | Beam Panel Gag | panel | |
-| `chain_panel_gag.json` | Chain Panel Gag | panel | |
-| `latex_gag.json` | Latex Gag | latex | |
-| `tube_gag.json` | Tube Gag | stuffed | |
-| `bite_gag.json` | Bite Gag | bite | |
-| `sponge_gag.json` | Sponge Gag | sponge | |
-| `baguette_gag.json` | Baguette Gag | baguette | |
-
-Tous ont : regions `["MOUTH"]`, gagging component avec material, lockable, resistance `{"id": "gag"}`, adjustable.
-
-**Référence :** `GagVariant.java` pour les materials et registry names. `ModConfig.ServerConfig.gagComprehension/gagRange` pour les valeurs runtime.
-
-### Blindfolds (2 fichiers)
-
-| Fichier | display_name |
-|---------|-------------|
-| `classic_blindfold.json` | Blindfold |
-| `blindfold_mask.json` | Blindfold Mask |
-
-Regions `["EYES"]`, components : blinding, lockable, resistance `{"id": "blindfold"}`, adjustable.
-
-### Earplugs (1 fichier)
-
-`classic_earplugs.json` — regions `["EARS"]`, lockable, resistance `{"id": "blindfold"}` (partage la résistance blindfold dans V1).
-
-### Mittens (1 fichier)
-
-`leather_mittens.json` — regions `["HANDS"]`, lockable.
-
-### Collars (5 fichiers)
-
-| Fichier | display_name | Components spécifiques |
-|---------|-------------|----------------------|
-| `classic_collar.json` | Classic Collar | ownership, lockable, resistance `{"id": "collar"}` |
-| `shock_collar.json` | Shock Collar | + shock `{"damage": 2.0}` |
-| `shock_collar_auto.json` | Auto Shock Collar | + shock `{"damage": 2.0, "auto_interval": 200}` |
-| `gps_collar.json` | GPS Collar | + gps `{"safe_zone_radius": 50}` |
-| `choke_collar.json` | Choke Collar | + choking |
-
-### Combos (3 fichiers)
-
-| Fichier | display_name | Regions | Components |
-|---------|-------------|---------|-----------|
-| `hood.json` | Hood | `["EYES"]` | blinding, gagging `{"material": "cloth"}`, lockable, blocked_regions: `["EYES", "EARS"]` |
-| `medical_gag.json` | Medical Gag | `["MOUTH"]` | gagging `{"material": "stuffed"}`, blinding, lockable |
-| `ball_gag_3d.json` | Ball Gag 3D | `["MOUTH"]` | gagging `{"material": "ball"}`, lockable, adjustable. Model 3D spécifique. |
-
----
-
-## Supprimer le fichier test
-
-Supprimer `src/main/resources/data/tiedup/tiedup_items/test_component_gag.json` (fichier de test Phase 1, plus nécessaire).
-
----
-
-## Vérification
-
-- [ ] `make build` — clean
-- [ ] `make run` — les 46 items data-driven apparaissent (via `/tiedup give` ou creative tab section data-driven)
-- [ ] Résistance = valeur config (changer dans config, vérifier que la résistance change)
-- [ ] Gag comprehension = valeur config (changer dans config, vérifier)
-- [ ] Collars ownership fonctionne via le nouveau OwnershipComponent
-- [ ] Items organiques (slime, vine, web, tape) ne peuvent pas recevoir de padlock
diff --git a/docs/plans/D01-branch-C-consumer-migration.md b/docs/plans/D01-branch-C-consumer-migration.md
deleted file mode 100644
index 14f2413..0000000
--- a/docs/plans/D01-branch-C-consumer-migration.md
+++ /dev/null
@@ -1,107 +0,0 @@
-# D-01 Branch C : Consumer Migration (~97 fichiers)
-
-> **Prérequis :** Branch A + B mergées.
-> **Branche :** `feature/d01-branch-c-migration`
-> **Objectif :** Remplacer TOUTES les références V1 (`instanceof ItemBind`, `ItemCollar.isOwner()`, `BindVariant.ROPES`, etc.) par les helpers/composants V2. Les classes V1 existent encore mais ne sont plus référencées. À la fin, `grep -r "instanceof ItemBind\|instanceof ItemGag\|instanceof ItemCollar\|instanceof ItemBlindfold\|instanceof ItemEarplugs\|instanceof ItemMittens\|BindVariant\|GagVariant" src/` retourne ZÉRO résultats (hors items/ lui-même).
-
----
-
-## Pattern migration
-
-| V1 | V2 | Notes |
-|----|-----|-------|
-| `instanceof ItemBind` | `BindModeHelper.isBindItem(stack)` | Ou `instanceof IV2BondageItem` si on a juste besoin de savoir que c'est un bondage item |
-| `ItemBind.hasArmsBound(stack)` | `BindModeHelper.hasArmsBound(stack)` | Mêmes NBT keys |
-| `ItemBind.hasLegsBound(stack)` | `BindModeHelper.hasLegsBound(stack)` | |
-| `ItemBind.getBindModeId(stack)` | `BindModeHelper.getBindModeId(stack)` | |
-| `itemBind.getPoseType()` | `PoseTypeHelper.getPoseType(stack)` | |
-| `BindVariant.ROPES` / `ModItems.getBind(variant)` | `DataDrivenBondageItem.createStack(rl("tiedup:ropes"))` | **LAZY !** Ne pas appeler dans des static initializers |
-| `instanceof ItemCollar` + methods | `CollarHelper.isCollar(stack)` + `CollarHelper.method(stack)` | |
-| `instanceof ItemShockCollar` | `CollarHelper.canShock(stack)` | |
-| `instanceof ItemGpsCollar` | `CollarHelper.hasGPS(stack)` | |
-| `instanceof ItemChokeCollar` | `CollarHelper.isChokeCollar(stack)` | |
-| `instanceof IHasGaggingEffect` | `DataDrivenBondageItem.getComponent(stack, GAGGING, GaggingComponent.class) != null` | Pour V2 items. V1 items gardent l'interface pendant la transition. |
-| `instanceof IHasBlindingEffect` | `DataDrivenBondageItem.getComponent(stack, BLINDING, BlindingComponent.class) != null` | Idem |
-| `instanceof ItemGag` + `getGagMaterial()` | `GaggingComponent comp = getComponent(stack, GAGGING, ...)` + `comp.getMaterial()` | |
-| `PoseType` enum direct | Inchangé — l'enum est conservé | |
-| `IHasResistance` methods | Inchangé — l'interface est conservée | |
-| `ILockable` methods | Inchangé — l'interface est conservée | |
-
----
-
-## Ordre de migration (critique d'abord)
-
-### Phase 1 : State core (12 fichiers)
-
-Ces fichiers sont la fondation — les migrer d'abord assure que le reste fonctionne.
-
-| Fichier | Changements |
-|---------|------------|
-| `state/IBondageState.java` | `hasArmsBound()`/`hasLegsBound()` → `BindModeHelper` |
-| `state/PlayerBindState.java` | `instanceof ItemCollar` → `CollarHelper`, `instanceof ItemBind` → `BindModeHelper` |
-| `state/PlayerCaptorManager.java` | `instanceof ItemCollar` → `CollarHelper.isCollar() + CollarHelper.getOwners()` |
-| `state/HumanChairHelper.java` | `PoseType` import OK (conservé) |
-| `state/components/PlayerEquipment.java` | **Garder `equipInRegion()` V1 fallback** (migre en Branch D). Remplacer `instanceof ItemBind/ItemCollar` dans resistance methods. |
-| `state/components/PlayerDataRetrieval.java` | `instanceof ItemCollar` → `CollarHelper.getNickname()` |
-| `state/components/PlayerLifecycle.java` | imports V1 → V2 helpers |
-| `state/components/PlayerShockCollar.java` | `instanceof ItemShockCollar` → `CollarHelper.canShock()` |
-| `state/struggle/StruggleBinds.java` | Déjà migré en Branch A9 — vérifier |
-| `state/struggle/StruggleCollar.java` | Déjà migré en Branch A9 — vérifier |
-| `state/struggle/StruggleAccessory.java` | Vérifier |
-
-### Phase 2 : Client animation/render (12 fichiers)
-
-| Fichier | Changements |
-|---------|------------|
-| `client/animation/tick/AnimationTickHandler.java` | `instanceof ItemBind` → `PoseTypeHelper.getPoseType()` + `BindModeHelper` |
-| `client/animation/tick/NpcAnimationTickHandler.java` | Idem |
-| `client/animation/render/PlayerArmHideEventHandler.java` | `instanceof ItemBind` → `PoseTypeHelper` |
-| `client/animation/render/DogPoseRenderHandler.java` | Idem |
-| `client/animation/render/PetBedRenderHandler.java` | Idem |
-| `client/animation/util/AnimationIdBuilder.java` | `PoseType` import OK (conservé) |
-| `client/animation/StaticPoseApplier.java` | `PoseType` import OK |
-| `client/model/DamselModel.java` | `PoseType` + `instanceof ItemBind` → helpers |
-| `client/FirstPersonMittensRenderer.java` | `BindVariant.ROPES` → lazy createStack |
-| `mixin/client/MixinPlayerModel.java` | `instanceof ItemBind` → `PoseTypeHelper` |
-| `mixin/client/MixinCamera.java` | Idem |
-| `mixin/client/MixinVillagerEntityBaseModelMCA.java` | Idem |
-
-### Phase 3 : Entity AI goals (15 fichiers)
-
-Principalement `instanceof ItemBind` → `BindModeHelper`, `ModItems.getBind(variant)` → `createStack(rl)`, `instanceof ItemCollar` → `CollarHelper`.
-
-### Phase 4 : Network packets (14 fichiers)
-
-`PacketSelfBondage` déjà migré en A11. Reste : `PacketSlaveAction`, `PacketMasterEquip`, `PacketAssignCellToCollar`, `PacketNpcCommand`, etc. → `CollarHelper`.
-
-### Phase 5 : Events (8 fichiers)
-
-`BondageItemRestrictionHandler`, `RestraintTaskTickHandler`, `PlayerEnslavementHandler`, `ChatEventHandler`, etc.
-
-### Phase 6 : Commands (6 fichiers)
-
-`BondageSubCommand` (1232 lignes) — le plus gros. `BindVariant` → `createStack()`. `NPCCommand`, `CollarCommand`, `KidnapSetCommand`.
-
-### Phase 7 : Entity classes (15 fichiers)
-
-`EntityKidnapper`, `KidnapperCaptureEquipment`, `KidnapperTheme`, `KidnapperItemSelector`, `KidnapperCollarConfig`, etc.
-
-### Phase 8 : Compat MCA (5 fichiers)
-
-`MCAKidnappedAdapter` (907 lignes) — `instanceof IHasGaggingEffect/IHasBlindingEffect` → component checks. `instanceof ItemCollar` → `CollarHelper`.
-
-### Phase 9 : Autres (10 fichiers)
-
-Dialogue (`GagTalkManager`, `PetRequestManager`), worldgen (`HangingCagePiece`), util (`RestraintApplicator`), blocks (`BondageItemBlockEntity`), dispenser, creative tab.
-
----
-
-## Vérification
-
-- [ ] `make build` — clean
-- [ ] `grep -r "instanceof ItemBind\b" src/ --include="*.java" | grep -v "items/"` → 0 résultats
-- [ ] `grep -r "instanceof ItemGag\b" src/ --include="*.java" | grep -v "items/"` → 0 résultats
-- [ ] `grep -r "instanceof ItemCollar\b" src/ --include="*.java" | grep -v "items/"` → 0 résultats
-- [ ] `grep -r "BindVariant\b" src/ --include="*.java" | grep -v "items/"` → 0 résultats
-- [ ] `grep -r "GagVariant\b" src/ --include="*.java" | grep -v "items/"` → 0 résultats
-- [ ] `make run` — le mod fonctionne normalement
diff --git a/docs/plans/D01-branch-D-v1-cleanup.md b/docs/plans/D01-branch-D-v1-cleanup.md
deleted file mode 100644
index 055420b..0000000
--- a/docs/plans/D01-branch-D-v1-cleanup.md
+++ /dev/null
@@ -1,188 +0,0 @@
-# D-01 Branch D : V1 Cleanup
-
-> **Prérequis :** Branch A + B + C mergées. Zero références V1 hors du package `items/`.
-> **Branche :** `feature/d01-branch-d-cleanup`
-> **Objectif :** Supprimer toutes les classes V1 bondage. Migrer `equipInRegion()` vers le flow V2 complet. Réécrire le creative tab. À la fin, zéro classe V1 bondage dans le mod.
-
----
-
-## Décisions
-
-- **Save compat :** Breaking change. Les items V1 dans les inventaires existants seront perdus. Mod en alpha.
-- **Pas de MissingMappingsEvent.** Simplement supprimer les registrations.
-
----
-
-## Tâches
-
-### D1. Migrer equipInRegion() → V2EquipmentHelper.equipItem()
-
-**Fichier :** `src/main/java/com/tiedup/remake/state/components/PlayerEquipment.java`
-
-Maintenant que tous les items sont `DataDrivenBondageItem` (qui implémente `IV2BondageItem`), le bypass direct `setInRegion()` n'est plus nécessaire.
-
-Remplacer `equipInRegion()` par un appel à `V2EquipmentHelper.equipItem()` qui fait la conflict resolution complète (swap, supersede, blocked regions).
-
-Vérifier que les méthodes `putBindOn()`, `putGagOn()`, `putCollarOn()`, etc. fonctionnent toujours via le nouveau path.
-
----
-
-### D2. Supprimer les classes V1 (~30 fichiers)
-
-**À supprimer :**
-
-Abstract bases :
-- `items/base/ItemBind.java` (637 lignes)
-- `items/base/ItemGag.java` (93 lignes)
-- `items/base/ItemBlindfold.java` (89 lignes)
-- `items/base/ItemCollar.java` (1407 lignes)
-- `items/base/ItemEarplugs.java` (90 lignes)
-- `items/base/ItemMittens.java` (72 lignes)
-
-Interfaces V1-only :
-- `items/base/IBondageItem.java` (102 lignes)
-- `items/base/IHasGaggingEffect.java` (33 lignes)
-- `items/base/IHasBlindingEffect.java` (33 lignes)
-- `items/base/IAdjustable.java` (49 lignes)
-- `items/base/ItemOwnerTarget.java`
-- `items/base/ItemColor.java`
-
-Variant enums :
-- `items/base/BindVariant.java` (90 lignes)
-- `items/base/GagVariant.java` (163 lignes)
-- `items/base/BlindfoldVariant.java` (48 lignes)
-- `items/base/EarplugsVariant.java` (33 lignes)
-- `items/base/MittensVariant.java` (35 lignes)
-
-Factory classes :
-- `items/GenericBind.java` (68 lignes)
-- `items/GenericGag.java` (72 lignes)
-- `items/GenericBlindfold.java` (37 lignes)
-- `items/GenericEarplugs.java` (37 lignes)
-- `items/GenericMittens.java` (37 lignes)
-
-Collars :
-- `items/ItemClassicCollar.java` (21 lignes)
-- `items/ItemShockCollar.java` (133 lignes)
-- `items/ItemShockCollarAuto.java` (58 lignes)
-- `items/ItemGpsCollar.java` (369 lignes)
-- `items/ItemChokeCollar.java` (154 lignes)
-
-Combos :
-- `items/ItemHood.java` (35 lignes)
-- `items/ItemMedicalGag.java` (24 lignes)
-- `items/bondage3d/gags/ItemBallGag3D.java` (78 lignes)
-- `items/bondage3d/IHas3DModelConfig.java`
-- `items/bondage3d/Model3DConfig.java`
-
-**À CONSERVER :**
-- `items/base/ILockable.java` — utilisé par V2 (AbstractV2BondageItem)
-- `items/base/IHasResistance.java` — utilisé par V2 (DataDrivenBondageItem)
-- `items/base/IKnife.java` — utilisé par GenericKnife (tool)
-- `items/base/PoseType.java` — utilisé par animation system
-- `items/base/KnifeVariant.java` — utilisé par GenericKnife (tool)
-- `items/base/AdjustmentHelper.java` — utilisé par adjustment packets
-- `items/GenericKnife.java` — tool, pas bondage
-- `items/clothes/GenericClothes.java` — déjà V2
-- `items/clothes/ClothesProperties.java`
-- `items/ModItems.java` — garde les tools, supprime les bondage
-- `items/ModCreativeTabs.java` — réécrit (voir D3)
-- Tous les tool items (whip, padlock, key, lockpick, taser, etc.)
-
----
-
-### D3. Réécrire ModItems — retirer les registrations V1
-
-**Fichier :** `src/main/java/com/tiedup/remake/items/ModItems.java`
-
-Supprimer :
-- `BINDS` map + `registerAllBinds()`
-- `GAGS` map + `registerAllGags()`
-- `BLINDFOLDS` map + `registerAllBlindfolds()`
-- `EARPLUGS` map + `registerAllEarplugs()`
-- `MITTENS` map + `registerAllMittens()`
-- `BALL_GAG_3D`, `MEDICAL_GAG`, `HOOD`
-- `CLASSIC_COLLAR`, `SHOCK_COLLAR`, `SHOCK_COLLAR_AUTO`, `GPS_COLLAR`, `CHOKE_COLLAR`
-- Les helper accessors `getBind()`, `getGag()`, etc.
-
-Garder : CLOTHES, tous les tools (WHIP, PADLOCK, KEY, etc.), KNIVES, spawn eggs.
-
----
-
-### D4. Réécrire ModCreativeTabs
-
-**Fichier :** `src/main/java/com/tiedup/remake/items/ModCreativeTabs.java`
-
-Remplacer l'itération par variant enums par :
-
-```java
-// Data-driven bondage items
-for (DataDrivenItemDefinition def : DataDrivenItemRegistry.getAll()) {
- output.accept(DataDrivenBondageItem.createStack(def.id()));
-}
-```
-
-Pour l'ordre : ajouter un champ optionnel `"creative_tab_order"` aux definitions JSON, ou trier par catégorie (regions) puis par nom.
-
-Pour les couleurs : si l'item a `supports_color`, ajouter les variantes colorées. Utiliser `tint_channels` du definition.
-
----
-
-### D5. Cleanup PoseTypeHelper — retirer le fallback V1
-
-**Fichier :** `src/main/java/com/tiedup/remake/v2/bondage/PoseTypeHelper.java`
-
-Supprimer le fallback `instanceof ItemBind` dans `getPoseType()`. Ne garder que le path data-driven.
-
----
-
-### D6. Cleanup CollarHelper — retirer les fallbacks V1
-
-**Fichier :** `src/main/java/com/tiedup/remake/v2/bondage/CollarHelper.java`
-
-Les méthodes comme `isCollar(stack)` checkent `instanceof ItemCollar` en fallback V1. Retirer ces checks.
-
----
-
-### D7. Cleanup BindModeHelper — retirer le fallback V1
-
-Idem — retirer `instanceof ItemBind` fallback dans `isBindItem()`.
-
----
-
-### D8. Cleanup imports orphelins
-
-Faire un pass sur tout le projet pour retirer les imports V1 orphelins.
-
-```bash
-grep -rn "import com.tiedup.remake.items.base.ItemBind" src/ --include="*.java"
-grep -rn "import com.tiedup.remake.items.base.ItemCollar" src/ --include="*.java"
-grep -rn "import com.tiedup.remake.items.base.IBondageItem" src/ --include="*.java"
-# etc. — tout doit retourner 0
-```
-
----
-
-## Vérification finale
-
-- [ ] `make build` — clean, zero errors
-- [ ] `make run` — le mod démarre, les items apparaissent dans le creative tab
-- [ ] `grep -r "items.base.ItemBind\|items.base.ItemGag\|items.base.ItemCollar\|items.base.ItemBlindfold\|items.base.ItemEarplugs\|items.base.ItemMittens\|items.base.IBondageItem\|BindVariant\|GagVariant\|BlindfoldVariant\|EarplugsVariant\|MittensVariant" src/main/java/ --include="*.java"` → **0 résultats**
-- [ ] Les items data-driven s'équipent/se déséquipent correctement
-- [ ] Le struggle fonctionne (binds + collars)
-- [ ] Le self-bondage fonctionne (routing par région)
-- [ ] Les collars gardent leur ownership/shock/GPS après equip/unequip
-- [ ] Les tooltips affichent toutes les infos composants
-- [ ] `equipInRegion()` utilise V2EquipmentManager (conflict resolution active)
-- [ ] MCP reindex final
-
----
-
-## Résultat attendu
-
-- **~6500 lignes de code V1 supprimées**
-- **46 items = 46 fichiers JSON** (data-driven, extensible par resource packs)
-- **1 seul Item singleton** (`DataDrivenBondageItem`)
-- **8 composants** gèrent toute la logique gameplay
-- **3 helpers** (`BindModeHelper`, `PoseTypeHelper`, `CollarHelper`) remplacent les anciennes classes
-- **Zero couplage V1** dans le reste du mod
diff --git a/docs/plans/D01-branch-E-resistance-rework.md b/docs/plans/D01-branch-E-resistance-rework.md
deleted file mode 100644
index 750d6d0..0000000
--- a/docs/plans/D01-branch-E-resistance-rework.md
+++ /dev/null
@@ -1,155 +0,0 @@
-# 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
diff --git a/docs/superpowers/plans/2026-04-13-d01-phase1-component-system.md b/docs/superpowers/plans/2026-04-13-d01-phase1-component-system.md
deleted file mode 100644
index de7a3be..0000000
--- a/docs/superpowers/plans/2026-04-13-d01-phase1-component-system.md
+++ /dev/null
@@ -1,869 +0,0 @@
-# D-01 Phase 1: Data-Driven Item Component System
-
-> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
-
-**Goal:** Create a reusable component system so data-driven bondage items can declare gameplay behaviors (lockable, shock, GPS, gagging, etc.) in their JSON definition instead of requiring per-item Java classes.
-
-**Architecture:** Each component is a self-contained behavior module implementing `IItemComponent`. Components are declared in item JSON (`"components": {"shock": {...}}`), parsed by an extended `DataDrivenItemParser`, stored on `DataDrivenItemDefinition`, and ticked/queried via `DataDrivenBondageItem` delegation. The existing `ILockable` and `IHasResistance` interfaces are preserved as shared contracts — components implement them.
-
-**Tech Stack:** Java 17, Forge 1.20.1, existing V2 data-driven infrastructure (`DataDrivenItemRegistry`, `DataDrivenItemParser`, `DataDrivenItemDefinition`, `DataDrivenBondageItem`)
-
-**Scope:** This plan builds ONLY the component infrastructure + 3 core components (lockable, resistance, gagging). The remaining 5 components (shock, GPS, blinding, choking, adjustable) follow the same pattern and will be added in subsequent tasks or a follow-up plan.
-
----
-
-## File Structure
-
-### New files
-
-| File | Responsibility |
-|------|---------------|
-| `v2/bondage/component/IItemComponent.java` | Component interface: lifecycle hooks, tick, query |
-| `v2/bondage/component/ComponentType.java` | Enum of all component types with factory methods |
-| `v2/bondage/component/ComponentHolder.java` | Container: holds instantiated components for an item stack |
-| `v2/bondage/component/LockableComponent.java` | Lock/unlock, padlock, key matching, jam, lock resistance |
-| `v2/bondage/component/ResistanceComponent.java` | Struggle resistance with configurable base value |
-| `v2/bondage/component/GaggingComponent.java` | Muffled speech, comprehension %, range limit |
-
-### Modified files
-
-| File | Changes |
-|------|---------|
-| `v2/bondage/datadriven/DataDrivenItemDefinition.java` | Add `Map componentConfigs` field |
-| `v2/bondage/datadriven/DataDrivenItemParser.java` | Parse `"components"` JSON block |
-| `v2/bondage/datadriven/DataDrivenBondageItem.java` | Delegate lifecycle hooks to components, expose `getComponent()` |
-| `v2/bondage/datadriven/DataDrivenItemRegistry.java` | Instantiate `ComponentHolder` per definition |
-
----
-
-## Tasks
-
-### Task 1: IItemComponent interface
-
-**Files:**
-- Create: `src/main/java/com/tiedup/remake/v2/bondage/component/IItemComponent.java`
-
-- [ ] **Step 1: Create the component interface**
-
-```java
-package com.tiedup.remake.v2.bondage.component;
-
-import com.google.gson.JsonObject;
-import net.minecraft.world.entity.LivingEntity;
-import net.minecraft.world.item.ItemStack;
-
-/**
- * A reusable behavior module for data-driven bondage items.
- * Components are declared in JSON and instantiated per item definition.
- *
- * Lifecycle: parse config once (from JSON), then tick/query per equipped entity.
- */
-public interface IItemComponent {
-
- /**
- * Called when the item is equipped on an entity.
- * @param stack The equipped item stack
- * @param entity The entity wearing the item
- */
- default void onEquipped(ItemStack stack, LivingEntity entity) {}
-
- /**
- * Called when the item is unequipped from an entity.
- * @param stack The unequipped item stack
- * @param entity The entity that was wearing the item
- */
- default void onUnequipped(ItemStack stack, LivingEntity entity) {}
-
- /**
- * Called every tick while the item is equipped.
- * @param stack The equipped item stack
- * @param entity The entity wearing the item
- */
- default void onWornTick(ItemStack stack, LivingEntity entity) {}
-
- /**
- * Whether this component prevents the item from being unequipped.
- * @param stack The equipped item stack
- * @param entity The entity wearing the item
- * @return true if unequip should be blocked
- */
- default boolean blocksUnequip(ItemStack stack, LivingEntity entity) {
- return false;
- }
-}
-```
-
-- [ ] **Step 2: Verify file compiles**
-
-Run: `cd /home/user/Documents/Projet/Open-TiedUp! && make build 2>&1 | tail -5`
-Expected: BUILD SUCCESSFUL
-
-- [ ] **Step 3: Commit**
-
-```bash
-git add src/main/java/com/tiedup/remake/v2/bondage/component/IItemComponent.java
-git commit -m "feat(D-01): add IItemComponent interface for data-driven item behaviors"
-```
-
----
-
-### Task 2: ComponentType enum
-
-**Files:**
-- Create: `src/main/java/com/tiedup/remake/v2/bondage/component/ComponentType.java`
-
-- [ ] **Step 1: Create the component type registry**
-
-```java
-package com.tiedup.remake.v2.bondage.component;
-
-import com.google.gson.JsonObject;
-import javax.annotation.Nullable;
-import java.util.function.Function;
-
-/**
- * All known component types. Each type knows how to instantiate itself from JSON config.
- */
-public enum ComponentType {
- LOCKABLE("lockable", LockableComponent::fromJson),
- RESISTANCE("resistance", ResistanceComponent::fromJson),
- GAGGING("gagging", GaggingComponent::fromJson);
- // Future: SHOCK, GPS, BLINDING, CHOKING, ADJUSTABLE
-
- private final String jsonKey;
- private final Function factory;
-
- ComponentType(String jsonKey, Function factory) {
- this.jsonKey = jsonKey;
- this.factory = factory;
- }
-
- public String getJsonKey() {
- return jsonKey;
- }
-
- public IItemComponent create(JsonObject config) {
- return factory.apply(config);
- }
-
- /**
- * Look up a ComponentType by its JSON key. Returns null if unknown.
- */
- @Nullable
- public static ComponentType fromKey(String key) {
- for (ComponentType type : values()) {
- if (type.jsonKey.equals(key)) {
- return type;
- }
- }
- return null;
- }
-}
-```
-
-Note: This file will not compile yet because `LockableComponent`, `ResistanceComponent`, and `GaggingComponent` don't exist. We'll create stub classes first, then implement them.
-
-- [ ] **Step 2: Create stub classes so the enum compiles**
-
-Create three empty stubs (they will be fully implemented in Tasks 4-6):
-
-`src/main/java/com/tiedup/remake/v2/bondage/component/LockableComponent.java`:
-```java
-package com.tiedup.remake.v2.bondage.component;
-
-import com.google.gson.JsonObject;
-
-public class LockableComponent implements IItemComponent {
- private LockableComponent() {}
-
- public static IItemComponent fromJson(JsonObject config) {
- return new LockableComponent();
- }
-}
-```
-
-`src/main/java/com/tiedup/remake/v2/bondage/component/ResistanceComponent.java`:
-```java
-package com.tiedup.remake.v2.bondage.component;
-
-import com.google.gson.JsonObject;
-
-public class ResistanceComponent implements IItemComponent {
- private ResistanceComponent() {}
-
- public static IItemComponent fromJson(JsonObject config) {
- return new ResistanceComponent();
- }
-}
-```
-
-`src/main/java/com/tiedup/remake/v2/bondage/component/GaggingComponent.java`:
-```java
-package com.tiedup.remake.v2.bondage.component;
-
-import com.google.gson.JsonObject;
-
-public class GaggingComponent implements IItemComponent {
- private GaggingComponent() {}
-
- public static IItemComponent fromJson(JsonObject config) {
- return new GaggingComponent();
- }
-}
-```
-
-- [ ] **Step 3: Verify all files compile**
-
-Run: `cd /home/user/Documents/Projet/Open-TiedUp! && make build 2>&1 | tail -5`
-Expected: BUILD SUCCESSFUL
-
-- [ ] **Step 4: Commit**
-
-```bash
-git add src/main/java/com/tiedup/remake/v2/bondage/component/
-git commit -m "feat(D-01): add ComponentType enum with stub component classes"
-```
-
----
-
-### Task 3: ComponentHolder container
-
-**Files:**
-- Create: `src/main/java/com/tiedup/remake/v2/bondage/component/ComponentHolder.java`
-
-- [ ] **Step 1: Create the component container**
-
-```java
-package com.tiedup.remake.v2.bondage.component;
-
-import net.minecraft.world.entity.LivingEntity;
-import net.minecraft.world.item.ItemStack;
-
-import javax.annotation.Nullable;
-import java.util.Collections;
-import java.util.EnumMap;
-import java.util.Map;
-
-/**
- * Holds instantiated components for an item definition.
- * Immutable after construction. One per DataDrivenItemDefinition.
- */
-public final class ComponentHolder {
-
- public static final ComponentHolder EMPTY = new ComponentHolder(Map.of());
-
- private final Map components;
-
- public ComponentHolder(Map components) {
- this.components = components.isEmpty()
- ? Map.of()
- : Collections.unmodifiableMap(new EnumMap<>(components));
- }
-
- /**
- * Get a component by type, or null if not present.
- */
- @Nullable
- public IItemComponent get(ComponentType type) {
- return components.get(type);
- }
-
- /**
- * Get a component by type, cast to the expected class.
- * Returns null if not present or wrong type.
- */
- @Nullable
- @SuppressWarnings("unchecked")
- public T get(ComponentType type, Class clazz) {
- IItemComponent component = components.get(type);
- if (clazz.isInstance(component)) {
- return (T) component;
- }
- return null;
- }
-
- /**
- * Check if a component type is present.
- */
- public boolean has(ComponentType type) {
- return components.containsKey(type);
- }
-
- /**
- * Fire onEquipped for all components.
- */
- public void onEquipped(ItemStack stack, LivingEntity entity) {
- for (IItemComponent component : components.values()) {
- component.onEquipped(stack, entity);
- }
- }
-
- /**
- * Fire onUnequipped for all components.
- */
- public void onUnequipped(ItemStack stack, LivingEntity entity) {
- for (IItemComponent component : components.values()) {
- component.onUnequipped(stack, entity);
- }
- }
-
- /**
- * Fire onWornTick for all components.
- */
- public void onWornTick(ItemStack stack, LivingEntity entity) {
- for (IItemComponent component : components.values()) {
- component.onWornTick(stack, entity);
- }
- }
-
- /**
- * Check if any component blocks unequip.
- */
- public boolean blocksUnequip(ItemStack stack, LivingEntity entity) {
- for (IItemComponent component : components.values()) {
- if (component.blocksUnequip(stack, entity)) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * Whether this holder has any components.
- */
- public boolean isEmpty() {
- return components.isEmpty();
- }
-}
-```
-
-- [ ] **Step 2: Verify compilation**
-
-Run: `cd /home/user/Documents/Projet/Open-TiedUp! && make build 2>&1 | tail -5`
-Expected: BUILD SUCCESSFUL
-
-- [ ] **Step 3: Commit**
-
-```bash
-git add src/main/java/com/tiedup/remake/v2/bondage/component/ComponentHolder.java
-git commit -m "feat(D-01): add ComponentHolder container for item components"
-```
-
----
-
-### Task 4: Integrate components into DataDrivenItemDefinition + Parser
-
-**Files:**
-- Modify: `src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemDefinition.java`
-- Modify: `src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemParser.java`
-
-- [ ] **Step 1: Add componentConfigs field to DataDrivenItemDefinition**
-
-Read the current record definition, then add a new field. The record should get a new parameter:
-
-```java
-/** Raw component configs from JSON, keyed by ComponentType. */
-Map componentConfigs
-```
-
-Add after the last existing field in the record. Also add a convenience method:
-
-```java
-/**
- * Whether this definition declares a specific component.
- */
-public boolean hasComponent(ComponentType type) {
- return componentConfigs != null && componentConfigs.containsKey(type);
-}
-```
-
-- [ ] **Step 2: Parse "components" block in DataDrivenItemParser**
-
-Read `DataDrivenItemParser.java` and add parsing for the `"components"` JSON field. After parsing all existing fields, add:
-
-```java
-// Parse components
-Map componentConfigs = new EnumMap<>(ComponentType.class);
-if (json.has("components")) {
- JsonObject componentsObj = json.getAsJsonObject("components");
- for (Map.Entry entry : componentsObj.entrySet()) {
- ComponentType type = ComponentType.fromKey(entry.getKey());
- if (type != null) {
- JsonObject config = entry.getValue().isJsonObject()
- ? entry.getValue().getAsJsonObject()
- : new JsonObject();
- componentConfigs.put(type, config);
- } else {
- LOGGER.warn("[DataDrivenItemParser] Unknown component type '{}' in item '{}'",
- entry.getKey(), id);
- }
- }
-}
-```
-
-Pass `componentConfigs` to the `DataDrivenItemDefinition` record constructor.
-
-- [ ] **Step 3: Update all existing call sites that construct DataDrivenItemDefinition**
-
-Search for all `new DataDrivenItemDefinition(` calls and add `Map.of()` for the new parameter (for the network sync deserialization path, etc.).
-
-- [ ] **Step 4: Verify compilation**
-
-Run: `cd /home/user/Documents/Projet/Open-TiedUp! && make build 2>&1 | tail -5`
-Expected: BUILD SUCCESSFUL
-
-- [ ] **Step 5: Commit**
-
-```bash
-git add src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemDefinition.java
-git add src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemParser.java
-git commit -m "feat(D-01): parse component configs from item JSON definitions"
-```
-
----
-
-### Task 5: Instantiate ComponentHolder in DataDrivenItemRegistry
-
-**Files:**
-- Modify: `src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemRegistry.java`
-
-- [ ] **Step 1: Add ComponentHolder cache**
-
-Read `DataDrivenItemRegistry.java`. Add a parallel cache that maps `ResourceLocation` to `ComponentHolder`. When definitions are loaded/reloaded, instantiate components from their `componentConfigs`.
-
-Add field:
-```java
-private static volatile Map COMPONENT_HOLDERS = Map.of();
-```
-
-In the reload/register method, after storing definitions, build component holders:
-```java
-Map holders = new HashMap<>();
-for (Map.Entry entry : newDefinitions.entrySet()) {
- DataDrivenItemDefinition def = entry.getValue();
- Map components = new EnumMap<>(ComponentType.class);
- for (Map.Entry compEntry : def.componentConfigs().entrySet()) {
- components.put(compEntry.getKey(), compEntry.getKey().create(compEntry.getValue()));
- }
- holders.put(entry.getKey(), new ComponentHolder(components));
-}
-COMPONENT_HOLDERS = Collections.unmodifiableMap(holders);
-```
-
-Add accessor:
-```java
-@Nullable
-public static ComponentHolder getComponents(ItemStack stack) {
- DataDrivenItemDefinition def = get(stack);
- if (def == null) return null;
- return COMPONENT_HOLDERS.get(def.id());
-}
-
-@Nullable
-public static ComponentHolder getComponents(ResourceLocation id) {
- return COMPONENT_HOLDERS.get(id);
-}
-```
-
-- [ ] **Step 2: Verify compilation**
-
-Run: `cd /home/user/Documents/Projet/Open-TiedUp! && make build 2>&1 | tail -5`
-Expected: BUILD SUCCESSFUL
-
-- [ ] **Step 3: Commit**
-
-```bash
-git add src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemRegistry.java
-git commit -m "feat(D-01): instantiate ComponentHolder per item definition on reload"
-```
-
----
-
-### Task 6: Delegate DataDrivenBondageItem lifecycle to components
-
-**Files:**
-- Modify: `src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenBondageItem.java`
-
-- [ ] **Step 1: Add component delegation in lifecycle hooks**
-
-Read `DataDrivenBondageItem.java`. In `onEquipped()` and `onUnequipped()`, delegate to components:
-
-```java
-@Override
-public void onEquipped(ItemStack stack, LivingEntity entity) {
- ComponentHolder holder = DataDrivenItemRegistry.getComponents(stack);
- if (holder != null) {
- holder.onEquipped(stack, entity);
- }
-}
-
-@Override
-public void onUnequipped(ItemStack stack, LivingEntity entity) {
- ComponentHolder holder = DataDrivenItemRegistry.getComponents(stack);
- if (holder != null) {
- holder.onUnequipped(stack, entity);
- }
-}
-```
-
-Override `canUnequip` to check component blocks:
-```java
-@Override
-public boolean canUnequip(ItemStack stack, LivingEntity entity) {
- ComponentHolder holder = DataDrivenItemRegistry.getComponents(stack);
- if (holder != null && holder.blocksUnequip(stack, entity)) {
- return false;
- }
- return super.canUnequip(stack, entity);
-}
-```
-
-Add a public static helper for external code to query components:
-```java
-/**
- * Get a specific component from a data-driven item stack.
- * @return The component, or null if the item is not data-driven or lacks this component.
- */
-@Nullable
-public static T getComponent(ItemStack stack, ComponentType type, Class clazz) {
- ComponentHolder holder = DataDrivenItemRegistry.getComponents(stack);
- if (holder == null) return null;
- return holder.get(type, clazz);
-}
-```
-
-- [ ] **Step 2: Verify compilation**
-
-Run: `cd /home/user/Documents/Projet/Open-TiedUp! && make build 2>&1 | tail -5`
-Expected: BUILD SUCCESSFUL
-
-- [ ] **Step 3: Commit**
-
-```bash
-git add src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenBondageItem.java
-git commit -m "feat(D-01): delegate DataDrivenBondageItem lifecycle to components"
-```
-
----
-
-### Task 7: Implement LockableComponent
-
-**Files:**
-- Modify: `src/main/java/com/tiedup/remake/v2/bondage/component/LockableComponent.java`
-
-- [ ] **Step 1: Implement full lockable logic**
-
-Replace the stub with the full implementation. Extract lock behavior from `ILockable` (which remains as a shared interface). The component reads its config from JSON and delegates to `ILockable` default methods on the item stack:
-
-```java
-package com.tiedup.remake.v2.bondage.component;
-
-import com.google.gson.JsonObject;
-import com.tiedup.remake.items.base.ILockable;
-import net.minecraft.world.entity.LivingEntity;
-import net.minecraft.world.item.ItemStack;
-
-/**
- * Component: lockable behavior for data-driven items.
- * Delegates to ILockable interface methods on the item.
- *
- * JSON config:
- * {"lockable": true}
- * or
- * {"lockable": {"lock_resistance": 300}}
- */
-public class LockableComponent implements IItemComponent {
-
- private final int lockResistance;
-
- private LockableComponent(int lockResistance) {
- this.lockResistance = lockResistance;
- }
-
- public static IItemComponent fromJson(JsonObject config) {
- int resistance = 250; // default from SettingsAccessor
- if (config.has("lock_resistance")) {
- resistance = config.get("lock_resistance").getAsInt();
- }
- return new LockableComponent(resistance);
- }
-
- public int getLockResistance() {
- return lockResistance;
- }
-
- @Override
- public boolean blocksUnequip(ItemStack stack, LivingEntity entity) {
- // If item implements ILockable, check if locked
- if (stack.getItem() instanceof ILockable lockable) {
- return lockable.isLocked(stack);
- }
- return false;
- }
-}
-```
-
-- [ ] **Step 2: Verify compilation**
-
-Run: `cd /home/user/Documents/Projet/Open-TiedUp! && make build 2>&1 | tail -5`
-Expected: BUILD SUCCESSFUL
-
-- [ ] **Step 3: Commit**
-
-```bash
-git add src/main/java/com/tiedup/remake/v2/bondage/component/LockableComponent.java
-git commit -m "feat(D-01): implement LockableComponent with configurable lock resistance"
-```
-
----
-
-### Task 8: Implement ResistanceComponent
-
-**Files:**
-- Modify: `src/main/java/com/tiedup/remake/v2/bondage/component/ResistanceComponent.java`
-
-- [ ] **Step 1: Implement resistance logic**
-
-```java
-package com.tiedup.remake.v2.bondage.component;
-
-import com.google.gson.JsonObject;
-import net.minecraft.world.entity.LivingEntity;
-import net.minecraft.world.item.ItemStack;
-
-/**
- * Component: struggle resistance for data-driven items.
- * Replaces IHasResistance for data-driven items.
- *
- * JSON config:
- * {"resistance": {"base": 150}}
- */
-public class ResistanceComponent implements IItemComponent {
-
- private final int baseResistance;
-
- private ResistanceComponent(int baseResistance) {
- this.baseResistance = baseResistance;
- }
-
- public static IItemComponent fromJson(JsonObject config) {
- int base = 100; // default
- if (config.has("base")) {
- base = config.get("base").getAsInt();
- }
- return new ResistanceComponent(base);
- }
-
- /**
- * Get the base resistance for this item.
- * Used by DataDrivenBondageItem.getBaseResistance() to replace the MAX-scan workaround.
- */
- public int getBaseResistance() {
- return baseResistance;
- }
-}
-```
-
-- [ ] **Step 2: Update DataDrivenBondageItem.getBaseResistance() to use ResistanceComponent**
-
-In `DataDrivenBondageItem.java`, update `getBaseResistance()`:
-
-```java
-@Override
-public int getBaseResistance(LivingEntity entity) {
- // Try stack-aware component lookup first (fixes I-03: no more MAX scan)
- // Note: This method is called WITHOUT a stack parameter by IHasResistance.
- // We still need the MAX scan as fallback until IHasResistance gets a stack-aware method.
- if (entity != null) {
- IV2BondageEquipment equip = V2EquipmentHelper.getEquipment(entity);
- if (equip != null) {
- int maxDifficulty = -1;
- for (Map.Entry entry : equip.getAllEquipped().entrySet()) {
- ItemStack stack = entry.getValue();
- if (stack.getItem() == this) {
- // Try component first
- ResistanceComponent comp = DataDrivenBondageItem.getComponent(
- stack, ComponentType.RESISTANCE, ResistanceComponent.class);
- if (comp != null) {
- maxDifficulty = Math.max(maxDifficulty, comp.getBaseResistance());
- continue;
- }
- // Fallback to escape_difficulty from definition
- DataDrivenItemDefinition def = DataDrivenItemRegistry.get(stack);
- if (def != null) {
- maxDifficulty = Math.max(maxDifficulty, def.escapeDifficulty());
- }
- }
- }
- if (maxDifficulty >= 0) return maxDifficulty;
- }
- }
- return 100;
-}
-```
-
-- [ ] **Step 3: Verify compilation**
-
-Run: `cd /home/user/Documents/Projet/Open-TiedUp! && make build 2>&1 | tail -5`
-Expected: BUILD SUCCESSFUL
-
-- [ ] **Step 4: Commit**
-
-```bash
-git add src/main/java/com/tiedup/remake/v2/bondage/component/ResistanceComponent.java
-git add src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenBondageItem.java
-git commit -m "feat(D-01): implement ResistanceComponent, fixes I-03 MAX scan for stack-aware items"
-```
-
----
-
-### Task 9: Implement GaggingComponent
-
-**Files:**
-- Modify: `src/main/java/com/tiedup/remake/v2/bondage/component/GaggingComponent.java`
-
-- [ ] **Step 1: Implement gagging logic**
-
-```java
-package com.tiedup.remake.v2.bondage.component;
-
-import com.google.gson.JsonObject;
-
-/**
- * Component: gagging behavior for data-driven items.
- * Replaces IHasGaggingEffect for data-driven items.
- *
- * JSON config:
- * {"gagging": {"comprehension": 0.2, "range": 10.0}}
- */
-public class GaggingComponent implements IItemComponent {
-
- private final double comprehension;
- private final double range;
-
- private GaggingComponent(double comprehension, double range) {
- this.comprehension = comprehension;
- this.range = range;
- }
-
- public static IItemComponent fromJson(JsonObject config) {
- double comprehension = 0.2; // default: 20% understandable
- double range = 10.0; // default: 10 blocks
- if (config.has("comprehension")) {
- comprehension = config.get("comprehension").getAsDouble();
- }
- if (config.has("range")) {
- range = config.get("range").getAsDouble();
- }
- return new GaggingComponent(comprehension, range);
- }
-
- /**
- * How much of the gagged speech is comprehensible (0.0 = nothing, 1.0 = full).
- */
- public double getComprehension() {
- return comprehension;
- }
-
- /**
- * Maximum range in blocks where muffled speech can be heard.
- */
- public double getRange() {
- return range;
- }
-}
-```
-
-- [ ] **Step 2: Verify compilation**
-
-Run: `cd /home/user/Documents/Projet/Open-TiedUp! && make build 2>&1 | tail -5`
-Expected: BUILD SUCCESSFUL
-
-- [ ] **Step 3: Commit**
-
-```bash
-git add src/main/java/com/tiedup/remake/v2/bondage/component/GaggingComponent.java
-git commit -m "feat(D-01): implement GaggingComponent with comprehension and range"
-```
-
----
-
-### Task 10: Create a test item JSON using components
-
-**Files:**
-- Create: `src/main/resources/data/tiedup/tiedup_items/test_gag.json`
-
-- [ ] **Step 1: Create a JSON definition that uses the new component system**
-
-```json
-{
- "type": "tiedup:bondage_item",
- "display_name": "Test Ball Gag",
- "model": "tiedup:models/gltf/v2/handcuffs/cuffs_prototype.glb",
- "regions": ["MOUTH"],
- "animation_bones": {
- "idle": []
- },
- "pose_priority": 10,
- "escape_difficulty": 3,
- "lockable": true,
- "components": {
- "lockable": {
- "lock_resistance": 200
- },
- "resistance": {
- "base": 80
- },
- "gagging": {
- "comprehension": 0.15,
- "range": 8.0
- }
- }
-}
-```
-
-- [ ] **Step 2: Verify the mod loads without errors**
-
-Run: `cd /home/user/Documents/Projet/Open-TiedUp! && make build 2>&1 | tail -5`
-Expected: BUILD SUCCESSFUL
-
-Check that the JSON parses by searching for component-related log output in the run logs (manual verification — start the game client with `make run`, check for errors in log).
-
-- [ ] **Step 3: Commit**
-
-```bash
-git add src/main/resources/data/tiedup/tiedup_items/test_gag.json
-git commit -m "feat(D-01): add test_gag.json demonstrating component system"
-```
-
----
-
-### Task 11: Verify and clean up
-
-- [ ] **Step 1: Full build verification**
-
-Run: `cd /home/user/Documents/Projet/Open-TiedUp! && make rebuild 2>&1 | tail -10`
-Expected: BUILD SUCCESSFUL with zero errors
-
-- [ ] **Step 2: Verify no regressions in existing items**
-
-Existing data-driven items (in `data/tiedup/tiedup_items/`) should continue working without the `"components"` field. The parser should handle missing components gracefully (empty map).
-
-- [ ] **Step 3: Reindex MCP**
-
-Run the MCP reindex to update the symbol table with new classes.
-
-- [ ] **Step 4: Final commit**
-
-```bash
-git add -A
-git commit -m "feat(D-01): Phase 1 complete - data-driven item component system
-
-Adds IItemComponent interface, ComponentType enum, ComponentHolder container,
-and 3 core components (LockableComponent, ResistanceComponent, GaggingComponent).
-
-Components are declared in item JSON 'components' field, parsed by DataDrivenItemParser,
-instantiated by DataDrivenItemRegistry, and delegated by DataDrivenBondageItem.
-
-Existing items without components continue to work unchanged."
-```
--
2.49.1