From 2504b7d657cc35a18003c7a3c99e2a66c3c69f74 Mon Sep 17 00:00:00 2001 From: NotEvil Date: Wed, 15 Apr 2026 01:55:16 +0200 Subject: [PATCH 1/6] =?UTF-8?q?feat(D-01/D):=20V1=20cleanup=20=E2=80=94=20?= =?UTF-8?q?delete=2028=20files,=20~5400=20lines=20removed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit D1: ThreadLocal alert suppression moved from ItemCollar to CollarHelper. onCollarRemoved() logic (kidnapper alert) moved to CollarHelper. D2+D3: Deleted 17 V1 item classes + 4 V1-only interfaces: ItemBind, ItemGag, ItemBlindfold, ItemCollar, ItemEarplugs, ItemMittens, ItemColor, ItemClassicCollar, ItemShockCollar, ItemShockCollarAuto, ItemGpsCollar, ItemChokeCollar, ItemHood, ItemMedicalGag, IBondageItem, IHasGaggingEffect, IHasBlindingEffect, IAdjustable D4: KidnapperTheme/KidnapperItemSelector/DispenserBehaviors migrated from variant enums to string-based DataDrivenItemRegistry IDs. D5: Deleted 11 variant enums + Generic* factories + ItemBallGag3D: BindVariant, GagVariant, BlindfoldVariant, EarplugsVariant, MittensVariant, GenericBind, GenericGag, GenericBlindfold, GenericEarplugs, GenericMittens D6: ModItems cleaned — all V1 bondage registrations removed. D7: ModCreativeTabs rewritten — iterates DataDrivenItemRegistry. D8+D9: All V2 helpers cleaned (V1 fallbacks removed), orphan imports removed. Zero V1 bondage code references remain (only Javadoc comments). All bondage items are now data-driven via 47 JSON definitions. --- 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 | 183 +++ .../2026-04-13-d01-phase1-component-system.md | 869 ++++++++++ .../blocks/entity/BondageItemBlockEntity.java | 29 +- .../entity/TrappedChestBlockEntity.java | 17 +- .../events/SelfBondageInputHandler.java | 5 +- .../client/gui/widgets/SlaveEntryWidget.java | 20 +- .../remake/commands/KidnapSetCommand.java | 23 +- .../tiedup/remake/commands/NPCCommand.java | 4 +- .../subcommands/BondageSubCommand.java | 6 +- .../mca/capability/MCAKidnappedAdapter.java | 7 +- .../remake/dialogue/GagTalkManager.java | 5 - .../remake/dispenser/DispenserBehaviors.java | 68 +- .../GenericBondageDispenseBehavior.java | 35 +- .../remake/entities/AbstractTiedUpNpc.java | 10 +- .../entities/EntityKidnapperMerchant.java | 95 +- .../entities/KidnapperCaptureEquipment.java | 3 +- .../entities/KidnapperItemSelector.java | 283 +--- .../remake/entities/KidnapperJobManager.java | 3 +- .../remake/entities/KidnapperTheme.java | 163 +- .../ai/maid/MaidDeliverCaptiveGoal.java | 13 +- .../ai/personality/NpcStruggleGoal.java | 14 +- .../armorstand/ArmorStandBondageHelper.java | 9 +- .../components/NpcEquipmentManager.java | 6 +- .../components/KidnapperAppearance.java | 12 +- .../master/components/MasterPetManager.java | 4 +- .../events/system/ChatEventHandler.java | 10 +- .../com/tiedup/remake/items/GenericBind.java | 68 - .../tiedup/remake/items/GenericBlindfold.java | 37 - .../tiedup/remake/items/GenericEarplugs.java | 37 - .../com/tiedup/remake/items/GenericGag.java | 72 - .../com/tiedup/remake/items/GenericKnife.java | 7 +- .../tiedup/remake/items/GenericMittens.java | 37 - .../tiedup/remake/items/ItemChokeCollar.java | 154 -- .../remake/items/ItemClassicCollar.java | 21 - .../tiedup/remake/items/ItemCommandWand.java | 6 +- .../tiedup/remake/items/ItemGpsCollar.java | 369 ----- .../tiedup/remake/items/ItemGpsLocator.java | 11 +- .../com/tiedup/remake/items/ItemHood.java | 35 - .../java/com/tiedup/remake/items/ItemKey.java | 9 +- .../com/tiedup/remake/items/ItemLockpick.java | 12 +- .../tiedup/remake/items/ItemMedicalGag.java | 24 - .../tiedup/remake/items/ItemShockCollar.java | 133 -- .../remake/items/ItemShockCollarAuto.java | 58 - .../remake/items/ItemShockerController.java | 17 +- .../tiedup/remake/items/ModCreativeTabs.java | 144 +- .../com/tiedup/remake/items/ModItems.java | 229 +-- .../remake/items/base/AdjustmentHelper.java | 21 +- .../tiedup/remake/items/base/BindVariant.java | 90 -- .../remake/items/base/BlindfoldVariant.java | 48 - .../remake/items/base/EarplugsVariant.java | 33 - .../tiedup/remake/items/base/GagVariant.java | 163 -- .../tiedup/remake/items/base/IAdjustable.java | 49 - .../remake/items/base/IBondageItem.java | 102 -- .../remake/items/base/IHasBlindingEffect.java | 33 - .../remake/items/base/IHasGaggingEffect.java | 33 - .../tiedup/remake/items/base/ItemBind.java | 637 -------- .../remake/items/base/ItemBlindfold.java | 89 -- .../tiedup/remake/items/base/ItemCollar.java | 1407 ----------------- .../remake/items/base/ItemEarplugs.java | 90 -- .../com/tiedup/remake/items/base/ItemGag.java | 93 -- .../tiedup/remake/items/base/ItemMittens.java | 72 - .../remake/items/base/ItemOwnerTarget.java | 6 +- .../remake/items/base/MittensVariant.java | 35 - .../items/bondage3d/gags/ItemBallGag3D.java | 78 - .../remake/network/action/PacketTighten.java | 7 +- .../remake/network/item/PacketAdjustItem.java | 3 +- .../network/item/PacketAdjustRemote.java | 10 +- .../minigame/PacketLockpickAttempt.java | 4 +- .../PacketRequestNpcInventory.java | 7 +- .../selfbondage/PacketSelfBondage.java | 134 +- .../network/slave/PacketSlaveItemManage.java | 23 +- .../network/trader/PacketBuyCaptive.java | 13 +- .../prison/service/PrisonerService.java | 5 +- .../state/components/PlayerShockCollar.java | 99 +- .../state/components/PlayerStateQuery.java | 14 +- .../state/struggle/StruggleAccessory.java | 9 +- .../remake/state/struggle/StruggleBinds.java | 7 - .../remake/state/struggle/StruggleCollar.java | 5 - .../remake/util/BondageItemLoaderUtility.java | 24 +- .../remake/v2/bondage/BindModeHelper.java | 5 +- .../remake/v2/bondage/CollarHelper.java | 111 +- .../remake/v2/bondage/PoseTypeHelper.java | 5 - .../bondage/component/OwnershipComponent.java | 3 +- .../remake/v2/furniture/EntityFurniture.java | 7 +- 89 files changed, 2647 insertions(+), 5423 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 delete mode 100644 src/main/java/com/tiedup/remake/items/GenericBind.java delete mode 100644 src/main/java/com/tiedup/remake/items/GenericBlindfold.java delete mode 100644 src/main/java/com/tiedup/remake/items/GenericEarplugs.java delete mode 100644 src/main/java/com/tiedup/remake/items/GenericGag.java delete mode 100644 src/main/java/com/tiedup/remake/items/GenericMittens.java delete mode 100644 src/main/java/com/tiedup/remake/items/ItemChokeCollar.java delete mode 100644 src/main/java/com/tiedup/remake/items/ItemClassicCollar.java delete mode 100644 src/main/java/com/tiedup/remake/items/ItemGpsCollar.java delete mode 100644 src/main/java/com/tiedup/remake/items/ItemHood.java delete mode 100644 src/main/java/com/tiedup/remake/items/ItemMedicalGag.java delete mode 100644 src/main/java/com/tiedup/remake/items/ItemShockCollar.java delete mode 100644 src/main/java/com/tiedup/remake/items/ItemShockCollarAuto.java delete mode 100644 src/main/java/com/tiedup/remake/items/base/BindVariant.java delete mode 100644 src/main/java/com/tiedup/remake/items/base/BlindfoldVariant.java delete mode 100644 src/main/java/com/tiedup/remake/items/base/EarplugsVariant.java delete mode 100644 src/main/java/com/tiedup/remake/items/base/GagVariant.java delete mode 100644 src/main/java/com/tiedup/remake/items/base/IAdjustable.java delete mode 100644 src/main/java/com/tiedup/remake/items/base/IBondageItem.java delete mode 100644 src/main/java/com/tiedup/remake/items/base/IHasBlindingEffect.java delete mode 100644 src/main/java/com/tiedup/remake/items/base/IHasGaggingEffect.java delete mode 100644 src/main/java/com/tiedup/remake/items/base/ItemBind.java delete mode 100644 src/main/java/com/tiedup/remake/items/base/ItemBlindfold.java delete mode 100644 src/main/java/com/tiedup/remake/items/base/ItemCollar.java delete mode 100644 src/main/java/com/tiedup/remake/items/base/ItemEarplugs.java delete mode 100644 src/main/java/com/tiedup/remake/items/base/ItemGag.java delete mode 100644 src/main/java/com/tiedup/remake/items/base/ItemMittens.java delete mode 100644 src/main/java/com/tiedup/remake/items/base/MittensVariant.java delete mode 100644 src/main/java/com/tiedup/remake/items/bondage3d/gags/ItemBallGag3D.java 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..c8a946f --- /dev/null +++ b/docs/plans/D01-branch-E-resistance-rework.md @@ -0,0 +1,183 @@ +# D-01 Branch E : Resistance & Lock System Rework + +> **Prérequis :** Branch D (V1 cleanup) mergée. +> **Branche :** `feature/d01-branch-e-resistance` +> **Objectif :** Redesign complet du système de résistance/lock. + +--- + +## Nouveau modèle + +### Principes + +1. **La résistance vient de l'item** — définie dans le JSON via `ResistanceComponent`, point final. +2. **Le lock est binaire** — on/off. Pas de "lock resistance" séparée. Le lock *active* la nécessité de struggle pour les items non-ARMS. +3. **ARMS = toujours actif** — un bind aux bras nécessite toujours un struggle pour s'en libérer soi-même, locké ou non. +4. **Non-ARMS + pas locké = libre** — un gag/blindfold/collar non-locké peut être retiré librement (par soi-même ou un autre joueur). +5. **Non-ARMS + locké = struggle requis** — le lock active la résistance de l'item. +6. **Un autre joueur peut aider** — retirer un item non-locké sur un autre joueur ne nécessite pas de struggle (aide). + +### Matrice de struggle + +| Région | Locké ? | Self-remove | Autre joueur remove | +|--------|---------|-------------|---------------------| +| ARMS | Non | Struggle (résistance item) | Libre (aide) | +| ARMS | Oui | Struggle (résistance item) | Struggle (résistance item) | +| Non-ARMS | Non | Libre | Libre | +| Non-ARMS | Oui | Struggle (résistance item) | Struggle (résistance item) | + +### Items organiques (slime, vine, web, tape) + +Ces items sont "lockés par nature" — pas de padlock possible mais impossible à retirer sans struggle. + +**Option :** Nouveau composant `BuiltInLockComponent` dans le JSON : +```json +"components": { + "resistance": {"id": "slime"}, + "built_in_lock": {} +} +``` + +`BuiltInLockComponent` : +- `blocksUnequip()` retourne `true` (comme un lock, mais sans padlock) +- `ILockable.canAttachPadlock()` retourne `false` (déjà le cas pour les organiques) +- L'item se comporte comme un ARMS bind : toujours struggle required + +**Alternative :** Flag `"always_locked": true` sur la definition JSON. Plus simple, pas besoin de nouveau composant. + +--- + +## Problèmes actuels que ce rework corrige + +### P1. Singleton MAX scan + +`DataDrivenBondageItem.getBaseResistance(LivingEntity)` retourne le MAX de tous les items data-driven équipés. Un gag résistance 50 hérite de la résistance 200 du chain bind. + +**Fix :** Initialiser `currentResistance` dans le NBT à l'equip depuis `ResistanceComponent.getBaseResistance()`. Plus jamais de fallback au MAX scan runtime. Le `getBaseResistance(LivingEntity)` du singleton devient un no-op/fallback qui n'est plus utilisé par le struggle. + +### P2. isItemLocked() dead code + +`StruggleState.struggle()` ne call jamais `isItemLocked()`. Le x10 penalty n'est jamais appliqué. + +**Fix :** Supprimer le concept de "locked penalty". Avec le nouveau modèle, le lock **active** le struggle, il ne le ralentit pas. Si l'item est locké, il faut struggle avec la résistance complète de l'item. Si non locké (et non-ARMS), pas de struggle du tout. + +### P3. Lock resistance / item resistance déconnectés + +`ILockable.getLockResistance()` vs `IHasResistance.getBaseResistance()` sont deux systèmes indépendants. + +**Fix :** Supprimer `ILockable.getLockResistance()` / `getCurrentLockResistance()` / `setCurrentLockResistance()` / `initializeLockResistance()` / `clearLockResistance()`. La résistance du lockpick minigame utilise directement la résistance de l'item (ou un multiplicateur fixe). + +### P4. Dice-roll ignore le lock + +**Fix :** Avec le nouveau modèle, le dice-roll ne change pas. C'est `canStruggle()` qui gate l'accès : +```java +// StruggleBinds.canStruggle() +// ARMS: toujours struggle-able (self) +return true; + +// StruggleCollar/StruggleAccessory.canStruggle() +// Non-ARMS: seulement si locké +return isLocked(stack) || hasBuiltInLock(stack); +``` + +--- + +## Bugs pré-existants à corriger dans cette branche + +### B1. V1 ItemCollar.onUnequipped() — suppressed path skip unregister + +Quand `isRemovalAlertSuppressed()` est true, `ItemCollar.onUnequipped()` return early SANS appeler `CollarRegistry.unregisterWearer()`. Entrées fantômes persistées. + +**Fichier :** `items/base/ItemCollar.java` lignes 1382-1395 +**Fix :** Ajouter `unregisterWearer()` dans le branch suppressed. + +### B2. DataDrivenItemRegistry.clear() pas synchronisé + +`clear()` écrit `SNAPSHOT = EMPTY` sans acquérir `RELOAD_LOCK`. Race avec `mergeAll()`. + +**Fichier :** `v2/bondage/datadriven/DataDrivenItemRegistry.java` ligne 142 +**Fix :** Synchroniser sur `RELOAD_LOCK`. + +### B3. V2TyingPlayerTask.heldStack reference stale + +Le held item peut être remplacé entre début et fin du tying → item dupliqué. + +**Fichier :** `tasks/V2TyingPlayerTask.java` ligne 80 +**Fix :** Valider `heldStack` non-vide et matching avant equip dans `onComplete()`. + +### B4. PlayerShockCollar ignore complètement les V2 collars + +`checkAutoShockCollar()` dispatche exclusivement sur `instanceof ItemShockCollarAuto` et `instanceof ItemGpsCollar`. Les V2 data-driven collars avec ShockComponent ou GpsComponent ne déclenchent jamais les auto-shocks ni l'enforcement de zones GPS. + +**Fichier :** `state/components/PlayerShockCollar.java` lignes 139-189 +**Fix :** Utiliser `CollarHelper.canShock()`, `CollarHelper.getShockInterval()`, `CollarHelper.hasGPS()` pour la détection, avec fallback V1 pour les méthodes V1-specific (`getSafeSpots()`). + +### B5. EntityKidnapperMerchant.remove() memory leak + +`remove()` appelle `tradingPlayers.clear()` mais ne nettoie PAS la `playerToMerchant` ConcurrentHashMap statique. Entrées stales accumulées sur les serveurs long-running. + +**Fichier :** `entities/EntityKidnapperMerchant.java` ligne 966-981 +**Fix :** Itérer `tradingPlayers` et appeler `playerToMerchant.remove(uuid)` avant le clear. + +### B6. Timer division potentiellement inversée (auto-shock) + +`PlayerShockCollar.java` lignes 153-155 : `collarShock.getInterval() / GameConstants.TICKS_PER_SECOND`. Si Timer attend des ticks, la division réduit l'intervalle de 20x (shock toutes les 0.25s au lieu de 5s). + +**Fichier :** `state/components/PlayerShockCollar.java` lignes 153-155 et 179-182 +**Fix :** Vérifier le contrat du constructeur `Timer`. Si il attend des ticks, supprimer la division. + +### B7. StruggleState.isItemLocked() dead code + +`StruggleState.struggle()` ne call JAMAIS `isItemLocked()`. Le penalty x10 pour les items padlockés n'est jamais appliqué. + +**Fichier :** `state/struggle/StruggleState.java` ligne 53-133 +**Fix :** Inclus dans le rework E2 (nouveau modèle resistance/lock). + +--- + +## Tâches + +### E1. Initialiser currentResistance à l'equip + +Dans `DataDrivenBondageItem.onEquipped()` et les hooks V1 `onEquipped()` : +- Lire `ResistanceComponent.getBaseResistance()` (ou `IHasResistance.getBaseResistance()` pour V1) +- Écrire immédiatement dans le NBT via `setCurrentResistance(stack, base)` +- Élimine le MAX scan comme source d'initialisation + +### E2. Refactor canStruggle() — nouveau modèle + +- `StruggleBinds.canStruggle()` : ARMS → toujours true (self) si item existe +- Nouveau `StruggleAccessory` (ou refactor de StruggleCollar) : non-ARMS → true seulement si locké ou built-in lock +- Supprimer `isItemLocked()` penalty (dead code de toute façon) + +### E3. "Aide" — remove non-locké par un autre joueur + +Modifier `AbstractV2BondageItem.interactLivingEntity()` : +- Si clic sur un joueur qui porte l'item ET item non-locké ET clicker n'a pas l'item en main → retirer l'item (aide) +- Ou via un packet dédié (clic droit main vide sur joueur attaché) + +### E4. BuiltInLockComponent ou flag `always_locked` + +Pour les items organiques qui ne peuvent pas avoir de padlock mais nécessitent un struggle. + +### E5. Cleanup ILockable — supprimer lock resistance + +Supprimer : `getLockResistance()`, `getCurrentLockResistance()`, `setCurrentLockResistance()`, `initializeLockResistance()`, `clearLockResistance()`. + +Le lockpick minigame utilise la résistance de l'item directement (ou un multiplicateur config). + +### E6. Fix bugs pré-existants (B1, B2, B3) + +--- + +## Vérification + +- [ ] V2 bind résistance 50 + V2 gag résistance 80 : chacun a sa propre résistance (pas MAX) +- [ ] Gag non-locké → retirable sans struggle +- [ ] Gag locké → struggle avec résistance du gag +- [ ] Bind ARMS non-locké → self-struggle requis, autre joueur peut aider (libre) +- [ ] Bind ARMS locké → self-struggle requis, autre joueur aussi struggle +- [ ] Slime bind (built-in lock) → struggle obligatoire, pas de padlock possible +- [ ] `currentResistance` initialisé dans NBT dès l'equip +- [ ] CollarRegistry clean après removals légitimes +- [ ] Pas de duplication d'item via tying task 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 279bb5c..e02cbce 100644 --- a/src/main/java/com/tiedup/remake/blocks/entity/BondageItemBlockEntity.java +++ b/src/main/java/com/tiedup/remake/blocks/entity/BondageItemBlockEntity.java @@ -1,10 +1,5 @@ package com.tiedup.remake.blocks.entity; -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; @@ -199,45 +194,43 @@ public abstract class BondageItemBlockEntity @Override public void readBondageData(CompoundTag tag) { - // Read bind with type validation (V1 ItemBind or V2 ARMS-region item) + // Read bind with type validation (V2 ARMS-region item) if (tag.contains("bind")) { ItemStack bindStack = ItemStack.of(tag.getCompound("bind")); - if (!bindStack.isEmpty() && (bindStack.getItem() instanceof ItemBind || BindModeHelper.isBindItem(bindStack))) { + if (!bindStack.isEmpty() && BindModeHelper.isBindItem(bindStack)) { this.bind = bindStack; } } - // Read gag with type validation (V1 ItemGag or V2 GAGGING component) + // Read gag with type validation (V2 GAGGING component) if (tag.contains("gag")) { ItemStack gagStack = ItemStack.of(tag.getCompound("gag")); - if (!gagStack.isEmpty() && (gagStack.getItem() instanceof ItemGag - || DataDrivenBondageItem.getComponent(gagStack, ComponentType.GAGGING, GaggingComponent.class) != null)) { + if (!gagStack.isEmpty() + && DataDrivenBondageItem.getComponent(gagStack, ComponentType.GAGGING, GaggingComponent.class) != null) { this.gag = gagStack; } } - // Read blindfold with type validation (V1 ItemBlindfold or V2 EYES-region item) + // Read blindfold with type validation (V2 EYES-region item) if (tag.contains("blindfold")) { ItemStack blindfoldStack = ItemStack.of(tag.getCompound("blindfold")); - if (!blindfoldStack.isEmpty() && (blindfoldStack.getItem() instanceof ItemBlindfold - || isDataDrivenForRegion(blindfoldStack, BodyRegionV2.EYES))) { + if (!blindfoldStack.isEmpty() && isDataDrivenForRegion(blindfoldStack, BodyRegionV2.EYES)) { this.blindfold = blindfoldStack; } } - // Read earplugs with type validation (V1 ItemEarplugs or V2 EARS-region item) + // Read earplugs with type validation (V2 EARS-region item) if (tag.contains("earplugs")) { ItemStack earplugsStack = ItemStack.of(tag.getCompound("earplugs")); - if (!earplugsStack.isEmpty() && (earplugsStack.getItem() instanceof ItemEarplugs - || isDataDrivenForRegion(earplugsStack, BodyRegionV2.EARS))) { + if (!earplugsStack.isEmpty() && isDataDrivenForRegion(earplugsStack, BodyRegionV2.EARS)) { this.earplugs = earplugsStack; } } - // Read collar with type validation (V1 ItemCollar or V2 collar) + // Read collar with type validation (V2 collar) if (tag.contains("collar")) { ItemStack collarStack = ItemStack.of(tag.getCompound("collar")); - if (!collarStack.isEmpty() && (collarStack.getItem() instanceof ItemCollar || CollarHelper.isCollar(collarStack))) { + if (!collarStack.isEmpty() && CollarHelper.isCollar(collarStack)) { this.collar = collarStack; } } 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 73dd2da..add7b86 100644 --- a/src/main/java/com/tiedup/remake/blocks/entity/TrappedChestBlockEntity.java +++ b/src/main/java/com/tiedup/remake/blocks/entity/TrappedChestBlockEntity.java @@ -1,10 +1,5 @@ package com.tiedup.remake.blocks.entity; -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; @@ -57,7 +52,7 @@ public class TrappedChestBlockEntity @Override public void setBind(ItemStack stack) { - if (stack.isEmpty() || stack.getItem() instanceof ItemBind || BindModeHelper.isBindItem(stack)) { + if (stack.isEmpty() || BindModeHelper.isBindItem(stack)) { this.bind = stack; setChangedAndSync(); } @@ -70,7 +65,7 @@ public class TrappedChestBlockEntity @Override public void setGag(ItemStack stack) { - if (stack.isEmpty() || stack.getItem() instanceof ItemGag + if (stack.isEmpty() || DataDrivenBondageItem.getComponent(stack, ComponentType.GAGGING, GaggingComponent.class) != null) { this.gag = stack; setChangedAndSync(); @@ -84,8 +79,7 @@ public class TrappedChestBlockEntity @Override public void setBlindfold(ItemStack stack) { - if (stack.isEmpty() || stack.getItem() instanceof ItemBlindfold - || isDataDrivenForRegion(stack, BodyRegionV2.EYES)) { + if (stack.isEmpty() || isDataDrivenForRegion(stack, BodyRegionV2.EYES)) { this.blindfold = stack; setChangedAndSync(); } @@ -98,8 +92,7 @@ public class TrappedChestBlockEntity @Override public void setEarplugs(ItemStack stack) { - if (stack.isEmpty() || stack.getItem() instanceof ItemEarplugs - || isDataDrivenForRegion(stack, BodyRegionV2.EARS)) { + if (stack.isEmpty() || isDataDrivenForRegion(stack, BodyRegionV2.EARS)) { this.earplugs = stack; setChangedAndSync(); } @@ -112,7 +105,7 @@ public class TrappedChestBlockEntity @Override public void setCollar(ItemStack stack) { - if (stack.isEmpty() || stack.getItem() instanceof ItemCollar || CollarHelper.isCollar(stack)) { + if (stack.isEmpty() || CollarHelper.isCollar(stack)) { this.collar = stack; setChangedAndSync(); } 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 b876fbd..79efbb1 100644 --- a/src/main/java/com/tiedup/remake/client/events/SelfBondageInputHandler.java +++ b/src/main/java/com/tiedup/remake/client/events/SelfBondageInputHandler.java @@ -1,6 +1,5 @@ package com.tiedup.remake.client.events; -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; @@ -166,8 +165,8 @@ public class SelfBondageInputHandler { 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)) { + // Collar cannot be self-equipped (V2 ownership component) + if (CollarHelper.isCollar(stack)) { return false; } 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 00fdb2d..ab89a3a 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 @@ -4,7 +4,6 @@ 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.v2.bondage.CollarHelper; import com.tiedup.remake.state.IBondageState; import com.tiedup.remake.v2.BodyRegionV2; @@ -370,9 +369,8 @@ public class SlaveEntryWidget // GPS zone status (right of health) if (hasGPSCollar()) { ItemStack collarStack = slave.getEquipment(BodyRegionV2.NECK); - if (collarStack.getItem() instanceof ItemGpsCollar gps) { + if (CollarHelper.hasGPS(collarStack)) { boolean inSafeZone = isInAnySafeZone( - gps, collarStack, entity ); @@ -570,17 +568,25 @@ public class SlaveEntryWidget } private boolean isInAnySafeZone( - ItemGpsCollar gps, ItemStack collarStack, LivingEntity entity ) { if (!CollarHelper.isActive(collarStack)) return true; - var safeSpots = gps.getSafeSpots(collarStack); + // Read safe spots from NBT + net.minecraft.nbt.CompoundTag tag = collarStack.getTag(); + if (tag == null || !tag.contains("safeSpots", net.minecraft.nbt.Tag.TAG_LIST)) return true; + net.minecraft.nbt.ListTag safeSpots = tag.getList("safeSpots", net.minecraft.nbt.Tag.TAG_COMPOUND); if (safeSpots.isEmpty()) return true; - for (var spot : safeSpots) { - if (spot.isInside(entity)) { + for (int i = 0; i < safeSpots.size(); i++) { + net.minecraft.nbt.CompoundTag spot = safeSpots.getCompound(i); + double x = spot.getDouble("x"); + double y = spot.getDouble("y"); + double z = spot.getDouble("z"); + int radius = spot.contains("radius") ? spot.getInt("radius") : 50; + double dist = entity.distanceToSqr(x, y, z); + if (dist <= (double) radius * radius) { return true; } } diff --git a/src/main/java/com/tiedup/remake/commands/KidnapSetCommand.java b/src/main/java/com/tiedup/remake/commands/KidnapSetCommand.java index a269e13..759a819 100644 --- a/src/main/java/com/tiedup/remake/commands/KidnapSetCommand.java +++ b/src/main/java/com/tiedup/remake/commands/KidnapSetCommand.java @@ -93,16 +93,19 @@ public class KidnapSetCommand { given += giveDataDrivenItems(player, "classic_blindfold", 4); given += giveDataDrivenItems(player, "blindfold_mask", 2); - // Collars - given += giveItem( - player, - new ItemStack(ModItems.CLASSIC_COLLAR.get(), 4) - ); - given += giveItem( - player, - new ItemStack(ModItems.SHOCK_COLLAR.get(), 2) - ); - given += giveItem(player, new ItemStack(ModItems.GPS_COLLAR.get(), 2)); + // Collars (data-driven) + ItemStack classicCollars = com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem.createStack( + new net.minecraft.resources.ResourceLocation("tiedup", "classic_collar")); + classicCollars.setCount(4); + given += giveItem(player, classicCollars); + ItemStack shockCollars = com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem.createStack( + new net.minecraft.resources.ResourceLocation("tiedup", "shock_collar")); + shockCollars.setCount(2); + given += giveItem(player, shockCollars); + ItemStack gpsCollars = com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem.createStack( + new net.minecraft.resources.ResourceLocation("tiedup", "gps_collar")); + gpsCollars.setCount(2); + given += giveItem(player, gpsCollars); // Tools given += giveItem( diff --git a/src/main/java/com/tiedup/remake/commands/NPCCommand.java b/src/main/java/com/tiedup/remake/commands/NPCCommand.java index d2960a1..22968fb 100644 --- a/src/main/java/com/tiedup/remake/commands/NPCCommand.java +++ b/src/main/java/com/tiedup/remake/commands/NPCCommand.java @@ -603,7 +603,7 @@ public class NPCCommand { npc.equip( BodyRegionV2.NECK, - new ItemStack(ModItems.CLASSIC_COLLAR.get()) + DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "classic_collar")) ); context .getSource() @@ -658,7 +658,7 @@ public class NPCCommand { 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()), + DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "classic_collar")), 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 3e53e76..20b8301 100644 --- a/src/main/java/com/tiedup/remake/commands/subcommands/BondageSubCommand.java +++ b/src/main/java/com/tiedup/remake/commands/subcommands/BondageSubCommand.java @@ -493,7 +493,7 @@ public class BondageSubCommand { return 0; } - ItemStack collar = new ItemStack(ModItems.CLASSIC_COLLAR.get()); + ItemStack collar = com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem.createStack(new net.minecraft.resources.ResourceLocation("tiedup", "classic_collar")); if (context.getSource().getEntity() instanceof ServerPlayer executor) { CollarHelper.addOwner(collar, executor); @@ -1078,7 +1078,7 @@ public class BondageSubCommand { } if (!state.hasCollar()) { - ItemStack collar = new ItemStack(ModItems.CLASSIC_COLLAR.get()); + ItemStack collar = com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem.createStack(new net.minecraft.resources.ResourceLocation("tiedup", "classic_collar")); if ( context.getSource().getEntity() instanceof ServerPlayer executor ) { @@ -1163,7 +1163,7 @@ public class BondageSubCommand { state.putBlindfoldOn(blindfold); } if (!state.hasCollar()) { - ItemStack collar = new ItemStack(ModItems.CLASSIC_COLLAR.get()); + ItemStack collar = com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem.createStack(new net.minecraft.resources.ResourceLocation("tiedup", "classic_collar")); if ( context.getSource().getEntity() instanceof ServerPlayer executor ) { 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 2d10de1..698519b 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 @@ -8,7 +8,6 @@ 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; import com.tiedup.remake.state.ICaptor; import com.tiedup.remake.state.IRestrainable; import com.tiedup.remake.state.IRestrainableEntity; @@ -280,16 +279,14 @@ public class MCAKidnappedAdapter implements IRestrainable { public boolean hasGaggingEffect() { ItemStack gag = cap.getGag(); 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; + return DataDrivenBondageItem.getComponent(gag, ComponentType.GAGGING, GaggingComponent.class) != null; } @Override public boolean hasBlindingEffect() { ItemStack blindfold = cap.getBlindfold(); 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; + return DataDrivenBondageItem.getComponent(blindfold, ComponentType.BLINDING, BlindingComponent.class) != null; } @Override diff --git a/src/main/java/com/tiedup/remake/dialogue/GagTalkManager.java b/src/main/java/com/tiedup/remake/dialogue/GagTalkManager.java index 585e687..f9b27e7 100644 --- a/src/main/java/com/tiedup/remake/dialogue/GagTalkManager.java +++ b/src/main/java/com/tiedup/remake/dialogue/GagTalkManager.java @@ -3,7 +3,6 @@ package com.tiedup.remake.dialogue; 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; @@ -66,8 +65,6 @@ public class GagTalkManager { gagStack, ComponentType.GAGGING, GaggingComponent.class); if (gaggingComp != null && gaggingComp.getMaterial() != null) { material = gaggingComp.getMaterial(); - } else if (gagStack.getItem() instanceof ItemGag gag) { - material = gag.getGagMaterial(); } // 1. EFFET DE SUFFOCATION (Si message trop long) @@ -528,8 +525,6 @@ public class GagTalkManager { gagStack, ComponentType.GAGGING, GaggingComponent.class); if (comp != null && comp.getMaterial() != null) { material = comp.getMaterial(); - } else if (gagStack.getItem() instanceof ItemGag gag) { - material = gag.getGagMaterial(); } } diff --git a/src/main/java/com/tiedup/remake/dispenser/DispenserBehaviors.java b/src/main/java/com/tiedup/remake/dispenser/DispenserBehaviors.java index bb8389a..e1ce1e3 100644 --- a/src/main/java/com/tiedup/remake/dispenser/DispenserBehaviors.java +++ b/src/main/java/com/tiedup/remake/dispenser/DispenserBehaviors.java @@ -2,16 +2,19 @@ package com.tiedup.remake.dispenser; import com.tiedup.remake.core.TiedUpMod; import com.tiedup.remake.items.ModItems; -import com.tiedup.remake.items.base.*; import net.minecraft.world.level.block.DispenserBlock; /** * Registration class for all TiedUp dispenser behaviors. * * Allows dispensers to: - * - Equip bondage items (binds, gags, blindfolds, collars, earplugs, clothes) on entities + * - Equip bondage items (via data-driven V2 system) on entities * - Shoot rope arrows * + * Note: V1 per-variant dispenser registrations have been removed. + * Data-driven bondage items use a single universal dispenser behavior + * registered via DataDrivenBondageItem system. + * * Based on original behaviors package from 1.12.2 */ public class DispenserBehaviors { @@ -25,72 +28,17 @@ public class DispenserBehaviors { "[DispenserBehaviors] Registering dispenser behaviors..." ); - registerBindBehaviors(); - registerGagBehaviors(); - registerBlindfoldBehaviors(); - registerCollarBehaviors(); - registerEarplugsBehaviors(); registerClothesBehaviors(); registerRopeArrowBehavior(); + // V2 data-driven bondage items register their own dispenser behaviors + // via DataDrivenBondageItem.registerDispenserBehaviors() + TiedUpMod.LOGGER.info( "[DispenserBehaviors] Dispenser behaviors registered!" ); } - private static void registerBindBehaviors() { - var behavior = GenericBondageDispenseBehavior.forBind(); - for (BindVariant variant : BindVariant.values()) { - DispenserBlock.registerBehavior( - ModItems.getBind(variant), - behavior - ); - } - } - - private static void registerGagBehaviors() { - var behavior = GenericBondageDispenseBehavior.forGag(); - for (GagVariant variant : GagVariant.values()) { - DispenserBlock.registerBehavior(ModItems.getGag(variant), behavior); - } - DispenserBlock.registerBehavior(ModItems.MEDICAL_GAG.get(), behavior); - DispenserBlock.registerBehavior(ModItems.HOOD.get(), behavior); - } - - private static void registerBlindfoldBehaviors() { - var behavior = GenericBondageDispenseBehavior.forBlindfold(); - for (BlindfoldVariant variant : BlindfoldVariant.values()) { - DispenserBlock.registerBehavior( - ModItems.getBlindfold(variant), - behavior - ); - } - } - - private static void registerCollarBehaviors() { - var behavior = GenericBondageDispenseBehavior.forCollar(); - DispenserBlock.registerBehavior( - ModItems.CLASSIC_COLLAR.get(), - behavior - ); - DispenserBlock.registerBehavior(ModItems.SHOCK_COLLAR.get(), behavior); - DispenserBlock.registerBehavior( - ModItems.SHOCK_COLLAR_AUTO.get(), - behavior - ); - DispenserBlock.registerBehavior(ModItems.GPS_COLLAR.get(), behavior); - } - - private static void registerEarplugsBehaviors() { - var behavior = GenericBondageDispenseBehavior.forEarplugs(); - for (EarplugsVariant variant : EarplugsVariant.values()) { - DispenserBlock.registerBehavior( - ModItems.getEarplugs(variant), - behavior - ); - } - } - private static void registerClothesBehaviors() { DispenserBlock.registerBehavior( ModItems.CLOTHES.get(), diff --git a/src/main/java/com/tiedup/remake/dispenser/GenericBondageDispenseBehavior.java b/src/main/java/com/tiedup/remake/dispenser/GenericBondageDispenseBehavior.java index c97b45a..ffa536f 100644 --- a/src/main/java/com/tiedup/remake/dispenser/GenericBondageDispenseBehavior.java +++ b/src/main/java/com/tiedup/remake/dispenser/GenericBondageDispenseBehavior.java @@ -1,16 +1,21 @@ package com.tiedup.remake.dispenser; -import com.tiedup.remake.items.base.*; import com.tiedup.remake.state.IBondageState; 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.function.BiConsumer; import java.util.function.Predicate; -import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; /** * Generic dispenser behavior for equipping bondage items. - * Replaces individual BindDispenseBehavior, GagDispenseBehavior, etc. + * Uses V2 data-driven item detection instead of V1 class checks. * * Use factory methods to create instances for each bondage type. */ @@ -18,23 +23,23 @@ public class GenericBondageDispenseBehavior extends EquipBondageDispenseBehavior { - private final Class itemClass; + private final Predicate itemCheck; private final Predicate canEquipCheck; private final BiConsumer equipAction; private GenericBondageDispenseBehavior( - Class itemClass, + Predicate itemCheck, Predicate canEquipCheck, BiConsumer equipAction ) { - this.itemClass = itemClass; + this.itemCheck = itemCheck; this.canEquipCheck = canEquipCheck; this.equipAction = equipAction; } @Override protected boolean isValidItem(ItemStack stack) { - return !stack.isEmpty() && itemClass.isInstance(stack.getItem()); + return !stack.isEmpty() && itemCheck.test(stack); } @Override @@ -53,7 +58,7 @@ public class GenericBondageDispenseBehavior public static GenericBondageDispenseBehavior forBind() { return new GenericBondageDispenseBehavior( - ItemBind.class, + BindModeHelper::isBindItem, state -> !state.isTiedUp(), (s, i) -> s.equip(BodyRegionV2.ARMS, i) ); @@ -61,7 +66,7 @@ public class GenericBondageDispenseBehavior public static GenericBondageDispenseBehavior forGag() { return new GenericBondageDispenseBehavior( - ItemGag.class, + stack -> DataDrivenBondageItem.getComponent(stack, ComponentType.GAGGING, GaggingComponent.class) != null, state -> !state.isGagged(), (s, i) -> s.equip(BodyRegionV2.MOUTH, i) ); @@ -69,7 +74,10 @@ public class GenericBondageDispenseBehavior public static GenericBondageDispenseBehavior forBlindfold() { return new GenericBondageDispenseBehavior( - ItemBlindfold.class, + stack -> { + DataDrivenItemDefinition def = DataDrivenItemRegistry.get(stack); + return def != null && def.occupiedRegions().contains(BodyRegionV2.EYES); + }, state -> !state.isBlindfolded(), (s, i) -> s.equip(BodyRegionV2.EYES, i) ); @@ -77,7 +85,7 @@ public class GenericBondageDispenseBehavior public static GenericBondageDispenseBehavior forCollar() { return new GenericBondageDispenseBehavior( - ItemCollar.class, + CollarHelper::isCollar, state -> !state.hasCollar(), (s, i) -> s.equip(BodyRegionV2.NECK, i) ); @@ -85,7 +93,10 @@ public class GenericBondageDispenseBehavior public static GenericBondageDispenseBehavior forEarplugs() { return new GenericBondageDispenseBehavior( - ItemEarplugs.class, + stack -> { + DataDrivenItemDefinition def = DataDrivenItemRegistry.get(stack); + return def != null && def.occupiedRegions().contains(BodyRegionV2.EARS); + }, state -> !state.hasEarplugs(), (s, i) -> s.equip(BodyRegionV2.EARS, i) ); diff --git a/src/main/java/com/tiedup/remake/entities/AbstractTiedUpNpc.java b/src/main/java/com/tiedup/remake/entities/AbstractTiedUpNpc.java index 85f3945..f8d3f83 100644 --- a/src/main/java/com/tiedup/remake/entities/AbstractTiedUpNpc.java +++ b/src/main/java/com/tiedup/remake/entities/AbstractTiedUpNpc.java @@ -796,16 +796,14 @@ public abstract class AbstractTiedUpNpc public boolean hasGaggingEffect() { ItemStack gag = this.getEquipment(BodyRegionV2.MOUTH); 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; + return DataDrivenBondageItem.getComponent(gag, ComponentType.GAGGING, GaggingComponent.class) != null; } @Override public boolean hasBlindingEffect() { ItemStack blindfold = this.getEquipment(BodyRegionV2.EYES); 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; + return DataDrivenBondageItem.getComponent(blindfold, ComponentType.BLINDING, BlindingComponent.class) != null; } @Override @@ -1087,9 +1085,7 @@ public abstract class AbstractTiedUpNpc public boolean hasShockCollar() { ItemStack collar = this.getEquipment(BodyRegionV2.NECK); if (collar.isEmpty()) return false; - return ( - collar.getItem() instanceof com.tiedup.remake.items.ItemShockCollar - ); + return com.tiedup.remake.v2.bondage.CollarHelper.canShock(collar); } // BONDAGE SERVICE (delegated to BondageManager) diff --git a/src/main/java/com/tiedup/remake/entities/EntityKidnapperMerchant.java b/src/main/java/com/tiedup/remake/entities/EntityKidnapperMerchant.java index b874d21..1874af7 100644 --- a/src/main/java/com/tiedup/remake/entities/EntityKidnapperMerchant.java +++ b/src/main/java/com/tiedup/remake/entities/EntityKidnapperMerchant.java @@ -616,82 +616,10 @@ public class EntityKidnapperMerchant extends EntityKidnapperElite { private List collectAllModItems() { List items = new ArrayList<>(); - // All binds — iterate V1 variants, create V2 stacks - for (BindVariant variant : BindVariant.values()) { - if (variant.supportsColor()) { - for (ItemColor color : ItemColor.values()) { - if ( - color != ItemColor.CAUTION && color != ItemColor.CLEAR - ) { - ItemStack stack = DataDrivenBondageItem.createStack( - new net.minecraft.resources.ResourceLocation("tiedup", variant.getRegistryName()) - ); - KidnapperItemSelector.applyColor(stack, color); - items.add(stack); - } - } - } else { - items.add(DataDrivenBondageItem.createStack( - new net.minecraft.resources.ResourceLocation("tiedup", variant.getRegistryName()) - )); - } - } - - // All gags - for (GagVariant variant : GagVariant.values()) { - if (variant.supportsColor()) { - for (ItemColor color : ItemColor.values()) { - if ( - variant == GagVariant.TAPE_GAG || - (color != ItemColor.CAUTION && color != ItemColor.CLEAR) - ) { - ItemStack stack = DataDrivenBondageItem.createStack( - new net.minecraft.resources.ResourceLocation("tiedup", variant.getRegistryName()) - ); - KidnapperItemSelector.applyColor(stack, color); - items.add(stack); - } - } - } else { - items.add(DataDrivenBondageItem.createStack( - new net.minecraft.resources.ResourceLocation("tiedup", variant.getRegistryName()) - )); - } - } - - // All blindfolds - for (BlindfoldVariant variant : BlindfoldVariant.values()) { - if (variant.supportsColor()) { - for (ItemColor color : ItemColor.values()) { - if ( - color != ItemColor.CAUTION && color != ItemColor.CLEAR - ) { - ItemStack stack = DataDrivenBondageItem.createStack( - new net.minecraft.resources.ResourceLocation("tiedup", variant.getRegistryName()) - ); - KidnapperItemSelector.applyColor(stack, color); - items.add(stack); - } - } - } else { - items.add(DataDrivenBondageItem.createStack( - new net.minecraft.resources.ResourceLocation("tiedup", variant.getRegistryName()) - )); - } - } - - // Earplugs - no color support - for (EarplugsVariant variant : EarplugsVariant.values()) { - items.add(DataDrivenBondageItem.createStack( - new net.minecraft.resources.ResourceLocation("tiedup", variant.getRegistryName()) - )); - } - - // Mittens - no color support - for (MittensVariant variant : MittensVariant.values()) { - items.add(DataDrivenBondageItem.createStack( - new net.minecraft.resources.ResourceLocation("tiedup", variant.getRegistryName()) - )); + // All data-driven bondage items (binds, gags, blindfolds, earplugs, mittens, collars, etc.) + for (com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemDefinition def : + com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemRegistry.getAll()) { + items.add(DataDrivenBondageItem.createStack(def.id())); } // Knives - no color support @@ -699,16 +627,11 @@ public class EntityKidnapperMerchant extends EntityKidnapperElite { items.add(new ItemStack(ModItems.getKnife(variant))); } - // Complex items - items.add(new ItemStack(ModItems.CLASSIC_COLLAR.get())); - items.add(new ItemStack(ModItems.SHOCK_COLLAR.get())); - items.add(new ItemStack(ModItems.GPS_COLLAR.get())); + // Tools items.add(new ItemStack(ModItems.WHIP.get())); // BLACKLIST: TASER (too powerful) // BLACKLIST: LOCKPICK (now in guaranteed utilities) // BLACKLIST: MASTER_KEY (too OP - unlocks everything) - items.add(new ItemStack(ModItems.MEDICAL_GAG.get())); - items.add(new ItemStack(ModItems.HOOD.get())); items.add(new ItemStack(ModItems.CLOTHES.get())); return items; @@ -754,13 +677,13 @@ public class EntityKidnapperMerchant extends EntityKidnapperElite { Item i = item.getItem(); // Tier 4: GPS collar - if (i == ModItems.GPS_COLLAR.get()) { + if (com.tiedup.remake.v2.bondage.CollarHelper.hasGPS(item)) { return 4; } // Tier 3: Shock collar, taser, master key if ( - i == ModItems.SHOCK_COLLAR.get() || + com.tiedup.remake.v2.bondage.CollarHelper.canShock(item) || i == ModItems.TASER.get() || i == ModItems.MASTER_KEY.get() ) { @@ -769,11 +692,9 @@ public class EntityKidnapperMerchant extends EntityKidnapperElite { // Tier 2: Collars, whip, tools, complex items, clothes if ( - i == ModItems.CLASSIC_COLLAR.get() || + com.tiedup.remake.v2.bondage.CollarHelper.isCollar(item) || i == ModItems.WHIP.get() || i == ModItems.LOCKPICK.get() || - i == ModItems.MEDICAL_GAG.get() || - i == ModItems.HOOD.get() || i instanceof GenericClothes ) { return 2; diff --git a/src/main/java/com/tiedup/remake/entities/KidnapperCaptureEquipment.java b/src/main/java/com/tiedup/remake/entities/KidnapperCaptureEquipment.java index dce8890..c941c5f 100644 --- a/src/main/java/com/tiedup/remake/entities/KidnapperCaptureEquipment.java +++ b/src/main/java/com/tiedup/remake/entities/KidnapperCaptureEquipment.java @@ -115,7 +115,8 @@ public class KidnapperCaptureEquipment { @Nullable public ItemStack getCollarItem() { // Kidnappers always have a shock collar to mark their captives - return new ItemStack(ModItems.SHOCK_COLLAR.get()); + return com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem.createStack( + new net.minecraft.resources.ResourceLocation("tiedup", "shock_collar")); } // HELD ITEM MANAGEMENT diff --git a/src/main/java/com/tiedup/remake/entities/KidnapperItemSelector.java b/src/main/java/com/tiedup/remake/entities/KidnapperItemSelector.java index 75a655f..43a2c17 100644 --- a/src/main/java/com/tiedup/remake/entities/KidnapperItemSelector.java +++ b/src/main/java/com/tiedup/remake/entities/KidnapperItemSelector.java @@ -12,6 +12,8 @@ import org.jetbrains.annotations.Nullable; * Helper class for selecting themed items for kidnappers. * Handles probability-based item selection and color application. * + * All bondage items are now created via DataDrivenBondageItem.createStack(). + * * Item selection order (most to least common): * 1. Bind (arms) - 100% (always) * 2. Gag - 50% @@ -79,33 +81,10 @@ public class KidnapperItemSelector { this.blindfold = blindfold; } - /** - * Check if this selection has a gag. - */ - public boolean hasGag() { - return !gag.isEmpty(); - } - - /** - * Check if this selection has mittens. - */ - public boolean hasMittens() { - return !mittens.isEmpty(); - } - - /** - * Check if this selection has earplugs. - */ - public boolean hasEarplugs() { - return !earplugs.isEmpty(); - } - - /** - * Check if this selection has a blindfold. - */ - public boolean hasBlindfold() { - return !blindfold.isEmpty(); - } + public boolean hasGag() { return !gag.isEmpty(); } + public boolean hasMittens() { return !mittens.isEmpty(); } + public boolean hasEarplugs() { return !earplugs.isEmpty(); } + public boolean hasBlindfold() { return !blindfold.isEmpty(); } } /** @@ -129,16 +108,6 @@ public class KidnapperItemSelector { return selectItems(false, true); } - /** - * Calculate adjusted probability based on kidnapper type. - * - * @param baseProb Base probability for the item - * @param eliteBonus Bonus probability for elite kidnappers - * @param archerPenalty Penalty probability for archer kidnappers - * @param isElite Whether the kidnapper is elite - * @param isArcher Whether the kidnapper is an archer - * @return Adjusted probability - */ private static double getAdjustedProbability( double baseProb, double eliteBonus, @@ -152,9 +121,6 @@ public class KidnapperItemSelector { return prob; } - /** - * Internal item selection logic. - */ private static SelectionResult selectItems( boolean isElite, boolean isArcher @@ -163,132 +129,64 @@ public class KidnapperItemSelector { KidnapperTheme theme = KidnapperTheme.getRandomWeighted(); // 2. Select color (if theme supports it) - // Filter out colors that don't have textures for this theme's bind ItemColor color = theme.supportsColor() - ? getValidColorForBind(theme.getBind()) + ? ItemColor.getRandomStandard() : null; // 3. Create bind (always) - ItemStack bind = createBind(theme.getBind(), color); + ItemStack bind = createItemById(theme.getBindId(), color); - // 4. Roll for gag (randomly selected from theme's compatible gags) + // 4. Roll for gag ItemStack gag = ItemStack.EMPTY; double gagProb = getAdjustedProbability( - PROB_GAG, - ELITE_GAG_BONUS, - ARCHER_GAG_PENALTY, - isElite, - isArcher + PROB_GAG, ELITE_GAG_BONUS, ARCHER_GAG_PENALTY, isElite, isArcher ); if (RANDOM.nextDouble() < gagProb) { - gag = createGag(theme.getRandomGag(), color); + gag = createItemById(theme.getRandomGagId(), color); } - // 5. Roll for mittens (same for all themes) + // 5. Roll for mittens ItemStack mittens = ItemStack.EMPTY; double mittensProb = getAdjustedProbability( - PROB_MITTENS, - ELITE_MITTENS_BONUS, - ARCHER_MITTENS_PENALTY, - isElite, - isArcher + PROB_MITTENS, ELITE_MITTENS_BONUS, ARCHER_MITTENS_PENALTY, isElite, isArcher ); if (RANDOM.nextDouble() < mittensProb) { mittens = createMittens(); } - // 6. Roll for earplugs (same for all themes) + // 6. Roll for earplugs ItemStack earplugs = ItemStack.EMPTY; double earplugsProb = getAdjustedProbability( - PROB_EARPLUGS, - ELITE_EARPLUGS_BONUS, - ARCHER_EARPLUGS_PENALTY, - isElite, - isArcher + PROB_EARPLUGS, ELITE_EARPLUGS_BONUS, ARCHER_EARPLUGS_PENALTY, isElite, isArcher ); if (RANDOM.nextDouble() < earplugsProb) { earplugs = createEarplugs(); } - // 7. Roll for blindfold (last, most restrictive - randomly selected) + // 7. Roll for blindfold ItemStack blindfold = ItemStack.EMPTY; double blindfoldProb = getAdjustedProbability( - PROB_BLINDFOLD, - ELITE_BLINDFOLD_BONUS, - ARCHER_BLINDFOLD_PENALTY, - isElite, - isArcher + PROB_BLINDFOLD, ELITE_BLINDFOLD_BONUS, ARCHER_BLINDFOLD_PENALTY, isElite, isArcher ); if (theme.hasBlindfolds() && RANDOM.nextDouble() < blindfoldProb) { - blindfold = createBlindfold(theme.getRandomBlindfold(), color); + blindfold = createItemById(theme.getRandomBlindfoldId(), color); } return new SelectionResult( - theme, - color, - bind, - gag, - mittens, - earplugs, - blindfold + theme, color, bind, gag, mittens, earplugs, blindfold ); } // ITEM CREATION METHODS /** - * Create a bind ItemStack with optional color. + * Create a data-driven bondage item by registry name, with optional color. */ - public static ItemStack createBind( - BindVariant variant, - @Nullable ItemColor color - ) { + public static ItemStack createItemById(String id, @Nullable ItemColor color) { ItemStack stack = DataDrivenBondageItem.createStack( - new ResourceLocation("tiedup", variant.getRegistryName()) + new ResourceLocation("tiedup", id) ); - if (color != null && variant.supportsColor()) { - applyColor(stack, color); - } - return stack; - } - - /** - * Create a gag ItemStack with optional color. - * Validates that the color has a texture for this gag variant. - */ - public static ItemStack createGag( - GagVariant variant, - @Nullable ItemColor color - ) { - ItemStack stack = DataDrivenBondageItem.createStack( - new ResourceLocation("tiedup", variant.getRegistryName()) - ); - if ( - color != null && - variant.supportsColor() && - isColorValidForGag(color, variant) - ) { - applyColor(stack, color); - } - return stack; - } - - /** - * Create a blindfold ItemStack with optional color. - * Validates that the color has a texture for this blindfold variant. - */ - public static ItemStack createBlindfold( - BlindfoldVariant variant, - @Nullable ItemColor color - ) { - ItemStack stack = DataDrivenBondageItem.createStack( - new ResourceLocation("tiedup", variant.getRegistryName()) - ); - if ( - color != null && - variant.supportsColor() && - isColorValidForBlindfold(color, variant) - ) { + if (color != null) { applyColor(stack, color); } return stack; @@ -296,7 +194,6 @@ public class KidnapperItemSelector { /** * Create mittens ItemStack. - * Mittens don't have color variants. */ public static ItemStack createMittens() { return DataDrivenBondageItem.createStack( @@ -306,7 +203,6 @@ public class KidnapperItemSelector { /** * Create earplugs ItemStack. - * Earplugs don't have color variants. */ public static ItemStack createEarplugs() { return DataDrivenBondageItem.createStack( @@ -321,7 +217,6 @@ public class KidnapperItemSelector { /** * Apply color NBT to an ItemStack. - * Sets both the ItemColor name and CustomModelData for model selection. */ public static void applyColor(ItemStack stack, ItemColor color) { if (stack.isEmpty() || color == null) return; @@ -351,141 +246,9 @@ public class KidnapperItemSelector { /** * Get the texture suffix for an item's color. - * Example: "ropes" + "_red" = "ropes_red" - * @return The color suffix (e.g., "_red"), or empty string if no color */ public static String getColorSuffix(ItemStack stack) { ItemColor color = getColor(stack); return color != null ? "_" + color.getName() : ""; } - - // COLOR VALIDATION - - /** - * Get a random color that has a texture for the given bind variant. - * Excludes colors that don't have textures for specific variants. - */ - public static ItemColor getValidColorForBind(BindVariant variant) { - ItemColor color; - int attempts = 0; - do { - color = ItemColor.getRandomStandard(); - attempts++; - // Prevent infinite loop - if (attempts > 50) break; - } while (!isColorValidForBind(color, variant)); - return color; - } - - /** - * Check if a color has a texture for the given bind variant. - * Returns false for colors without textures. - */ - public static boolean isColorValidForBind( - ItemColor color, - BindVariant variant - ) { - if (color == null || variant == null) return true; - - // BROWN doesn't have textures for ROPES and SHIBARI - if ( - color == ItemColor.BROWN && - (variant == BindVariant.ROPES || variant == BindVariant.SHIBARI) - ) { - return false; - } - - // GRAY doesn't have texture for DUCT_TAPE - if (color == ItemColor.GRAY && variant == BindVariant.DUCT_TAPE) { - return false; - } - - return true; - } - - /** - * Check if a color has a texture for the given gag variant. - */ - public static boolean isColorValidForGag( - ItemColor color, - GagVariant variant - ) { - if (color == null || variant == null) return true; - - // GRAY doesn't have texture for TAPE_GAG - if (color == ItemColor.GRAY && variant == GagVariant.TAPE_GAG) { - return false; - } - - // WHITE doesn't have texture for CLOTH_GAG and CLEAVE_GAG - if ( - color == ItemColor.WHITE && - (variant == GagVariant.CLOTH_GAG || - variant == GagVariant.CLEAVE_GAG) - ) { - return false; - } - - // RED doesn't have texture for BALL_GAG and BALL_GAG_STRAP - if ( - color == ItemColor.RED && - (variant == GagVariant.BALL_GAG || - variant == GagVariant.BALL_GAG_STRAP) - ) { - return false; - } - - return true; - } - - /** - * Check if a color has a texture for the given blindfold variant. - */ - public static boolean isColorValidForBlindfold( - ItemColor color, - BlindfoldVariant variant - ) { - if (color == null || variant == null) return true; - - // BLACK doesn't have texture for CLASSIC or MASK blindfolds - if ( - color == ItemColor.BLACK && - (variant == BlindfoldVariant.CLASSIC || - variant == BlindfoldVariant.MASK) - ) { - return false; - } - - return true; - } - - /** - * Get a random color that has a texture for the given gag variant. - */ - public static ItemColor getValidColorForGag(GagVariant variant) { - ItemColor color; - int attempts = 0; - do { - color = ItemColor.getRandomStandard(); - attempts++; - if (attempts > 50) break; - } while (!isColorValidForGag(color, variant)); - return color; - } - - /** - * Get a random color that has a texture for the given blindfold variant. - */ - public static ItemColor getValidColorForBlindfold( - BlindfoldVariant variant - ) { - ItemColor color; - int attempts = 0; - do { - color = ItemColor.getRandomStandard(); - attempts++; - if (attempts > 50) break; - } while (!isColorValidForBlindfold(color, variant)); - return color; - } } diff --git a/src/main/java/com/tiedup/remake/entities/KidnapperJobManager.java b/src/main/java/com/tiedup/remake/entities/KidnapperJobManager.java index 1914b32..92fe3d4 100644 --- a/src/main/java/com/tiedup/remake/entities/KidnapperJobManager.java +++ b/src/main/java/com/tiedup/remake/entities/KidnapperJobManager.java @@ -161,7 +161,8 @@ public class KidnapperJobManager { ); // Put a shock collar on the worker AFTER untie/free - ItemStack shockCollar = new ItemStack(ModItems.SHOCK_COLLAR_AUTO.get()); + ItemStack shockCollar = com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem.createStack( + new net.minecraft.resources.ResourceLocation("tiedup", "shock_collar_auto")); // Add kidnapper as owner so the collar is linked CollarHelper.addOwner( shockCollar, diff --git a/src/main/java/com/tiedup/remake/entities/KidnapperTheme.java b/src/main/java/com/tiedup/remake/entities/KidnapperTheme.java index 408005c..a7b72ab 100644 --- a/src/main/java/com/tiedup/remake/entities/KidnapperTheme.java +++ b/src/main/java/com/tiedup/remake/entities/KidnapperTheme.java @@ -1,42 +1,33 @@ package com.tiedup.remake.entities; -import com.tiedup.remake.items.base.BindVariant; -import com.tiedup.remake.items.base.BlindfoldVariant; -import com.tiedup.remake.items.base.GagVariant; import java.util.Random; /** * Defines themed item sets for kidnappers. - * Each theme groups compatible binds, gags, and blindfolds. + * Each theme groups compatible binds, gags, and blindfolds by registry name. * * Themes are selected randomly with weighted probabilities. * Higher weight = more common theme. * * Note: Natural themes (SLIME, VINE, WEB) are reserved for monsters. + * + * Registry names correspond to data-driven bondage item IDs in DataDrivenItemRegistry. */ public enum KidnapperTheme { // === ROPE THEMES (most common) === ROPE( - BindVariant.ROPES, - new GagVariant[] { - GagVariant.ROPES_GAG, - GagVariant.CLOTH_GAG, - GagVariant.CLEAVE_GAG, - }, - new BlindfoldVariant[] { BlindfoldVariant.CLASSIC }, + "ropes", + new String[] { "ropes_gag", "cloth_gag", "cleave_gag" }, + new String[] { "classic_blindfold" }, true, // supportsColor 30 // weight (spawn probability) ), SHIBARI( - BindVariant.SHIBARI, - new GagVariant[] { - GagVariant.ROPES_GAG, - GagVariant.CLOTH_GAG, - GagVariant.RIBBON_GAG, - }, - new BlindfoldVariant[] { BlindfoldVariant.CLASSIC }, + "shibari", + new String[] { "ropes_gag", "cloth_gag", "ribbon_gag" }, + new String[] { "classic_blindfold" }, true, 15 ), @@ -44,9 +35,9 @@ public enum KidnapperTheme { // === TAPE THEME === TAPE( - BindVariant.DUCT_TAPE, - new GagVariant[] { GagVariant.TAPE_GAG, GagVariant.WRAP_GAG }, - new BlindfoldVariant[] { BlindfoldVariant.MASK }, + "duct_tape", + new String[] { "tape_gag", "wrap_gag" }, + new String[] { "mask_blindfold" }, true, 20 ), @@ -54,13 +45,9 @@ public enum KidnapperTheme { // === LEATHER/BDSM THEME === LEATHER( - BindVariant.LEATHER_STRAPS, - new GagVariant[] { - GagVariant.BALL_GAG, - GagVariant.BALL_GAG_STRAP, - GagVariant.PANEL_GAG, - }, - new BlindfoldVariant[] { BlindfoldVariant.MASK }, + "leather_straps", + new String[] { "ball_gag", "ball_gag_strap", "panel_gag" }, + new String[] { "mask_blindfold" }, false, 15 ), @@ -68,12 +55,9 @@ public enum KidnapperTheme { // === CHAIN THEME === CHAIN( - BindVariant.CHAIN, - new GagVariant[] { - GagVariant.CHAIN_PANEL_GAG, - GagVariant.BALL_GAG_STRAP, - }, - new BlindfoldVariant[] { BlindfoldVariant.MASK }, + "chain", + new String[] { "chain_panel_gag", "ball_gag_strap" }, + new String[] { "mask_blindfold" }, false, 10 ), @@ -81,13 +65,9 @@ public enum KidnapperTheme { // === MEDICAL THEME === MEDICAL( - BindVariant.MEDICAL_STRAPS, - new GagVariant[] { - GagVariant.TUBE_GAG, - GagVariant.SPONGE_GAG, - GagVariant.BALL_GAG, - }, - new BlindfoldVariant[] { BlindfoldVariant.MASK }, + "medical_straps", + new String[] { "tube_gag", "sponge_gag", "ball_gag" }, + new String[] { "mask_blindfold" }, false, 8 ), @@ -95,9 +75,9 @@ public enum KidnapperTheme { // === SCI-FI/BEAM THEME === BEAM( - BindVariant.BEAM_CUFFS, - new GagVariant[] { GagVariant.BEAM_PANEL_GAG, GagVariant.LATEX_GAG }, - new BlindfoldVariant[] { BlindfoldVariant.MASK }, + "beam_cuffs", + new String[] { "beam_panel_gag", "latex_gag" }, + new String[] { "mask_blindfold" }, false, 5 ), @@ -105,9 +85,9 @@ public enum KidnapperTheme { // === LATEX THEME (rare) === LATEX( - BindVariant.LATEX_SACK, - new GagVariant[] { GagVariant.LATEX_GAG, GagVariant.TUBE_GAG }, - new BlindfoldVariant[] { BlindfoldVariant.MASK }, + "latex_sack", + new String[] { "latex_gag", "tube_gag" }, + new String[] { "mask_blindfold" }, false, 3 ), @@ -115,13 +95,9 @@ public enum KidnapperTheme { // === ASYLUM THEME (rare) === ASYLUM( - BindVariant.STRAITJACKET, - new GagVariant[] { - GagVariant.BITE_GAG, - GagVariant.SPONGE_GAG, - GagVariant.BALL_GAG, - }, - new BlindfoldVariant[] { BlindfoldVariant.MASK }, + "straitjacket", + new String[] { "bite_gag", "sponge_gag", "ball_gag" }, + new String[] { "mask_blindfold" }, false, 5 ), @@ -129,9 +105,9 @@ public enum KidnapperTheme { // === RIBBON THEME (cute/playful) === RIBBON( - BindVariant.RIBBON, - new GagVariant[] { GagVariant.RIBBON_GAG, GagVariant.CLOTH_GAG }, - new BlindfoldVariant[] { BlindfoldVariant.CLASSIC }, + "ribbon", + new String[] { "ribbon_gag", "cloth_gag" }, + new String[] { "classic_blindfold" }, false, 8 ), @@ -139,54 +115,54 @@ public enum KidnapperTheme { // === WRAP THEME === WRAP( - BindVariant.WRAP, - new GagVariant[] { GagVariant.WRAP_GAG, GagVariant.TAPE_GAG }, - new BlindfoldVariant[] { BlindfoldVariant.MASK }, + "wrap", + new String[] { "wrap_gag", "tape_gag" }, + new String[] { "mask_blindfold" }, false, 5 ); private static final Random RANDOM = new Random(); - private final BindVariant bind; - private final GagVariant[] gags; - private final BlindfoldVariant[] blindfolds; + private final String bindId; + private final String[] gagIds; + private final String[] blindfoldIds; private final boolean supportsColor; private final int weight; KidnapperTheme( - BindVariant bind, - GagVariant[] gags, - BlindfoldVariant[] blindfolds, + String bindId, + String[] gagIds, + String[] blindfoldIds, boolean supportsColor, int weight ) { - this.bind = bind; - this.gags = gags; - this.blindfolds = blindfolds; + this.bindId = bindId; + this.gagIds = gagIds; + this.blindfoldIds = blindfoldIds; this.supportsColor = supportsColor; this.weight = weight; } /** - * Get the primary bind for this theme. + * Get the primary bind registry name for this theme. */ - public BindVariant getBind() { - return bind; + public String getBindId() { + return bindId; } /** - * Get compatible gags for this theme (ordered by preference). + * Get compatible gag IDs for this theme (ordered by preference). */ - public GagVariant[] getGags() { - return gags; + public String[] getGagIds() { + return gagIds; } /** - * Get compatible blindfolds for this theme. + * Get compatible blindfold IDs for this theme. */ - public BlindfoldVariant[] getBlindfolds() { - return blindfolds; + public String[] getBlindfoldIds() { + return blindfoldIds; } /** @@ -206,41 +182,40 @@ public enum KidnapperTheme { } /** - * Get primary gag (first in list). - * Used when only one gag is selected. + * Get primary gag ID (first in list). */ - public GagVariant getPrimaryGag() { - return gags.length > 0 ? gags[0] : GagVariant.BALL_GAG; + public String getPrimaryGagId() { + return gagIds.length > 0 ? gagIds[0] : "ball_gag"; } /** - * Get a random gag from this theme's compatible list. + * Get a random gag ID from this theme's compatible list. */ - public GagVariant getRandomGag() { - if (gags.length == 0) return GagVariant.BALL_GAG; - return gags[RANDOM.nextInt(gags.length)]; + public String getRandomGagId() { + if (gagIds.length == 0) return "ball_gag"; + return gagIds[RANDOM.nextInt(gagIds.length)]; } /** - * Get primary blindfold (first in list). + * Get primary blindfold ID (first in list). */ - public BlindfoldVariant getPrimaryBlindfold() { - return blindfolds.length > 0 ? blindfolds[0] : BlindfoldVariant.CLASSIC; + public String getPrimaryBlindfoldId() { + return blindfoldIds.length > 0 ? blindfoldIds[0] : "classic_blindfold"; } /** - * Get a random blindfold from this theme's compatible list. + * Get a random blindfold ID from this theme's compatible list. */ - public BlindfoldVariant getRandomBlindfold() { - if (blindfolds.length == 0) return BlindfoldVariant.CLASSIC; - return blindfolds[RANDOM.nextInt(blindfolds.length)]; + public String getRandomBlindfoldId() { + if (blindfoldIds.length == 0) return "classic_blindfold"; + return blindfoldIds[RANDOM.nextInt(blindfoldIds.length)]; } /** * Check if this theme has any blindfolds. */ public boolean hasBlindfolds() { - return blindfolds.length > 0; + return blindfoldIds.length > 0; } /** diff --git a/src/main/java/com/tiedup/remake/entities/ai/maid/MaidDeliverCaptiveGoal.java b/src/main/java/com/tiedup/remake/entities/ai/maid/MaidDeliverCaptiveGoal.java index 4b642db..8b8aabd 100644 --- a/src/main/java/com/tiedup/remake/entities/ai/maid/MaidDeliverCaptiveGoal.java +++ b/src/main/java/com/tiedup/remake/entities/ai/maid/MaidDeliverCaptiveGoal.java @@ -229,20 +229,21 @@ public class MaidDeliverCaptiveGoal extends Goal { kidnappedState.getEquipment(BodyRegionV2.NECK); if ( !collar.isEmpty() && - collar.getItem() instanceof - com.tiedup.remake.items.base.ItemCollar collarItem + com.tiedup.remake.v2.bondage.CollarHelper.isCollar(collar) ) { for (java.util.UUID ownerId : new java.util.ArrayList<>( - collarItem.getOwners(collar) + com.tiedup.remake.v2.bondage.CollarHelper.getOwners(collar) )) { - collarItem.removeOwner(collar, ownerId); + com.tiedup.remake.v2.bondage.CollarHelper.removeOwner(collar, ownerId); } - collarItem.addOwner( + com.tiedup.remake.v2.bondage.CollarHelper.addOwner( collar, buyerEntity.getUUID(), buyerEntity.getName().getString() ); - collarItem.setLocked(collar, false); + if (collar.getItem() instanceof com.tiedup.remake.items.base.ILockable lockable) { + lockable.setLocked(collar, false); + } kidnappedState.equip(BodyRegionV2.NECK, collar); if ( diff --git a/src/main/java/com/tiedup/remake/entities/ai/personality/NpcStruggleGoal.java b/src/main/java/com/tiedup/remake/entities/ai/personality/NpcStruggleGoal.java index ced4324..c009bfd 100644 --- a/src/main/java/com/tiedup/remake/entities/ai/personality/NpcStruggleGoal.java +++ b/src/main/java/com/tiedup/remake/entities/ai/personality/NpcStruggleGoal.java @@ -305,11 +305,8 @@ public class NpcStruggleGoal extends Goal { ItemStack collar = npc.getEquipment(BodyRegionV2.NECK); if (collar.isEmpty()) return false; - if ( - collar.getItem() instanceof - com.tiedup.remake.items.base.ItemCollar collarItem - ) { - List ownerUUIDs = collarItem.getOwners(collar); + if (com.tiedup.remake.v2.bondage.CollarHelper.isCollar(collar)) { + List ownerUUIDs = com.tiedup.remake.v2.bondage.CollarHelper.getOwners(collar); if (!ownerUUIDs.isEmpty()) { // Check if any owner is nearby List players = npc @@ -338,11 +335,8 @@ public class NpcStruggleGoal extends Goal { ItemStack collar = npc.getEquipment(BodyRegionV2.NECK); if (collar.isEmpty()) return null; - if ( - collar.getItem() instanceof - com.tiedup.remake.items.base.ItemCollar collarItem - ) { - List ownerUUIDs = collarItem.getOwners(collar); + if (com.tiedup.remake.v2.bondage.CollarHelper.isCollar(collar)) { + List ownerUUIDs = com.tiedup.remake.v2.bondage.CollarHelper.getOwners(collar); if (!ownerUUIDs.isEmpty()) { return ownerUUIDs.get(0); } diff --git a/src/main/java/com/tiedup/remake/entities/armorstand/ArmorStandBondageHelper.java b/src/main/java/com/tiedup/remake/entities/armorstand/ArmorStandBondageHelper.java index 8e8a2a1..b869b77 100644 --- a/src/main/java/com/tiedup/remake/entities/armorstand/ArmorStandBondageHelper.java +++ b/src/main/java/com/tiedup/remake/entities/armorstand/ArmorStandBondageHelper.java @@ -1,7 +1,7 @@ package com.tiedup.remake.entities.armorstand; -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.v2.BodyRegionV2; import java.util.Map; import net.minecraft.core.Rotations; @@ -185,11 +185,8 @@ public class ArmorStandBondageHelper { // Save original pose if not already saved saveOriginalPose(stand); - // Get pose type from bind - PoseType poseType = PoseType.STANDARD; - if (bindStack.getItem() instanceof ItemBind bind) { - poseType = bind.getPoseType(); - } + // Get pose type from bind (V2 data-driven) + PoseType poseType = PoseTypeHelper.getPoseType(bindStack); // Apply pose applyBondagePose(stand, poseType); 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 2d4d1c5..ee0b986 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 @@ -116,15 +116,13 @@ public class NpcEquipmentManager { public boolean hasGaggingEffect() { ItemStack gag = getCurrentGag(); 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; + return DataDrivenBondageItem.getComponent(gag, ComponentType.GAGGING, GaggingComponent.class) != null; } public boolean hasBlindingEffect() { ItemStack blindfold = getCurrentBlindfold(); 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; + return DataDrivenBondageItem.getComponent(blindfold, ComponentType.BLINDING, BlindingComponent.class) != null; } public boolean hasKnives() { diff --git a/src/main/java/com/tiedup/remake/entities/kidnapper/components/KidnapperAppearance.java b/src/main/java/com/tiedup/remake/entities/kidnapper/components/KidnapperAppearance.java index 808459a..e5089f3 100644 --- a/src/main/java/com/tiedup/remake/entities/kidnapper/components/KidnapperAppearance.java +++ b/src/main/java/com/tiedup/remake/entities/kidnapper/components/KidnapperAppearance.java @@ -361,19 +361,19 @@ public class KidnapperAppearance { this.itemSelection = new KidnapperItemSelector.SelectionResult( this.currentTheme, this.themeColor, - KidnapperItemSelector.createBind( - this.currentTheme.getBind(), + KidnapperItemSelector.createItemById( + this.currentTheme.getBindId(), this.themeColor ), - KidnapperItemSelector.createGag( - this.currentTheme.getPrimaryGag(), + KidnapperItemSelector.createItemById( + this.currentTheme.getPrimaryGagId(), this.themeColor ), KidnapperItemSelector.createMittens(), KidnapperItemSelector.createEarplugs(), this.currentTheme.hasBlindfolds() - ? KidnapperItemSelector.createBlindfold( - this.currentTheme.getPrimaryBlindfold(), + ? KidnapperItemSelector.createItemById( + this.currentTheme.getPrimaryBlindfoldId(), this.themeColor ) : ItemStack.EMPTY 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 ddd5b01..9fb7325 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 @@ -119,8 +119,8 @@ public class MasterPetManager { if (state == null) return; // Create a choke collar for pet play - ItemStack chokeCollar = new ItemStack( - com.tiedup.remake.items.ModItems.CHOKE_COLLAR.get() + ItemStack chokeCollar = com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem.createStack( + new net.minecraft.resources.ResourceLocation("tiedup", "choke_collar") ); // Configure for pet play BEFORE equipping 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 96ff8d1..411e808 100644 --- a/src/main/java/com/tiedup/remake/events/system/ChatEventHandler.java +++ b/src/main/java/com/tiedup/remake/events/system/ChatEventHandler.java @@ -4,7 +4,6 @@ import com.tiedup.remake.core.SettingsAccessor; import com.tiedup.remake.core.SystemMessageManager; import com.tiedup.remake.core.TiedUpMod; 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; @@ -57,11 +56,10 @@ public class ChatEventHandler { BodyRegionV2.MOUTH ); - // V2: check gagging component, V1 fallback: instanceof ItemGag + // V2: check gagging component GaggingComponent gaggingComp = DataDrivenBondageItem.getComponent( gagStack, ComponentType.GAGGING, GaggingComponent.class); - boolean isGagItem = gaggingComp != null - || gagStack.getItem() instanceof ItemGag; + boolean isGagItem = gaggingComp != null; if (!gagStack.isEmpty() && isGagItem) { String originalMessage = event.getRawText(); @@ -70,9 +68,7 @@ public class ChatEventHandler { if (gaggingComp != null) { material = gaggingComp.getMaterial(); } - if (material == null && gagStack.getItem() instanceof ItemGag gagItem) { - material = gagItem.getGagMaterial(); - } + // material stays null if no component; GagTalkManager handles null → CLOTH fallback // 1. Process the message through our GagTalkManager V2 Component muffledMessage = GagTalkManager.processGagMessage( diff --git a/src/main/java/com/tiedup/remake/items/GenericBind.java b/src/main/java/com/tiedup/remake/items/GenericBind.java deleted file mode 100644 index 360658e..0000000 --- a/src/main/java/com/tiedup/remake/items/GenericBind.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.tiedup.remake.items; - -import com.tiedup.remake.items.base.BindVariant; -import com.tiedup.remake.items.base.ItemBind; -import com.tiedup.remake.items.base.PoseType; -import net.minecraft.world.item.Item; - -/** - * Generic bind item created from BindVariant enum. - * Replaces individual bind classes (ItemRopes, ItemChain, ItemStraitjacket, etc.) - * - * Factory pattern: All bind variants are created using this single class. - */ -public class GenericBind extends ItemBind { - - private final BindVariant variant; - - public GenericBind(BindVariant variant) { - super(new Item.Properties().stacksTo(16)); - this.variant = variant; - } - - @Override - public String getItemName() { - return variant.getItemName(); - } - - @Override - public PoseType getPoseType() { - return variant.getPoseType(); - } - - /** - * Get the variant this bind was created from. - */ - public BindVariant getVariant() { - return variant; - } - - /** - * Get the default resistance value for this bind variant. - * Note: Actual resistance is managed by GameRules, this is just the configured default. - */ - public int getDefaultResistance() { - return variant.getResistance(); - } - - /** - * Check if this bind can have a padlock attached via anvil. - * Adhesive (tape) and organic (slime, vine, web) binds cannot have padlocks. - */ - @Override - public boolean canAttachPadlock() { - return switch (variant) { - case DUCT_TAPE, SLIME, VINE_SEED, WEB_BIND -> false; - default -> true; - }; - } - - /** - * Get the texture subfolder for this bind variant. - * Issue #12 fix: Eliminates string checks in renderers. - */ - @Override - public String getTextureSubfolder() { - return variant.getTextureSubfolder(); - } -} diff --git a/src/main/java/com/tiedup/remake/items/GenericBlindfold.java b/src/main/java/com/tiedup/remake/items/GenericBlindfold.java deleted file mode 100644 index b030aae..0000000 --- a/src/main/java/com/tiedup/remake/items/GenericBlindfold.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.tiedup.remake.items; - -import com.tiedup.remake.items.base.BlindfoldVariant; -import com.tiedup.remake.items.base.ItemBlindfold; -import net.minecraft.world.item.Item; - -/** - * Generic blindfold item created from BlindfoldVariant enum. - * Replaces individual blindfold classes (ItemClassicBlindfold, ItemBlindfoldMask). - * - * Factory pattern: All blindfold variants are created using this single class. - */ -public class GenericBlindfold extends ItemBlindfold { - - private final BlindfoldVariant variant; - - public GenericBlindfold(BlindfoldVariant variant) { - super(new Item.Properties().stacksTo(16)); - this.variant = variant; - } - - /** - * Get the variant this blindfold was created from. - */ - public BlindfoldVariant getVariant() { - return variant; - } - - /** - * Get the texture subfolder for this blindfold variant. - * Issue #12 fix: Eliminates string checks in renderers. - */ - @Override - public String getTextureSubfolder() { - return variant.getTextureSubfolder(); - } -} diff --git a/src/main/java/com/tiedup/remake/items/GenericEarplugs.java b/src/main/java/com/tiedup/remake/items/GenericEarplugs.java deleted file mode 100644 index 8d6d065..0000000 --- a/src/main/java/com/tiedup/remake/items/GenericEarplugs.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.tiedup.remake.items; - -import com.tiedup.remake.items.base.EarplugsVariant; -import com.tiedup.remake.items.base.ItemEarplugs; -import net.minecraft.world.item.Item; - -/** - * Generic earplugs item created from EarplugsVariant enum. - * Replaces individual earplugs classes (ItemClassicEarplugs). - * - * Factory pattern: All earplugs variants are created using this single class. - */ -public class GenericEarplugs extends ItemEarplugs { - - private final EarplugsVariant variant; - - public GenericEarplugs(EarplugsVariant variant) { - super(new Item.Properties().stacksTo(16)); - this.variant = variant; - } - - /** - * Get the variant this earplugs was created from. - */ - public EarplugsVariant getVariant() { - return variant; - } - - /** - * Get the texture subfolder for this earplugs variant. - * Issue #12 fix: Eliminates string checks in renderers. - */ - @Override - public String getTextureSubfolder() { - return variant.getTextureSubfolder(); - } -} diff --git a/src/main/java/com/tiedup/remake/items/GenericGag.java b/src/main/java/com/tiedup/remake/items/GenericGag.java deleted file mode 100644 index 824f96e..0000000 --- a/src/main/java/com/tiedup/remake/items/GenericGag.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.tiedup.remake.items; - -import com.tiedup.remake.items.base.GagVariant; -import com.tiedup.remake.items.base.ItemGag; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.item.Item; -import org.jetbrains.annotations.Nullable; - -/** - * Generic gag item created from GagVariant enum. - * Replaces individual gag classes (ItemBallGag, ItemTapeGag, etc.) - * - * Factory pattern: All gag variants are created using this single class. - * - * Note: ItemMedicalGag is NOT handled by this class because it implements - * IHasBlindingEffect (combo item with special behavior). - */ -public class GenericGag extends ItemGag { - - private final GagVariant variant; - - public GenericGag(GagVariant variant) { - super(new Item.Properties().stacksTo(16), variant.getMaterial()); - this.variant = variant; - } - - /** - * Get the variant this gag was created from. - */ - public GagVariant getVariant() { - return variant; - } - - /** - * Check if this gag can have a padlock attached via anvil. - * Adhesive (tape) and organic (slime, vine, web) gags cannot have padlocks. - */ - @Override - public boolean canAttachPadlock() { - return switch (variant) { - case TAPE_GAG, SLIME_GAG, VINE_GAG, WEB_GAG -> false; - default -> true; - }; - } - - /** - * Get the texture subfolder for this gag variant. - * Issue #12 fix: Eliminates string checks in renderers. - */ - @Override - public String getTextureSubfolder() { - return variant.getTextureSubfolder(); - } - - /** - * Check if this gag uses a 3D OBJ model. - */ - @Override - public boolean uses3DModel() { - return variant.uses3DModel(); - } - - /** - * Get the 3D model location for this gag. - */ - @Override - @Nullable - public ResourceLocation get3DModelLocation() { - String path = variant.getModelPath(); - return path != null ? ResourceLocation.tryParse(path) : null; - } -} diff --git a/src/main/java/com/tiedup/remake/items/GenericKnife.java b/src/main/java/com/tiedup/remake/items/GenericKnife.java index aac49d0..7018f6d 100644 --- a/src/main/java/com/tiedup/remake/items/GenericKnife.java +++ b/src/main/java/com/tiedup/remake/items/GenericKnife.java @@ -3,8 +3,8 @@ package com.tiedup.remake.items; import com.tiedup.remake.core.TiedUpMod; import com.tiedup.remake.items.base.IKnife; import com.tiedup.remake.items.base.ILockable; -import com.tiedup.remake.items.base.ItemBind; import com.tiedup.remake.items.base.KnifeVariant; +import com.tiedup.remake.v2.bondage.BindModeHelper; import com.tiedup.remake.network.sync.SyncManager; import com.tiedup.remake.state.IBondageState; import com.tiedup.remake.state.PlayerBindState; @@ -306,10 +306,7 @@ public class GenericKnife extends Item implements IKnife { player, BodyRegionV2.ARMS ); - if ( - bindStack.isEmpty() || - !(bindStack.getItem() instanceof ItemBind bind) - ) { + if (bindStack.isEmpty() || !BindModeHelper.isBindItem(bindStack)) { player.stopUsingItem(); return; } diff --git a/src/main/java/com/tiedup/remake/items/GenericMittens.java b/src/main/java/com/tiedup/remake/items/GenericMittens.java deleted file mode 100644 index 5b25a61..0000000 --- a/src/main/java/com/tiedup/remake/items/GenericMittens.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.tiedup.remake.items; - -import com.tiedup.remake.items.base.ItemMittens; -import com.tiedup.remake.items.base.MittensVariant; -import net.minecraft.world.item.Item; - -/** - * Generic mittens item created from MittensVariant enum. - * - * Factory pattern: All mittens variants are created using this single class. - * - */ -public class GenericMittens extends ItemMittens { - - private final MittensVariant variant; - - public GenericMittens(MittensVariant variant) { - super(new Item.Properties().stacksTo(16)); - this.variant = variant; - } - - /** - * Get the variant this mittens was created from. - */ - public MittensVariant getVariant() { - return variant; - } - - /** - * Get the texture subfolder for this mittens variant. - * Issue #12 fix: Eliminates string checks in renderers. - */ - @Override - public String getTextureSubfolder() { - return variant.getTextureSubfolder(); - } -} diff --git a/src/main/java/com/tiedup/remake/items/ItemChokeCollar.java b/src/main/java/com/tiedup/remake/items/ItemChokeCollar.java deleted file mode 100644 index b586381..0000000 --- a/src/main/java/com/tiedup/remake/items/ItemChokeCollar.java +++ /dev/null @@ -1,154 +0,0 @@ -package com.tiedup.remake.items; - -import com.tiedup.remake.items.base.ItemCollar; -import com.tiedup.remake.items.bondage3d.IHas3DModelConfig; -import com.tiedup.remake.items.bondage3d.Model3DConfig; -import java.util.List; -import java.util.Set; -import net.minecraft.ChatFormatting; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.network.chat.Component; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.item.Item; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.TooltipFlag; -import net.minecraft.world.level.Level; -import org.jetbrains.annotations.Nullable; - -/** - * Choke Collar - Pet play collar used by Masters. - * - *

Special feature: Can be put in "choke mode" which applies a drowning effect.

- *

Used by Masters for punishment. The effect simulates choking by reducing air supply, - * which triggers drowning damage if left active for too long.

- * - *

Mechanics:

- *
    - *
  • When choking is active, the wearer's air supply decreases rapidly
  • - *
  • This creates the drowning effect (damage and bubble particles)
  • - *
  • Masters should deactivate the choke before the pet dies
  • - *
  • The choke is controlled by the Master's punishment system
  • - *
- * - * @see com.tiedup.remake.entities.ai.master.MasterPunishGoal - * @see com.tiedup.remake.events.restriction.PetPlayRestrictionHandler - */ -public class ItemChokeCollar extends ItemCollar implements IHas3DModelConfig { - - private static final String NBT_CHOKING = "choking"; - - private static final Model3DConfig CONFIG = new Model3DConfig( - "tiedup:models/obj/choke_collar_leather/model.obj", - "tiedup:models/obj/choke_collar_leather/texture.png", - 0.0f, - 1.47f, - 0.0f, // Collar band centered at neck level - 1.0f, - 0.0f, - 0.0f, - 180.0f, // Flip Y to match rendering convention - Set.of() - ); - - public ItemChokeCollar() { - super(new Item.Properties()); - } - - @Override - public String getItemName() { - return "choke_collar"; - } - - /** - * Check if choke mode is active. - * - * @param stack The collar ItemStack - * @return true if choking is active - */ - public boolean isChoking(ItemStack stack) { - CompoundTag tag = stack.getTag(); - return tag != null && tag.getBoolean(NBT_CHOKING); - } - - /** - * Set choke mode on/off. - * When active, applies drowning effect to wearer (handled by PetPlayRestrictionHandler). - * - * @param stack The collar ItemStack - * @param choking true to activate choking, false to deactivate - */ - public void setChoking(ItemStack stack, boolean choking) { - stack.getOrCreateTag().putBoolean(NBT_CHOKING, choking); - } - - /** - * Check if collar is in pet play mode (from Master). - * - * @param stack The collar ItemStack - * @return true if this is a pet play collar - */ - public boolean isPetPlayMode(ItemStack stack) { - CompoundTag tag = stack.getTag(); - return tag != null && tag.getBoolean("petPlayMode"); - } - - @Override - public void appendHoverText( - ItemStack stack, - @Nullable Level level, - List tooltip, - TooltipFlag flag - ) { - super.appendHoverText(stack, level, tooltip, flag); - - // Show choke status - if (isChoking(stack)) { - tooltip.add( - Component.literal("CHOKING ACTIVE!") - .withStyle(ChatFormatting.DARK_RED) - .withStyle(ChatFormatting.BOLD) - ); - } - - // Show pet play mode status - if (isPetPlayMode(stack)) { - tooltip.add( - Component.literal("Pet Play Mode").withStyle( - ChatFormatting.LIGHT_PURPLE - ) - ); - } - - // Description - tooltip.add( - Component.literal("A special collar used for pet play punishment") - .withStyle(ChatFormatting.DARK_GRAY) - .withStyle(ChatFormatting.ITALIC) - ); - } - - /** - * Choke collar cannot shock like shock collar. - */ - @Override - public boolean canShock() { - return false; - } - - // 3D Model Support - - @Override - public boolean uses3DModel() { - return true; - } - - @Override - public ResourceLocation get3DModelLocation() { - return ResourceLocation.tryParse(CONFIG.objPath()); - } - - @Override - public Model3DConfig getModelConfig() { - return CONFIG; - } -} diff --git a/src/main/java/com/tiedup/remake/items/ItemClassicCollar.java b/src/main/java/com/tiedup/remake/items/ItemClassicCollar.java deleted file mode 100644 index 3fc120e..0000000 --- a/src/main/java/com/tiedup/remake/items/ItemClassicCollar.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.tiedup.remake.items; - -import com.tiedup.remake.items.base.ItemCollar; -import net.minecraft.world.item.Item; - -/** - * Classic Collar - Basic collar item - * Standard collar for marking ownership. - * - * Based on original ItemCollar from 1.12.2 - * Note: Collars have maxStackSize of 1 (unique items) - */ -public class ItemClassicCollar extends ItemCollar { - - public ItemClassicCollar() { - super( - new Item.Properties() - // stacksTo(1) is set by ItemCollar base class - ); - } -} diff --git a/src/main/java/com/tiedup/remake/items/ItemCommandWand.java b/src/main/java/com/tiedup/remake/items/ItemCommandWand.java index ac70562..7e9e073 100644 --- a/src/main/java/com/tiedup/remake/items/ItemCommandWand.java +++ b/src/main/java/com/tiedup/remake/items/ItemCommandWand.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.ModNetwork; +import com.tiedup.remake.v2.bondage.CollarHelper; import com.tiedup.remake.network.personality.PacketOpenCommandWandScreen; import com.tiedup.remake.personality.HomeType; import com.tiedup.remake.personality.JobExperience; @@ -349,11 +349,11 @@ public class ItemCommandWand extends Item { // Get collar and verify ownership ItemStack collar = damsel.getEquipment(BodyRegionV2.NECK); - if (!(collar.getItem() instanceof ItemCollar collarItem)) { + if (!CollarHelper.isCollar(collar)) { return InteractionResult.PASS; } - if (!collarItem.getOwners(collar).contains(player.getUUID())) { + if (!CollarHelper.isOwner(collar, player)) { SystemMessageManager.sendToPlayer( player, SystemMessageManager.MessageCategory.ERROR, diff --git a/src/main/java/com/tiedup/remake/items/ItemGpsCollar.java b/src/main/java/com/tiedup/remake/items/ItemGpsCollar.java deleted file mode 100644 index 5396c5e..0000000 --- a/src/main/java/com/tiedup/remake/items/ItemGpsCollar.java +++ /dev/null @@ -1,369 +0,0 @@ -package com.tiedup.remake.items; - -import com.tiedup.remake.state.IRestrainable; -import com.tiedup.remake.util.KidnappedHelper; -import java.util.ArrayList; -import java.util.List; -import net.minecraft.ChatFormatting; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.nbt.ListTag; -import net.minecraft.network.chat.Component; -import net.minecraft.world.entity.Entity; -import net.minecraft.world.entity.LivingEntity; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.TooltipFlag; -import net.minecraft.world.level.Level; -import org.jetbrains.annotations.Nullable; - -/** - - * GPS Collar - Advanced shock collar with tracking and safe zone features. - - * - - *

Mechanics:

- - *
    - - *
  • Safe Zones: Can store multiple coordinates (SafeSpots) where the wearer is allowed to be.
  • - - *
  • Auto-Shock: If the wearer is outside ALL active safe zones, they are shocked at intervals.
  • - - *
  • Master Warning: Masters receive an alert message when a safe zone violation is detected.
  • - - *
  • Public Tracking: If enabled, allows anyone with a Locator to see distance and direction.
  • - - *
- - */ - -public class ItemGpsCollar extends ItemShockCollar { - - private static final String NBT_PUBLIC_TRACKING = "publicTracking"; - - private static final String NBT_GPS_ACTIVE = "gpsActive"; - - private static final String NBT_SAFE_SPOTS = "gpsSafeSpots"; - - private static final String NBT_SHOCK_INTERVAL = "gpsShockInterval"; - - private static final String NBT_WARN_MASTERS = "warn_masters"; - - private final int defaultInterval; - - public ItemGpsCollar() { - this(200); // 10 seconds default - } - - public ItemGpsCollar(int defaultInterval) { - super(); - this.defaultInterval = defaultInterval; - } - - @Override - public boolean hasGPS() { - return true; - } - - /** - - * Renders detailed GPS status, safe zone list, and alert settings in the item tooltip. - - */ - - @Override - public void appendHoverText( - ItemStack stack, - @Nullable Level level, - List tooltip, - TooltipFlag flag - ) { - super.appendHoverText(stack, level, tooltip, flag); - - tooltip.add( - Component.literal("GPS Enabled").withStyle( - ChatFormatting.DARK_GREEN - ) - ); - - if (hasPublicTracking(stack)) { - tooltip.add( - Component.literal("Public Tracking Enabled").withStyle( - ChatFormatting.GREEN - ) - ); - } - - if (shouldWarnMasters(stack)) { - tooltip.add( - Component.literal("Alert Masters on Violation").withStyle( - ChatFormatting.GOLD - ) - ); - } - - List safeSpots = getSafeSpots(stack); - - if (!safeSpots.isEmpty()) { - tooltip.add( - Component.literal("GPS Shocks: ") - .withStyle(ChatFormatting.GREEN) - .append( - Component.literal( - isActive(stack) ? "ENABLED" : "DISABLED" - ).withStyle( - isActive(stack) - ? ChatFormatting.RED - : ChatFormatting.GRAY - ) - ) - ); - - tooltip.add( - Component.literal( - "Safe Spots (" + safeSpots.size() + "):" - ).withStyle(ChatFormatting.GREEN) - ); - - for (int i = 0; i < safeSpots.size(); i++) { - SafeSpot spot = safeSpots.get(i); - - tooltip.add( - Component.literal( - (spot.active ? "[+] " : "[-] ") + - (i + 1) + - ": " + - spot.x + - "," + - spot.y + - "," + - spot.z + - " (Range: " + - spot.distance + - "m)" - ).withStyle(ChatFormatting.GRAY) - ); - } - } - } - - public boolean shouldWarnMasters(ItemStack stack) { - CompoundTag tag = stack.getTag(); - - // Default to true if tag doesn't exist - - return ( - tag == null || - !tag.contains(NBT_WARN_MASTERS) || - tag.getBoolean(NBT_WARN_MASTERS) - ); - } - - public void setWarnMasters(ItemStack stack, boolean warn) { - stack.getOrCreateTag().putBoolean(NBT_WARN_MASTERS, warn); - } - - public boolean hasPublicTracking(ItemStack stack) { - CompoundTag tag = stack.getTag(); - - return tag != null && tag.getBoolean(NBT_PUBLIC_TRACKING); - } - - public void setPublicTracking(ItemStack stack, boolean publicTracking) { - stack.getOrCreateTag().putBoolean(NBT_PUBLIC_TRACKING, publicTracking); - } - - public boolean isActive(ItemStack stack) { - CompoundTag tag = stack.getTag(); - - // Default to active if tag doesn't exist - - return ( - tag == null || - !tag.contains(NBT_GPS_ACTIVE) || - tag.getBoolean(NBT_GPS_ACTIVE) - ); - } - - public void setActive(ItemStack stack, boolean active) { - stack.getOrCreateTag().putBoolean(NBT_GPS_ACTIVE, active); - } - - /** - - * Parses the NBT List into a Java List of SafeSpot objects. - - */ - - public List getSafeSpots(ItemStack stack) { - List list = new ArrayList<>(); - - CompoundTag tag = stack.getTag(); - - if (tag != null && tag.contains(NBT_SAFE_SPOTS)) { - ListTag spotList = tag.getList(NBT_SAFE_SPOTS, 10); - - for (int i = 0; i < spotList.size(); i++) { - list.add(new SafeSpot(spotList.getCompound(i))); - } - } - - return list; - } - - /** - - * Adds a new safe zone to the collar's NBT data. - - */ - - public void addSafeSpot( - ItemStack stack, - int x, - int y, - int z, - String dimension, - int distance - ) { - CompoundTag tag = stack.getOrCreateTag(); - - ListTag spotList = tag.getList(NBT_SAFE_SPOTS, 10); - - SafeSpot spot = new SafeSpot(x, y, z, dimension, distance, true); - - spotList.add(spot.toNBT()); - - tag.put(NBT_SAFE_SPOTS, spotList); - } - - /** - - * Gets frequency of GPS violation shocks. - - */ - - public int getShockInterval(ItemStack stack) { - CompoundTag tag = stack.getTag(); - - if (tag != null && tag.contains(NBT_SHOCK_INTERVAL)) { - return tag.getInt(NBT_SHOCK_INTERVAL); - } - - return defaultInterval; - } - - /** - */ - @Override - public void onUnequipped(ItemStack stack, LivingEntity entity) { - // Use IRestrainable interface instead of Player-only - IRestrainable state = KidnappedHelper.getKidnappedState(entity); - if (state != null) { - state.resetAutoShockTimer(); - } - - super.onUnequipped(stack, entity); - } - - /** - - * Represents a defined safe zone in the 3D world. - - */ - - public static class SafeSpot { - - public int x, y, z; - - public String dimension; - - public int distance; - - public boolean active; - - public SafeSpot( - int x, - int y, - int z, - String dimension, - int distance, - boolean active - ) { - this.x = x; - - this.y = y; - - this.z = z; - - this.dimension = dimension; - - this.distance = distance; - - this.active = active; - } - - public SafeSpot(CompoundTag nbt) { - this.x = nbt.getInt("x"); - - this.y = nbt.getInt("y"); - - this.z = nbt.getInt("z"); - - this.dimension = nbt.getString("dim"); - - this.distance = nbt.getInt("dist"); - - this.active = !nbt.contains("active") || nbt.getBoolean("active"); - } - - public CompoundTag toNBT() { - CompoundTag nbt = new CompoundTag(); - - nbt.putInt("x", x); - - nbt.putInt("y", y); - - nbt.putInt("z", z); - - nbt.putString("dim", dimension); - - nbt.putInt("dist", distance); - - nbt.putBoolean("active", active); - - return nbt; - } - - /** - - * Checks if an entity is within the cuboid boundaries of this safe zone. - - * Faithful to original 1.12.2 distance logic. - - */ - - public boolean isInside(Entity entity) { - if (!active) return true; - - // LOW FIX: Cross-dimension GPS fix - // If entity is in a different dimension, consider them as "inside" the zone - // to prevent false positive shocks when traveling between dimensions - if ( - !entity - .level() - .dimension() - .location() - .toString() - .equals(dimension) - ) return true; // Changed from false to true - - // Cuboid distance check - - return ( - Math.abs(entity.getX() - x) < distance && - Math.abs(entity.getY() - y) < distance && - Math.abs(entity.getZ() - z) < distance - ); - } - } -} diff --git a/src/main/java/com/tiedup/remake/items/ItemGpsLocator.java b/src/main/java/com/tiedup/remake/items/ItemGpsLocator.java index dd77a54..85ef45a 100644 --- a/src/main/java/com/tiedup/remake/items/ItemGpsLocator.java +++ b/src/main/java/com/tiedup/remake/items/ItemGpsLocator.java @@ -1,8 +1,8 @@ package com.tiedup.remake.items; import com.tiedup.remake.core.SystemMessageManager; -import com.tiedup.remake.items.base.ItemCollar; import com.tiedup.remake.items.base.ItemOwnerTarget; +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; @@ -91,13 +91,10 @@ public class ItemGpsLocator extends ItemOwnerTarget { ItemStack collarStack = targetState.getEquipment( BodyRegionV2.NECK ); - if ( - collarStack.getItem() instanceof - ItemGpsCollar collarItem - ) { + if (CollarHelper.hasGPS(collarStack)) { if ( - collarItem.isOwner(collarStack, player) || - collarItem.hasPublicTracking(collarStack) + CollarHelper.isOwner(collarStack, player) || + CollarHelper.hasPublicTracking(collarStack) ) { // Check if same dimension boolean sameDimension = player diff --git a/src/main/java/com/tiedup/remake/items/ItemHood.java b/src/main/java/com/tiedup/remake/items/ItemHood.java deleted file mode 100644 index 96802b8..0000000 --- a/src/main/java/com/tiedup/remake/items/ItemHood.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.tiedup.remake.items; - -import com.tiedup.remake.items.base.IHasGaggingEffect; -import com.tiedup.remake.items.base.ItemBlindfold; -import com.tiedup.remake.util.GagMaterial; -import net.minecraft.world.item.Item; - -/** - * Hood - Covers the head completely - * Combines blindfold effect with gagging effect. - * - * Extends ItemBlindfold for slot behavior, implements IHasGaggingEffect for speech muffling. - */ -public class ItemHood extends ItemBlindfold implements IHasGaggingEffect { - - private final GagMaterial gagMaterial; - - public ItemHood() { - super(new Item.Properties().stacksTo(16)); - this.gagMaterial = GagMaterial.STUFFED; // Hoods muffle speech like stuffed gags - } - - /** - * Get the gag material type for speech conversion. - * @return The gag material (STUFFED for hoods) - */ - public GagMaterial getGagMaterial() { - return gagMaterial; - } - - @Override - public String getTextureSubfolder() { - return "hoods"; - } -} diff --git a/src/main/java/com/tiedup/remake/items/ItemKey.java b/src/main/java/com/tiedup/remake/items/ItemKey.java index 71c441c..bbebb14 100644 --- a/src/main/java/com/tiedup/remake/items/ItemKey.java +++ b/src/main/java/com/tiedup/remake/items/ItemKey.java @@ -259,13 +259,10 @@ public class ItemKey extends ItemOwnerTarget { ItemStack collarStack = targetState.getEquipment(BodyRegionV2.NECK); if (collarStack.isEmpty()) return; - if ( - collarStack.getItem() instanceof - com.tiedup.remake.items.base.ItemCollar collar - ) { + if (com.tiedup.remake.v2.bondage.CollarHelper.isCollar(collarStack)) { // Add player as owner to the collar (if not already) - if (!collar.getOwners(collarStack).contains(player.getUUID())) { - collar.addOwner(collarStack, player); + if (!com.tiedup.remake.v2.bondage.CollarHelper.isOwner(collarStack, player)) { + com.tiedup.remake.v2.bondage.CollarHelper.addOwner(collarStack, player); // Update the collar in the target's inventory targetState.equip(BodyRegionV2.NECK, collarStack); diff --git a/src/main/java/com/tiedup/remake/items/ItemLockpick.java b/src/main/java/com/tiedup/remake/items/ItemLockpick.java index 6318095..6814ceb 100644 --- a/src/main/java/com/tiedup/remake/items/ItemLockpick.java +++ b/src/main/java/com/tiedup/remake/items/ItemLockpick.java @@ -332,15 +332,12 @@ public class ItemLockpick extends Item { ); if (collar.isEmpty()) return; - if ( - collar.getItem() instanceof - com.tiedup.remake.items.ItemShockCollar shockCollar - ) { + if (com.tiedup.remake.v2.bondage.CollarHelper.canShock(collar)) { // Shock the player state.shockKidnapped(" (Failed lockpick attempt)", 2.0f); // Notify owners - notifyOwnersLockpickAttempt(player, collar, shockCollar); + notifyOwnersLockpickAttempt(player, collar); TiedUpMod.LOGGER.info( "[LOCKPICK] {} was shocked for failed lockpick attempt", @@ -354,8 +351,7 @@ public class ItemLockpick extends Item { */ private static void notifyOwnersLockpickAttempt( Player player, - ItemStack collar, - com.tiedup.remake.items.ItemShockCollar shockCollar + ItemStack collar ) { if (player.getServer() == null) return; @@ -367,7 +363,7 @@ public class ItemLockpick extends Item { ).withStyle(ChatFormatting.GOLD) ); - List owners = shockCollar.getOwners(collar); + List owners = com.tiedup.remake.v2.bondage.CollarHelper.getOwners(collar); for (UUID ownerId : owners) { ServerPlayer owner = player .getServer() diff --git a/src/main/java/com/tiedup/remake/items/ItemMedicalGag.java b/src/main/java/com/tiedup/remake/items/ItemMedicalGag.java deleted file mode 100644 index f4e84a6..0000000 --- a/src/main/java/com/tiedup/remake/items/ItemMedicalGag.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.tiedup.remake.items; - -import com.tiedup.remake.items.base.IHasBlindingEffect; -import com.tiedup.remake.items.base.ItemGag; -import com.tiedup.remake.util.GagMaterial; -import net.minecraft.world.item.Item; - -/** - * Medical Gag - Full face medical restraint - * Combines gag effect with blinding effect. - * - * Extends ItemGag for slot behavior, implements IHasBlindingEffect for vision obstruction. - */ -public class ItemMedicalGag extends ItemGag implements IHasBlindingEffect { - - public ItemMedicalGag() { - super(new Item.Properties().stacksTo(16), GagMaterial.PANEL); - } - - @Override - public String getTextureSubfolder() { - return "straps"; - } -} diff --git a/src/main/java/com/tiedup/remake/items/ItemShockCollar.java b/src/main/java/com/tiedup/remake/items/ItemShockCollar.java deleted file mode 100644 index 3a22f36..0000000 --- a/src/main/java/com/tiedup/remake/items/ItemShockCollar.java +++ /dev/null @@ -1,133 +0,0 @@ -package com.tiedup.remake.items; - -import com.tiedup.remake.core.SystemMessageManager; -import com.tiedup.remake.items.base.ItemCollar; -import com.tiedup.remake.state.IBondageState; -import com.tiedup.remake.util.KidnappedHelper; -import java.util.List; -import net.minecraft.ChatFormatting; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.network.chat.Component; -import net.minecraft.world.InteractionHand; -import net.minecraft.world.InteractionResultHolder; -import net.minecraft.world.entity.LivingEntity; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.item.Item; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.TooltipFlag; -import net.minecraft.world.level.Level; -import org.jetbrains.annotations.Nullable; - -/** - * Shock Collar - Advanced collar that can be remotely triggered. - * - *

Mechanics:

- *
    - *
  • Remote Shocking: Can be triggered by anyone holding a linked Shocker Controller.
  • - *
  • Struggle Penalty: If locked, has a chance to shock the wearer during struggle attempts, interrupting them.
  • - *
  • Public Mode: Can be set to public mode, allowing anyone to shock the wearer even if they aren't the owner.
  • - *
- */ -public class ItemShockCollar extends ItemCollar { - - private static final String NBT_PUBLIC_MODE = "public_mode"; - - public ItemShockCollar() { - super(new Item.Properties()); - } - - @Override - public boolean canShock() { - return true; - } - - /** - * Shows current mode (PUBLIC/PRIVATE) and usage instructions in tooltip. - */ - @Override - public void appendHoverText( - ItemStack stack, - @Nullable Level level, - List tooltip, - TooltipFlag flag - ) { - super.appendHoverText(stack, level, tooltip, flag); - - tooltip.add( - Component.literal("Shock Feature: ") - .withStyle(ChatFormatting.YELLOW) - .append( - Component.literal( - isPublic(stack) ? "PUBLIC" : "PRIVATE" - ).withStyle( - isPublic(stack) - ? ChatFormatting.GREEN - : ChatFormatting.RED - ) - ) - ); - - tooltip.add( - Component.literal("Shift + Right-click to toggle public mode") - .withStyle(ChatFormatting.DARK_GRAY) - .withStyle(ChatFormatting.ITALIC) - ); - } - - /** - * Toggles Public mode when shift-right-clicking in air. - */ - @Override - public InteractionResultHolder use( - Level level, - Player player, - InteractionHand hand - ) { - ItemStack stack = player.getItemInHand(hand); - - if (player.isShiftKeyDown()) { - if (!level.isClientSide) { - boolean newState = !isPublic(stack); - setPublic(stack, newState); - SystemMessageManager.sendToPlayer( - player, - SystemMessageManager.MessageCategory.SHOCKER_MODE_SET, - (newState ? "PUBLIC" : "PRIVATE") - ); - } - return InteractionResultHolder.sidedSuccess( - stack, - level.isClientSide() - ); - } - - return super.use(level, player, hand); - } - - /** - * Handles the risk of shocking the wearer during a struggle attempt. - * - * NOTE: For the new continuous struggle mini-game, shock logic is handled - * directly in MiniGameSessionManager.tickContinuousSessions(). This method - * is now a no-op that always returns true, kept for API compatibility. - * - * @param entity The wearer of the collar - * @param stack The collar instance - * @return Always true (shock logic moved to MiniGameSessionManager) - */ - public boolean notifyStruggle(LivingEntity entity, ItemStack stack) { - // Shock collar checks during continuous struggle are now handled by - // MiniGameSessionManager.shouldTriggerShock() with 10% chance every 5 seconds. - // This method is kept for backwards compatibility but no longer performs the check. - return true; - } - - public boolean isPublic(ItemStack stack) { - CompoundTag tag = stack.getTag(); - return tag != null && tag.getBoolean(NBT_PUBLIC_MODE); - } - - public void setPublic(ItemStack stack, boolean publicMode) { - stack.getOrCreateTag().putBoolean(NBT_PUBLIC_MODE, publicMode); - } -} diff --git a/src/main/java/com/tiedup/remake/items/ItemShockCollarAuto.java b/src/main/java/com/tiedup/remake/items/ItemShockCollarAuto.java deleted file mode 100644 index 07eb8a4..0000000 --- a/src/main/java/com/tiedup/remake/items/ItemShockCollarAuto.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.tiedup.remake.items; - -import com.tiedup.remake.state.IRestrainable; -import com.tiedup.remake.util.KidnappedHelper; -import net.minecraft.world.entity.LivingEntity; -import net.minecraft.world.item.ItemStack; - -/** - * Automatic Shock Collar - Shocks the wearer at regular intervals. - * - * - *

Mechanics:

- *
    - *
  • Self-Triggering: Has an internal timer stored in NBT that shocks the entity when it reaches 0.
  • - *
  • Unstruggable: By default, cannot be escaped via struggle mechanics (requires key).
  • - *
- */ -public class ItemShockCollarAuto extends ItemShockCollar { - - private final int interval; - - /** - * @param interval Frequency of shocks in TICKS (20 ticks = 1 second). - */ - public ItemShockCollarAuto() { - this(600); // 30 seconds default - } - - public ItemShockCollarAuto(int interval) { - super(); - this.interval = interval; - } - - public int getInterval() { - return interval; - } - - /** - * Ensures the internal shock timer is cleaned up when the item is removed. - * - */ - @Override - public void onUnequipped(ItemStack stack, LivingEntity entity) { - IRestrainable state = KidnappedHelper.getKidnappedState(entity); - if (state != null) { - state.resetAutoShockTimer(); - } - super.onUnequipped(stack, entity); - } - - /** - * Prevents escaping through struggle mechanics for this specific collar type. - */ - @Override - public boolean canBeStruggledOut(ItemStack stack) { - return false; - } -} diff --git a/src/main/java/com/tiedup/remake/items/ItemShockerController.java b/src/main/java/com/tiedup/remake/items/ItemShockerController.java index 696f13c..9678ebd 100644 --- a/src/main/java/com/tiedup/remake/items/ItemShockerController.java +++ b/src/main/java/com/tiedup/remake/items/ItemShockerController.java @@ -3,8 +3,8 @@ package com.tiedup.remake.items; import com.tiedup.remake.core.ModSounds; import com.tiedup.remake.core.SystemMessageManager; import com.tiedup.remake.core.TiedUpMod; -import com.tiedup.remake.items.base.ItemCollar; import com.tiedup.remake.items.base.ItemOwnerTarget; +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; @@ -77,12 +77,11 @@ public class ItemShockerController extends ItemOwnerTarget { BodyRegionV2.NECK ); if ( - collar.getItem() instanceof - ItemCollar collarItem && - collarItem.hasNickname(collar) + CollarHelper.isCollar(collar) && + CollarHelper.hasNickname(collar) ) { displayName = - collarItem.getNickname(collar) + + CollarHelper.getNickname(collar) + " (" + displayName + ")"; @@ -330,12 +329,10 @@ public class ItemShockerController extends ItemOwnerTarget { IRestrainable state = KidnappedHelper.getKidnappedState(entity); if (state != null && state.hasCollar()) { ItemStack collarStack = state.getEquipment(BodyRegionV2.NECK); - if ( - collarStack.getItem() instanceof ItemShockCollar collarItem - ) { + if (CollarHelper.canShock(collarStack)) { if ( - collarItem.getOwners(collarStack).contains(ownerId) || - collarItem.isPublic(collarStack) + CollarHelper.isOwner(collarStack, ownerId) || + CollarHelper.isPublicShock(collarStack) ) { targets.add(entity); } diff --git a/src/main/java/com/tiedup/remake/items/ModCreativeTabs.java b/src/main/java/com/tiedup/remake/items/ModCreativeTabs.java index 62a1318..dd33824 100644 --- a/src/main/java/com/tiedup/remake/items/ModCreativeTabs.java +++ b/src/main/java/com/tiedup/remake/items/ModCreativeTabs.java @@ -2,9 +2,11 @@ package com.tiedup.remake.items; import com.tiedup.remake.blocks.ModBlocks; import com.tiedup.remake.core.TiedUpMod; -import com.tiedup.remake.entities.KidnapperItemSelector; import com.tiedup.remake.items.base.*; import com.tiedup.remake.v2.V2Items; +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.core.registries.Registries; import net.minecraft.network.chat.Component; import net.minecraft.world.item.CreativeModeTab; @@ -16,7 +18,7 @@ import net.minecraftforge.registries.RegistryObject; * Creative Mode Tabs Registration * Defines the creative inventory tabs where TiedUp items will appear. * - * Updated to use factory pattern with enum-based item registration. + * All bondage items are now sourced from DataDrivenItemRegistry. */ @SuppressWarnings("null") // Minecraft API guarantees non-null returns public class ModCreativeTabs { @@ -28,126 +30,27 @@ public class ModCreativeTabs { CREATIVE_MODE_TABS.register("tiedup_tab", () -> CreativeModeTab.builder() .title(Component.translatable("itemGroup.tiedup")) - .icon(() -> new ItemStack(ModItems.getBind(BindVariant.ROPES))) + .icon(() -> { + // Use first data-driven item as icon, or fallback to whip + var allDefs = DataDrivenItemRegistry.getAll(); + if (!allDefs.isEmpty()) { + return DataDrivenBondageItem.createStack(allDefs.iterator().next().id()); + } + return new ItemStack(ModItems.WHIP.get()); + }) .displayItems((parameters, output) -> { - // ========== BINDS (from enum) ========== - for (BindVariant variant : BindVariant.values()) { - // Add base item - output.accept(ModItems.getBind(variant)); - - // Add colored variants if supported - if (variant.supportsColor()) { - for (ItemColor color : ItemColor.values()) { - // Skip special colors (caution, clear) except for duct tape - if ( - color.isSpecial() && - variant != BindVariant.DUCT_TAPE - ) continue; - // Use validation method to check if color has texture - if ( - KidnapperItemSelector.isColorValidForBind( - color, - variant - ) - ) { - output.accept( - KidnapperItemSelector.createBind( - variant, - color - ) - ); - } - } - } + // ========== DATA-DRIVEN BONDAGE ITEMS ========== + // All binds, gags, blindfolds, earplugs, mittens, collars, + // hood, medical gag, etc. are now data-driven + for (DataDrivenItemDefinition def : DataDrivenItemRegistry.getAll()) { + output.accept( + DataDrivenBondageItem.createStack(def.id()) + ); } - // ========== GAGS (from enum) ========== - for (GagVariant variant : GagVariant.values()) { - // Add base item - output.accept(ModItems.getGag(variant)); - - // Add colored variants if supported - if (variant.supportsColor()) { - for (ItemColor color : ItemColor.values()) { - // Skip special colors (caution, clear) except for tape gag - if ( - color.isSpecial() && - variant != GagVariant.TAPE_GAG - ) continue; - // Use validation method to check if color has texture - if ( - KidnapperItemSelector.isColorValidForGag( - color, - variant - ) - ) { - output.accept( - KidnapperItemSelector.createGag( - variant, - color - ) - ); - } - } - } - } - - // ========== BLINDFOLDS (from enum) ========== - for (BlindfoldVariant variant : BlindfoldVariant.values()) { - // Add base item - output.accept(ModItems.getBlindfold(variant)); - - // Add colored variants if supported - if (variant.supportsColor()) { - for (ItemColor color : ItemColor.values()) { - // Skip special colors for blindfolds - if (color.isSpecial()) continue; - // Use validation method to check if color has texture - if ( - KidnapperItemSelector.isColorValidForBlindfold( - color, - variant - ) - ) { - output.accept( - KidnapperItemSelector.createBlindfold( - variant, - color - ) - ); - } - } - } - } - - // Hood (combo item, not in enum) - output.accept(ModItems.HOOD.get()); - - // ========== 3D ITEMS ========== - output.accept(ModItems.BALL_GAG_3D.get()); - - // ========== COMBO ITEMS ========== - output.accept(ModItems.MEDICAL_GAG.get()); - // ========== CLOTHES ========== output.accept(ModItems.CLOTHES.get()); - // ========== COLLARS ========== - output.accept(ModItems.CLASSIC_COLLAR.get()); - output.accept(ModItems.CHOKE_COLLAR.get()); - output.accept(ModItems.SHOCK_COLLAR.get()); - output.accept(ModItems.GPS_COLLAR.get()); - - // ========== EARPLUGS (from enum) ========== - for (EarplugsVariant variant : EarplugsVariant.values()) { - output.accept(ModItems.getEarplugs(variant)); - } - - // ========== MITTENS (from enum) ========== - for (MittensVariant variant : MittensVariant.values()) { - output.accept(ModItems.getMittens(variant)); - } - // ========== KNIVES (from enum) ========== for (KnifeVariant variant : KnifeVariant.values()) { output.accept(ModItems.getKnife(variant)); @@ -196,15 +99,6 @@ public class ModCreativeTabs { com.tiedup.remake.v2.bondage.V2BondageItems.V2_HANDCUFFS.get() ); - // ========== DATA-DRIVEN BONDAGE ITEMS ========== - for (com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemDefinition def : com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemRegistry.getAll()) { - output.accept( - com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem.createStack( - def.id() - ) - ); - } - // ========== FURNITURE PLACER ITEMS ========== for (com.tiedup.remake.v2.furniture.FurnitureDefinition def : com.tiedup.remake.v2.furniture.FurnitureRegistry.getAll()) { output.accept( diff --git a/src/main/java/com/tiedup/remake/items/ModItems.java b/src/main/java/com/tiedup/remake/items/ModItems.java index abb8a60..5388d1d 100644 --- a/src/main/java/com/tiedup/remake/items/ModItems.java +++ b/src/main/java/com/tiedup/remake/items/ModItems.java @@ -3,7 +3,6 @@ package com.tiedup.remake.items; import com.tiedup.remake.core.TiedUpMod; import com.tiedup.remake.entities.ModEntities; import com.tiedup.remake.items.base.*; -import com.tiedup.remake.items.bondage3d.gags.ItemBallGag3D; import com.tiedup.remake.items.clothes.GenericClothes; import java.util.EnumMap; import java.util.Map; @@ -17,14 +16,9 @@ import net.minecraftforge.registries.RegistryObject; * Mod Items Registration * Handles registration of all TiedUp items using DeferredRegister. * - * Refactored with Factory Pattern: - * - Binds, Gags, Blindfolds, Earplugs, Knives use EnumMaps and factory methods - * - Complex items (collars, whip, chloroform, etc.) remain individual registrations - * - * Usage: - * - ModItems.getBind(BindVariant.ROPES) - Get a specific bind item - * - ModItems.getGag(GagVariant.BALL_GAG) - Get a specific gag item - * - ModItems.WHIP.get() - Get complex items directly + * V1 bondage items (binds, gags, blindfolds, earplugs, mittens, collars, hood, medical gag) + * have been removed. All bondage items are now data-driven via DataDrivenItemRegistry. + * Only non-bondage items and knives remain here. */ public class ModItems { @@ -34,42 +28,7 @@ public class ModItems { TiedUpMod.MOD_ID ); - // ========== FACTORY-BASED ITEMS ========== - - /** - * All bind items (15 variants via BindVariant enum) - */ - public static final Map> BINDS = - registerAllBinds(); - - /** - * All gag items (via GagVariant enum) - * Note: ItemMedicalGag is registered separately as it has special behavior - * Note: BALL_GAG_3D is a separate 3D item (not in enum) - */ - public static final Map> GAGS = - registerAllGags(); - - /** - * Ball Gag 3D - Uses 3D OBJ model rendering via dedicated class. - * This is a separate item from BALL_GAG (which uses 2D textures). - */ - public static final RegistryObject BALL_GAG_3D = ITEMS.register( - "ball_gag_3d", - ItemBallGag3D::new - ); - - /** - * All blindfold items (2 variants via BlindfoldVariant enum) - */ - public static final Map> BLINDFOLDS = - registerAllBlindfolds(); - - /** - * All earplugs items (1 variant via EarplugsVariant enum) - */ - public static final Map> EARPLUGS = - registerAllEarplugs(); + // ========== KNIVES (still V1 — not bondage items) ========== /** * All knife items (3 variants via KnifeVariant enum) @@ -77,12 +36,6 @@ public class ModItems { public static final Map> KNIVES = registerAllKnives(); - /** - * All mittens items (1 variant via MittensVariant enum) - */ - public static final Map> MITTENS = - registerAllMittens(); - /** * Clothes item - uses dynamic textures from URLs. * Users can create presets via anvil naming. @@ -92,48 +45,8 @@ public class ModItems { GenericClothes::new ); - // ========== COMPLEX ITEMS (individual registrations) ========== + // ========== TOOLS ========== - // Medical gag - combo item with IHasBlindingEffect - public static final RegistryObject MEDICAL_GAG = ITEMS.register( - "medical_gag", - ItemMedicalGag::new - ); - - // Hood - combo item - public static final RegistryObject HOOD = ITEMS.register( - "hood", - ItemHood::new - ); - - // Collars - complex logic - public static final RegistryObject CLASSIC_COLLAR = ITEMS.register( - "classic_collar", - ItemClassicCollar::new - ); - - public static final RegistryObject SHOCK_COLLAR = ITEMS.register( - "shock_collar", - ItemShockCollar::new - ); - - public static final RegistryObject SHOCK_COLLAR_AUTO = ITEMS.register( - "shock_collar_auto", - ItemShockCollarAuto::new - ); - - public static final RegistryObject GPS_COLLAR = ITEMS.register( - "gps_collar", - ItemGpsCollar::new - ); - - // Choke Collar - Pet play collar used by Masters - public static final RegistryObject CHOKE_COLLAR = ITEMS.register( - "choke_collar", - ItemChokeCollar::new - ); - - // Tools with complex behavior public static final RegistryObject WHIP = ITEMS.register( "whip", ItemWhip::new @@ -213,13 +126,11 @@ public class ModItems { // ========== CELL SYSTEM ITEMS ========== - // Admin Wand - Structure marker placement and Cell Core management public static final RegistryObject ADMIN_WAND = ITEMS.register( "admin_wand", ItemAdminWand::new ); - // Cell Key - Universal key for iron bar doors public static final RegistryObject CELL_KEY = ITEMS.register( "cell_key", ItemCellKey::new @@ -227,7 +138,6 @@ public class ModItems { // ========== SLAVE TRADER SYSTEM ========== - // Token - Access pass for kidnapper camps public static final RegistryObject TOKEN = ITEMS.register( "token", ItemToken::new @@ -252,72 +162,6 @@ public class ModItems { // ========== FACTORY METHODS ========== - private static Map> registerAllBinds() { - Map> map = new EnumMap<>( - BindVariant.class - ); - for (BindVariant variant : BindVariant.values()) { - map.put( - variant, - ITEMS.register(variant.getRegistryName(), () -> - new GenericBind(variant) - ) - ); - } - return map; - } - - private static Map> registerAllGags() { - Map> map = new EnumMap<>( - GagVariant.class - ); - for (GagVariant variant : GagVariant.values()) { - map.put( - variant, - ITEMS.register(variant.getRegistryName(), () -> - new GenericGag(variant) - ) - ); - } - return map; - } - - private static Map< - BlindfoldVariant, - RegistryObject - > registerAllBlindfolds() { - Map> map = new EnumMap<>( - BlindfoldVariant.class - ); - for (BlindfoldVariant variant : BlindfoldVariant.values()) { - map.put( - variant, - ITEMS.register(variant.getRegistryName(), () -> - new GenericBlindfold(variant) - ) - ); - } - return map; - } - - private static Map< - EarplugsVariant, - RegistryObject - > registerAllEarplugs() { - Map> map = new EnumMap<>( - EarplugsVariant.class - ); - for (EarplugsVariant variant : EarplugsVariant.values()) { - map.put( - variant, - ITEMS.register(variant.getRegistryName(), () -> - new GenericEarplugs(variant) - ) - ); - } - return map; - } - private static Map> registerAllKnives() { Map> map = new EnumMap<>( KnifeVariant.class @@ -333,62 +177,8 @@ public class ModItems { return map; } - private static Map< - MittensVariant, - RegistryObject - > registerAllMittens() { - Map> map = new EnumMap<>( - MittensVariant.class - ); - for (MittensVariant variant : MittensVariant.values()) { - map.put( - variant, - ITEMS.register(variant.getRegistryName(), () -> - new GenericMittens(variant) - ) - ); - } - return map; - } - // ========== HELPER ACCESSORS ========== - /** - * Get a bind item by variant. - * @param variant The bind variant - * @return The bind item - */ - public static Item getBind(BindVariant variant) { - return BINDS.get(variant).get(); - } - - /** - * Get a gag item by variant. - * @param variant The gag variant - * @return The gag item - */ - public static Item getGag(GagVariant variant) { - return GAGS.get(variant).get(); - } - - /** - * Get a blindfold item by variant. - * @param variant The blindfold variant - * @return The blindfold item - */ - public static Item getBlindfold(BlindfoldVariant variant) { - return BLINDFOLDS.get(variant).get(); - } - - /** - * Get an earplugs item by variant. - * @param variant The earplugs variant - * @return The earplugs item - */ - public static Item getEarplugs(EarplugsVariant variant) { - return EARPLUGS.get(variant).get(); - } - /** * Get a knife item by variant. * @param variant The knife variant @@ -397,13 +187,4 @@ public class ModItems { public static Item getKnife(KnifeVariant variant) { return KNIVES.get(variant).get(); } - - /** - * Get a mittens item by variant. - * @param variant The mittens variant - * @return The mittens item - */ - public static Item getMittens(MittensVariant variant) { - return MITTENS.get(variant).get(); - } } diff --git a/src/main/java/com/tiedup/remake/items/base/AdjustmentHelper.java b/src/main/java/com/tiedup/remake/items/base/AdjustmentHelper.java index e7d6727..e3cbfeb 100644 --- a/src/main/java/com/tiedup/remake/items/base/AdjustmentHelper.java +++ b/src/main/java/com/tiedup/remake/items/base/AdjustmentHelper.java @@ -1,5 +1,8 @@ package com.tiedup.remake.items.base; +import com.tiedup.remake.v2.bondage.component.AdjustableComponent; +import com.tiedup.remake.v2.bondage.component.ComponentType; +import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem; import net.minecraft.nbt.CompoundTag; import net.minecraft.util.Mth; import net.minecraft.world.item.ItemStack; @@ -56,9 +59,12 @@ public class AdjustmentHelper { return tag.getFloat(NBT_ADJUSTMENT_Y); } - // Fallback to item's default adjustment - if (stack.getItem() instanceof IAdjustable adj) { - return adj.getDefaultAdjustment(); + // Fallback to item's default adjustment from V2 AdjustableComponent + AdjustableComponent comp = DataDrivenBondageItem.getComponent( + stack, ComponentType.ADJUSTABLE, AdjustableComponent.class + ); + if (comp != null) { + return comp.getDefaultValue(); } return DEFAULT_VALUE; @@ -127,16 +133,15 @@ public class AdjustmentHelper { * Check if an ItemStack's item supports adjustment. * * @param stack The ItemStack to check - * @return true if the item implements IAdjustable and canBeAdjusted() returns true + * @return true if the item has an AdjustableComponent */ public static boolean isAdjustable(ItemStack stack) { if (stack.isEmpty()) { return false; } - if (stack.getItem() instanceof IAdjustable adj) { - return adj.canBeAdjusted(); - } - return false; + return DataDrivenBondageItem.getComponent( + stack, ComponentType.ADJUSTABLE, AdjustableComponent.class + ) != null; } /** diff --git a/src/main/java/com/tiedup/remake/items/base/BindVariant.java b/src/main/java/com/tiedup/remake/items/base/BindVariant.java deleted file mode 100644 index 400b378..0000000 --- a/src/main/java/com/tiedup/remake/items/base/BindVariant.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.tiedup.remake.items.base; - -/** - * Enum defining all bind variants with their properties. - * Used by GenericBind to create bind items via factory pattern. - * - *

Issue #12 fix: Added textureSubfolder to eliminate 40+ string checks in renderers. - */ -public enum BindVariant { - // Standard binds (PoseType.STANDARD) - ROPES("ropes", PoseType.STANDARD, true, "ropes"), - ARMBINDER("armbinder", PoseType.STANDARD, false, "armbinder"), - DOGBINDER("dogbinder", PoseType.DOG, false, "armbinder"), - CHAIN("chain", PoseType.STANDARD, false, "chain"), - RIBBON("ribbon", PoseType.STANDARD, false, "ribbon"), - SLIME("slime", PoseType.STANDARD, false, "slime"), - VINE_SEED("vine_seed", PoseType.STANDARD, false, "vine"), - WEB_BIND("web_bind", PoseType.STANDARD, false, "web"), - SHIBARI("shibari", PoseType.STANDARD, true, "shibari"), - LEATHER_STRAPS("leather_straps", PoseType.STANDARD, false, "straps"), - MEDICAL_STRAPS("medical_straps", PoseType.STANDARD, false, "straps"), - BEAM_CUFFS("beam_cuffs", PoseType.STANDARD, false, "beam"), - DUCT_TAPE("duct_tape", PoseType.STANDARD, true, "tape"), - - // Pose items (special PoseType) - STRAITJACKET("straitjacket", PoseType.STRAITJACKET, false, "straitjacket"), - WRAP("wrap", PoseType.WRAP, false, "wrap"), - LATEX_SACK("latex_sack", PoseType.LATEX_SACK, false, "latex"); - - private final String registryName; - private final PoseType poseType; - private final boolean supportsColor; - private final String textureSubfolder; - - BindVariant( - String registryName, - PoseType poseType, - boolean supportsColor, - String textureSubfolder - ) { - this.registryName = registryName; - this.poseType = poseType; - this.supportsColor = supportsColor; - this.textureSubfolder = textureSubfolder; - } - - public String getRegistryName() { - return registryName; - } - - /** - * Get the configured resistance for this bind variant. - * Delegates to {@link com.tiedup.remake.core.SettingsAccessor#getBindResistance(String)}. - */ - public int getResistance() { - return com.tiedup.remake.core.SettingsAccessor.getBindResistance( - registryName - ); - } - - public PoseType getPoseType() { - return poseType; - } - - /** - * Check if this bind variant supports color variations. - * Items with colors: ropes, shibari, duct_tape - */ - public boolean supportsColor() { - return supportsColor; - } - - /** - * Get the texture subfolder for this bind variant. - * Used by renderers to locate texture files. - * - * @return Subfolder path under textures/entity/bondage/ (e.g., "ropes", "straps") - */ - public String getTextureSubfolder() { - return textureSubfolder; - } - - /** - * Get the item name used for textures and translations. - * For most variants this is the same as registryName. - */ - public String getItemName() { - return registryName; - } -} diff --git a/src/main/java/com/tiedup/remake/items/base/BlindfoldVariant.java b/src/main/java/com/tiedup/remake/items/base/BlindfoldVariant.java deleted file mode 100644 index 41cbc36..0000000 --- a/src/main/java/com/tiedup/remake/items/base/BlindfoldVariant.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.tiedup.remake.items.base; - -/** - * Enum defining all blindfold variants. - * Used by GenericBlindfold to create blindfold items via factory pattern. - * - *

Issue #12 fix: Added textureSubfolder to eliminate string checks in renderers. - */ -public enum BlindfoldVariant { - CLASSIC("classic_blindfold", true, "blindfolds"), - MASK("blindfold_mask", true, "blindfolds/mask"); - - private final String registryName; - private final boolean supportsColor; - private final String textureSubfolder; - - BlindfoldVariant( - String registryName, - boolean supportsColor, - String textureSubfolder - ) { - this.registryName = registryName; - this.supportsColor = supportsColor; - this.textureSubfolder = textureSubfolder; - } - - public String getRegistryName() { - return registryName; - } - - /** - * Check if this blindfold variant supports color variations. - * Both variants support colors in the original mod. - */ - public boolean supportsColor() { - return supportsColor; - } - - /** - * Get the texture subfolder for this blindfold variant. - * Used by renderers to locate texture files. - * - * @return Subfolder path under textures/entity/bondage/ (e.g., "blindfolds", "blindfolds/mask") - */ - public String getTextureSubfolder() { - return textureSubfolder; - } -} diff --git a/src/main/java/com/tiedup/remake/items/base/EarplugsVariant.java b/src/main/java/com/tiedup/remake/items/base/EarplugsVariant.java deleted file mode 100644 index 97368f9..0000000 --- a/src/main/java/com/tiedup/remake/items/base/EarplugsVariant.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.tiedup.remake.items.base; - -/** - * Enum defining all earplugs variants. - * Used by GenericEarplugs to create earplugs items via factory pattern. - * - *

Issue #12 fix: Added textureSubfolder to eliminate string checks in renderers. - */ -public enum EarplugsVariant { - CLASSIC("classic_earplugs", "earplugs"); - - private final String registryName; - private final String textureSubfolder; - - EarplugsVariant(String registryName, String textureSubfolder) { - this.registryName = registryName; - this.textureSubfolder = textureSubfolder; - } - - public String getRegistryName() { - return registryName; - } - - /** - * Get the texture subfolder for this earplugs variant. - * Used by renderers to locate texture files. - * - * @return Subfolder path under textures/entity/bondage/ (e.g., "earplugs") - */ - public String getTextureSubfolder() { - return textureSubfolder; - } -} diff --git a/src/main/java/com/tiedup/remake/items/base/GagVariant.java b/src/main/java/com/tiedup/remake/items/base/GagVariant.java deleted file mode 100644 index 4b3e2eb..0000000 --- a/src/main/java/com/tiedup/remake/items/base/GagVariant.java +++ /dev/null @@ -1,163 +0,0 @@ -package com.tiedup.remake.items.base; - -import com.tiedup.remake.util.GagMaterial; - -/** - * Enum defining all gag variants with their properties. - * Used by GenericGag to create gag items via factory pattern. - * - *

Note: ItemMedicalGag is NOT included here because it implements - * IHasBlindingEffect (combo item with special behavior). - * - *

Issue #12 fix: Added textureSubfolder to eliminate 40+ string checks in renderers. - */ -public enum GagVariant { - // Cloth-based gags - CLOTH_GAG("cloth_gag", GagMaterial.CLOTH, true, "cloth", false, null), - ROPES_GAG("ropes_gag", GagMaterial.CLOTH, true, "shibari", false, null), - CLEAVE_GAG("cleave_gag", GagMaterial.CLOTH, true, "cleave", false, null), - RIBBON_GAG("ribbon_gag", GagMaterial.CLOTH, false, "ribbon", false, null), - - // Ball gags - standard 2D texture rendering - BALL_GAG( - "ball_gag", - GagMaterial.BALL, - true, - "ballgags/normal", - false, - null - ), - BALL_GAG_STRAP( - "ball_gag_strap", - GagMaterial.BALL, - true, - "ballgags/harness", - false, - null - ), - - // Tape gags - TAPE_GAG("tape_gag", GagMaterial.TAPE, true, "tape", false, null), - - // Stuffed/filling gags (no colors) - WRAP_GAG("wrap_gag", GagMaterial.STUFFED, false, "wrap", false, null), - SLIME_GAG("slime_gag", GagMaterial.STUFFED, false, "slime", false, null), - VINE_GAG("vine_gag", GagMaterial.STUFFED, false, "vine", false, null), - WEB_GAG("web_gag", GagMaterial.STUFFED, false, "web", false, null), - - // Panel gags (no colors) - PANEL_GAG( - "panel_gag", - GagMaterial.PANEL, - false, - "straitjacket", - false, - null - ), - BEAM_PANEL_GAG( - "beam_panel_gag", - GagMaterial.PANEL, - false, - "beam", - false, - null - ), - CHAIN_PANEL_GAG( - "chain_panel_gag", - GagMaterial.PANEL, - false, - "chain", - false, - null - ), - - // Latex gags (no colors) - LATEX_GAG("latex_gag", GagMaterial.LATEX, false, "latex", false, null), - - // Ring/tube gags (no colors) - TUBE_GAG("tube_gag", GagMaterial.RING, false, "tube", false, null), - - // Bite gags (no colors) - BITE_GAG("bite_gag", GagMaterial.BITE, false, "armbinder", false, null), - - // Sponge gags (no colors) - SPONGE_GAG("sponge_gag", GagMaterial.SPONGE, false, "sponge", false, null), - - // Baguette gags (no colors) - BAGUETTE_GAG( - "baguette_gag", - GagMaterial.BAGUETTE, - false, - "baguette", - false, - null - ); - - private final String registryName; - private final GagMaterial material; - private final boolean supportsColor; - private final String textureSubfolder; - private final boolean uses3DModel; - private final String modelPath; - - GagVariant( - String registryName, - GagMaterial material, - boolean supportsColor, - String textureSubfolder, - boolean uses3DModel, - String modelPath - ) { - this.registryName = registryName; - this.material = material; - this.supportsColor = supportsColor; - this.textureSubfolder = textureSubfolder; - this.uses3DModel = uses3DModel; - this.modelPath = modelPath; - } - - public String getRegistryName() { - return registryName; - } - - public GagMaterial getMaterial() { - return material; - } - - /** - * Check if this gag variant supports color variations. - * Items with colors: cloth_gag, ropes_gag, cleave_gag, ribbon_gag, - * ball_gag, ball_gag_strap, tape_gag - */ - public boolean supportsColor() { - return supportsColor; - } - - /** - * Get the texture subfolder for this gag variant. - * Used by renderers to locate texture files. - * - * @return Subfolder path under textures/entity/bondage/ (e.g., "cloth", "ballgags/normal") - */ - public String getTextureSubfolder() { - return textureSubfolder; - } - - /** - * Check if this gag variant uses a 3D OBJ model. - * - * @return true if this variant uses a 3D model - */ - public boolean uses3DModel() { - return uses3DModel; - } - - /** - * Get the model path for 3D rendering. - * - * @return ResourceLocation string path (e.g., "tiedup:models/obj/ball_gag.obj"), or null if no 3D model - */ - public String getModelPath() { - return modelPath; - } -} diff --git a/src/main/java/com/tiedup/remake/items/base/IAdjustable.java b/src/main/java/com/tiedup/remake/items/base/IAdjustable.java deleted file mode 100644 index b5b592a..0000000 --- a/src/main/java/com/tiedup/remake/items/base/IAdjustable.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.tiedup.remake.items.base; - -/** - * Interface for items that can have their render position adjusted. - * Typically gags and blindfolds that render on the player's head. - * - * Players can adjust the Y position of these items to better fit their skin. - * Adjustment values are stored in the ItemStack's NBT via AdjustmentHelper. - */ -public interface IAdjustable { - /** - * Whether this item supports position adjustment. - * @return true if adjustable - */ - boolean canBeAdjusted(); - - /** - * Default Y offset for this item type (in pixels, 1 pixel = 1/16 block). - * Override for items that need a non-zero default position. - * @return default adjustment value - */ - default float getDefaultAdjustment() { - return 0.0f; - } - - /** - * Minimum allowed adjustment value (pixels). - * @return minimum value (typically -4.0) - */ - default float getMinAdjustment() { - return -4.0f; - } - - /** - * Maximum allowed adjustment value (pixels). - * @return maximum value (typically +4.0) - */ - default float getMaxAdjustment() { - return 4.0f; - } - - /** - * Step size for GUI slider (smaller = more precise). - * @return step size (typically 0.25) - */ - default float getAdjustmentStep() { - return 0.25f; - } -} diff --git a/src/main/java/com/tiedup/remake/items/base/IBondageItem.java b/src/main/java/com/tiedup/remake/items/base/IBondageItem.java deleted file mode 100644 index 8a099de..0000000 --- a/src/main/java/com/tiedup/remake/items/base/IBondageItem.java +++ /dev/null @@ -1,102 +0,0 @@ -package com.tiedup.remake.items.base; - -import com.tiedup.remake.v2.BodyRegionV2; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.entity.LivingEntity; -import net.minecraft.world.item.ItemStack; -import org.jetbrains.annotations.Nullable; - -/** - * Interface for all bondage equipment items. - * Defines the core behavior for items that can be equipped in custom bondage slots. - * - * Based on original IExtraBondageItem from 1.12.2 - */ -public interface IBondageItem { - /** - * Get the body region this item occupies when equipped. - * @return The body region - */ - BodyRegionV2 getBodyRegion(); - - /** - * Called every tick while this item is equipped on an entity. - * @param stack The equipped item stack - * @param entity The entity wearing the item - */ - default void onWornTick(ItemStack stack, LivingEntity entity) { - // Default: do nothing - } - - /** - * Called when this 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) { - // Default: do nothing - } - - /** - * Called when this 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) { - // Default: do nothing - } - - /** - * Check if this item can be equipped on the given entity. - * @param stack The item stack to equip - * @param entity The target entity - * @return true if the item can be equipped, false otherwise - */ - default boolean canEquip(ItemStack stack, LivingEntity entity) { - return true; - } - - /** - * Check if this item can be unequipped from the given entity. - * @param stack The equipped item stack - * @param entity The entity wearing the item - * @return true if the item can be unequipped, false otherwise - */ - default boolean canUnequip(ItemStack stack, LivingEntity entity) { - return true; - } - - /** - * Get the texture subfolder for this bondage item. - * Used by renderers to locate texture files. - * - *

Issue #12 fix: Eliminates 40+ string checks in renderers by letting - * each item type declare its own texture subfolder. - * - * @return Subfolder path under textures/entity/bondage/ (e.g., "ropes", "ballgags/normal") - */ - default String getTextureSubfolder() { - return "misc"; // Fallback for items without explicit subfolder - } - - /** - * Check if this bondage item uses a 3D OBJ model instead of a flat texture. - * Items with 3D models will be rendered using ObjModelRenderer. - * - * @return true if this item uses a 3D model, false for standard texture rendering - */ - default boolean uses3DModel() { - return false; - } - - /** - * Get the ResourceLocation of the 3D model for this item. - * Only called if uses3DModel() returns true. - * - * @return ResourceLocation pointing to the .obj file, or null if no 3D model - */ - @Nullable - default ResourceLocation get3DModelLocation() { - return null; - } -} diff --git a/src/main/java/com/tiedup/remake/items/base/IHasBlindingEffect.java b/src/main/java/com/tiedup/remake/items/base/IHasBlindingEffect.java deleted file mode 100644 index cf5890e..0000000 --- a/src/main/java/com/tiedup/remake/items/base/IHasBlindingEffect.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.tiedup.remake.items.base; - -/** - * Marker interface for items that have a blinding visual effect. - * - *

Items implementing this interface will: - *

    - *
  • Apply a screen overlay when worn (client-side)
  • - *
  • Reduce the player's visibility
  • - *
  • Potentially disable certain UI elements
  • - *
- * - *

Usage

- *
{@code
- * if (blindfold.getItem() instanceof IHasBlindingEffect) {
- *     // Apply blinding overlay
- *     renderBlindingOverlay();
- * }
- * }
- * - *

Implementations

- *
    - *
  • {@link ItemBlindfold} - All blindfold items
  • - *
- * - *

Based on original IHasBlindingEffect.java from 1.12.2 - * - * @see ItemBlindfold - */ -public interface IHasBlindingEffect { - // Marker interface - no methods required - // Presence of this interface indicates the item has a blinding effect -} diff --git a/src/main/java/com/tiedup/remake/items/base/IHasGaggingEffect.java b/src/main/java/com/tiedup/remake/items/base/IHasGaggingEffect.java deleted file mode 100644 index e16ca51..0000000 --- a/src/main/java/com/tiedup/remake/items/base/IHasGaggingEffect.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.tiedup.remake.items.base; - -/** - * Marker interface for items that have a gagging (speech muffling) effect. - * - *

Items implementing this interface will: - *

    - *
  • Convert chat messages to "mmpphh" sounds
  • - *
  • Play gagged speech sounds
  • - *
  • Potentially block certain chat commands
  • - *
- * - *

Usage

- *
{@code
- * if (gag.getItem() instanceof IHasGaggingEffect) {
- *     // Convert chat message to gagged speech
- *     message = GagTalkConverter.convert(message);
- * }
- * }
- * - *

Implementations

- *
    - *
  • {@link ItemGag} - Ball gags, tape gags, cloth gags, etc.
  • - *
- * - *

Based on original ItemGaggingEffect.java from 1.12.2 - * - * @see ItemGag - */ -public interface IHasGaggingEffect { - // Marker interface - no methods required - // Presence of this interface indicates the item has a gagging effect -} diff --git a/src/main/java/com/tiedup/remake/items/base/ItemBind.java b/src/main/java/com/tiedup/remake/items/base/ItemBind.java deleted file mode 100644 index 5745ed8..0000000 --- a/src/main/java/com/tiedup/remake/items/base/ItemBind.java +++ /dev/null @@ -1,637 +0,0 @@ -package com.tiedup.remake.items.base; - -import com.tiedup.remake.core.SettingsAccessor; -import com.tiedup.remake.core.TiedUpMod; -import com.tiedup.remake.network.ModNetwork; -import com.tiedup.remake.network.action.PacketTying; -import com.tiedup.remake.state.IBondageState; -import com.tiedup.remake.state.PlayerBindState; -import com.tiedup.remake.tasks.TyingPlayerTask; -import com.tiedup.remake.tasks.TyingTask; -import com.tiedup.remake.util.KidnappedHelper; -import com.tiedup.remake.util.RestraintEffectUtils; -import com.tiedup.remake.util.TiedUpSounds; -import com.tiedup.remake.v2.BodyRegionV2; -import java.util.List; -import java.util.UUID; -import net.minecraft.ChatFormatting; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.network.chat.Component; -import net.minecraft.server.level.ServerPlayer; -import net.minecraft.sounds.SoundEvents; -import net.minecraft.world.InteractionHand; -import net.minecraft.world.InteractionResult; -import net.minecraft.world.InteractionResultHolder; -import net.minecraft.world.entity.LivingEntity; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.item.Item; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.TooltipFlag; -import net.minecraft.world.item.context.UseOnContext; -import net.minecraft.world.level.Level; -import org.jetbrains.annotations.Nullable; - -/** - * Base class for binding/restraint items (ropes, chains, straitjacket, etc.) - * These items restrain a player's movement and actions when equipped. - * - *

Implements {@link IHasResistance} for the struggle/escape system. - *

Implements {@link ILockable} for the padlock system. - * - * Based on original ItemBind from 1.12.2 - * - */ -public abstract class ItemBind - extends Item - implements IBondageItem, IHasResistance, ILockable -{ - - // ========== Leg Binding: Bind Mode NBT Key ========== - private static final String NBT_BIND_MODE = "bindMode"; - - public ItemBind(Properties properties) { - super(properties); - } - - @Override - public BodyRegionV2 getBodyRegion() { - return BodyRegionV2.ARMS; - } - - // ========== Leg Binding: Bind Mode Methods ========== - - // String constants matching NBT values - public static final String BIND_MODE_FULL = "full"; - private static final String MODE_FULL = BIND_MODE_FULL; - private static final String MODE_ARMS = "arms"; - private static final String MODE_LEGS = "legs"; - private static final String[] MODE_CYCLE = { - MODE_FULL, - MODE_ARMS, - MODE_LEGS, - }; - private static final java.util.Map MODE_TRANSLATION_KEYS = - java.util.Map.of( - MODE_FULL, - "tiedup.bindmode.full", - MODE_ARMS, - "tiedup.bindmode.arms", - MODE_LEGS, - "tiedup.bindmode.legs" - ); - - /** - * Get the bind mode ID string from the stack's NBT. - * @param stack The bind ItemStack - * @return "full", "arms", or "legs" (defaults to "full" if absent) - */ - public static String getBindModeId(ItemStack stack) { - if (stack.isEmpty()) return MODE_FULL; - CompoundTag tag = stack.getTag(); - if (tag == null || !tag.contains(NBT_BIND_MODE)) return MODE_FULL; - String value = tag.getString(NBT_BIND_MODE); - if ( - MODE_FULL.equals(value) || - MODE_ARMS.equals(value) || - MODE_LEGS.equals(value) - ) { - return value; - } - return MODE_FULL; - } - - /** - * Check if arms are bound (mode is "arms" or "full"). - * @param stack The bind ItemStack - * @return true if arms are restrained - */ - public static boolean hasArmsBound(ItemStack stack) { - String mode = getBindModeId(stack); - return MODE_ARMS.equals(mode) || MODE_FULL.equals(mode); - } - - /** - * Check if legs are bound (mode is "legs" or "full"). - * @param stack The bind ItemStack - * @return true if legs are restrained - */ - public static boolean hasLegsBound(ItemStack stack) { - String mode = getBindModeId(stack); - return MODE_LEGS.equals(mode) || MODE_FULL.equals(mode); - } - - /** - * Cycle bind mode: full -> arms -> legs -> full. - * @param stack The bind ItemStack - * @return the new mode ID string - */ - public static String cycleBindModeId(ItemStack stack) { - String current = getBindModeId(stack); - String next = MODE_FULL; - for (int i = 0; i < MODE_CYCLE.length; i++) { - if (MODE_CYCLE[i].equals(current)) { - next = MODE_CYCLE[(i + 1) % MODE_CYCLE.length]; - break; - } - } - stack.getOrCreateTag().putString(NBT_BIND_MODE, next); - return next; - } - - /** - * Get the translation key for the current bind mode. - * @param stack The bind ItemStack - * @return the i18n key for the mode - */ - public static String getBindModeTranslationKey(ItemStack stack) { - return MODE_TRANSLATION_KEYS.getOrDefault( - getBindModeId(stack), - "tiedup.bindmode.full" - ); - } - - /** - * Called when player right-clicks in air with bind item. - * Sneak+click cycles the bind mode. - */ - @Override - public InteractionResultHolder use( - Level level, - Player player, - InteractionHand hand - ) { - ItemStack stack = player.getItemInHand(hand); - - // Sneak+click in air cycles bind mode - if (player.isShiftKeyDown()) { - if (!level.isClientSide) { - String newModeId = cycleBindModeId(stack); - - // Play feedback sound - player.playSound(SoundEvents.CHAIN_STEP, 0.5f, 1.2f); - - // Show action bar message - player.displayClientMessage( - Component.translatable( - "tiedup.message.bindmode_changed", - Component.translatable(getBindModeTranslationKey(stack)) - ), - true - ); - - TiedUpMod.LOGGER.debug( - "[ItemBind] {} cycled bind mode to {}", - player.getName().getString(), - newModeId - ); - } - return InteractionResultHolder.sidedSuccess( - stack, - level.isClientSide - ); - } - - return super.use(level, player, hand); - } - - @Override - public void appendHoverText( - ItemStack stack, - @Nullable Level level, - List tooltip, - TooltipFlag flag - ) { - super.appendHoverText(stack, level, tooltip, flag); - - // Show bind mode - tooltip.add( - Component.translatable( - "item.tiedup.tooltip.bindmode", - Component.translatable(getBindModeTranslationKey(stack)) - ).withStyle(ChatFormatting.GRAY) - ); - - // Show lock status - if (isLockable(stack)) { - if (isLocked(stack)) { - tooltip.add( - Component.translatable( - "item.tiedup.tooltip.locked" - ).withStyle(ChatFormatting.RED) - ); - } else { - tooltip.add( - Component.translatable( - "item.tiedup.tooltip.lockable" - ).withStyle(ChatFormatting.GOLD) - ); - } - } - } - - /** - * Called when the bind is equipped on an entity. - * Applies movement speed reduction only if legs are bound. - * - * Leg Binding: Speed reduction conditional on mode - * Based on original ItemBind.onEquipped() (1.12.2) - */ - @Override - public void onEquipped(ItemStack stack, LivingEntity entity) { - String modeId = getBindModeId(stack); - - // Only apply speed reduction if legs are bound - if (hasLegsBound(stack)) { - // H6 fix: For players, speed is handled exclusively by MovementStyleManager - // (V2 tick-based system) via MovementStyleResolver V1 fallback. - // Applying V1 RestraintEffectUtils here would cause double stacking (different - // UUIDs, ADDITION vs MULTIPLY_BASE) leading to quasi-immobility. - if (entity instanceof Player) { - TiedUpMod.LOGGER.debug( - "[ItemBind] Applied bind (mode={}, pose={}) to player {} - speed delegated to MovementStyleManager", - modeId, - getPoseType().getAnimationId(), - entity.getName().getString() - ); - } else { - // NPCs: MovementStyleManager only handles ServerPlayer, so NPCs - // still need the legacy RestraintEffectUtils speed modifier. - PoseType poseType = getPoseType(); - boolean fullImmobilization = - poseType == PoseType.WRAP || - poseType == PoseType.LATEX_SACK; - - RestraintEffectUtils.applyBindSpeedReduction( - entity, - fullImmobilization - ); - TiedUpMod.LOGGER.debug( - "[ItemBind] Applied bind (mode={}, pose={}) to NPC {} - speed reduced (full={})", - modeId, - poseType.getAnimationId(), - entity.getName().getString(), - fullImmobilization - ); - } - } else { - TiedUpMod.LOGGER.debug( - "[ItemBind] Applied bind (mode={}) to {} - no speed reduction", - modeId, - entity.getName().getString() - ); - } - } - - /** - * Called when the bind is unequipped from an entity. - * Restores normal movement speed for all entities. - * - * Based on original ItemBind.onUnequipped() (1.12.2) - */ - @Override - public void onUnequipped(ItemStack stack, LivingEntity entity) { - // H6 fix: For players, speed cleanup is handled by MovementStyleManager - // (V2 tick-based system). On the next tick, the resolver will see the item - // is gone, deactivate the style, and remove the modifier automatically. - // NPCs still need the legacy RestraintEffectUtils cleanup. - if (!(entity instanceof Player)) { - RestraintEffectUtils.removeBindSpeedReduction(entity); - } - - IHasResistance.super.resetCurrentResistance(stack); - - TiedUpMod.LOGGER.debug( - "[ItemBind] Removed bind from {} - speed {} resistance reset", - entity.getName().getString(), - entity instanceof Player - ? "delegated to MovementStyleManager," - : "restored," - ); - } - - // ========== Tying Interaction ========== - - /** - * Called when player right-clicks another entity with this bind item. - * Starts or continues a tying task to tie up the target entity. - * - * - Players: Uses tying task with progress bar - * - NPCs: Instant bind (no tying mini-game) - * - * Based on original ItemBind.itemInteractionForEntity() (1.12.2) - * - * @param stack The item stack - * @param player The player using the item (kidnapper) - * @param target The entity being interacted with - * @param hand The hand holding the item - * @return SUCCESS if tying started/continued, PASS otherwise - */ - @Override - public InteractionResult interactLivingEntity( - ItemStack stack, - Player player, - LivingEntity target, - InteractionHand hand - ) { - // Only run on server side - if (player.level().isClientSide) { - return InteractionResult.SUCCESS; - } - - IBondageState targetState = KidnappedHelper.getKidnappedState(target); - if (targetState == null) { - return InteractionResult.PASS; // Target cannot be restrained - } - - // Get kidnapper state (player using the item) - IBondageState kidnapperState = KidnappedHelper.getKidnappedState( - player - ); - if (kidnapperState == null) { - return InteractionResult.FAIL; - } - - // Already tied - try to swap binds (if not locked) - // Check stack.isEmpty() first to prevent accidental unbinding when - // the original stack was consumed (e.g., rapid clicks after tying completes) - if (targetState.isTiedUp()) { - if (stack.isEmpty()) { - // No bind in hand - can't swap, just pass - return InteractionResult.PASS; - } - ItemStack oldBind = targetState.replaceEquipment( - BodyRegionV2.ARMS, - stack.copy(), - false - ); - if (!oldBind.isEmpty()) { - stack.shrink(1); - targetState.kidnappedDropItem(oldBind); - TiedUpMod.LOGGER.debug( - "[ItemBind] Swapped bind on {} - dropped old bind", - target.getName().getString() - ); - return InteractionResult.SUCCESS; - } - // Locked or failed - can't swap - return InteractionResult.PASS; - } - - if (kidnapperState.isTiedUp()) { - TiedUpMod.LOGGER.debug( - "[ItemBind] {} tried to tie but is tied themselves", - player.getName().getString() - ); - return InteractionResult.PASS; - } - - // SECURITY: Distance and line-of-sight validation (skip for self-tying) - boolean isSelfTying = player.equals(target); - if (!isSelfTying) { - double maxTieDistance = 4.0; // Max distance to tie (blocks) - double distance = player.distanceTo(target); - if (distance > maxTieDistance) { - TiedUpMod.LOGGER.warn( - "[ItemBind] {} tried to tie {} from too far away ({} blocks)", - player.getName().getString(), - target.getName().getString(), - String.format("%.1f", distance) - ); - return InteractionResult.PASS; - } - - // Check line-of-sight (must be able to see target) - if (!player.hasLineOfSight(target)) { - TiedUpMod.LOGGER.warn( - "[ItemBind] {} tried to tie {} without line of sight", - player.getName().getString(), - target.getName().getString() - ); - return InteractionResult.PASS; - } - } - - return handleTying(stack, player, target, targetState); - } - - /** - * Handle tying any target entity (Player or NPC). - * - * Uses progress-based system: - * - update() marks the tick as active - * - tick() in RestraintTaskTickHandler.onPlayerTick() handles progress increment/decrement - */ - private InteractionResult handleTying( - ItemStack stack, - Player player, - LivingEntity target, - IBondageState targetState - ) { - // Get kidnapper's state to track the tying task - PlayerBindState kidnapperState = PlayerBindState.getInstance(player); - if (kidnapperState == null) { - return InteractionResult.FAIL; - } - - // Get tying duration from GameRule (default: 5 seconds) - int tyingSeconds = getTyingDuration(player); - - // Get current tying task (if any) - TyingTask currentTask = kidnapperState.getCurrentTyingTask(); - - // Check if we should start a new task or continue existing one - if ( - currentTask == null || - !currentTask.isSameTarget(target) || - currentTask.isStopped() || - !ItemStack.matches(currentTask.getBind(), stack) - ) { - // Create new tying task (works for both Players and NPCs) - TyingPlayerTask newTask = new TyingPlayerTask( - stack.copy(), - targetState, - target, - tyingSeconds, - player.level(), - player // Pass kidnapper for SystemMessage - ); - - // FIX: Store the inventory slot for consumption when task completes - // This prevents duplication AND allows refund if task is cancelled - int sourceSlot = player.getInventory().selected; - newTask.setSourceSlot(sourceSlot); - newTask.setSourcePlayer(player); - - // Start new task - kidnapperState.setCurrentTyingTask(newTask); - newTask.setUpTargetState(); // Initialize target's restraint state (only for players) - newTask.start(); - currentTask = newTask; - - TiedUpMod.LOGGER.debug( - "[ItemBind] {} started tying {} ({} seconds, slot={})", - player.getName().getString(), - target.getName().getString(), - tyingSeconds, - sourceSlot - ); - } else { - // Continue existing task - ensure kidnapper is set - if (currentTask instanceof TyingPlayerTask playerTask) { - playerTask.setKidnapper(player); - } - } - - // Mark this tick as active (progress will increase in onPlayerTick) - // The tick() method in RestraintTaskTickHandler.onPlayerTick handles progress increment/decrement - currentTask.update(); - - return InteractionResult.SUCCESS; - } - - /** - * Called when player right-clicks with the bind item (not targeting an entity). - * Cancels any ongoing tying task. - * - * Based on original ItemBind.onItemRightClick() (1.12.2) - * - * @param context The use context - * @return FAIL to cancel the action - */ - @Override - public InteractionResult useOn(UseOnContext context) { - // Only run on server side - if (context.getLevel().isClientSide) { - return InteractionResult.SUCCESS; - } - - Player player = context.getPlayer(); - if (player == null) { - return InteractionResult.FAIL; - } - - // Cancel any ongoing tying task - PlayerBindState state = PlayerBindState.getInstance(player); - if (state == null) { - return InteractionResult.FAIL; - } - - // Check for active tying task (unified for both players and NPCs) - TyingTask task = state.getCurrentTyingTask(); - if (task != null) { - task.stop(); - state.setCurrentTyingTask(null); - - LivingEntity target = task.getTargetEntity(); - String targetName = - target != null ? target.getName().getString() : "???"; - String kidnapperName = player.getName().getString(); - - // Send cancellation packet to kidnapper - if (player instanceof ServerPlayer serverPlayer) { - PacketTying packet = new PacketTying( - -1, - task.getMaxSeconds(), - true, - targetName - ); - ModNetwork.sendToPlayer(packet, serverPlayer); - } - - // Send cancellation packet to target (if it's a player) - if (target instanceof ServerPlayer serverTarget) { - PacketTying packet = new PacketTying( - -1, - task.getMaxSeconds(), - false, - kidnapperName - ); - ModNetwork.sendToPlayer(packet, serverTarget); - } - - TiedUpMod.LOGGER.debug( - "[ItemBind] {} cancelled tying task", - player.getName().getString() - ); - } - - return InteractionResult.FAIL; - } - - /** - * Get the tying duration in seconds from GameRule. - * - * @param player The player (for accessing world/GameRules) - * @return Duration in seconds (default: 5) - */ - private int getTyingDuration(Player player) { - return SettingsAccessor.getTyingPlayerTime( - player.level().getGameRules() - ); - } - - // ========== Resistance System (via IHasResistance) ========== - - /** - * Get the item name for GameRule lookup. - * Each subclass must implement this to return its identifier (e.g., "rope", "chain", etc.) - * - * @return Item name for resistance GameRule lookup - */ - public abstract String getItemName(); - - // ========== Pose System ========== - - /** - * Get the pose type for this bind item. - * Determines which animation/pose is applied when this item is equipped. - * - * Override in subclasses for special poses (straitjacket, wrap, latex_sack). - * - * @return PoseType for this bind (default: STANDARD) - */ - public PoseType getPoseType() { - return PoseType.STANDARD; - } - - /** - * Implementation of IHasResistance.getResistanceId(). - * Delegates to getItemName() for backward compatibility with subclasses. - * - * @return Item identifier for resistance lookup - */ - @Override - public String getResistanceId() { - return getItemName(); - } - - /** - * Called when the entity struggles against this bind. - * Plays struggle sound and shows message. - * - * Based on original ItemBind struggle notification (1.12.2) - * - * @param entity The entity struggling - */ - @Override - public void notifyStruggle(LivingEntity entity) { - // Play struggle sound - TiedUpSounds.playStruggleSound(entity); - - // Log the struggle attempt - TiedUpMod.LOGGER.debug( - "[ItemBind] {} is struggling against bind", - entity.getName().getString() - ); - - // Notify nearby players if the entity is a player - if (entity instanceof ServerPlayer serverPlayer) { - serverPlayer.displayClientMessage( - Component.translatable("tiedup.message.struggling"), - true // Action bar - ); - } - } - - // ILockable implementation inherited from interface default methods -} diff --git a/src/main/java/com/tiedup/remake/items/base/ItemBlindfold.java b/src/main/java/com/tiedup/remake/items/base/ItemBlindfold.java deleted file mode 100644 index f3d2371..0000000 --- a/src/main/java/com/tiedup/remake/items/base/ItemBlindfold.java +++ /dev/null @@ -1,89 +0,0 @@ -package com.tiedup.remake.items.base; - -import com.tiedup.remake.core.SystemMessageManager; -import com.tiedup.remake.util.EquipmentInteractionHelper; -import com.tiedup.remake.v2.BodyRegionV2; -import java.util.List; -import net.minecraft.network.chat.Component; -import net.minecraft.world.InteractionHand; -import net.minecraft.world.InteractionResult; -import net.minecraft.world.entity.LivingEntity; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.item.Item; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.TooltipFlag; -import net.minecraft.world.level.Level; -import org.jetbrains.annotations.Nullable; - -/** - * Base class for blindfold items (classic blindfold, mask, hood, etc.) - * These items obstruct a player's vision when equipped. - * - * Based on original ItemBlindfold from 1.12.2 - * - */ -public abstract class ItemBlindfold - extends Item - implements IBondageItem, IHasBlindingEffect, IAdjustable, ILockable -{ - - public ItemBlindfold(Properties properties) { - super(properties); - } - - @Override - public BodyRegionV2 getBodyRegion() { - return BodyRegionV2.EYES; - } - - @Override - public void appendHoverText( - ItemStack stack, - @Nullable Level level, - List tooltip, - TooltipFlag flag - ) { - super.appendHoverText(stack, level, tooltip, flag); - appendLockTooltip(stack, tooltip); - } - - /** - * All blindfolds can be adjusted to better fit player skins. - * @return true - blindfolds support position adjustment - */ - @Override - public boolean canBeAdjusted() { - return true; - } - - /** - * Called when player right-clicks another entity with this blindfold. - * Allows putting blindfold on tied-up entities (Players and NPCs). - */ - @Override - public InteractionResult interactLivingEntity( - ItemStack stack, - Player player, - LivingEntity target, - InteractionHand hand - ) { - return EquipmentInteractionHelper.equipOnTarget( - stack, - player, - target, - state -> state.isBlindfolded(), - (state, item) -> state.equip(BodyRegionV2.EYES, item), - (state, item) -> - state.replaceEquipment(BodyRegionV2.EYES, item, false), - (p, t) -> - SystemMessageManager.sendToTarget( - p, - t, - SystemMessageManager.MessageCategory.BLINDFOLDED - ), - "ItemBlindfold" - ); - } - - // ILockable implementation inherited from interface default methods -} diff --git a/src/main/java/com/tiedup/remake/items/base/ItemCollar.java b/src/main/java/com/tiedup/remake/items/base/ItemCollar.java deleted file mode 100644 index d026488..0000000 --- a/src/main/java/com/tiedup/remake/items/base/ItemCollar.java +++ /dev/null @@ -1,1407 +0,0 @@ -package com.tiedup.remake.items.base; - -import com.tiedup.remake.core.SettingsAccessor; -import com.tiedup.remake.core.SystemMessageManager; -import com.tiedup.remake.core.TiedUpMod; -import com.tiedup.remake.network.sync.SyncManager; -import com.tiedup.remake.state.CollarRegistry; -import com.tiedup.remake.state.IBondageState; -import com.tiedup.remake.util.KidnappedHelper; -import com.tiedup.remake.util.teleport.Position; -import com.tiedup.remake.v2.BodyRegionV2; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; -import net.minecraft.ChatFormatting; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.nbt.ListTag; -import net.minecraft.nbt.Tag; -import net.minecraft.network.chat.Component; -import net.minecraft.server.level.ServerPlayer; -import net.minecraft.world.InteractionHand; -import net.minecraft.world.InteractionResult; -import net.minecraft.world.entity.LivingEntity; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.item.Item; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.TooltipFlag; -import net.minecraft.world.level.Level; -import org.jetbrains.annotations.Nullable; - -/** - * Base class for collar items (classic collar, shock collar, GPS collar, etc.) - * These items mark ownership and can have various special effects. - * - * Based on original ItemCollar from 1.12.2 - * - * - * Note: Collars have maxStackSize of 1 (unique items) - */ -public abstract class ItemCollar - extends Item - implements IBondageItem, ILockable -{ - - // NBT Keys - Basic - private static final String NBT_OWNERS = "owners"; - private static final String NBT_LOCKED = "locked"; - private static final String NBT_LOCKED_BY_KEY_UUID = "lockedByKeyUUID"; - private static final String NBT_NICKNAME = "nickname"; - private static final String NBT_CURRENT_RESISTANCE = "currentResistance"; - private static final String NBT_CURRENT_RESISTANCE_LEGACY = - "currentresistance"; - private static final String NBT_CAN_BE_STRUGGLED_OUT = "canBeStruggledOut"; - - // NBT Keys - Kidnapping Mode - private static final String NBT_KIDNAPPING_MODE = "kidnappingMode"; - private static final String NBT_TIE_TO_POLE = "tieToPole"; - private static final String NBT_WARN_MASTERS = "warnMasters"; - private static final String NBT_BONDAGE_SERVICE = "bondageservice"; - private static final String NBT_SERVICE_SENTENCE = "servicesentence"; - - // NBT Keys - Blacklist/Whitelist - private static final String NBT_BLACKLIST = "blacklist"; - private static final String NBT_WHITELIST = "whitelist"; - - // NBT Keys - Cell System - private static final String NBT_CELL_ID = "cellId"; - - /** - * ThreadLocal flag to suppress alert on legitimate collar removals. - * Set this to true before removing a collar legitimately (camp death, ransom paid, etc.) - * and reset to false afterward. - * - * CRITICAL FIX: Use runWithSuppressedAlert() wrapper to ensure proper cleanup. - */ - private static final ThreadLocal SUPPRESS_REMOVAL_ALERT = - ThreadLocal.withInitial(() -> false); - - /** - * CRITICAL FIX: Safe wrapper for legitimate collar removals. - * Automatically handles ThreadLocal lifecycle to prevent memory leaks. - * - * Use this instead of manual beginLegitimateRemoval() / endLegitimateRemoval(). - * - * @param action The action to perform with suppressed alerts - */ - public static void runWithSuppressedAlert(Runnable action) { - SUPPRESS_REMOVAL_ALERT.set(true); - try { - action.run(); - } finally { - SUPPRESS_REMOVAL_ALERT.set(false); - } - } - - /** - * DEPRECATED: Use runWithSuppressedAlert() instead. - * Begin a legitimate collar removal that should NOT alert kidnappers. - * @deprecated Use {@link #runWithSuppressedAlert(Runnable)} to prevent ThreadLocal leaks - */ - @Deprecated - public static void beginLegitimateRemoval() { - SUPPRESS_REMOVAL_ALERT.set(true); - } - - /** - * DEPRECATED: Use runWithSuppressedAlert() instead. - * End a legitimate collar removal sequence. - * @deprecated Use {@link #runWithSuppressedAlert(Runnable)} to prevent ThreadLocal leaks - */ - @Deprecated - public static void endLegitimateRemoval() { - SUPPRESS_REMOVAL_ALERT.set(false); - } - - /** - * Check if removal alerts are currently suppressed. - */ - public static boolean isRemovalAlertSuppressed() { - return SUPPRESS_REMOVAL_ALERT.get(); - } - - public ItemCollar(Properties properties) { - super(properties.stacksTo(1)); // Collars are unique items - } - - @Override - public void appendHoverText( - ItemStack stack, - @Nullable Level level, - List tooltip, - TooltipFlag flag - ) { - // Nickname - if (hasNickname(stack)) { - tooltip.add( - Component.literal("Nickname: ") - .withStyle(ChatFormatting.AQUA) - .append( - Component.literal(getNickname(stack)).withStyle( - ChatFormatting.WHITE - ) - ) - ); - } - - // Locked status - if (isLocked(stack)) { - tooltip.add( - Component.literal("Locked").withStyle(ChatFormatting.RED) - ); - } - - // Kidnapping mode - if (isKidnappingModeEnabled(stack)) { - tooltip.add( - Component.literal("Kidnapping Mode: ON").withStyle( - ChatFormatting.DARK_RED - ) - ); - } - - // Additional flags - if (shouldTieToPole(stack)) { - tooltip.add( - Component.literal("Tie to Pole: ON").withStyle( - ChatFormatting.GRAY - ) - ); - } - if (isBondageServiceEnabled(stack)) { - tooltip.add( - Component.literal("Bondage Service: ON").withStyle( - ChatFormatting.LIGHT_PURPLE - ) - ); - } - - // Cell assignment info - UUID cellId = getCellId(stack); - if (cellId != null) { - tooltip.add( - Component.literal("Cell: ") - .withStyle(ChatFormatting.DARK_PURPLE) - .append( - Component.literal( - cellId.toString().substring(0, 8) + "..." - ).withStyle(ChatFormatting.LIGHT_PURPLE) - ) - ); - } - } - - public String getNickname(ItemStack stack) { - CompoundTag tag = stack.getTag(); - if (tag != null && tag.contains(NBT_NICKNAME)) { - return tag.getString(NBT_NICKNAME); - } - return null; - } - - public void setNickname(ItemStack stack, String nickname) { - stack.getOrCreateTag().putString(NBT_NICKNAME, nickname); - } - - public boolean hasNickname(ItemStack stack) { - return stack.hasTag() && stack.getTag().contains(NBT_NICKNAME); - } - - @Override - public BodyRegionV2 getBodyRegion() { - return BodyRegionV2.NECK; - } - - /** - * Get the item name for GameRule lookup. - * Used by SettingsAccessor to find resistance value. - * - * @return "collar" - */ - public String getItemName() { - return "collar"; - } - - /** - * Check if this collar can shock the wearer. - * Override in shock collar subclasses. - * - * @return true if collar has shock capability - */ - public boolean canShock() { - return false; - } - - /** - * Check if this collar has GPS tracking. - * Override in GPS collar subclasses. - * - * @return true if collar has GPS capability - */ - public boolean hasGPS() { - return false; - } - - /** - * Called when player right-clicks another entity with this collar. - * Allows putting collar on tied-up entities (Players and NPCs) and adds the player as owner. - * - * Based on original ItemCollar.itemInteractionForEntity() - * - * @param stack The item stack - * @param player The player using the item - * @param target The entity being interacted with - * @param hand The hand holding the item - * @return SUCCESS if collar equipped/replaced, PASS otherwise - */ - @Override - public InteractionResult interactLivingEntity( - ItemStack stack, - Player player, - LivingEntity target, - InteractionHand hand - ) { - // Server-side only - if (player.level().isClientSide) { - return InteractionResult.SUCCESS; - } - - // Check if target can be collared (Player, EntityDamsel, EntityKidnapper) - IBondageState targetState = KidnappedHelper.getKidnappedState(target); - if (targetState == null) { - return InteractionResult.PASS; // Entity cannot be collared - } - - // Must be tied up - if (!targetState.isTiedUp()) { - return InteractionResult.PASS; - } - - ItemStack newCollar = stack.copy(); - if (!isOwner(newCollar, player)) { - addOwner(newCollar, player); - } - - // Case 1: No collar yet - equip new one - if (!targetState.hasCollar()) { - targetState.equip(BodyRegionV2.NECK, newCollar); - stack.shrink(1); - - registerCollarInRegistry(target, newCollar, player); - - // Send screen message to target - SystemMessageManager.sendToTarget( - player, - target, - SystemMessageManager.MessageCategory.COLLARED - ); - - // Sync equipment to all tracking clients - if (target instanceof ServerPlayer serverPlayer) { - SyncManager.syncInventory(serverPlayer); - } - - TiedUpMod.LOGGER.info( - "[ItemCollar] {} put collar on {}", - player.getName().getString(), - target.getName().getString() - ); - - // Play custom put sound - player - .level() - .playSound( - null, - player.blockPosition(), - com.tiedup.remake.core.ModSounds.COLLAR_PUT.get(), - net.minecraft.sounds.SoundSource.PLAYERS, - 1.0f, - 1.0f - ); - - return InteractionResult.SUCCESS; - } - // Case 2: Already has collar - replace it - else { - ItemStack oldCollar = targetState.getEquipment(BodyRegionV2.NECK); - if ( - oldCollar != null && oldCollar.getItem() instanceof ItemCollar - ) { - ItemCollar oldCollarItem = (ItemCollar) oldCollar.getItem(); - - // Check if old collar is locked - if (oldCollarItem.isLocked(oldCollar)) { - // Cannot replace locked collar - TiedUpMod.LOGGER.info( - "[ItemCollar] {} tried to replace locked collar on {}", - player.getName().getString(), - target.getName().getString() - ); - - // Send error message to player (kidnapper) - SystemMessageManager.sendToPlayer( - player, - SystemMessageManager.MessageCategory.ERROR, - "Target's collar is locked! You cannot replace it." - ); - return InteractionResult.FAIL; - } - - // Old collar not locked - replace it - ItemStack replacedCollar = targetState.replaceEquipment( - BodyRegionV2.NECK, - newCollar, - false - ); - if (replacedCollar != null) { - stack.shrink(1); - targetState.kidnappedDropItem(replacedCollar); - - unregisterCollarFromRegistry(target); - registerCollarInRegistry(target, newCollar, player); - - // Send screen message to target - SystemMessageManager.sendToTarget( - player, - target, - SystemMessageManager.MessageCategory.COLLARED - ); - - // Sync equipment to all tracking clients - if (target instanceof ServerPlayer serverPlayer) { - SyncManager.syncInventory(serverPlayer); - } - - TiedUpMod.LOGGER.info( - "[ItemCollar] {} replaced collar on {}", - player.getName().getString(), - target.getName().getString() - ); - - return InteractionResult.SUCCESS; - } - } - } - - return InteractionResult.PASS; - } - - /** - * Add an owner to this collar. - * Owners are stored as UUID + name pairs in NBT. - * - * From original ItemCollar.addOwner() - * - * @param stack The collar ItemStack - * @param ownerUUID The owner's UUID - * @param ownerName The owner's display name - */ - public void addOwner(ItemStack stack, UUID ownerUUID, String ownerName) { - if (stack.isEmpty() || ownerUUID == null) { - return; - } - - CompoundTag tag = stack.getOrCreateTag(); - ListTag owners = tag.contains(NBT_OWNERS) - ? tag.getList(NBT_OWNERS, Tag.TAG_COMPOUND) - : new ListTag(); - - // Check if already owner - for (int i = 0; i < owners.size(); i++) { - CompoundTag ownerTag = owners.getCompound(i); - if (ownerTag.getUUID("uuid").equals(ownerUUID)) { - TiedUpMod.LOGGER.debug( - "[ItemCollar] {} is already an owner", - ownerName - ); - return; - } - } - - // Add new owner - CompoundTag ownerTag = new CompoundTag(); - ownerTag.putUUID("uuid", ownerUUID); - ownerTag.putString("name", ownerName != null ? ownerName : "Unknown"); - owners.add(ownerTag); - tag.put(NBT_OWNERS, owners); - - TiedUpMod.LOGGER.info("[ItemCollar] Added {} as owner", ownerName); - } - - /** - * Add an owner to this collar (player version). - * - * @param stack The collar ItemStack - * @param owner The owner player - */ - public void addOwner(ItemStack stack, Player owner) { - if (owner == null) { - return; - } - addOwner(stack, owner.getUUID(), owner.getName().getString()); - } - - /** - * Remove an owner from this collar. - * - * @param stack The collar ItemStack - * @param ownerUUID The owner's UUID to remove - */ - public void removeOwner(ItemStack stack, UUID ownerUUID) { - if (stack.isEmpty() || ownerUUID == null) { - return; - } - - CompoundTag tag = stack.getTag(); - if (tag == null || !tag.contains(NBT_OWNERS)) { - return; - } - - ListTag owners = tag.getList(NBT_OWNERS, Tag.TAG_COMPOUND); - ListTag newOwners = new ListTag(); - - for (int i = 0; i < owners.size(); i++) { - CompoundTag ownerTag = owners.getCompound(i); - if (!ownerTag.getUUID("uuid").equals(ownerUUID)) { - newOwners.add(ownerTag); - } - } - - tag.put(NBT_OWNERS, newOwners); - TiedUpMod.LOGGER.info("[ItemCollar] Removed owner {}", ownerUUID); - } - - /** - * Get the list of all owners. - * - * @param stack The collar ItemStack - * @return List of owner UUIDs - */ - public List getOwners(ItemStack stack) { - if (stack.isEmpty()) { - return new ArrayList<>(); - } - - CompoundTag tag = stack.getTag(); - if (tag == null || !tag.contains(NBT_OWNERS)) { - return new ArrayList<>(); - } - - ListTag owners = tag.getList(NBT_OWNERS, Tag.TAG_COMPOUND); - List result = new ArrayList<>(); - - for (int i = 0; i < owners.size(); i++) { - result.add(owners.getCompound(i).getUUID("uuid")); - } - - return result; - } - - /** - * Check if the given player is an owner of this collar. - * Optimized: uses direct NBT lookup with early-return instead of building list. - * - * @param stack The collar ItemStack - * @param player The player to check - * @return true if player is an owner - */ - public boolean isOwner(ItemStack stack, Player player) { - if (player == null) { - return false; - } - return hasUUIDInList(stack, NBT_OWNERS, player.getUUID()); - } - - /** - * Check if this collar has any owners. - * - * @param stack The collar ItemStack - * @return true if has at least one owner - */ - public boolean hasOwner(ItemStack stack) { - return !getOwners(stack).isEmpty(); - } - - /** - * Add a player to this collar's blacklist. - * Blacklisted players will not be targeted when kidnapping mode is active. - * - * @param stack The collar ItemStack - * @param uuid The player's UUID - * @param name The player's display name - */ - public void addToBlacklist(ItemStack stack, UUID uuid, String name) { - addToList(stack, NBT_BLACKLIST, uuid, name); - } - - /** - * Add a player to this collar's blacklist (player version). - */ - public void addToBlacklist(ItemStack stack, Player player) { - if (player == null) return; - addToBlacklist(stack, player.getUUID(), player.getName().getString()); - } - - /** - * Remove a player from this collar's blacklist. - */ - public void removeFromBlacklist(ItemStack stack, UUID uuid) { - removeFromList(stack, NBT_BLACKLIST, uuid); - } - - /** - * Get all blacklisted player UUIDs. - */ - public List getBlacklist(ItemStack stack) { - return getListUUIDs(stack, NBT_BLACKLIST); - } - - /** - * Check if a player is blacklisted. - * Optimized: uses direct NBT lookup with early-return instead of building list. - */ - public boolean isBlacklisted(ItemStack stack, UUID uuid) { - return hasUUIDInList(stack, NBT_BLACKLIST, uuid); - } - - /** - * Check if a player is blacklisted (player version). - */ - public boolean isBlacklisted(ItemStack stack, Player player) { - return player != null && isBlacklisted(stack, player.getUUID()); - } - - /** - * Add a player to this collar's whitelist. - * When whitelist is not empty, ONLY whitelisted players will be targeted. - * - * @param stack The collar ItemStack - * @param uuid The player's UUID - * @param name The player's display name - */ - public void addToWhitelist(ItemStack stack, UUID uuid, String name) { - addToList(stack, NBT_WHITELIST, uuid, name); - } - - /** - * Add a player to this collar's whitelist (player version). - */ - public void addToWhitelist(ItemStack stack, Player player) { - if (player == null) return; - addToWhitelist(stack, player.getUUID(), player.getName().getString()); - } - - /** - * Remove a player from this collar's whitelist. - */ - public void removeFromWhitelist(ItemStack stack, UUID uuid) { - removeFromList(stack, NBT_WHITELIST, uuid); - } - - /** - * Get all whitelisted player UUIDs. - */ - public List getWhitelist(ItemStack stack) { - return getListUUIDs(stack, NBT_WHITELIST); - } - - /** - * Check if a player is whitelisted. - * Optimized: uses direct NBT lookup with early-return instead of building list. - */ - public boolean isWhitelisted(ItemStack stack, UUID uuid) { - return hasUUIDInList(stack, NBT_WHITELIST, uuid); - } - - /** - * Check if a player is whitelisted (player version). - */ - public boolean isWhitelisted(ItemStack stack, Player player) { - return player != null && isWhitelisted(stack, player.getUUID()); - } - - // ========== Helper methods for list management ========== - - /** - * Check if a UUID exists in a specific list (owners, blacklist, whitelist). - * Optimized O(n) with early-return instead of O(2n) from building list + contains. - * - * @param stack The collar ItemStack - * @param listKey The NBT key for the list - * @param uuid The UUID to check - * @return true if UUID is in the list - */ - private boolean hasUUIDInList(ItemStack stack, String listKey, UUID uuid) { - if (stack.isEmpty() || uuid == null) return false; - - CompoundTag tag = stack.getTag(); - if (tag == null || !tag.contains(listKey)) return false; - - ListTag list = tag.getList(listKey, Tag.TAG_COMPOUND); - for (int i = 0; i < list.size(); i++) { - if (list.getCompound(i).getUUID("uuid").equals(uuid)) { - return true; // Early return - no need to build full list - } - } - return false; - } - - private void addToList( - ItemStack stack, - String listKey, - UUID uuid, - String name - ) { - if (stack.isEmpty() || uuid == null) return; - - CompoundTag tag = stack.getOrCreateTag(); - ListTag list = tag.contains(listKey) - ? tag.getList(listKey, Tag.TAG_COMPOUND) - : new ListTag(); - - // Check if already in list - for (int i = 0; i < list.size(); i++) { - CompoundTag entry = list.getCompound(i); - if (entry.getUUID("uuid").equals(uuid)) { - return; // Already in list - } - } - - // Add new entry - CompoundTag entry = new CompoundTag(); - entry.putUUID("uuid", uuid); - entry.putString("name", name != null ? name : "Unknown"); - list.add(entry); - tag.put(listKey, list); - } - - private void removeFromList(ItemStack stack, String listKey, UUID uuid) { - if (stack.isEmpty() || uuid == null) return; - - CompoundTag tag = stack.getTag(); - if (tag == null || !tag.contains(listKey)) return; - - ListTag list = tag.getList(listKey, Tag.TAG_COMPOUND); - ListTag newList = new ListTag(); - - for (int i = 0; i < list.size(); i++) { - CompoundTag entry = list.getCompound(i); - if (!entry.getUUID("uuid").equals(uuid)) { - newList.add(entry); - } - } - - tag.put(listKey, newList); - } - - private List getListUUIDs(ItemStack stack, String listKey) { - if (stack.isEmpty()) return new ArrayList<>(); - - CompoundTag tag = stack.getTag(); - if (tag == null || !tag.contains(listKey)) return new ArrayList<>(); - - ListTag list = tag.getList(listKey, Tag.TAG_COMPOUND); - List result = new ArrayList<>(); - - for (int i = 0; i < list.size(); i++) { - result.add(list.getCompound(i).getUUID("uuid")); - } - - return result; - } - - /** - * Check if this collar is locked. - * Locked collars cannot be removed normally. - * - * - * @param stack The collar ItemStack - * @return true if locked - */ - @Override - public boolean isLocked(ItemStack stack) { - if (stack.isEmpty()) { - return false; - } - CompoundTag tag = stack.getTag(); - return tag != null && tag.getBoolean(NBT_LOCKED); - } - - /** - * Set the locked state of this collar. - * - * - * @param stack The collar ItemStack - * @param locked true to lock, false to unlock - * @return The modified ItemStack for chaining - */ - @Override - public ItemStack setLocked(ItemStack stack, boolean locked) { - if (stack.isEmpty()) { - return stack; - } - stack.getOrCreateTag().putBoolean(NBT_LOCKED, locked); - TiedUpMod.LOGGER.debug("[ItemCollar] Set locked={}", locked); - return stack; - } - - /** - * Check if this collar can be locked. - * By default, all collars are lockable. - * - * - * @param stack The collar ItemStack - * @return true if lockable (can accept a padlock) - */ - @Override - public boolean isLockable(ItemStack stack) { - if (stack.isEmpty()) { - return false; - } - CompoundTag tag = stack.getTag(); - // By default, collars are lockable unless explicitly set to false - return ( - tag == null || - !tag.contains("lockable") || - tag.getBoolean("lockable") - ); - } - - /** - * Set whether this collar can be locked (lockable state). - * - * - * @param stack The collar ItemStack - * @param state true to make lockable, false to prevent locking - * @return The modified ItemStack for chaining - */ - @Override - public ItemStack setLockable(ItemStack stack, boolean state) { - if (stack.isEmpty()) { - return stack; - } - stack.getOrCreateTag().putBoolean("lockable", state); - return stack; - } - - /** - * Check if the padlock should be dropped when unlocking. - * Collars have a built-in lock mechanism, so no padlock to drop. - * - * - * @return false (no padlock to drop for collars) - */ - @Override - public boolean dropLockOnUnlock() { - return false; - } - - // ========== Key-Lock System ========== - - /** - * Get the UUID of the key that locked this collar. - * - * @param stack The collar ItemStack - * @return The key UUID or null if not locked with a key - */ - @Override - @Nullable - public UUID getLockedByKeyUUID(ItemStack stack) { - if (stack.isEmpty()) return null; - CompoundTag tag = stack.getTag(); - if (tag == null || !tag.hasUUID(NBT_LOCKED_BY_KEY_UUID)) return null; - return tag.getUUID(NBT_LOCKED_BY_KEY_UUID); - } - - /** - * Set the key UUID that locks this collar. - * Setting a non-null UUID will also set locked=true. - * Setting null will unlock the collar. - * - * @param stack The collar ItemStack - * @param keyUUID The key UUID or null to unlock - */ - @Override - public void setLockedByKeyUUID(ItemStack stack, @Nullable UUID keyUUID) { - if (stack.isEmpty()) return; - CompoundTag tag = stack.getOrCreateTag(); - if (keyUUID == null) { - tag.remove(NBT_LOCKED_BY_KEY_UUID); - tag.putBoolean(NBT_LOCKED, false); - } else { - tag.putUUID(NBT_LOCKED_BY_KEY_UUID, keyUUID); - tag.putBoolean(NBT_LOCKED, true); - } - } - - /** - * Get the base resistance for this collar from config. - * - * BUG-003 fix: Now reads from SettingsAccessor (config) instead of GameRules - * - * @param entity The entity (kept for API compatibility) - * @return Base resistance value from config - */ - public int getBaseResistance(LivingEntity entity) { - return SettingsAccessor.getBindResistance(getItemName()); - } - - /** - * Get the current resistance of this collar. - * Returns base resistance if not yet set. - * - * - * @param stack The collar ItemStack - * @param entity The entity (for GameRules lookup) - * @return Current resistance - */ - public int getCurrentResistance(ItemStack stack, LivingEntity entity) { - if (stack.isEmpty()) { - return 0; - } - - CompoundTag tag = stack.getTag(); - if (tag != null) { - // Check new camelCase key first - if (tag.contains(NBT_CURRENT_RESISTANCE)) { - return tag.getInt(NBT_CURRENT_RESISTANCE); - } - // Migration: check legacy lowercase key - else if (tag.contains(NBT_CURRENT_RESISTANCE_LEGACY)) { - int resistance = tag.getInt(NBT_CURRENT_RESISTANCE_LEGACY); - // Migrate to new key - tag.remove(NBT_CURRENT_RESISTANCE_LEGACY); - if (resistance > 0) { - tag.putInt(NBT_CURRENT_RESISTANCE, resistance); - } - return resistance > 0 ? resistance : getBaseResistance(entity); - } - } - - // Not set yet - return base resistance - return getBaseResistance(entity); - } - - /** - * Set the current resistance of this collar. - * - * @param stack The collar ItemStack - * @param resistance The new resistance value - */ - public void setCurrentResistance(ItemStack stack, int resistance) { - if (stack.isEmpty()) { - return; - } - stack.getOrCreateTag().putInt(NBT_CURRENT_RESISTANCE, resistance); - } - - /** - * Reset the current resistance to base value. - * - * - * @param stack The collar ItemStack - * @param entity The entity (for GameRules lookup) - */ - public void resetCurrentResistance(ItemStack stack, LivingEntity entity) { - setCurrentResistance(stack, getBaseResistance(entity)); - } - - /** - * Check if this collar can be struggled out of. - * - * @param stack The collar ItemStack - * @return true if struggle is enabled - */ - public boolean canBeStruggledOut(ItemStack stack) { - if (stack.isEmpty()) { - return false; - } - - CompoundTag tag = stack.getTag(); - if (tag != null && tag.contains(NBT_CAN_BE_STRUGGLED_OUT)) { - return tag.getBoolean(NBT_CAN_BE_STRUGGLED_OUT); - } - - return true; // Default: can struggle - } - - /** - * Set whether this collar can be struggled out of. - * - * @param stack The collar ItemStack - * @param canStruggle true to enable struggle - */ - public void setCanBeStruggledOut(ItemStack stack, boolean canStruggle) { - if (stack.isEmpty()) { - return; - } - stack - .getOrCreateTag() - .putBoolean(NBT_CAN_BE_STRUGGLED_OUT, canStruggle); - } - - // Cell ID (Assigned Cell) - - /** - * Get the assigned cell ID from this collar. - * - * @param stack The collar ItemStack - * @return Cell UUID or null if not assigned - */ - @Nullable - public UUID getCellId(ItemStack stack) { - if (stack.isEmpty()) { - return null; - } - - CompoundTag tag = stack.getTag(); - if (tag == null || !tag.hasUUID(NBT_CELL_ID)) { - return null; - } - - return tag.getUUID(NBT_CELL_ID); - } - - /** - * Set the assigned cell ID on this collar. - * - * @param stack The collar ItemStack - * @param cellId The cell UUID, or null to clear - */ - public void setCellId(ItemStack stack, @Nullable UUID cellId) { - if (stack.isEmpty()) { - return; - } - - CompoundTag tag = stack.getOrCreateTag(); - if (cellId == null) { - tag.remove(NBT_CELL_ID); - } else { - tag.putUUID(NBT_CELL_ID, cellId); - } - } - - /** - * Check if this collar has a cell assigned. - * - * @param stack The collar ItemStack - * @return true if a cell is assigned - */ - public boolean hasCellAssigned(ItemStack stack) { - return getCellId(stack) != null; - } - - // Kidnapping Mode - - /** - * Check if kidnapping mode is enabled. - * Kidnapping mode allows NPC kidnappers to auto-capture and teleport to prison. - * - * @param stack The collar ItemStack - * @return true if kidnapping mode enabled - */ - public boolean isKidnappingModeEnabled(ItemStack stack) { - if (stack.isEmpty()) { - return false; - } - - CompoundTag tag = stack.getTag(); - return tag != null && tag.getBoolean(NBT_KIDNAPPING_MODE); - } - - /** - * Set kidnapping mode state. - * - * @param stack The collar ItemStack - * @param enabled true to enable kidnapping mode - */ - public void setKidnappingModeEnabled(ItemStack stack, boolean enabled) { - if (stack.isEmpty()) { - return; - } - - stack.getOrCreateTag().putBoolean(NBT_KIDNAPPING_MODE, enabled); - TiedUpMod.LOGGER.info( - "[ItemCollar] Kidnapping mode set to {}", - enabled - ); - } - - /** - * Check if kidnapping mode is fully configured and ready. - * Requires: kidnapping mode ON + cell assigned - * - * @param stack The collar ItemStack - * @return true if ready for automated kidnapping - */ - public boolean isKidnappingModeReady(ItemStack stack) { - if (!isKidnappingModeEnabled(stack)) return false; - return hasCellAssigned(stack); - } - - // Tie to Pole (Auto-tie slave in cell) - - /** - * Check if slave should be auto-tied to nearest pole in prison. - * - * @param stack The collar ItemStack - * @return true if should tie to pole - */ - public boolean shouldTieToPole(ItemStack stack) { - if (stack.isEmpty()) { - return false; - } - - CompoundTag tag = stack.getTag(); - return tag != null && tag.getBoolean(NBT_TIE_TO_POLE); - } - - /** - * Set whether slave should be tied to pole in prison. - * - * @param stack The collar ItemStack - * @param enabled true to enable tie to pole - */ - public void setTieToPole(ItemStack stack, boolean enabled) { - if (stack.isEmpty()) { - return; - } - - stack.getOrCreateTag().putBoolean(NBT_TIE_TO_POLE, enabled); - } - - // Warn Masters - - /** - * Check if owners should be warned when slave is captured. - * - * @param stack The collar ItemStack - * @return true if should warn masters - */ - public boolean shouldWarnMasters(ItemStack stack) { - if (stack.isEmpty()) { - return true; // Default: warn - } - - CompoundTag tag = stack.getTag(); - if (tag != null && tag.contains(NBT_WARN_MASTERS)) { - return tag.getBoolean(NBT_WARN_MASTERS); - } - - return true; // Default: warn - } - - /** - * Set whether owners should be warned. - * - * @param stack The collar ItemStack - * @param enabled true to enable warnings - */ - public void setWarnMasters(ItemStack stack, boolean enabled) { - if (stack.isEmpty()) { - return; - } - - stack.getOrCreateTag().putBoolean(NBT_WARN_MASTERS, enabled); - } - - // Bondage Service - - /** - * Check if bondage service is enabled on this collar. - * Bondage service allows a Damsel to automatically capture attacking players. - * - * @param stack The collar ItemStack - * @return true if bondage service is enabled - */ - public boolean isBondageServiceEnabled(ItemStack stack) { - if (stack.isEmpty()) { - return false; - } - - CompoundTag tag = stack.getTag(); - return tag != null && tag.getBoolean(NBT_BONDAGE_SERVICE); - } - - /** - * Enable or disable bondage service on this collar. - * - * @param stack The collar ItemStack - * @param enabled true to enable bondage service - */ - public void setBondageServiceEnabled(ItemStack stack, boolean enabled) { - if (stack.isEmpty()) { - return; - } - - stack.getOrCreateTag().putBoolean(NBT_BONDAGE_SERVICE, enabled); - } - - /** - * Get the custom service sentence message. - * - * @param stack The collar ItemStack - * @return Custom message or null if not set - */ - @Nullable - public String getServiceSentence(ItemStack stack) { - if (stack.isEmpty()) { - return null; - } - - CompoundTag tag = stack.getTag(); - if (tag != null && tag.contains(NBT_SERVICE_SENTENCE)) { - return tag.getString(NBT_SERVICE_SENTENCE); - } - return null; - } - - /** - * Set a custom service sentence message. - * - * @param stack The collar ItemStack - * @param sentence The custom message - */ - public void setServiceSentence(ItemStack stack, String sentence) { - if (stack.isEmpty()) { - return; - } - - if (sentence == null || sentence.isEmpty()) { - stack.getOrCreateTag().remove(NBT_SERVICE_SENTENCE); - } else { - stack.getOrCreateTag().putString(NBT_SERVICE_SENTENCE, sentence); - } - } - - /** - * Register a collar in the global CollarRegistry. - * Called when a collar is put on an entity. - * - * @param wearer The entity wearing the collar - * @param collarStack The collar ItemStack - * @param primaryOwner The player putting the collar on (primary owner) - */ - private void registerCollarInRegistry( - LivingEntity wearer, - ItemStack collarStack, - Player primaryOwner - ) { - if (wearer == null || wearer.level().isClientSide()) { - return; - } - - // Get server-side registry - if ( - !(wearer.level() instanceof - net.minecraft.server.level.ServerLevel serverLevel) - ) { - return; - } - - CollarRegistry registry = CollarRegistry.get(serverLevel); - if (registry == null) { - return; - } - - // Get all owners from the collar NBT - java.util.List owners = getOwners(collarStack); - if (owners.isEmpty() && primaryOwner != null) { - // If no owners yet, use the player putting the collar on - owners = java.util.List.of(primaryOwner.getUUID()); - } - - // Register all owners - for (UUID ownerUUID : owners) { - registry.registerCollar(wearer.getUUID(), ownerUUID); - } - - // Sync to affected owners - syncRegistryToOwners(serverLevel, owners); - - TiedUpMod.LOGGER.debug( - "[CollarRegistry] Registered {} with {} owners", - wearer.getName().getString(), - owners.size() - ); - } - - /** - * Unregister a collar from the global CollarRegistry. - * Called when a collar is removed from an entity. - * - * @param wearer The entity whose collar is being removed - */ - private void unregisterCollarFromRegistry(LivingEntity wearer) { - if (wearer == null || wearer.level().isClientSide()) { - return; - } - - if ( - !(wearer.level() instanceof - net.minecraft.server.level.ServerLevel serverLevel) - ) { - return; - } - - CollarRegistry registry = CollarRegistry.get(serverLevel); - if (registry == null) { - return; - } - - // Get owners before unregistering (for sync) - java.util.Set owners = registry.getOwners(wearer.getUUID()); - - // Unregister the wearer - registry.unregisterWearer(wearer.getUUID()); - - // Sync to affected owners - syncRegistryToOwners(serverLevel, owners); - - TiedUpMod.LOGGER.debug( - "[CollarRegistry] Unregistered {}", - wearer.getName().getString() - ); - } - - // ESCAPE DETECTION: Collar Removal Alert - - /** - * Alert nearby kidnappers when a collar is forcibly removed. - * This indicates a potential escape attempt. - * - * Should be called when: - * - Player struggles out of a collar - * - Collar is removed by another player without permission - * - Collar is broken/picked - * - * @param wearer The entity whose collar was removed - * @param forced Whether the removal was forced (struggle, lockpick, etc.) - */ - public static void onCollarRemoved(LivingEntity wearer, boolean forced) { - if (wearer == null || wearer.level().isClientSide()) { - return; - } - - if (!forced) { - return; // Only alert on forced removals - } - - if ( - !(wearer.level() instanceof - net.minecraft.server.level.ServerLevel serverLevel) - ) { - return; - } - - TiedUpMod.LOGGER.info( - "[ItemCollar] {} collar was forcibly removed - alerting kidnappers", - wearer.getName().getString() - ); - - // Unregister from CollarRegistry - CollarRegistry registry = CollarRegistry.get(serverLevel); - if (registry != null) { - registry.unregisterWearer(wearer.getUUID()); - } - - // Find and alert nearby kidnappers - net.minecraft.world.phys.AABB searchBox = wearer - .getBoundingBox() - .inflate(50, 20, 50); - java.util.List kidnappers = - serverLevel.getEntitiesOfClass( - com.tiedup.remake.entities.EntityKidnapper.class, - searchBox - ); - - for (com.tiedup.remake.entities.EntityKidnapper kidnapper : kidnappers) { - // Check if this kidnapper owned the collar - // For now, alert all kidnappers - they'll check on their own - if (!kidnapper.hasCaptives() && !kidnapper.isTiedUp()) { - kidnapper.setAlertTarget(wearer); - kidnapper.setCurrentState( - com.tiedup.remake.entities.ai.kidnapper.KidnapperState.ALERT - ); - kidnapper.broadcastAlert(wearer); - - TiedUpMod.LOGGER.debug( - "[ItemCollar] Alerted kidnapper {} about collar removal", - kidnapper.getNpcName() - ); - } - } - } - - /** - * Sync the CollarRegistry to specific owners. - * - * @param level The server level - * @param ownerUUIDs The owners to sync to - */ - private void syncRegistryToOwners( - net.minecraft.server.level.ServerLevel level, - java.util.Collection ownerUUIDs - ) { - net.minecraft.server.MinecraftServer server = level.getServer(); - if (server == null) { - return; - } - - CollarRegistry registry = CollarRegistry.get(server); - if (registry == null) { - return; - } - - for (UUID ownerUUID : ownerUUIDs) { - net.minecraft.server.level.ServerPlayer owner = server - .getPlayerList() - .getPlayer(ownerUUID); - if (owner != null) { - // Send full sync to this owner - java.util.Set slaves = registry.getSlaves(ownerUUID); - com.tiedup.remake.network.ModNetwork.sendToPlayer( - new com.tiedup.remake.network.sync.PacketSyncCollarRegistry( - slaves - ), - owner - ); - } - } - } - - // LIFECYCLE HOOKS - - /** - * Called when a collar is unequipped from an entity. - * Triggers escape detection for forced removals. - * - * @param stack The unequipped collar - * @param entity The entity that was wearing the collar - */ - @Override - public void onUnequipped(ItemStack stack, LivingEntity entity) { - // Check if this is a legitimate removal (camp death, ransom paid, etc.) - if (isRemovalAlertSuppressed()) { - TiedUpMod.LOGGER.debug( - "[ItemCollar] Collar removal for {} - alert suppressed (legitimate removal)", - entity.getName().getString() - ); - return; - } - - // Alert kidnappers about the collar removal (forced/escape) - onCollarRemoved(entity, true); - } - - // TEXTURE SUBFOLDER - - /** - * Get the texture subfolder for collar items. - * Issue #12 fix: Eliminates string checks in renderers. - */ - @Override - public String getTextureSubfolder() { - return "collars"; - } -} diff --git a/src/main/java/com/tiedup/remake/items/base/ItemEarplugs.java b/src/main/java/com/tiedup/remake/items/base/ItemEarplugs.java deleted file mode 100644 index 9e7ef97..0000000 --- a/src/main/java/com/tiedup/remake/items/base/ItemEarplugs.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.tiedup.remake.items.base; - -import com.tiedup.remake.core.SystemMessageManager; -import com.tiedup.remake.util.EquipmentInteractionHelper; -import com.tiedup.remake.util.TiedUpSounds; -import com.tiedup.remake.v2.BodyRegionV2; -import java.util.List; -import net.minecraft.network.chat.Component; -import net.minecraft.world.InteractionHand; -import net.minecraft.world.InteractionResult; -import net.minecraft.world.entity.LivingEntity; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.item.Item; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.TooltipFlag; -import net.minecraft.world.level.Level; -import org.jetbrains.annotations.Nullable; - -/** - * Base class for earplug items. - * These items block or reduce sounds when equipped. - * - * Based on original ItemEarplugs from 1.12.2 - * - * Phase future: Sound blocking effect - */ -public abstract class ItemEarplugs - extends Item - implements IBondageItem, ILockable -{ - - public ItemEarplugs(Properties properties) { - super(properties.stacksTo(16)); // Earplugs can stack to 16 - } - - @Override - public BodyRegionV2 getBodyRegion() { - return BodyRegionV2.EARS; - } - - @Override - public void appendHoverText( - ItemStack stack, - @Nullable Level level, - List tooltip, - TooltipFlag flag - ) { - super.appendHoverText(stack, level, tooltip, flag); - appendLockTooltip(stack, tooltip); - } - - /** - * Called when player right-clicks another entity with earplugs. - * Allows putting earplugs on tied-up entities (Players and NPCs). - */ - @Override - public InteractionResult interactLivingEntity( - ItemStack stack, - Player player, - LivingEntity target, - InteractionHand hand - ) { - return EquipmentInteractionHelper.equipOnTarget( - stack, - player, - target, - state -> state.hasEarplugs(), - (state, item) -> state.equip(BodyRegionV2.EARS, item), - (state, item) -> - state.replaceEquipment(BodyRegionV2.EARS, item, false), - (p, t) -> - SystemMessageManager.sendToTarget( - p, - t, - SystemMessageManager.MessageCategory.EARPLUGS_ON - ), - "ItemEarplugs", - null, // No pre-equip hook - (s, p, t, state) -> TiedUpSounds.playEarplugsEquipSound(t), // Post-equip: play sound - null // No replace check - ); - } - - // Sound blocking implemented in: - // - client/events/EarplugSoundHandler.java (event interception) - // - client/MuffledSoundInstance.java (volume/pitch wrapper) - // - Configurable via ModConfig.CLIENT.earplugVolumeMultiplier - - // ILockable implementation inherited from interface default methods -} diff --git a/src/main/java/com/tiedup/remake/items/base/ItemGag.java b/src/main/java/com/tiedup/remake/items/base/ItemGag.java deleted file mode 100644 index a7eee05..0000000 --- a/src/main/java/com/tiedup/remake/items/base/ItemGag.java +++ /dev/null @@ -1,93 +0,0 @@ -package com.tiedup.remake.items.base; - -import com.tiedup.remake.core.SystemMessageManager; -import com.tiedup.remake.util.EquipmentInteractionHelper; -import com.tiedup.remake.util.GagMaterial; -import com.tiedup.remake.v2.BodyRegionV2; -import java.util.List; -import net.minecraft.ChatFormatting; -import net.minecraft.network.chat.Component; -import net.minecraft.world.InteractionHand; -import net.minecraft.world.InteractionResult; -import net.minecraft.world.entity.LivingEntity; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.item.Item; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.TooltipFlag; -import net.minecraft.world.level.Level; -import org.jetbrains.annotations.Nullable; - -/** - * Base class for gag items (ball gag, cloth gag, tape, etc.) - * These items prevent or muffle a player's speech when equipped. - * - * Based on original ItemGag from 1.12.2 - * - */ -public abstract class ItemGag - extends Item - implements IBondageItem, IHasGaggingEffect, IAdjustable, ILockable -{ - - private final GagMaterial material; - - public ItemGag(Properties properties, GagMaterial material) { - super(properties); - this.material = material; - } - - public GagMaterial getGagMaterial() { - return this.material; - } - - @Override - public BodyRegionV2 getBodyRegion() { - return BodyRegionV2.MOUTH; - } - - @Override - public void appendHoverText( - ItemStack stack, - @Nullable Level level, - List tooltip, - TooltipFlag flag - ) { - super.appendHoverText(stack, level, tooltip, flag); - appendLockTooltip(stack, tooltip); - } - - /** - * All gags can be adjusted to better fit player skins. - * @return true - gags support position adjustment - */ - @Override - public boolean canBeAdjusted() { - return true; - } - - /** - * Called when player right-clicks another entity with this gag. - * Allows putting gag on tied-up entities (Players and NPCs). - */ - @Override - public InteractionResult interactLivingEntity( - ItemStack stack, - Player player, - LivingEntity target, - InteractionHand hand - ) { - return EquipmentInteractionHelper.equipOnTarget( - stack, - player, - target, - state -> state.isGagged(), - (state, item) -> state.equip(BodyRegionV2.MOUTH, item), - (state, item) -> - state.replaceEquipment(BodyRegionV2.MOUTH, item, false), - SystemMessageManager::sendGagged, - "ItemGag" - ); - } - - // ILockable implementation inherited from interface default methods -} diff --git a/src/main/java/com/tiedup/remake/items/base/ItemMittens.java b/src/main/java/com/tiedup/remake/items/base/ItemMittens.java deleted file mode 100644 index 7616782..0000000 --- a/src/main/java/com/tiedup/remake/items/base/ItemMittens.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.tiedup.remake.items.base; - -import com.tiedup.remake.core.SystemMessageManager; -import com.tiedup.remake.util.EquipmentInteractionHelper; -import com.tiedup.remake.v2.BodyRegionV2; -import net.minecraft.world.InteractionHand; -import net.minecraft.world.InteractionResult; -import net.minecraft.world.entity.LivingEntity; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.item.Item; -import net.minecraft.world.item.ItemStack; - -/** - * Base class for mittens items. - * These items block hand interactions (mining, placing, using items) when equipped. - * - * - * Restrictions when wearing mittens: - * - Cannot mine/break blocks - * - Cannot place blocks - * - Cannot use items - * - Cannot attack (0 damage punch allowed) - * - * Allowed: - * - Push buttons/levers - * - Open doors - */ -public abstract class ItemMittens - extends Item - implements IBondageItem, ILockable -{ - - public ItemMittens(Properties properties) { - super(properties); - } - - @Override - public BodyRegionV2 getBodyRegion() { - return BodyRegionV2.HANDS; - } - - /** - * Called when player right-clicks another entity with these mittens. - * Allows putting mittens on tied-up entities (Players and NPCs). - */ - @Override - public InteractionResult interactLivingEntity( - ItemStack stack, - Player player, - LivingEntity target, - InteractionHand hand - ) { - return EquipmentInteractionHelper.equipOnTarget( - stack, - player, - target, - state -> state.hasMittens(), - (state, item) -> state.equip(BodyRegionV2.HANDS, item), - (state, item) -> - state.replaceEquipment(BodyRegionV2.HANDS, item, false), - (p, t) -> - SystemMessageManager.sendToTarget( - p, - t, - SystemMessageManager.MessageCategory.MITTENS_ON - ), - "ItemMittens" - ); - } - - // ILockable implementation inherited from interface default methods -} diff --git a/src/main/java/com/tiedup/remake/items/base/ItemOwnerTarget.java b/src/main/java/com/tiedup/remake/items/base/ItemOwnerTarget.java index 2c4be8e..b368982 100644 --- a/src/main/java/com/tiedup/remake/items/base/ItemOwnerTarget.java +++ b/src/main/java/com/tiedup/remake/items/base/ItemOwnerTarget.java @@ -195,11 +195,11 @@ public abstract class ItemOwnerTarget extends Item { BodyRegionV2.NECK ); if ( - collar.getItem() instanceof ItemCollar collarItem && - collarItem.hasNickname(collar) + com.tiedup.remake.v2.bondage.CollarHelper.isCollar(collar) && + com.tiedup.remake.v2.bondage.CollarHelper.hasNickname(collar) ) { displayName = - collarItem.getNickname(collar) + + com.tiedup.remake.v2.bondage.CollarHelper.getNickname(collar) + " (" + displayName + ")"; diff --git a/src/main/java/com/tiedup/remake/items/base/MittensVariant.java b/src/main/java/com/tiedup/remake/items/base/MittensVariant.java deleted file mode 100644 index 686d286..0000000 --- a/src/main/java/com/tiedup/remake/items/base/MittensVariant.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.tiedup.remake.items.base; - -/** - * Enum defining all mittens variants. - * Used by GenericMittens to create mittens items via factory pattern. - * - *

Mittens system - blocks hand interactions when equipped. - * - *

Issue #12 fix: Added textureSubfolder to eliminate string checks in renderers. - */ -public enum MittensVariant { - LEATHER("mittens", "mittens"); - - private final String registryName; - private final String textureSubfolder; - - MittensVariant(String registryName, String textureSubfolder) { - this.registryName = registryName; - this.textureSubfolder = textureSubfolder; - } - - public String getRegistryName() { - return registryName; - } - - /** - * Get the texture subfolder for this mittens variant. - * Used by renderers to locate texture files. - * - * @return Subfolder path under textures/entity/bondage/ (e.g., "mittens") - */ - public String getTextureSubfolder() { - return textureSubfolder; - } -} diff --git a/src/main/java/com/tiedup/remake/items/bondage3d/gags/ItemBallGag3D.java b/src/main/java/com/tiedup/remake/items/bondage3d/gags/ItemBallGag3D.java deleted file mode 100644 index 63f79fb..0000000 --- a/src/main/java/com/tiedup/remake/items/bondage3d/gags/ItemBallGag3D.java +++ /dev/null @@ -1,78 +0,0 @@ -package com.tiedup.remake.items.bondage3d.gags; - -import com.tiedup.remake.items.base.ItemGag; -import com.tiedup.remake.items.bondage3d.IHas3DModelConfig; -import com.tiedup.remake.items.bondage3d.Model3DConfig; -import com.tiedup.remake.util.GagMaterial; -import java.util.Set; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.item.Item; -import org.jetbrains.annotations.Nullable; - -/** - * Ball Gag 3D - Extends ItemGag with 3D OBJ model rendering. - * All 3D configuration is defined here. - * Supports color variants via tinting the "Ball" material. - */ -public class ItemBallGag3D extends ItemGag implements IHas3DModelConfig { - - // 3D config with "Ball" material tintable for color variants - private static final Model3DConfig CONFIG = new Model3DConfig( - "tiedup:models/obj/ball_gag/model.obj", // OBJ - "tiedup:models/obj/ball_gag/texture.png", // Explicit texture - 0.0f, // posX - 1.55f, // posY - 0.0f, // posZ - 1.0f, // scale - 0.0f, - 0.0f, - 180.0f, // rotation - Set.of("Ball") // Tintable materials (for color variants) - ); - - public ItemBallGag3D() { - super(new Item.Properties().stacksTo(16), GagMaterial.BALL); - } - - // ===== 3D Model Support ===== - - @Override - public boolean uses3DModel() { - return true; - } - - @Override - @Nullable - public ResourceLocation get3DModelLocation() { - return ResourceLocation.tryParse(CONFIG.objPath()); - } - - /** - * Returns the complete 3D configuration for the renderer. - */ - @Override - public Model3DConfig getModelConfig() { - return CONFIG; - } - - /** - * Explicit texture (if non-null, overrides MTL map_Kd). - */ - @Nullable - public ResourceLocation getExplicitTexture() { - String path = CONFIG.texturePath(); - return path != null ? ResourceLocation.tryParse(path) : null; - } - - // ===== Gag Properties ===== - - @Override - public String getTextureSubfolder() { - return "ballgags/normal"; // Fallback if 3D fails - } - - @Override - public boolean canAttachPadlock() { - return true; - } -} diff --git a/src/main/java/com/tiedup/remake/network/action/PacketTighten.java b/src/main/java/com/tiedup/remake/network/action/PacketTighten.java index 820898b..a745e51 100644 --- a/src/main/java/com/tiedup/remake/network/action/PacketTighten.java +++ b/src/main/java/com/tiedup/remake/network/action/PacketTighten.java @@ -166,11 +166,8 @@ public class PacketTighten { // Check if sender owns collar if (!hasPermission && closestState.hasCollar()) { var collarStack = closestState.getEquipment(BodyRegionV2.NECK); - if ( - collarStack.getItem() instanceof - com.tiedup.remake.items.base.ItemCollar collar - ) { - if (collar.isOwner(collarStack, tightener)) { + if (com.tiedup.remake.v2.bondage.CollarHelper.isCollar(collarStack)) { + if (com.tiedup.remake.v2.bondage.CollarHelper.isOwner(collarStack, tightener)) { hasPermission = true; } } diff --git a/src/main/java/com/tiedup/remake/network/item/PacketAdjustItem.java b/src/main/java/com/tiedup/remake/network/item/PacketAdjustItem.java index 9a9447d..0777e04 100644 --- a/src/main/java/com/tiedup/remake/network/item/PacketAdjustItem.java +++ b/src/main/java/com/tiedup/remake/network/item/PacketAdjustItem.java @@ -2,7 +2,6 @@ package com.tiedup.remake.network.item; import com.tiedup.remake.core.TiedUpMod; import com.tiedup.remake.items.base.AdjustmentHelper; -import com.tiedup.remake.items.base.IAdjustable; import com.tiedup.remake.network.sync.SyncManager; import com.tiedup.remake.state.PlayerBindState; import com.tiedup.remake.v2.BodyRegionV2; @@ -152,7 +151,7 @@ public class PacketAdjustItem { return; } - if (!(stack.getItem() instanceof IAdjustable)) { + if (!AdjustmentHelper.isAdjustable(stack)) { TiedUpMod.LOGGER.warn( "[PACKET] PacketAdjustItem: Item {} is not adjustable", stack.getItem() diff --git a/src/main/java/com/tiedup/remake/network/item/PacketAdjustRemote.java b/src/main/java/com/tiedup/remake/network/item/PacketAdjustRemote.java index 521a397..cd1d486 100644 --- a/src/main/java/com/tiedup/remake/network/item/PacketAdjustRemote.java +++ b/src/main/java/com/tiedup/remake/network/item/PacketAdjustRemote.java @@ -3,7 +3,6 @@ package com.tiedup.remake.network.item; import com.tiedup.remake.core.SystemMessageManager; import com.tiedup.remake.core.TiedUpMod; import com.tiedup.remake.items.base.AdjustmentHelper; -import com.tiedup.remake.items.base.IAdjustable; import com.tiedup.remake.network.sync.SyncManager; import com.tiedup.remake.state.IBondageState; import com.tiedup.remake.state.PlayerBindState; @@ -195,7 +194,7 @@ public class PacketAdjustRemote { return; } - if (!(stack.getItem() instanceof IAdjustable)) { + if (!AdjustmentHelper.isAdjustable(stack)) { TiedUpMod.LOGGER.warn( "[PACKET] PacketAdjustRemote: Item {} is not adjustable", stack.getItem() @@ -255,11 +254,8 @@ public class PacketAdjustRemote { ItemStack collarStack = kidnapped.getEquipment( BodyRegionV2.NECK ); - if ( - collarStack.getItem() instanceof - com.tiedup.remake.items.base.ItemCollar collar - ) { - if (collar.isOwner(collarStack, sender)) { + if (com.tiedup.remake.v2.bondage.CollarHelper.isCollar(collarStack)) { + if (com.tiedup.remake.v2.bondage.CollarHelper.isOwner(collarStack, sender)) { return kidnapped; } } diff --git a/src/main/java/com/tiedup/remake/network/minigame/PacketLockpickAttempt.java b/src/main/java/com/tiedup/remake/network/minigame/PacketLockpickAttempt.java index af45b29..1505d90 100644 --- a/src/main/java/com/tiedup/remake/network/minigame/PacketLockpickAttempt.java +++ b/src/main/java/com/tiedup/remake/network/minigame/PacketLockpickAttempt.java @@ -454,9 +454,7 @@ public class PacketLockpickAttempt { ); if (collar.isEmpty()) return; - if ( - collar.getItem() instanceof com.tiedup.remake.items.ItemShockCollar - ) { + if (com.tiedup.remake.v2.bondage.CollarHelper.canShock(collar)) { state.shockKidnapped(" (Failed lockpick attempt)", 2.0f); TiedUpMod.LOGGER.info( "[PacketLockpickAttempt] Player {} shocked for failed lockpick", diff --git a/src/main/java/com/tiedup/remake/network/personality/PacketRequestNpcInventory.java b/src/main/java/com/tiedup/remake/network/personality/PacketRequestNpcInventory.java index 6c4da6a..80d1d51 100644 --- a/src/main/java/com/tiedup/remake/network/personality/PacketRequestNpcInventory.java +++ b/src/main/java/com/tiedup/remake/network/personality/PacketRequestNpcInventory.java @@ -70,11 +70,8 @@ public class PacketRequestNpcInventory { // Verify player is owner of collar (or NPC has no collar) if (damsel.hasCollar()) { ItemStack collar = damsel.getEquipment(BodyRegionV2.NECK); - if ( - collar.getItem() instanceof - com.tiedup.remake.items.base.ItemCollar collarItem - ) { - if (!collarItem.isOwner(collar, player)) { + if (com.tiedup.remake.v2.bondage.CollarHelper.isCollar(collar)) { + if (!com.tiedup.remake.v2.bondage.CollarHelper.isOwner(collar, player)) { TiedUpMod.LOGGER.warn( "[PacketRequestNpcInventory] Player {} is not owner of NPC collar", player.getName().getString() diff --git a/src/main/java/com/tiedup/remake/network/selfbondage/PacketSelfBondage.java b/src/main/java/com/tiedup/remake/network/selfbondage/PacketSelfBondage.java index 392178f..13024ec 100644 --- a/src/main/java/com/tiedup/remake/network/selfbondage/PacketSelfBondage.java +++ b/src/main/java/com/tiedup/remake/network/selfbondage/PacketSelfBondage.java @@ -2,7 +2,7 @@ package com.tiedup.remake.network.selfbondage; import com.tiedup.remake.core.SettingsAccessor; import com.tiedup.remake.core.TiedUpMod; -import com.tiedup.remake.items.base.*; +import com.tiedup.remake.items.base.ILockable; import com.tiedup.remake.network.ModNetwork; import com.tiedup.remake.network.PacketRateLimiter; import com.tiedup.remake.network.action.PacketTying; @@ -79,141 +79,13 @@ public class PacketSelfBondage { // V2 bondage items — use tying task with V2 equip if (item instanceof IV2BondageItem v2Item) { handleV2SelfBondage(player, stack, v2Item, state); - return; } - - // V1 routes below (legacy) - if (item instanceof ItemBind bind) { - handleSelfBind(player, stack, bind, state); - } else if (item instanceof ItemGag) { - handleSelfAccessory( - player, - stack, - state, - "gag", - s -> s.isGagged(), - s -> s.getEquipment(BodyRegionV2.MOUTH), - s -> s.unequip(BodyRegionV2.MOUTH), - (s, i) -> s.equip(BodyRegionV2.MOUTH, i) - ); - } else if (item instanceof ItemBlindfold) { - handleSelfAccessory( - player, - stack, - state, - "blindfold", - s -> s.isBlindfolded(), - s -> s.getEquipment(BodyRegionV2.EYES), - s -> s.unequip(BodyRegionV2.EYES), - (s, i) -> s.equip(BodyRegionV2.EYES, i) - ); - } else if (item instanceof ItemMittens) { - handleSelfAccessory( - player, - stack, - state, - "mittens", - s -> s.hasMittens(), - s -> s.getEquipment(BodyRegionV2.HANDS), - s -> s.unequip(BodyRegionV2.HANDS), - (s, i) -> s.equip(BodyRegionV2.HANDS, i) - ); - } else if (item instanceof ItemEarplugs) { - handleSelfAccessory( - player, - stack, - state, - "earplugs", - s -> s.hasEarplugs(), - s -> s.getEquipment(BodyRegionV2.EARS), - s -> s.unequip(BodyRegionV2.EARS), - (s, i) -> s.equip(BodyRegionV2.EARS, i) - ); - } - // ItemCollar: NOT handled - cannot self-collar + // V1 routes removed — all bondage items are now V2/data-driven }); ctx.get().setPacketHandled(true); } - /** - * Handle self-binding with a bind item (rope, chain, etc.). - * Uses tying task system - requires holding left-click. - */ - private static void handleSelfBind( - ServerPlayer player, - ItemStack stack, - ItemBind bind, - IBondageState state - ) { - // Can't self-tie if already tied - if (state.isTiedUp()) { - TiedUpMod.LOGGER.debug( - "[SelfBondage] {} tried to self-tie but is already tied", - player.getName().getString() - ); - return; - } - - // Get player's bind state for tying task management - PlayerBindState playerState = PlayerBindState.getInstance(player); - if (playerState == null) return; - - // Get tying duration from GameRule - int tyingSeconds = SettingsAccessor.getTyingPlayerTime( - player.level().getGameRules() - ); - - // Create self-tying task (target == kidnapper) - TyingPlayerTask newTask = new TyingPlayerTask( - stack.copy(), - state, - player, // Target is self - tyingSeconds, - player.level(), - player // Kidnapper is also self - ); - - // Get current tying task - TyingTask currentTask = playerState.getCurrentTyingTask(); - - // Check if we should start a new task or continue existing one - if ( - currentTask == null || - !currentTask.isSameTarget(player) || - currentTask.isOutdated() || - !ItemStack.matches(currentTask.getBind(), stack) - ) { - // Start new self-tying task - playerState.setCurrentTyingTask(newTask); - newTask.start(); - - TiedUpMod.LOGGER.debug( - "[SelfBondage] {} started self-tying ({} seconds)", - player.getName().getString(), - tyingSeconds - ); - } else { - // Continue existing task - newTask = (TyingPlayerTask) currentTask; - } - - // Update task progress - newTask.update(); - - // Check if task completed - if (newTask.isStopped()) { - // Self-tying complete! Consume the item - stack.shrink(1); - playerState.setCurrentTyingTask(null); - - TiedUpMod.LOGGER.info( - "[SelfBondage] {} successfully self-tied", - player.getName().getString() - ); - } - } - /** * Handle self-bondage with a V2 bondage item. * Uses V2TyingPlayerTask for progress, V2EquipmentHelper for equip. @@ -355,7 +227,7 @@ public class PacketSelfBondage { // Can't equip if arms are fully bound (need hands to put on accessories) ItemStack currentBind = state.getEquipment(BodyRegionV2.ARMS); if (!currentBind.isEmpty()) { - if (ItemBind.hasArmsBound(currentBind)) { + if (BindModeHelper.hasArmsBound(currentBind)) { TiedUpMod.LOGGER.debug( "[SelfBondage] {} can't self-{} - arms are bound", player.getName().getString(), diff --git a/src/main/java/com/tiedup/remake/network/slave/PacketSlaveItemManage.java b/src/main/java/com/tiedup/remake/network/slave/PacketSlaveItemManage.java index f78c3bd..6b941c7 100644 --- a/src/main/java/com/tiedup/remake/network/slave/PacketSlaveItemManage.java +++ b/src/main/java/com/tiedup/remake/network/slave/PacketSlaveItemManage.java @@ -163,12 +163,9 @@ public class PacketSlaveItemManage { ItemStack collarStack = targetState.getEquipment( BodyRegionV2.NECK ); - if ( - collarStack.getItem() instanceof - com.tiedup.remake.items.base.ItemCollar collar - ) { + if (com.tiedup.remake.v2.bondage.CollarHelper.isCollar(collarStack)) { if ( - !collar.isOwner(collarStack, sender) && + !com.tiedup.remake.v2.bondage.CollarHelper.isOwner(collarStack, sender) && !sender.hasPermissions(2) ) { TiedUpMod.LOGGER.debug( @@ -437,15 +434,14 @@ public class PacketSlaveItemManage { if ( region == BodyRegionV2.ARMS && - itemStack.getItem() instanceof - com.tiedup.remake.items.base.ItemBind bind + itemStack.getItem() instanceof com.tiedup.remake.items.base.IHasResistance resistanceItem ) { - int currentResistance = bind.getCurrentResistance( + int currentResistance = resistanceItem.getCurrentResistance( itemStack, target ); int lockResistance = lockable.getLockResistance(); // Configurable via ModConfig - bind.setCurrentResistance( + resistanceItem.setCurrentResistance( itemStack, currentResistance + lockResistance ); @@ -599,8 +595,7 @@ public class PacketSlaveItemManage { ) { if ( collarStack.isEmpty() || - !(collarStack.getItem() instanceof - com.tiedup.remake.items.base.ItemCollar collar) + !com.tiedup.remake.v2.bondage.CollarHelper.isCollar(collarStack) ) { TiedUpMod.LOGGER.debug( "[PacketSlaveItemManage] No collar for bondage service toggle" @@ -609,7 +604,7 @@ public class PacketSlaveItemManage { } // Check if cell is configured (required for bondage service) - if (!collar.hasCellAssigned(collarStack)) { + if (!com.tiedup.remake.v2.bondage.CollarHelper.hasCellAssigned(collarStack)) { TiedUpMod.LOGGER.debug( "[PacketSlaveItemManage] Cannot enable bondage service: no cell configured" ); @@ -623,8 +618,8 @@ public class PacketSlaveItemManage { } // Toggle bondage service - boolean currentState = collar.isBondageServiceEnabled(collarStack); - collar.setBondageServiceEnabled(collarStack, !currentState); + boolean currentState = com.tiedup.remake.v2.bondage.CollarHelper.isBondageServiceEnabled(collarStack); + com.tiedup.remake.v2.bondage.CollarHelper.setBondageServiceEnabled(collarStack, !currentState); String newState = !currentState ? "enabled" : "disabled"; TiedUpMod.LOGGER.info( diff --git a/src/main/java/com/tiedup/remake/network/trader/PacketBuyCaptive.java b/src/main/java/com/tiedup/remake/network/trader/PacketBuyCaptive.java index 01da680..ac1d891 100644 --- a/src/main/java/com/tiedup/remake/network/trader/PacketBuyCaptive.java +++ b/src/main/java/com/tiedup/remake/network/trader/PacketBuyCaptive.java @@ -243,22 +243,23 @@ public class PacketBuyCaptive { kidnappedState.getEquipment(BodyRegionV2.NECK); if ( !collar.isEmpty() && - collar.getItem() instanceof - com.tiedup.remake.items.base.ItemCollar collarItem + com.tiedup.remake.v2.bondage.CollarHelper.isCollar(collar) ) { // Remove all existing owners from collar NBT for (UUID ownerId : new java.util.ArrayList<>( - collarItem.getOwners(collar) + com.tiedup.remake.v2.bondage.CollarHelper.getOwners(collar) )) { - collarItem.removeOwner(collar, ownerId); + com.tiedup.remake.v2.bondage.CollarHelper.removeOwner(collar, ownerId); } // Add buyer as new owner - collarItem.addOwner( + com.tiedup.remake.v2.bondage.CollarHelper.addOwner( collar, buyer.getUUID(), buyer.getName().getString() ); - collarItem.setLocked(collar, false); + if (collar.getItem() instanceof com.tiedup.remake.items.base.ILockable lockable) { + lockable.setLocked(collar, false); + } // Re-apply modified collar to persist NBT changes kidnappedState.equip(BodyRegionV2.NECK, collar); diff --git a/src/main/java/com/tiedup/remake/prison/service/PrisonerService.java b/src/main/java/com/tiedup/remake/prison/service/PrisonerService.java index 0c1a32b..9b55d06 100644 --- a/src/main/java/com/tiedup/remake/prison/service/PrisonerService.java +++ b/src/main/java/com/tiedup/remake/prison/service/PrisonerService.java @@ -757,10 +757,9 @@ public class PrisonerService { if ( !collar.isEmpty() && - collar.getItem() instanceof - com.tiedup.remake.items.base.ItemCollar collarItem + com.tiedup.remake.v2.bondage.CollarHelper.isCollar(collar) ) { - List nbtOwners = collarItem.getOwners(collar); + List nbtOwners = com.tiedup.remake.v2.bondage.CollarHelper.getOwners(collar); if (!nbtOwners.isEmpty()) { for (UUID ownerUUID : nbtOwners) { collars.registerCollar(playerId, ownerUUID); diff --git a/src/main/java/com/tiedup/remake/state/components/PlayerShockCollar.java b/src/main/java/com/tiedup/remake/state/components/PlayerShockCollar.java index 3385eef..42f8b79 100644 --- a/src/main/java/com/tiedup/remake/state/components/PlayerShockCollar.java +++ b/src/main/java/com/tiedup/remake/state/components/PlayerShockCollar.java @@ -4,12 +4,14 @@ import com.tiedup.remake.core.ModSounds; import com.tiedup.remake.core.SystemMessageManager; import com.tiedup.remake.core.SystemMessageManager.MessageCategory; import com.tiedup.remake.core.TiedUpMod; -import com.tiedup.remake.items.ItemGpsCollar; -import com.tiedup.remake.items.ItemShockCollarAuto; import com.tiedup.remake.state.hosts.IPlayerBindStateHost; import com.tiedup.remake.util.GameConstants; import com.tiedup.remake.util.time.Timer; import com.tiedup.remake.v2.BodyRegionV2; +import com.tiedup.remake.v2.bondage.CollarHelper; +import com.tiedup.remake.v2.bondage.component.ComponentType; +import com.tiedup.remake.v2.bondage.component.GpsComponent; +import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem; import java.util.List; import java.util.UUID; import net.minecraft.ChatFormatting; @@ -128,17 +130,16 @@ public class PlayerShockCollar { // Flags set inside lock, actions performed outside boolean shouldShockAuto = false; boolean shouldShockGPS = false; - ItemGpsCollar gpsCollar = null; - ItemStack gpsStack = null; + ItemStack gpsStackCopy = null; synchronized (lockTimerAutoShock) { ItemStack collarStack = getCurrentCollar(); if (collarStack.isEmpty()) return; - // Auto-Shock Collar handling - if ( - collarStack.getItem() instanceof ItemShockCollarAuto collarShock - ) { + // Auto-Shock Collar handling: collar can shock AND has auto interval > 0 + if (CollarHelper.canShock(collarStack) && CollarHelper.getShockInterval(collarStack) > 0) { + int interval = CollarHelper.getShockInterval(collarStack); + if ( timerAutoShockCollar != null && timerAutoShockCollar.isExpired() @@ -151,40 +152,38 @@ public class PlayerShockCollar { timerAutoShockCollar.isExpired() ) { timerAutoShockCollar = new Timer( - collarShock.getInterval() / - GameConstants.TICKS_PER_SECOND, + interval / GameConstants.TICKS_PER_SECOND, player.level() ); } } // GPS Collar handling - else if (collarStack.getItem() instanceof ItemGpsCollar gps) { + else if (CollarHelper.hasGPS(collarStack)) { if ( - gps.isActive(collarStack) && + CollarHelper.isActive(collarStack) && (timerAutoShockCollar == null || timerAutoShockCollar.isExpired()) ) { - List safeSpots = gps.getSafeSpots( - collarStack + GpsComponent gpsComp = DataDrivenBondageItem.getComponent( + collarStack, ComponentType.GPS, GpsComponent.class ); - if (safeSpots != null && !safeSpots.isEmpty()) { - boolean isSafe = false; - for (ItemGpsCollar.SafeSpot spot : safeSpots) { - if (spot.isInside(player)) { - isSafe = true; - break; - } - } - if (!isSafe) { - timerAutoShockCollar = new Timer( - gps.getShockInterval(collarStack) / - GameConstants.TICKS_PER_SECOND, - player.level() - ); - shouldShockGPS = true; - gpsCollar = gps; - gpsStack = collarStack.copy(); - } + int safeZoneRadius = gpsComp != null ? gpsComp.getSafeZoneRadius() : 50; + // Shock interval from ShockComponent (GPS collars also have shock capability) + int shockInterval = CollarHelper.getShockInterval(collarStack); + if (shockInterval <= 0) shockInterval = 100; // Fallback: 5 seconds + + // Check safe spots from NBT (CollarHelper reads "safeSpots" NBT) + // For now, use safeZoneRadius from GpsComponent + // GPS safe zone check: if the collar has safe spots in NBT, check them + boolean isSafe = isInSafeZone(collarStack, player, safeZoneRadius); + + if (!isSafe) { + timerAutoShockCollar = new Timer( + shockInterval / GameConstants.TICKS_PER_SECOND, + player.level() + ); + shouldShockGPS = true; + gpsStackCopy = collarStack.copy(); } } } @@ -195,30 +194,58 @@ public class PlayerShockCollar { this.shockKidnapped(); } - if (shouldShockGPS && gpsCollar != null) { + if (shouldShockGPS && gpsStackCopy != null) { this.shockKidnapped( " Return back to your allowed area!", GameConstants.DEFAULT_SHOCK_DAMAGE ); - warnOwnersGPSViolation(gpsCollar, gpsStack); + warnOwnersGPSViolation(gpsStackCopy); } } + /** + * Check if the player is inside any safe zone defined on the collar. + * Reads safe spots from NBT "safeSpots" ListTag. + */ + private boolean isInSafeZone(ItemStack collarStack, Player player, int defaultRadius) { + net.minecraft.nbt.CompoundTag tag = collarStack.getTag(); + if (tag == null || !tag.contains("safeSpots", net.minecraft.nbt.Tag.TAG_LIST)) { + return true; // No safe spots defined = always safe + } + net.minecraft.nbt.ListTag safeSpots = tag.getList("safeSpots", net.minecraft.nbt.Tag.TAG_COMPOUND); + if (safeSpots.isEmpty()) return true; + + for (int i = 0; i < safeSpots.size(); i++) { + net.minecraft.nbt.CompoundTag spot = safeSpots.getCompound(i); + double x = spot.getDouble("x"); + double y = spot.getDouble("y"); + double z = spot.getDouble("z"); + int radius = spot.contains("radius") ? spot.getInt("radius") : defaultRadius; + double dist = player.distanceToSqr(x, y, z); + if (dist <= (double) radius * radius) { + return true; + } + } + return false; + } + /** * Sends a global alert to masters when a slave violates their GPS zone. * Private helper method. */ - private void warnOwnersGPSViolation(ItemGpsCollar gps, ItemStack stack) { + private void warnOwnersGPSViolation(ItemStack stack) { Player player = host.getPlayer(); if (player.getServer() == null) return; + if (!CollarHelper.shouldWarnMasters(stack)) return; + // Format: "ALERT: is outside the safe zone!" String alertMessage = String.format( SystemMessageManager.getTemplate(MessageCategory.GPS_OWNER_ALERT), player.getName().getString() ); - for (UUID ownerId : gps.getOwners(stack)) { + for (UUID ownerId : CollarHelper.getOwners(stack)) { ServerPlayer owner = player .getServer() .getPlayerList() diff --git a/src/main/java/com/tiedup/remake/state/components/PlayerStateQuery.java b/src/main/java/com/tiedup/remake/state/components/PlayerStateQuery.java index 8cb10b7..9c50354 100644 --- a/src/main/java/com/tiedup/remake/state/components/PlayerStateQuery.java +++ b/src/main/java/com/tiedup/remake/state/components/PlayerStateQuery.java @@ -133,10 +133,9 @@ public class PlayerStateQuery { BodyRegionV2.MOUTH ); if (gag.isEmpty()) return false; - return ( - gag.getItem() instanceof - com.tiedup.remake.items.base.IHasGaggingEffect - ); + return com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem.getComponent( + gag, com.tiedup.remake.v2.bondage.component.ComponentType.GAGGING, + com.tiedup.remake.v2.bondage.component.GaggingComponent.class) != null; } /** @@ -150,10 +149,9 @@ public class PlayerStateQuery { BodyRegionV2.EYES ); if (blindfold.isEmpty()) return false; - return ( - blindfold.getItem() instanceof - com.tiedup.remake.items.base.IHasBlindingEffect - ); + return com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem.getComponent( + blindfold, com.tiedup.remake.v2.bondage.component.ComponentType.BLINDING, + com.tiedup.remake.v2.bondage.component.BlindingComponent.class) != null; } /** diff --git a/src/main/java/com/tiedup/remake/state/struggle/StruggleAccessory.java b/src/main/java/com/tiedup/remake/state/struggle/StruggleAccessory.java index 54edfdc..b61bbc3 100644 --- a/src/main/java/com/tiedup/remake/state/struggle/StruggleAccessory.java +++ b/src/main/java/com/tiedup/remake/state/struggle/StruggleAccessory.java @@ -199,10 +199,13 @@ public class StruggleAccessory extends StruggleState { if ( !collar.isEmpty() && - collar.getItem() instanceof - com.tiedup.remake.items.ItemShockCollar shockCollar + com.tiedup.remake.v2.bondage.CollarHelper.canShock(collar) ) { - return shockCollar.notifyStruggle(player, collar); + // V2 shock collar — notify via IHasResistance + if (collar.getItem() instanceof com.tiedup.remake.items.base.IHasResistance resistance) { + resistance.notifyStruggle(player); + } + return true; } return true; // No collar, proceed normally } diff --git a/src/main/java/com/tiedup/remake/state/struggle/StruggleBinds.java b/src/main/java/com/tiedup/remake/state/struggle/StruggleBinds.java index e1bddf6..833db65 100644 --- a/src/main/java/com/tiedup/remake/state/struggle/StruggleBinds.java +++ b/src/main/java/com/tiedup/remake/state/struggle/StruggleBinds.java @@ -6,7 +6,6 @@ import com.tiedup.remake.core.SystemMessageManager.MessageCategory; import com.tiedup.remake.core.TiedUpMod; import com.tiedup.remake.items.base.IHasResistance; import com.tiedup.remake.items.base.ILockable; -import com.tiedup.remake.items.base.ItemBind; import com.tiedup.remake.state.IPlayerLeashAccess; import com.tiedup.remake.state.PlayerBindState; import com.tiedup.remake.v2.BodyRegionV2; @@ -155,10 +154,6 @@ public class StruggleBinds extends StruggleState { ); if (!collar.isEmpty() && CollarHelper.canShock(collar)) { - // V1 shock collar - if (collar.getItem() instanceof com.tiedup.remake.items.ItemShockCollar shockCollar) { - return shockCollar.notifyStruggle(player, collar); - } // V2 shock collar — notify via IHasResistance if available if (collar.getItem() instanceof IHasResistance resistance) { resistance.notifyStruggle(player); @@ -339,8 +334,6 @@ public class StruggleBinds extends StruggleState { ); if (comp != null) { baseResistance = comp.getBaseResistance(); - } else if (bindStack.getItem() instanceof ItemBind bind) { - baseResistance = SettingsAccessor.getBindResistance(bind.getItemName()); } else { baseResistance = 100; } diff --git a/src/main/java/com/tiedup/remake/state/struggle/StruggleCollar.java b/src/main/java/com/tiedup/remake/state/struggle/StruggleCollar.java index f8a6b9c..fbe2b1d 100644 --- a/src/main/java/com/tiedup/remake/state/struggle/StruggleCollar.java +++ b/src/main/java/com/tiedup/remake/state/struggle/StruggleCollar.java @@ -4,7 +4,6 @@ import com.tiedup.remake.core.SystemMessageManager.MessageCategory; import com.tiedup.remake.core.TiedUpMod; 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.state.PlayerBindState; import com.tiedup.remake.v2.BodyRegionV2; import com.tiedup.remake.v2.bondage.CollarHelper; @@ -139,10 +138,6 @@ public class StruggleCollar extends StruggleState { ItemStack collar = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK); if (!collar.isEmpty() && CollarHelper.canShock(collar)) { - // V1 shock collar - if (collar.getItem() instanceof com.tiedup.remake.items.ItemShockCollar shockCollar) { - return shockCollar.notifyStruggle(player, collar); - } // V2 shock collar — notify via IHasResistance if (collar.getItem() instanceof IHasResistance resistance) { resistance.notifyStruggle(player); diff --git a/src/main/java/com/tiedup/remake/util/BondageItemLoaderUtility.java b/src/main/java/com/tiedup/remake/util/BondageItemLoaderUtility.java index b25395b..5256363 100644 --- a/src/main/java/com/tiedup/remake/util/BondageItemLoaderUtility.java +++ b/src/main/java/com/tiedup/remake/util/BondageItemLoaderUtility.java @@ -1,11 +1,6 @@ package com.tiedup.remake.util; import com.tiedup.remake.blocks.entity.IBondageItemHolder; -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; @@ -53,31 +48,30 @@ public final class BondageItemLoaderUtility { ItemStack stack, Player player ) { - if ((stack.getItem() instanceof ItemBind || BindModeHelper.isBindItem(stack)) && holder.getBind().isEmpty()) { + if (BindModeHelper.isBindItem(stack) && holder.getBind().isEmpty()) { holder.setBind(stack.copyWithCount(1)); if (!player.isCreative()) stack.shrink(1); return true; } - if ((stack.getItem() instanceof ItemGag - || DataDrivenBondageItem.getComponent(stack, ComponentType.GAGGING, GaggingComponent.class) != null) + if (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 || isDataDrivenForRegion(stack, BodyRegionV2.EYES)) + if (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 || isDataDrivenForRegion(stack, BodyRegionV2.EARS)) + if (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 || CollarHelper.isCollar(stack)) + if (CollarHelper.isCollar(stack) && holder.getCollar().isEmpty()) { holder.setCollar(stack.copyWithCount(1)); if (!player.isCreative()) stack.shrink(1); @@ -96,14 +90,6 @@ public final class BondageItemLoaderUtility { */ public static boolean isLoadableBondageItem(ItemStack stack) { 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; diff --git a/src/main/java/com/tiedup/remake/v2/bondage/BindModeHelper.java b/src/main/java/com/tiedup/remake/v2/bondage/BindModeHelper.java index bbfe851..62f6fde 100644 --- a/src/main/java/com/tiedup/remake/v2/bondage/BindModeHelper.java +++ b/src/main/java/com/tiedup/remake/v2/bondage/BindModeHelper.java @@ -1,6 +1,5 @@ package com.tiedup.remake.v2.bondage; -import com.tiedup.remake.items.base.ItemBind; import com.tiedup.remake.v2.BodyRegionV2; import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemDefinition; import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemRegistry; @@ -42,8 +41,8 @@ public final class BindModeHelper { if (def != null) { return def.occupiedRegions().contains(BodyRegionV2.ARMS); } - // V1 fallback - return stack.getItem() instanceof ItemBind; + // No V2 definition found + return false; } /** diff --git a/src/main/java/com/tiedup/remake/v2/bondage/CollarHelper.java b/src/main/java/com/tiedup/remake/v2/bondage/CollarHelper.java index c85641d..37ba240 100644 --- a/src/main/java/com/tiedup/remake/v2/bondage/CollarHelper.java +++ b/src/main/java/com/tiedup/remake/v2/bondage/CollarHelper.java @@ -1,10 +1,9 @@ package com.tiedup.remake.v2.bondage; -import com.tiedup.remake.items.ItemChokeCollar; -import com.tiedup.remake.items.ItemGpsCollar; -import com.tiedup.remake.items.ItemShockCollar; -import com.tiedup.remake.items.ItemShockCollarAuto; -import com.tiedup.remake.items.base.ItemCollar; +import com.tiedup.remake.core.TiedUpMod; +import com.tiedup.remake.entities.EntityKidnapper; +import com.tiedup.remake.entities.ai.kidnapper.KidnapperState; +import com.tiedup.remake.state.CollarRegistry; import com.tiedup.remake.v2.bondage.component.ChokingComponent; import com.tiedup.remake.v2.bondage.component.ComponentType; import com.tiedup.remake.v2.bondage.component.GpsComponent; @@ -17,8 +16,11 @@ import java.util.UUID; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; import net.minecraft.nbt.Tag; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.phys.AABB; import org.jetbrains.annotations.Nullable; /** @@ -29,15 +31,16 @@ public final class CollarHelper { private CollarHelper() {} + // Thread-local flag to suppress collar removal alerts during programmatic unequip + private static final ThreadLocal SUPPRESS_REMOVAL_ALERT = + ThreadLocal.withInitial(() -> false); + // ===== DETECTION ===== - // True if the stack is any kind of collar (V2 ownership component or V1 ItemCollar) + // True if the stack is a collar (V2 data-driven item with OwnershipComponent) public static boolean isCollar(ItemStack stack) { if (stack.isEmpty()) return false; - if (DataDrivenBondageItem.getComponent(stack, ComponentType.OWNERSHIP, OwnershipComponent.class) != null) { - return true; - } - return stack.getItem() instanceof ItemCollar; + return DataDrivenBondageItem.getComponent(stack, ComponentType.OWNERSHIP, OwnershipComponent.class) != null; } // ===== OWNERSHIP (NBT: "owners") ===== @@ -267,13 +270,10 @@ public final class CollarHelper { // ===== SHOCK ===== - // True if the collar can shock (V2 ShockComponent or V1 ItemShockCollar) + // True if the collar can shock (V2 ShockComponent) public static boolean canShock(ItemStack stack) { if (stack.isEmpty()) return false; - if (DataDrivenBondageItem.getComponent(stack, ComponentType.SHOCK, ShockComponent.class) != null) { - return true; - } - return stack.getItem() instanceof ItemShockCollar; + return DataDrivenBondageItem.getComponent(stack, ComponentType.SHOCK, ShockComponent.class) != null; } public static boolean isPublicShock(ItemStack stack) { @@ -285,27 +285,21 @@ public final class CollarHelper { stack.getOrCreateTag().putBoolean("public_mode", publicMode); } - // V2: from ShockComponent auto interval, V1: from ItemShockCollarAuto field, else 0 + // V2: from ShockComponent auto interval, else 0 public static int getShockInterval(ItemStack stack) { ShockComponent comp = DataDrivenBondageItem.getComponent( stack, ComponentType.SHOCK, ShockComponent.class ); if (comp != null) return comp.getAutoInterval(); - if (stack.getItem() instanceof ItemShockCollarAuto auto) { - return auto.getInterval(); - } return 0; } // ===== GPS ===== - // True if the collar has GPS capabilities + // True if the collar has GPS capabilities (V2 GpsComponent) public static boolean hasGPS(ItemStack stack) { if (stack.isEmpty()) return false; - if (DataDrivenBondageItem.getComponent(stack, ComponentType.GPS, GpsComponent.class) != null) { - return true; - } - return stack.getItem() instanceof ItemGpsCollar; + return DataDrivenBondageItem.getComponent(stack, ComponentType.GPS, GpsComponent.class) != null; } public static boolean hasPublicTracking(ItemStack stack) { @@ -330,13 +324,10 @@ public final class CollarHelper { // ===== CHOKE ===== - // True if the collar is a choke collar + // True if the collar is a choke collar (V2 ChokingComponent) public static boolean isChokeCollar(ItemStack stack) { if (stack.isEmpty()) return false; - if (DataDrivenBondageItem.getComponent(stack, ComponentType.CHOKING, ChokingComponent.class) != null) { - return true; - } - return stack.getItem() instanceof ItemChokeCollar; + return DataDrivenBondageItem.getComponent(stack, ComponentType.CHOKING, ChokingComponent.class) != null; } public static boolean isChoking(ItemStack stack) { @@ -361,11 +352,69 @@ public final class CollarHelper { // Executes the action with collar removal alerts suppressed public static void runWithSuppressedAlert(Runnable action) { - ItemCollar.runWithSuppressedAlert(action); + SUPPRESS_REMOVAL_ALERT.set(true); + try { + action.run(); + } finally { + SUPPRESS_REMOVAL_ALERT.set(false); + } } // True if removal alerts are currently suppressed (ThreadLocal state) public static boolean isRemovalAlertSuppressed() { - return ItemCollar.isRemovalAlertSuppressed(); + return SUPPRESS_REMOVAL_ALERT.get(); + } + + // ===== COLLAR REMOVAL ===== + + /** + * Alert nearby kidnappers when a collar is forcibly removed. + * Handles CollarRegistry unregistration and kidnapper alerting. + * + * @param wearer The entity whose collar was removed + * @param forced Whether the removal was forced (struggle, lockpick, etc.) + */ + public static void onCollarRemoved(LivingEntity wearer, boolean forced) { + if (wearer == null || wearer.level().isClientSide()) { + return; + } + + if (!forced) { + return; // Only alert on forced removals + } + + if (!(wearer.level() instanceof ServerLevel serverLevel)) { + return; + } + + TiedUpMod.LOGGER.info( + "[CollarHelper] {} collar was forcibly removed - alerting kidnappers", + wearer.getName().getString() + ); + + // Unregister from CollarRegistry + CollarRegistry registry = CollarRegistry.get(serverLevel); + if (registry != null) { + registry.unregisterWearer(wearer.getUUID()); + } + + // Find and alert nearby kidnappers + AABB searchBox = wearer.getBoundingBox().inflate(50, 20, 50); + List kidnappers = serverLevel.getEntitiesOfClass( + EntityKidnapper.class, searchBox + ); + + for (EntityKidnapper kidnapper : kidnappers) { + if (!kidnapper.hasCaptives() && !kidnapper.isTiedUp()) { + kidnapper.setAlertTarget(wearer); + kidnapper.setCurrentState(KidnapperState.ALERT); + kidnapper.broadcastAlert(wearer); + + TiedUpMod.LOGGER.debug( + "[CollarHelper] Alerted kidnapper {} about collar removal", + kidnapper.getNpcName() + ); + } + } } } diff --git a/src/main/java/com/tiedup/remake/v2/bondage/PoseTypeHelper.java b/src/main/java/com/tiedup/remake/v2/bondage/PoseTypeHelper.java index 5804147..5bad5a8 100644 --- a/src/main/java/com/tiedup/remake/v2/bondage/PoseTypeHelper.java +++ b/src/main/java/com/tiedup/remake/v2/bondage/PoseTypeHelper.java @@ -1,6 +1,5 @@ package com.tiedup.remake.v2.bondage; -import com.tiedup.remake.items.base.ItemBind; import com.tiedup.remake.items.base.PoseType; import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemDefinition; import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemRegistry; @@ -26,10 +25,6 @@ public final class PoseTypeHelper { return PoseType.STANDARD; } } - // V1 fallback - if (stack.getItem() instanceof ItemBind bind) { - return bind.getPoseType(); - } return PoseType.STANDARD; } } diff --git a/src/main/java/com/tiedup/remake/v2/bondage/component/OwnershipComponent.java b/src/main/java/com/tiedup/remake/v2/bondage/component/OwnershipComponent.java index 07be4a8..75f3bfd 100644 --- a/src/main/java/com/tiedup/remake/v2/bondage/component/OwnershipComponent.java +++ b/src/main/java/com/tiedup/remake/v2/bondage/component/OwnershipComponent.java @@ -2,7 +2,6 @@ package com.tiedup.remake.v2.bondage.component; import com.google.gson.JsonObject; import com.tiedup.remake.core.TiedUpMod; -import com.tiedup.remake.items.base.ItemCollar; import com.tiedup.remake.state.CollarRegistry; import com.tiedup.remake.v2.bondage.CollarHelper; import java.util.HashSet; @@ -71,7 +70,7 @@ public class OwnershipComponent implements IItemComponent { // onCollarRemoved handles both the alert AND the unregister call internally, // so we do NOT call registry.unregisterWearer() separately to avoid double unregister. if (!CollarHelper.isRemovalAlertSuppressed()) { - ItemCollar.onCollarRemoved(entity, true); + CollarHelper.onCollarRemoved(entity, true); } else { // Suppressed alert path: still need to unregister, just skip the alert try { diff --git a/src/main/java/com/tiedup/remake/v2/furniture/EntityFurniture.java b/src/main/java/com/tiedup/remake/v2/furniture/EntityFurniture.java index 716daee..7de1979 100644 --- a/src/main/java/com/tiedup/remake/v2/furniture/EntityFurniture.java +++ b/src/main/java/com/tiedup/remake/v2/furniture/EntityFurniture.java @@ -2,7 +2,7 @@ package com.tiedup.remake.v2.furniture; import com.tiedup.remake.core.TiedUpMod; 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.state.PlayerBindState; import com.tiedup.remake.state.PlayerCaptorManager; @@ -713,11 +713,10 @@ public class EntityFurniture ); if ( collarStack.isEmpty() || - !(collarStack.getItem() instanceof - ItemCollar collar) + !CollarHelper.isCollar(collarStack) ) continue; if ( - !collar.isOwner(collarStack, serverPlayer) && + !CollarHelper.isOwner(collarStack, serverPlayer) && !serverPlayer.hasPermissions(2) ) continue; -- 2.49.1 From c265206128a4ae0170ea100ca059dc217d4db8e4 Mon Sep 17 00:00:00 2001 From: NotEvil Date: Wed, 15 Apr 2026 01:59:16 +0200 Subject: [PATCH 2/6] chore: remove docs from branch D tracking --- 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 | 183 ---- .../2026-04-13-d01-phase1-component-system.md | 869 ------------------ 7 files changed, 2172 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 c8a946f..0000000 --- a/docs/plans/D01-branch-E-resistance-rework.md +++ /dev/null @@ -1,183 +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()`. - -### B4. PlayerShockCollar ignore complètement les V2 collars - -`checkAutoShockCollar()` dispatche exclusivement sur `instanceof ItemShockCollarAuto` et `instanceof ItemGpsCollar`. Les V2 data-driven collars avec ShockComponent ou GpsComponent ne déclenchent jamais les auto-shocks ni l'enforcement de zones GPS. - -**Fichier :** `state/components/PlayerShockCollar.java` lignes 139-189 -**Fix :** Utiliser `CollarHelper.canShock()`, `CollarHelper.getShockInterval()`, `CollarHelper.hasGPS()` pour la détection, avec fallback V1 pour les méthodes V1-specific (`getSafeSpots()`). - -### B5. EntityKidnapperMerchant.remove() memory leak - -`remove()` appelle `tradingPlayers.clear()` mais ne nettoie PAS la `playerToMerchant` ConcurrentHashMap statique. Entrées stales accumulées sur les serveurs long-running. - -**Fichier :** `entities/EntityKidnapperMerchant.java` ligne 966-981 -**Fix :** Itérer `tradingPlayers` et appeler `playerToMerchant.remove(uuid)` avant le clear. - -### B6. Timer division potentiellement inversée (auto-shock) - -`PlayerShockCollar.java` lignes 153-155 : `collarShock.getInterval() / GameConstants.TICKS_PER_SECOND`. Si Timer attend des ticks, la division réduit l'intervalle de 20x (shock toutes les 0.25s au lieu de 5s). - -**Fichier :** `state/components/PlayerShockCollar.java` lignes 153-155 et 179-182 -**Fix :** Vérifier le contrat du constructeur `Timer`. Si il attend des ticks, supprimer la division. - -### B7. StruggleState.isItemLocked() dead code - -`StruggleState.struggle()` ne call JAMAIS `isItemLocked()`. Le penalty x10 pour les items padlockés n'est jamais appliqué. - -**Fichier :** `state/struggle/StruggleState.java` ligne 53-133 -**Fix :** Inclus dans le rework E2 (nouveau modèle resistance/lock). - ---- - -## Tâches - -### E1. Initialiser currentResistance à l'equip - -Dans `DataDrivenBondageItem.onEquipped()` et les hooks V1 `onEquipped()` : -- Lire `ResistanceComponent.getBaseResistance()` (ou `IHasResistance.getBaseResistance()` pour V1) -- Écrire immédiatement dans le NBT via `setCurrentResistance(stack, base)` -- Élimine le MAX scan comme source d'initialisation - -### E2. Refactor canStruggle() — nouveau modèle - -- `StruggleBinds.canStruggle()` : ARMS → toujours true (self) si item existe -- Nouveau `StruggleAccessory` (ou refactor de StruggleCollar) : non-ARMS → true seulement si locké ou built-in lock -- Supprimer `isItemLocked()` penalty (dead code de toute façon) - -### E3. "Aide" — remove non-locké par un autre joueur - -Modifier `AbstractV2BondageItem.interactLivingEntity()` : -- Si clic sur un joueur qui porte l'item ET item non-locké ET clicker n'a pas l'item en main → retirer l'item (aide) -- Ou via un packet dédié (clic droit main vide sur joueur attaché) - -### E4. BuiltInLockComponent ou flag `always_locked` - -Pour les items organiques qui ne peuvent pas avoir de padlock mais nécessitent un struggle. - -### E5. Cleanup ILockable — supprimer lock resistance - -Supprimer : `getLockResistance()`, `getCurrentLockResistance()`, `setCurrentLockResistance()`, `initializeLockResistance()`, `clearLockResistance()`. - -Le lockpick minigame utilise la résistance de l'item directement (ou un multiplicateur config). - -### E6. Fix bugs pré-existants (B1, B2, B3) - ---- - -## Vérification - -- [ ] V2 bind résistance 50 + V2 gag résistance 80 : chacun a sa propre résistance (pas MAX) -- [ ] Gag non-locké → retirable sans struggle -- [ ] Gag locké → struggle avec résistance du gag -- [ ] Bind ARMS non-locké → self-struggle requis, autre joueur peut aider (libre) -- [ ] Bind ARMS locké → self-struggle requis, autre joueur aussi struggle -- [ ] Slime bind (built-in lock) → struggle obligatoire, pas de padlock possible -- [ ] `currentResistance` initialisé dans NBT dès l'equip -- [ ] CollarRegistry clean après removals légitimes -- [ ] Pas de duplication d'item via tying task 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 From ad48b71182177601bbeab04690db4bcf033ecff7 Mon Sep 17 00:00:00 2001 From: NotEvil Date: Wed, 15 Apr 2026 02:10:25 +0200 Subject: [PATCH 3/6] fix(D-01/D): blindfold ID mismatch + dispenser V2 registration (review) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit KidnapperTheme: fix "mask_blindfold" → "blindfold_mask" across 8 themes. The incorrect ID produced ghost items with no definition. DispenserBehaviors: register GenericBondageDispenseBehavior.forAnyDataDriven() for the V2 DATA_DRIVEN_ITEM singleton. Dispatches by region from the stack's definition. V1 per-variant registrations were deleted but V2 replacement was missing. --- .../remake/dispenser/DispenserBehaviors.java | 15 ++++++++--- .../GenericBondageDispenseBehavior.java | 26 +++++++++++++++++++ .../remake/entities/KidnapperTheme.java | 16 ++++++------ 3 files changed, 46 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/tiedup/remake/dispenser/DispenserBehaviors.java b/src/main/java/com/tiedup/remake/dispenser/DispenserBehaviors.java index e1ce1e3..1fbb23a 100644 --- a/src/main/java/com/tiedup/remake/dispenser/DispenserBehaviors.java +++ b/src/main/java/com/tiedup/remake/dispenser/DispenserBehaviors.java @@ -30,9 +30,7 @@ public class DispenserBehaviors { registerClothesBehaviors(); registerRopeArrowBehavior(); - - // V2 data-driven bondage items register their own dispenser behaviors - // via DataDrivenBondageItem.registerDispenserBehaviors() + registerBondageItemBehavior(); TiedUpMod.LOGGER.info( "[DispenserBehaviors] Dispenser behaviors registered!" @@ -46,6 +44,17 @@ public class DispenserBehaviors { ); } + private static void registerBondageItemBehavior() { + // Single registration for the V2 data-driven item singleton. + // GenericBondageDispenseBehavior inspects the stack's definition to determine behavior. + if (com.tiedup.remake.v2.bondage.V2BondageItems.DATA_DRIVEN_ITEM != null) { + DispenserBlock.registerBehavior( + com.tiedup.remake.v2.bondage.V2BondageItems.DATA_DRIVEN_ITEM.get(), + GenericBondageDispenseBehavior.forAnyDataDriven() + ); + } + } + private static void registerRopeArrowBehavior() { DispenserBlock.registerBehavior( ModItems.ROPE_ARROW.get(), diff --git a/src/main/java/com/tiedup/remake/dispenser/GenericBondageDispenseBehavior.java b/src/main/java/com/tiedup/remake/dispenser/GenericBondageDispenseBehavior.java index ffa536f..f15143f 100644 --- a/src/main/java/com/tiedup/remake/dispenser/GenericBondageDispenseBehavior.java +++ b/src/main/java/com/tiedup/remake/dispenser/GenericBondageDispenseBehavior.java @@ -56,6 +56,32 @@ public class GenericBondageDispenseBehavior // Factory Methods + /** Universal behavior for the V2 data-driven item singleton. Dispatches by region. */ + public static GenericBondageDispenseBehavior forAnyDataDriven() { + return new GenericBondageDispenseBehavior( + stack -> DataDrivenItemRegistry.get(stack) != null, + state -> true, // let equip() handle the check + (state, stack) -> { + DataDrivenItemDefinition def = DataDrivenItemRegistry.get(stack); + if (def == null) return; + java.util.Set regions = def.occupiedRegions(); + if (regions.contains(BodyRegionV2.ARMS) && !state.isTiedUp()) { + state.equip(BodyRegionV2.ARMS, stack); + } else if (regions.contains(BodyRegionV2.MOUTH) && !state.isGagged()) { + state.equip(BodyRegionV2.MOUTH, stack); + } else if (regions.contains(BodyRegionV2.EYES) && !state.isBlindfolded()) { + state.equip(BodyRegionV2.EYES, stack); + } else if (regions.contains(BodyRegionV2.NECK) && !state.hasCollar()) { + state.equip(BodyRegionV2.NECK, stack); + } else if (regions.contains(BodyRegionV2.EARS) && !state.hasEarplugs()) { + state.equip(BodyRegionV2.EARS, stack); + } else if (regions.contains(BodyRegionV2.HANDS) && !state.hasMittens()) { + state.equip(BodyRegionV2.HANDS, stack); + } + } + ); + } + public static GenericBondageDispenseBehavior forBind() { return new GenericBondageDispenseBehavior( BindModeHelper::isBindItem, diff --git a/src/main/java/com/tiedup/remake/entities/KidnapperTheme.java b/src/main/java/com/tiedup/remake/entities/KidnapperTheme.java index a7b72ab..4dcadee 100644 --- a/src/main/java/com/tiedup/remake/entities/KidnapperTheme.java +++ b/src/main/java/com/tiedup/remake/entities/KidnapperTheme.java @@ -37,7 +37,7 @@ public enum KidnapperTheme { TAPE( "duct_tape", new String[] { "tape_gag", "wrap_gag" }, - new String[] { "mask_blindfold" }, + new String[] { "blindfold_mask" }, true, 20 ), @@ -47,7 +47,7 @@ public enum KidnapperTheme { LEATHER( "leather_straps", new String[] { "ball_gag", "ball_gag_strap", "panel_gag" }, - new String[] { "mask_blindfold" }, + new String[] { "blindfold_mask" }, false, 15 ), @@ -57,7 +57,7 @@ public enum KidnapperTheme { CHAIN( "chain", new String[] { "chain_panel_gag", "ball_gag_strap" }, - new String[] { "mask_blindfold" }, + new String[] { "blindfold_mask" }, false, 10 ), @@ -67,7 +67,7 @@ public enum KidnapperTheme { MEDICAL( "medical_straps", new String[] { "tube_gag", "sponge_gag", "ball_gag" }, - new String[] { "mask_blindfold" }, + new String[] { "blindfold_mask" }, false, 8 ), @@ -77,7 +77,7 @@ public enum KidnapperTheme { BEAM( "beam_cuffs", new String[] { "beam_panel_gag", "latex_gag" }, - new String[] { "mask_blindfold" }, + new String[] { "blindfold_mask" }, false, 5 ), @@ -87,7 +87,7 @@ public enum KidnapperTheme { LATEX( "latex_sack", new String[] { "latex_gag", "tube_gag" }, - new String[] { "mask_blindfold" }, + new String[] { "blindfold_mask" }, false, 3 ), @@ -97,7 +97,7 @@ public enum KidnapperTheme { ASYLUM( "straitjacket", new String[] { "bite_gag", "sponge_gag", "ball_gag" }, - new String[] { "mask_blindfold" }, + new String[] { "blindfold_mask" }, false, 5 ), @@ -117,7 +117,7 @@ public enum KidnapperTheme { WRAP( "wrap", new String[] { "wrap_gag", "tape_gag" }, - new String[] { "mask_blindfold" }, + new String[] { "blindfold_mask" }, false, 5 ); -- 2.49.1 From 75cf1358f943f5e2d2e13a780d0194ba8fcb9281 Mon Sep 17 00:00:00 2001 From: NotEvil Date: Wed, 15 Apr 2026 02:12:05 +0200 Subject: [PATCH 4/6] chore(D-01/D): remove dead V1 handleSelfAccessory method --- .../selfbondage/PacketSelfBondage.java | 71 ------------------- 1 file changed, 71 deletions(-) diff --git a/src/main/java/com/tiedup/remake/network/selfbondage/PacketSelfBondage.java b/src/main/java/com/tiedup/remake/network/selfbondage/PacketSelfBondage.java index 13024ec..746092c 100644 --- a/src/main/java/com/tiedup/remake/network/selfbondage/PacketSelfBondage.java +++ b/src/main/java/com/tiedup/remake/network/selfbondage/PacketSelfBondage.java @@ -208,75 +208,4 @@ public class PacketSelfBondage { } } - /** - * Handle self-equipping an accessory (gag, blindfold, mittens, earplugs). - * Can be used anytime (no need to be tied). - * Blocked only if arms are fully bound. - * Supports swapping: if same type already equipped (and not locked), swap them. - */ - private static void handleSelfAccessory( - ServerPlayer player, - ItemStack stack, - IBondageState state, - String itemType, - java.util.function.Predicate isEquipped, - java.util.function.Function getCurrent, - java.util.function.Function takeOff, - java.util.function.BiConsumer putOn - ) { - // Can't equip if arms are fully bound (need hands to put on accessories) - ItemStack currentBind = state.getEquipment(BodyRegionV2.ARMS); - if (!currentBind.isEmpty()) { - if (BindModeHelper.hasArmsBound(currentBind)) { - TiedUpMod.LOGGER.debug( - "[SelfBondage] {} can't self-{} - arms are bound", - player.getName().getString(), - itemType - ); - return; - } - } - - // Already equipped? Try to swap - if (isEquipped.test(state)) { - ItemStack currentItem = getCurrent.apply(state); - - // Check if current item is locked - if ( - currentItem.getItem() instanceof ILockable lockable && - lockable.isLocked(currentItem) - ) { - TiedUpMod.LOGGER.debug( - "[SelfBondage] {} can't swap {} - current is locked", - player.getName().getString(), - itemType - ); - return; - } - - // Remove current and drop it - ItemStack removed = takeOff.apply(state); - if (!removed.isEmpty()) { - state.kidnappedDropItem(removed); - TiedUpMod.LOGGER.debug( - "[SelfBondage] {} swapping {} - dropped old one", - player.getName().getString(), - itemType - ); - } - } - - // Equip new item on self - putOn.accept(state, stack.copy()); - stack.shrink(1); - - // Sync to client - SyncManager.syncInventory(player); - - TiedUpMod.LOGGER.info( - "[SelfBondage] {} self-equipped {}", - player.getName().getString(), - itemType - ); - } } -- 2.49.1 From ab1b3f66bbefd6d71655eb10c60020100421b713 Mon Sep 17 00:00:00 2001 From: NotEvil Date: Wed, 15 Apr 2026 02:52:27 +0200 Subject: [PATCH 5/6] =?UTF-8?q?fix(D-01/D):=20checkup=20cleanup=20?= =?UTF-8?q?=E2=80=94=205=20issues=20resolved?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. LockableComponent: remove duplicate "Lockable" tooltip line (ILockable.appendLockTooltip already handles lock status display) 2. ILockable/IHasResistance Javadoc: update @link refs from deleted V1 classes to V2 AbstractV2BondageItem/DataDrivenBondageItem 3. SettingsAccessor Javadoc: remove stale BindVariant @link references 4. DataDrivenBondageItem: update NECK block comment (remove branch ref) 5. Delete empty bondage3d/gags/ directory --- 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 | 183 ++++ .../2026-04-13-d01-phase1-component-system.md | 869 ++++++++++++++++++ .../tiedup/remake/core/SettingsAccessor.java | 5 +- .../remake/items/base/IHasResistance.java | 6 +- .../tiedup/remake/items/base/ILockable.java | 7 +- .../bondage/component/LockableComponent.java | 3 +- .../datadriven/DataDrivenBondageItem.java | 8 +- 12 files changed, 2184 insertions(+), 17 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..c8a946f --- /dev/null +++ b/docs/plans/D01-branch-E-resistance-rework.md @@ -0,0 +1,183 @@ +# D-01 Branch E : Resistance & Lock System Rework + +> **Prérequis :** Branch D (V1 cleanup) mergée. +> **Branche :** `feature/d01-branch-e-resistance` +> **Objectif :** Redesign complet du système de résistance/lock. + +--- + +## Nouveau modèle + +### Principes + +1. **La résistance vient de l'item** — définie dans le JSON via `ResistanceComponent`, point final. +2. **Le lock est binaire** — on/off. Pas de "lock resistance" séparée. Le lock *active* la nécessité de struggle pour les items non-ARMS. +3. **ARMS = toujours actif** — un bind aux bras nécessite toujours un struggle pour s'en libérer soi-même, locké ou non. +4. **Non-ARMS + pas locké = libre** — un gag/blindfold/collar non-locké peut être retiré librement (par soi-même ou un autre joueur). +5. **Non-ARMS + locké = struggle requis** — le lock active la résistance de l'item. +6. **Un autre joueur peut aider** — retirer un item non-locké sur un autre joueur ne nécessite pas de struggle (aide). + +### Matrice de struggle + +| Région | Locké ? | Self-remove | Autre joueur remove | +|--------|---------|-------------|---------------------| +| ARMS | Non | Struggle (résistance item) | Libre (aide) | +| ARMS | Oui | Struggle (résistance item) | Struggle (résistance item) | +| Non-ARMS | Non | Libre | Libre | +| Non-ARMS | Oui | Struggle (résistance item) | Struggle (résistance item) | + +### Items organiques (slime, vine, web, tape) + +Ces items sont "lockés par nature" — pas de padlock possible mais impossible à retirer sans struggle. + +**Option :** Nouveau composant `BuiltInLockComponent` dans le JSON : +```json +"components": { + "resistance": {"id": "slime"}, + "built_in_lock": {} +} +``` + +`BuiltInLockComponent` : +- `blocksUnequip()` retourne `true` (comme un lock, mais sans padlock) +- `ILockable.canAttachPadlock()` retourne `false` (déjà le cas pour les organiques) +- L'item se comporte comme un ARMS bind : toujours struggle required + +**Alternative :** Flag `"always_locked": true` sur la definition JSON. Plus simple, pas besoin de nouveau composant. + +--- + +## Problèmes actuels que ce rework corrige + +### P1. Singleton MAX scan + +`DataDrivenBondageItem.getBaseResistance(LivingEntity)` retourne le MAX de tous les items data-driven équipés. Un gag résistance 50 hérite de la résistance 200 du chain bind. + +**Fix :** Initialiser `currentResistance` dans le NBT à l'equip depuis `ResistanceComponent.getBaseResistance()`. Plus jamais de fallback au MAX scan runtime. Le `getBaseResistance(LivingEntity)` du singleton devient un no-op/fallback qui n'est plus utilisé par le struggle. + +### P2. isItemLocked() dead code + +`StruggleState.struggle()` ne call jamais `isItemLocked()`. Le x10 penalty n'est jamais appliqué. + +**Fix :** Supprimer le concept de "locked penalty". Avec le nouveau modèle, le lock **active** le struggle, il ne le ralentit pas. Si l'item est locké, il faut struggle avec la résistance complète de l'item. Si non locké (et non-ARMS), pas de struggle du tout. + +### P3. Lock resistance / item resistance déconnectés + +`ILockable.getLockResistance()` vs `IHasResistance.getBaseResistance()` sont deux systèmes indépendants. + +**Fix :** Supprimer `ILockable.getLockResistance()` / `getCurrentLockResistance()` / `setCurrentLockResistance()` / `initializeLockResistance()` / `clearLockResistance()`. La résistance du lockpick minigame utilise directement la résistance de l'item (ou un multiplicateur fixe). + +### P4. Dice-roll ignore le lock + +**Fix :** Avec le nouveau modèle, le dice-roll ne change pas. C'est `canStruggle()` qui gate l'accès : +```java +// StruggleBinds.canStruggle() +// ARMS: toujours struggle-able (self) +return true; + +// StruggleCollar/StruggleAccessory.canStruggle() +// Non-ARMS: seulement si locké +return isLocked(stack) || hasBuiltInLock(stack); +``` + +--- + +## Bugs pré-existants à corriger dans cette branche + +### B1. V1 ItemCollar.onUnequipped() — suppressed path skip unregister + +Quand `isRemovalAlertSuppressed()` est true, `ItemCollar.onUnequipped()` return early SANS appeler `CollarRegistry.unregisterWearer()`. Entrées fantômes persistées. + +**Fichier :** `items/base/ItemCollar.java` lignes 1382-1395 +**Fix :** Ajouter `unregisterWearer()` dans le branch suppressed. + +### B2. DataDrivenItemRegistry.clear() pas synchronisé + +`clear()` écrit `SNAPSHOT = EMPTY` sans acquérir `RELOAD_LOCK`. Race avec `mergeAll()`. + +**Fichier :** `v2/bondage/datadriven/DataDrivenItemRegistry.java` ligne 142 +**Fix :** Synchroniser sur `RELOAD_LOCK`. + +### B3. V2TyingPlayerTask.heldStack reference stale + +Le held item peut être remplacé entre début et fin du tying → item dupliqué. + +**Fichier :** `tasks/V2TyingPlayerTask.java` ligne 80 +**Fix :** Valider `heldStack` non-vide et matching avant equip dans `onComplete()`. + +### B4. PlayerShockCollar ignore complètement les V2 collars + +`checkAutoShockCollar()` dispatche exclusivement sur `instanceof ItemShockCollarAuto` et `instanceof ItemGpsCollar`. Les V2 data-driven collars avec ShockComponent ou GpsComponent ne déclenchent jamais les auto-shocks ni l'enforcement de zones GPS. + +**Fichier :** `state/components/PlayerShockCollar.java` lignes 139-189 +**Fix :** Utiliser `CollarHelper.canShock()`, `CollarHelper.getShockInterval()`, `CollarHelper.hasGPS()` pour la détection, avec fallback V1 pour les méthodes V1-specific (`getSafeSpots()`). + +### B5. EntityKidnapperMerchant.remove() memory leak + +`remove()` appelle `tradingPlayers.clear()` mais ne nettoie PAS la `playerToMerchant` ConcurrentHashMap statique. Entrées stales accumulées sur les serveurs long-running. + +**Fichier :** `entities/EntityKidnapperMerchant.java` ligne 966-981 +**Fix :** Itérer `tradingPlayers` et appeler `playerToMerchant.remove(uuid)` avant le clear. + +### B6. Timer division potentiellement inversée (auto-shock) + +`PlayerShockCollar.java` lignes 153-155 : `collarShock.getInterval() / GameConstants.TICKS_PER_SECOND`. Si Timer attend des ticks, la division réduit l'intervalle de 20x (shock toutes les 0.25s au lieu de 5s). + +**Fichier :** `state/components/PlayerShockCollar.java` lignes 153-155 et 179-182 +**Fix :** Vérifier le contrat du constructeur `Timer`. Si il attend des ticks, supprimer la division. + +### B7. StruggleState.isItemLocked() dead code + +`StruggleState.struggle()` ne call JAMAIS `isItemLocked()`. Le penalty x10 pour les items padlockés n'est jamais appliqué. + +**Fichier :** `state/struggle/StruggleState.java` ligne 53-133 +**Fix :** Inclus dans le rework E2 (nouveau modèle resistance/lock). + +--- + +## Tâches + +### E1. Initialiser currentResistance à l'equip + +Dans `DataDrivenBondageItem.onEquipped()` et les hooks V1 `onEquipped()` : +- Lire `ResistanceComponent.getBaseResistance()` (ou `IHasResistance.getBaseResistance()` pour V1) +- Écrire immédiatement dans le NBT via `setCurrentResistance(stack, base)` +- Élimine le MAX scan comme source d'initialisation + +### E2. Refactor canStruggle() — nouveau modèle + +- `StruggleBinds.canStruggle()` : ARMS → toujours true (self) si item existe +- Nouveau `StruggleAccessory` (ou refactor de StruggleCollar) : non-ARMS → true seulement si locké ou built-in lock +- Supprimer `isItemLocked()` penalty (dead code de toute façon) + +### E3. "Aide" — remove non-locké par un autre joueur + +Modifier `AbstractV2BondageItem.interactLivingEntity()` : +- Si clic sur un joueur qui porte l'item ET item non-locké ET clicker n'a pas l'item en main → retirer l'item (aide) +- Ou via un packet dédié (clic droit main vide sur joueur attaché) + +### E4. BuiltInLockComponent ou flag `always_locked` + +Pour les items organiques qui ne peuvent pas avoir de padlock mais nécessitent un struggle. + +### E5. Cleanup ILockable — supprimer lock resistance + +Supprimer : `getLockResistance()`, `getCurrentLockResistance()`, `setCurrentLockResistance()`, `initializeLockResistance()`, `clearLockResistance()`. + +Le lockpick minigame utilise la résistance de l'item directement (ou un multiplicateur config). + +### E6. Fix bugs pré-existants (B1, B2, B3) + +--- + +## Vérification + +- [ ] V2 bind résistance 50 + V2 gag résistance 80 : chacun a sa propre résistance (pas MAX) +- [ ] Gag non-locké → retirable sans struggle +- [ ] Gag locké → struggle avec résistance du gag +- [ ] Bind ARMS non-locké → self-struggle requis, autre joueur peut aider (libre) +- [ ] Bind ARMS locké → self-struggle requis, autre joueur aussi struggle +- [ ] Slime bind (built-in lock) → struggle obligatoire, pas de padlock possible +- [ ] `currentResistance` initialisé dans NBT dès l'equip +- [ ] CollarRegistry clean après removals légitimes +- [ ] Pas de duplication d'item via tying task 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/core/SettingsAccessor.java b/src/main/java/com/tiedup/remake/core/SettingsAccessor.java index 6ad2bd4..aa377c3 100644 --- a/src/main/java/com/tiedup/remake/core/SettingsAccessor.java +++ b/src/main/java/com/tiedup/remake/core/SettingsAccessor.java @@ -160,7 +160,7 @@ public class SettingsAccessor { *

BUG-003 fix: Previously, {@code IHasResistance.getBaseResistance()} * called {@code ModGameRules.getResistance()} which only knew 4 types (rope, gag, * blindfold, collar) and returned hardcoded 100 for the other 10 types. Meanwhile - * {@code BindVariant.getResistance()} read from ModConfig which had all 14 types. + * the old BindVariant.getResistance() read from ModConfig which had all 14 types. * This caused a display-vs-struggle desync (display: 250, struggle: 100). * Now both paths use this method. * @@ -207,8 +207,7 @@ public class SettingsAccessor { /** * Normalize a raw bind item name to its config key. * - *

Replicates the mapping from - * {@link com.tiedup.remake.items.base.BindVariant#getResistance()} so that + *

Normalizes raw item names to config keys so that * every call site resolves to the same config entry. * * @param bindType Raw item name (e.g., "ropes", "vine_seed", "collar") diff --git a/src/main/java/com/tiedup/remake/items/base/IHasResistance.java b/src/main/java/com/tiedup/remake/items/base/IHasResistance.java index 7f26455..26689ad 100644 --- a/src/main/java/com/tiedup/remake/items/base/IHasResistance.java +++ b/src/main/java/com/tiedup/remake/items/base/IHasResistance.java @@ -22,10 +22,8 @@ import net.minecraft.world.item.ItemStack; * *

Implementations

*
    - *
  • {@link ItemBind} - Ropes, chains, straitjackets
  • - *
  • {@link ItemGag} - Ball gags, tape gags, cloth gags
  • - *
  • {@link ItemBlindfold} - Blindfolds
  • - *
  • {@link ItemCollar} - Collars (special: may not be struggleable)
  • + *
  • {@link com.tiedup.remake.v2.bondage.items.AbstractV2BondageItem} - All V2 bondage items
  • + *
  • {@link com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem} - Data-driven items (resistance via ResistanceComponent)
  • *
* *

Based on original IHasResistance.java from 1.12.2 diff --git a/src/main/java/com/tiedup/remake/items/base/ILockable.java b/src/main/java/com/tiedup/remake/items/base/ILockable.java index f1b84e5..3ded9e7 100644 --- a/src/main/java/com/tiedup/remake/items/base/ILockable.java +++ b/src/main/java/com/tiedup/remake/items/base/ILockable.java @@ -24,11 +24,8 @@ import org.jetbrains.annotations.Nullable; * *

Implementations

*
    - *
  • {@link ItemGag} - Gags can be locked
  • - *
  • {@link ItemBlindfold} - Blindfolds can be locked
  • - *
  • {@link ItemCollar} - Collars can be locked
  • - *
  • {@link ItemEarplugs} - Earplugs can be locked
  • - *
  • ItemBind - Binds can be locked (future)
  • + *
  • {@link com.tiedup.remake.v2.bondage.items.AbstractV2BondageItem} - All V2 bondage items
  • + *
  • {@link com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem} - Data-driven items (via AbstractV2BondageItem)
  • *
* *

NBT Storage

diff --git a/src/main/java/com/tiedup/remake/v2/bondage/component/LockableComponent.java b/src/main/java/com/tiedup/remake/v2/bondage/component/LockableComponent.java index ef8ddcd..257d2a4 100644 --- a/src/main/java/com/tiedup/remake/v2/bondage/component/LockableComponent.java +++ b/src/main/java/com/tiedup/remake/v2/bondage/component/LockableComponent.java @@ -51,7 +51,8 @@ public class LockableComponent implements IItemComponent { @Override public void appendTooltip(ItemStack stack, @Nullable Level level, List tooltip, TooltipFlag flag) { - tooltip.add(Component.translatable("item.tiedup.tooltip.lockable").withStyle(ChatFormatting.GOLD)); + // Lock status ("Locked"/"Lockable") is handled by ILockable.appendLockTooltip() + // called from AbstractV2BondageItem.appendHoverText() — no duplicate here. if (flag.isAdvanced()) { tooltip.add(Component.translatable("item.tiedup.tooltip.lock_resistance", lockResistance) .withStyle(ChatFormatting.DARK_GRAY)); diff --git a/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenBondageItem.java b/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenBondageItem.java index f73ecf4..ef34529 100644 --- a/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenBondageItem.java +++ b/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenBondageItem.java @@ -167,11 +167,11 @@ public class DataDrivenBondageItem extends AbstractV2BondageItem { return TyingInteractionHelper.handleTying(serverPlayer, target, stack, hand); } - // NECK: blocked until Branch C wires the full collar equip flow - // (add owner to NBT, register in CollarRegistry, play sound, sync). - // Without this, V2 collars equip without ownership — breaking GPS, shock, alerts. + // NECK: blocked — collar equip requires owner setup (add owner to NBT, + // register in CollarRegistry, play sound, sync) which is not yet wired + // through interactLivingEntity. TODO: implement collar equip flow. if (regions.contains(BodyRegionV2.NECK)) { - TiedUpMod.LOGGER.debug("[DataDrivenBondageItem] NECK equip blocked — collar flow not wired yet"); + TiedUpMod.LOGGER.debug("[DataDrivenBondageItem] NECK equip via right-click not yet implemented"); return InteractionResult.PASS; } -- 2.49.1 From 19b4c4a3d0f3d8076bd4cd473f556b1b2cb2a92b Mon Sep 17 00:00:00 2001 From: NotEvil Date: Wed, 15 Apr 2026 02:52:37 +0200 Subject: [PATCH 6/6] chore: gitignore docs/ to stop them from being tracked --- .gitignore | 1 + 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 | 183 ---- .../2026-04-13-d01-phase1-component-system.md | 869 ------------------ 8 files changed, 1 insertion(+), 2172 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/.gitignore b/.gitignore index f3b98f0..86a5f23 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,4 @@ build_output.log .DS_Store Thumbs.db desktop.ini +docs/ 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 c8a946f..0000000 --- a/docs/plans/D01-branch-E-resistance-rework.md +++ /dev/null @@ -1,183 +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()`. - -### B4. PlayerShockCollar ignore complètement les V2 collars - -`checkAutoShockCollar()` dispatche exclusivement sur `instanceof ItemShockCollarAuto` et `instanceof ItemGpsCollar`. Les V2 data-driven collars avec ShockComponent ou GpsComponent ne déclenchent jamais les auto-shocks ni l'enforcement de zones GPS. - -**Fichier :** `state/components/PlayerShockCollar.java` lignes 139-189 -**Fix :** Utiliser `CollarHelper.canShock()`, `CollarHelper.getShockInterval()`, `CollarHelper.hasGPS()` pour la détection, avec fallback V1 pour les méthodes V1-specific (`getSafeSpots()`). - -### B5. EntityKidnapperMerchant.remove() memory leak - -`remove()` appelle `tradingPlayers.clear()` mais ne nettoie PAS la `playerToMerchant` ConcurrentHashMap statique. Entrées stales accumulées sur les serveurs long-running. - -**Fichier :** `entities/EntityKidnapperMerchant.java` ligne 966-981 -**Fix :** Itérer `tradingPlayers` et appeler `playerToMerchant.remove(uuid)` avant le clear. - -### B6. Timer division potentiellement inversée (auto-shock) - -`PlayerShockCollar.java` lignes 153-155 : `collarShock.getInterval() / GameConstants.TICKS_PER_SECOND`. Si Timer attend des ticks, la division réduit l'intervalle de 20x (shock toutes les 0.25s au lieu de 5s). - -**Fichier :** `state/components/PlayerShockCollar.java` lignes 153-155 et 179-182 -**Fix :** Vérifier le contrat du constructeur `Timer`. Si il attend des ticks, supprimer la division. - -### B7. StruggleState.isItemLocked() dead code - -`StruggleState.struggle()` ne call JAMAIS `isItemLocked()`. Le penalty x10 pour les items padlockés n'est jamais appliqué. - -**Fichier :** `state/struggle/StruggleState.java` ligne 53-133 -**Fix :** Inclus dans le rework E2 (nouveau modèle resistance/lock). - ---- - -## Tâches - -### E1. Initialiser currentResistance à l'equip - -Dans `DataDrivenBondageItem.onEquipped()` et les hooks V1 `onEquipped()` : -- Lire `ResistanceComponent.getBaseResistance()` (ou `IHasResistance.getBaseResistance()` pour V1) -- Écrire immédiatement dans le NBT via `setCurrentResistance(stack, base)` -- Élimine le MAX scan comme source d'initialisation - -### E2. Refactor canStruggle() — nouveau modèle - -- `StruggleBinds.canStruggle()` : ARMS → toujours true (self) si item existe -- Nouveau `StruggleAccessory` (ou refactor de StruggleCollar) : non-ARMS → true seulement si locké ou built-in lock -- Supprimer `isItemLocked()` penalty (dead code de toute façon) - -### E3. "Aide" — remove non-locké par un autre joueur - -Modifier `AbstractV2BondageItem.interactLivingEntity()` : -- Si clic sur un joueur qui porte l'item ET item non-locké ET clicker n'a pas l'item en main → retirer l'item (aide) -- Ou via un packet dédié (clic droit main vide sur joueur attaché) - -### E4. BuiltInLockComponent ou flag `always_locked` - -Pour les items organiques qui ne peuvent pas avoir de padlock mais nécessitent un struggle. - -### E5. Cleanup ILockable — supprimer lock resistance - -Supprimer : `getLockResistance()`, `getCurrentLockResistance()`, `setCurrentLockResistance()`, `initializeLockResistance()`, `clearLockResistance()`. - -Le lockpick minigame utilise la résistance de l'item directement (ou un multiplicateur config). - -### E6. Fix bugs pré-existants (B1, B2, B3) - ---- - -## Vérification - -- [ ] V2 bind résistance 50 + V2 gag résistance 80 : chacun a sa propre résistance (pas MAX) -- [ ] Gag non-locké → retirable sans struggle -- [ ] Gag locké → struggle avec résistance du gag -- [ ] Bind ARMS non-locké → self-struggle requis, autre joueur peut aider (libre) -- [ ] Bind ARMS locké → self-struggle requis, autre joueur aussi struggle -- [ ] Slime bind (built-in lock) → struggle obligatoire, pas de padlock possible -- [ ] `currentResistance` initialisé dans NBT dès l'equip -- [ ] CollarRegistry clean après removals légitimes -- [ ] Pas de duplication d'item via tying task 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