feat(D-01/D): V1 cleanup — delete 28 files, ~5400 lines removed

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.
This commit is contained in:
NotEvil
2026-04-15 01:55:16 +02:00
parent f085fbfec2
commit 2504b7d657
89 changed files with 2647 additions and 5423 deletions

413
docs/AUDIT.md Normal file
View File

@@ -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. |

View File

@@ -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<Component> tooltip, TooltipFlag flag) {}
// ComponentHolder
public void appendTooltip(ItemStack stack, @Nullable Level level, List<Component> 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<UUID>
- `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<BodyRegionV2> 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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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<ComponentType, JsonObject> 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.
*
* <p>Lifecycle: parse config once (from JSON), then tick/query per equipped entity.</p>
*/
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<JsonObject, IItemComponent> factory;
ComponentType(String jsonKey, Function<JsonObject, IItemComponent> 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<ComponentType, IItemComponent> components;
public ComponentHolder(Map<ComponentType, IItemComponent> 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 extends IItemComponent> T get(ComponentType type, Class<T> 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<ComponentType, JsonObject> 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<ComponentType, JsonObject> componentConfigs = new EnumMap<>(ComponentType.class);
if (json.has("components")) {
JsonObject componentsObj = json.getAsJsonObject("components");
for (Map.Entry<String, com.google.gson.JsonElement> 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<ResourceLocation, ComponentHolder> COMPONENT_HOLDERS = Map.of();
```
In the reload/register method, after storing definitions, build component holders:
```java
Map<ResourceLocation, ComponentHolder> holders = new HashMap<>();
for (Map.Entry<ResourceLocation, DataDrivenItemDefinition> entry : newDefinitions.entrySet()) {
DataDrivenItemDefinition def = entry.getValue();
Map<ComponentType, IItemComponent> components = new EnumMap<>(ComponentType.class);
for (Map.Entry<ComponentType, JsonObject> 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 extends IItemComponent> T getComponent(ItemStack stack, ComponentType type, Class<T> 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:
* <pre>{"lockable": true}</pre>
* or
* <pre>{"lockable": {"lock_resistance": 300}}</pre>
*/
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:
* <pre>{"resistance": {"base": 150}}</pre>
*/
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<BodyRegionV2, ItemStack> 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:
* <pre>{"gagging": {"comprehension": 0.2, "range": 10.0}}</pre>
*/
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."
```

View File

@@ -1,10 +1,5 @@
package com.tiedup.remake.blocks.entity; 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.items.clothes.GenericClothes;
import com.tiedup.remake.v2.BodyRegionV2; import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.BindModeHelper; import com.tiedup.remake.v2.bondage.BindModeHelper;
@@ -199,45 +194,43 @@ public abstract class BondageItemBlockEntity
@Override @Override
public void readBondageData(CompoundTag tag) { 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")) { if (tag.contains("bind")) {
ItemStack bindStack = ItemStack.of(tag.getCompound("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; 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")) { if (tag.contains("gag")) {
ItemStack gagStack = ItemStack.of(tag.getCompound("gag")); ItemStack gagStack = ItemStack.of(tag.getCompound("gag"));
if (!gagStack.isEmpty() && (gagStack.getItem() instanceof ItemGag if (!gagStack.isEmpty()
|| DataDrivenBondageItem.getComponent(gagStack, ComponentType.GAGGING, GaggingComponent.class) != null)) { && DataDrivenBondageItem.getComponent(gagStack, ComponentType.GAGGING, GaggingComponent.class) != null) {
this.gag = gagStack; 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")) { if (tag.contains("blindfold")) {
ItemStack blindfoldStack = ItemStack.of(tag.getCompound("blindfold")); ItemStack blindfoldStack = ItemStack.of(tag.getCompound("blindfold"));
if (!blindfoldStack.isEmpty() && (blindfoldStack.getItem() instanceof ItemBlindfold if (!blindfoldStack.isEmpty() && isDataDrivenForRegion(blindfoldStack, BodyRegionV2.EYES)) {
|| isDataDrivenForRegion(blindfoldStack, BodyRegionV2.EYES))) {
this.blindfold = blindfoldStack; 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")) { if (tag.contains("earplugs")) {
ItemStack earplugsStack = ItemStack.of(tag.getCompound("earplugs")); ItemStack earplugsStack = ItemStack.of(tag.getCompound("earplugs"));
if (!earplugsStack.isEmpty() && (earplugsStack.getItem() instanceof ItemEarplugs if (!earplugsStack.isEmpty() && isDataDrivenForRegion(earplugsStack, BodyRegionV2.EARS)) {
|| isDataDrivenForRegion(earplugsStack, BodyRegionV2.EARS))) {
this.earplugs = earplugsStack; this.earplugs = earplugsStack;
} }
} }
// Read collar with type validation (V1 ItemCollar or V2 collar) // Read collar with type validation (V2 collar)
if (tag.contains("collar")) { if (tag.contains("collar")) {
ItemStack collarStack = ItemStack.of(tag.getCompound("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; this.collar = collarStack;
} }
} }

View File

@@ -1,10 +1,5 @@
package com.tiedup.remake.blocks.entity; 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.items.clothes.GenericClothes;
import com.tiedup.remake.v2.BodyRegionV2; import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.BindModeHelper; import com.tiedup.remake.v2.bondage.BindModeHelper;
@@ -57,7 +52,7 @@ public class TrappedChestBlockEntity
@Override @Override
public void setBind(ItemStack stack) { public void setBind(ItemStack stack) {
if (stack.isEmpty() || stack.getItem() instanceof ItemBind || BindModeHelper.isBindItem(stack)) { if (stack.isEmpty() || BindModeHelper.isBindItem(stack)) {
this.bind = stack; this.bind = stack;
setChangedAndSync(); setChangedAndSync();
} }
@@ -70,7 +65,7 @@ public class TrappedChestBlockEntity
@Override @Override
public void setGag(ItemStack stack) { public void setGag(ItemStack stack) {
if (stack.isEmpty() || stack.getItem() instanceof ItemGag if (stack.isEmpty()
|| DataDrivenBondageItem.getComponent(stack, ComponentType.GAGGING, GaggingComponent.class) != null) { || DataDrivenBondageItem.getComponent(stack, ComponentType.GAGGING, GaggingComponent.class) != null) {
this.gag = stack; this.gag = stack;
setChangedAndSync(); setChangedAndSync();
@@ -84,8 +79,7 @@ public class TrappedChestBlockEntity
@Override @Override
public void setBlindfold(ItemStack stack) { public void setBlindfold(ItemStack stack) {
if (stack.isEmpty() || stack.getItem() instanceof ItemBlindfold if (stack.isEmpty() || isDataDrivenForRegion(stack, BodyRegionV2.EYES)) {
|| isDataDrivenForRegion(stack, BodyRegionV2.EYES)) {
this.blindfold = stack; this.blindfold = stack;
setChangedAndSync(); setChangedAndSync();
} }
@@ -98,8 +92,7 @@ public class TrappedChestBlockEntity
@Override @Override
public void setEarplugs(ItemStack stack) { public void setEarplugs(ItemStack stack) {
if (stack.isEmpty() || stack.getItem() instanceof ItemEarplugs if (stack.isEmpty() || isDataDrivenForRegion(stack, BodyRegionV2.EARS)) {
|| isDataDrivenForRegion(stack, BodyRegionV2.EARS)) {
this.earplugs = stack; this.earplugs = stack;
setChangedAndSync(); setChangedAndSync();
} }
@@ -112,7 +105,7 @@ public class TrappedChestBlockEntity
@Override @Override
public void setCollar(ItemStack stack) { public void setCollar(ItemStack stack) {
if (stack.isEmpty() || stack.getItem() instanceof ItemCollar || CollarHelper.isCollar(stack)) { if (stack.isEmpty() || CollarHelper.isCollar(stack)) {
this.collar = stack; this.collar = stack;
setChangedAndSync(); setChangedAndSync();
} }

View File

@@ -1,6 +1,5 @@
package com.tiedup.remake.client.events; package com.tiedup.remake.client.events;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.network.ModNetwork; import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.network.selfbondage.PacketSelfBondage; import com.tiedup.remake.network.selfbondage.PacketSelfBondage;
import com.tiedup.remake.v2.bondage.BindModeHelper; import com.tiedup.remake.v2.bondage.BindModeHelper;
@@ -166,8 +165,8 @@ public class SelfBondageInputHandler {
private static boolean isSelfBondageItem(ItemStack stack) { private static boolean isSelfBondageItem(ItemStack stack) {
if (stack.isEmpty()) return false; if (stack.isEmpty()) return false;
// Collar cannot be self-equipped (V1 collar guard + V2 ownership component) // Collar cannot be self-equipped (V2 ownership component)
if (stack.getItem() instanceof ItemCollar || CollarHelper.isCollar(stack)) { if (CollarHelper.isCollar(stack)) {
return false; return false;
} }

View File

@@ -4,7 +4,6 @@ import static com.tiedup.remake.client.gui.util.GuiLayoutConstants.*;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.tiedup.remake.client.gui.util.GuiColors; 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.v2.bondage.CollarHelper;
import com.tiedup.remake.state.IBondageState; import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.v2.BodyRegionV2; import com.tiedup.remake.v2.BodyRegionV2;
@@ -370,9 +369,8 @@ public class SlaveEntryWidget
// GPS zone status (right of health) // GPS zone status (right of health)
if (hasGPSCollar()) { if (hasGPSCollar()) {
ItemStack collarStack = slave.getEquipment(BodyRegionV2.NECK); ItemStack collarStack = slave.getEquipment(BodyRegionV2.NECK);
if (collarStack.getItem() instanceof ItemGpsCollar gps) { if (CollarHelper.hasGPS(collarStack)) {
boolean inSafeZone = isInAnySafeZone( boolean inSafeZone = isInAnySafeZone(
gps,
collarStack, collarStack,
entity entity
); );
@@ -570,17 +568,25 @@ public class SlaveEntryWidget
} }
private boolean isInAnySafeZone( private boolean isInAnySafeZone(
ItemGpsCollar gps,
ItemStack collarStack, ItemStack collarStack,
LivingEntity entity LivingEntity entity
) { ) {
if (!CollarHelper.isActive(collarStack)) return true; 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; if (safeSpots.isEmpty()) return true;
for (var spot : safeSpots) { for (int i = 0; i < safeSpots.size(); i++) {
if (spot.isInside(entity)) { 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; return true;
} }
} }

View File

@@ -93,16 +93,19 @@ public class KidnapSetCommand {
given += giveDataDrivenItems(player, "classic_blindfold", 4); given += giveDataDrivenItems(player, "classic_blindfold", 4);
given += giveDataDrivenItems(player, "blindfold_mask", 2); given += giveDataDrivenItems(player, "blindfold_mask", 2);
// Collars // Collars (data-driven)
given += giveItem( ItemStack classicCollars = com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem.createStack(
player, new net.minecraft.resources.ResourceLocation("tiedup", "classic_collar"));
new ItemStack(ModItems.CLASSIC_COLLAR.get(), 4) classicCollars.setCount(4);
); given += giveItem(player, classicCollars);
given += giveItem( ItemStack shockCollars = com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem.createStack(
player, new net.minecraft.resources.ResourceLocation("tiedup", "shock_collar"));
new ItemStack(ModItems.SHOCK_COLLAR.get(), 2) shockCollars.setCount(2);
); given += giveItem(player, shockCollars);
given += giveItem(player, new ItemStack(ModItems.GPS_COLLAR.get(), 2)); 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 // Tools
given += giveItem( given += giveItem(

View File

@@ -603,7 +603,7 @@ public class NPCCommand {
npc.equip( npc.equip(
BodyRegionV2.NECK, BodyRegionV2.NECK,
new ItemStack(ModItems.CLASSIC_COLLAR.get()) DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "classic_collar"))
); );
context context
.getSource() .getSource()
@@ -658,7 +658,7 @@ public class NPCCommand {
DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "cloth_gag")), DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "cloth_gag")),
DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "classic_blindfold")), DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "classic_blindfold")),
DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "classic_earplugs")), DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "classic_earplugs")),
new ItemStack(ModItems.CLASSIC_COLLAR.get()), DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "classic_collar")),
ItemStack.EMPTY // No clothes ItemStack.EMPTY // No clothes
); );
} }

View File

@@ -493,7 +493,7 @@ public class BondageSubCommand {
return 0; 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) { if (context.getSource().getEntity() instanceof ServerPlayer executor) {
CollarHelper.addOwner(collar, executor); CollarHelper.addOwner(collar, executor);
@@ -1078,7 +1078,7 @@ public class BondageSubCommand {
} }
if (!state.hasCollar()) { 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 ( if (
context.getSource().getEntity() instanceof ServerPlayer executor context.getSource().getEntity() instanceof ServerPlayer executor
) { ) {
@@ -1163,7 +1163,7 @@ public class BondageSubCommand {
state.putBlindfoldOn(blindfold); state.putBlindfoldOn(blindfold);
} }
if (!state.hasCollar()) { 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 ( if (
context.getSource().getEntity() instanceof ServerPlayer executor context.getSource().getEntity() instanceof ServerPlayer executor
) { ) {

View File

@@ -8,7 +8,6 @@ import com.tiedup.remake.v2.bondage.component.GaggingComponent;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem; import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
import com.tiedup.remake.items.base.IHasResistance; import com.tiedup.remake.items.base.IHasResistance;
import com.tiedup.remake.items.base.ILockable; 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.ICaptor;
import com.tiedup.remake.state.IRestrainable; import com.tiedup.remake.state.IRestrainable;
import com.tiedup.remake.state.IRestrainableEntity; import com.tiedup.remake.state.IRestrainableEntity;
@@ -280,16 +279,14 @@ public class MCAKidnappedAdapter implements IRestrainable {
public boolean hasGaggingEffect() { public boolean hasGaggingEffect() {
ItemStack gag = cap.getGag(); ItemStack gag = cap.getGag();
if (gag.isEmpty()) return false; if (gag.isEmpty()) return false;
if (DataDrivenBondageItem.getComponent(gag, ComponentType.GAGGING, GaggingComponent.class) != null) return true; return DataDrivenBondageItem.getComponent(gag, ComponentType.GAGGING, GaggingComponent.class) != null;
return gag.getItem() instanceof com.tiedup.remake.items.base.IHasGaggingEffect;
} }
@Override @Override
public boolean hasBlindingEffect() { public boolean hasBlindingEffect() {
ItemStack blindfold = cap.getBlindfold(); ItemStack blindfold = cap.getBlindfold();
if (blindfold.isEmpty()) return false; if (blindfold.isEmpty()) return false;
if (DataDrivenBondageItem.getComponent(blindfold, ComponentType.BLINDING, BlindingComponent.class) != null) return true; return DataDrivenBondageItem.getComponent(blindfold, ComponentType.BLINDING, BlindingComponent.class) != null;
return blindfold.getItem() instanceof com.tiedup.remake.items.base.IHasBlindingEffect;
} }
@Override @Override

View File

@@ -3,7 +3,6 @@ package com.tiedup.remake.dialogue;
import static com.tiedup.remake.util.GameConstants.*; import static com.tiedup.remake.util.GameConstants.*;
import com.tiedup.remake.dialogue.EmotionalContext.EmotionType; import com.tiedup.remake.dialogue.EmotionalContext.EmotionType;
import com.tiedup.remake.items.base.ItemGag;
import com.tiedup.remake.state.IBondageState; import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.v2.bondage.component.ComponentType; import com.tiedup.remake.v2.bondage.component.ComponentType;
import com.tiedup.remake.v2.bondage.component.GaggingComponent; import com.tiedup.remake.v2.bondage.component.GaggingComponent;
@@ -66,8 +65,6 @@ public class GagTalkManager {
gagStack, ComponentType.GAGGING, GaggingComponent.class); gagStack, ComponentType.GAGGING, GaggingComponent.class);
if (gaggingComp != null && gaggingComp.getMaterial() != null) { if (gaggingComp != null && gaggingComp.getMaterial() != null) {
material = gaggingComp.getMaterial(); material = gaggingComp.getMaterial();
} else if (gagStack.getItem() instanceof ItemGag gag) {
material = gag.getGagMaterial();
} }
// 1. EFFET DE SUFFOCATION (Si message trop long) // 1. EFFET DE SUFFOCATION (Si message trop long)
@@ -528,8 +525,6 @@ public class GagTalkManager {
gagStack, ComponentType.GAGGING, GaggingComponent.class); gagStack, ComponentType.GAGGING, GaggingComponent.class);
if (comp != null && comp.getMaterial() != null) { if (comp != null && comp.getMaterial() != null) {
material = comp.getMaterial(); material = comp.getMaterial();
} else if (gagStack.getItem() instanceof ItemGag gag) {
material = gag.getGagMaterial();
} }
} }

View File

@@ -2,16 +2,19 @@ package com.tiedup.remake.dispenser;
import com.tiedup.remake.core.TiedUpMod; import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.ModItems; import com.tiedup.remake.items.ModItems;
import com.tiedup.remake.items.base.*;
import net.minecraft.world.level.block.DispenserBlock; import net.minecraft.world.level.block.DispenserBlock;
/** /**
* Registration class for all TiedUp dispenser behaviors. * Registration class for all TiedUp dispenser behaviors.
* *
* Allows dispensers to: * 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 * - 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 * Based on original behaviors package from 1.12.2
*/ */
public class DispenserBehaviors { public class DispenserBehaviors {
@@ -25,72 +28,17 @@ public class DispenserBehaviors {
"[DispenserBehaviors] Registering dispenser behaviors..." "[DispenserBehaviors] Registering dispenser behaviors..."
); );
registerBindBehaviors();
registerGagBehaviors();
registerBlindfoldBehaviors();
registerCollarBehaviors();
registerEarplugsBehaviors();
registerClothesBehaviors(); registerClothesBehaviors();
registerRopeArrowBehavior(); registerRopeArrowBehavior();
// V2 data-driven bondage items register their own dispenser behaviors
// via DataDrivenBondageItem.registerDispenserBehaviors()
TiedUpMod.LOGGER.info( TiedUpMod.LOGGER.info(
"[DispenserBehaviors] Dispenser behaviors registered!" "[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() { private static void registerClothesBehaviors() {
DispenserBlock.registerBehavior( DispenserBlock.registerBehavior(
ModItems.CLOTHES.get(), ModItems.CLOTHES.get(),

View File

@@ -1,16 +1,21 @@
package com.tiedup.remake.dispenser; package com.tiedup.remake.dispenser;
import com.tiedup.remake.items.base.*;
import com.tiedup.remake.state.IBondageState; import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.v2.BodyRegionV2; 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.BiConsumer;
import java.util.function.Predicate; import java.util.function.Predicate;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
/** /**
* Generic dispenser behavior for equipping bondage items. * 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. * Use factory methods to create instances for each bondage type.
*/ */
@@ -18,23 +23,23 @@ public class GenericBondageDispenseBehavior
extends EquipBondageDispenseBehavior extends EquipBondageDispenseBehavior
{ {
private final Class<? extends Item> itemClass; private final Predicate<ItemStack> itemCheck;
private final Predicate<IBondageState> canEquipCheck; private final Predicate<IBondageState> canEquipCheck;
private final BiConsumer<IBondageState, ItemStack> equipAction; private final BiConsumer<IBondageState, ItemStack> equipAction;
private GenericBondageDispenseBehavior( private GenericBondageDispenseBehavior(
Class<? extends Item> itemClass, Predicate<ItemStack> itemCheck,
Predicate<IBondageState> canEquipCheck, Predicate<IBondageState> canEquipCheck,
BiConsumer<IBondageState, ItemStack> equipAction BiConsumer<IBondageState, ItemStack> equipAction
) { ) {
this.itemClass = itemClass; this.itemCheck = itemCheck;
this.canEquipCheck = canEquipCheck; this.canEquipCheck = canEquipCheck;
this.equipAction = equipAction; this.equipAction = equipAction;
} }
@Override @Override
protected boolean isValidItem(ItemStack stack) { protected boolean isValidItem(ItemStack stack) {
return !stack.isEmpty() && itemClass.isInstance(stack.getItem()); return !stack.isEmpty() && itemCheck.test(stack);
} }
@Override @Override
@@ -53,7 +58,7 @@ public class GenericBondageDispenseBehavior
public static GenericBondageDispenseBehavior forBind() { public static GenericBondageDispenseBehavior forBind() {
return new GenericBondageDispenseBehavior( return new GenericBondageDispenseBehavior(
ItemBind.class, BindModeHelper::isBindItem,
state -> !state.isTiedUp(), state -> !state.isTiedUp(),
(s, i) -> s.equip(BodyRegionV2.ARMS, i) (s, i) -> s.equip(BodyRegionV2.ARMS, i)
); );
@@ -61,7 +66,7 @@ public class GenericBondageDispenseBehavior
public static GenericBondageDispenseBehavior forGag() { public static GenericBondageDispenseBehavior forGag() {
return new GenericBondageDispenseBehavior( return new GenericBondageDispenseBehavior(
ItemGag.class, stack -> DataDrivenBondageItem.getComponent(stack, ComponentType.GAGGING, GaggingComponent.class) != null,
state -> !state.isGagged(), state -> !state.isGagged(),
(s, i) -> s.equip(BodyRegionV2.MOUTH, i) (s, i) -> s.equip(BodyRegionV2.MOUTH, i)
); );
@@ -69,7 +74,10 @@ public class GenericBondageDispenseBehavior
public static GenericBondageDispenseBehavior forBlindfold() { public static GenericBondageDispenseBehavior forBlindfold() {
return new GenericBondageDispenseBehavior( return new GenericBondageDispenseBehavior(
ItemBlindfold.class, stack -> {
DataDrivenItemDefinition def = DataDrivenItemRegistry.get(stack);
return def != null && def.occupiedRegions().contains(BodyRegionV2.EYES);
},
state -> !state.isBlindfolded(), state -> !state.isBlindfolded(),
(s, i) -> s.equip(BodyRegionV2.EYES, i) (s, i) -> s.equip(BodyRegionV2.EYES, i)
); );
@@ -77,7 +85,7 @@ public class GenericBondageDispenseBehavior
public static GenericBondageDispenseBehavior forCollar() { public static GenericBondageDispenseBehavior forCollar() {
return new GenericBondageDispenseBehavior( return new GenericBondageDispenseBehavior(
ItemCollar.class, CollarHelper::isCollar,
state -> !state.hasCollar(), state -> !state.hasCollar(),
(s, i) -> s.equip(BodyRegionV2.NECK, i) (s, i) -> s.equip(BodyRegionV2.NECK, i)
); );
@@ -85,7 +93,10 @@ public class GenericBondageDispenseBehavior
public static GenericBondageDispenseBehavior forEarplugs() { public static GenericBondageDispenseBehavior forEarplugs() {
return new GenericBondageDispenseBehavior( return new GenericBondageDispenseBehavior(
ItemEarplugs.class, stack -> {
DataDrivenItemDefinition def = DataDrivenItemRegistry.get(stack);
return def != null && def.occupiedRegions().contains(BodyRegionV2.EARS);
},
state -> !state.hasEarplugs(), state -> !state.hasEarplugs(),
(s, i) -> s.equip(BodyRegionV2.EARS, i) (s, i) -> s.equip(BodyRegionV2.EARS, i)
); );

View File

@@ -796,16 +796,14 @@ public abstract class AbstractTiedUpNpc
public boolean hasGaggingEffect() { public boolean hasGaggingEffect() {
ItemStack gag = this.getEquipment(BodyRegionV2.MOUTH); ItemStack gag = this.getEquipment(BodyRegionV2.MOUTH);
if (gag.isEmpty()) return false; if (gag.isEmpty()) return false;
if (DataDrivenBondageItem.getComponent(gag, ComponentType.GAGGING, GaggingComponent.class) != null) return true; return DataDrivenBondageItem.getComponent(gag, ComponentType.GAGGING, GaggingComponent.class) != null;
return gag.getItem() instanceof com.tiedup.remake.items.base.IHasGaggingEffect;
} }
@Override @Override
public boolean hasBlindingEffect() { public boolean hasBlindingEffect() {
ItemStack blindfold = this.getEquipment(BodyRegionV2.EYES); ItemStack blindfold = this.getEquipment(BodyRegionV2.EYES);
if (blindfold.isEmpty()) return false; if (blindfold.isEmpty()) return false;
if (DataDrivenBondageItem.getComponent(blindfold, ComponentType.BLINDING, BlindingComponent.class) != null) return true; return DataDrivenBondageItem.getComponent(blindfold, ComponentType.BLINDING, BlindingComponent.class) != null;
return blindfold.getItem() instanceof com.tiedup.remake.items.base.IHasBlindingEffect;
} }
@Override @Override
@@ -1087,9 +1085,7 @@ public abstract class AbstractTiedUpNpc
public boolean hasShockCollar() { public boolean hasShockCollar() {
ItemStack collar = this.getEquipment(BodyRegionV2.NECK); ItemStack collar = this.getEquipment(BodyRegionV2.NECK);
if (collar.isEmpty()) return false; if (collar.isEmpty()) return false;
return ( return com.tiedup.remake.v2.bondage.CollarHelper.canShock(collar);
collar.getItem() instanceof com.tiedup.remake.items.ItemShockCollar
);
} }
// BONDAGE SERVICE (delegated to BondageManager) // BONDAGE SERVICE (delegated to BondageManager)

View File

@@ -616,82 +616,10 @@ public class EntityKidnapperMerchant extends EntityKidnapperElite {
private List<ItemStack> collectAllModItems() { private List<ItemStack> collectAllModItems() {
List<ItemStack> items = new ArrayList<>(); List<ItemStack> items = new ArrayList<>();
// All binds — iterate V1 variants, create V2 stacks // All data-driven bondage items (binds, gags, blindfolds, earplugs, mittens, collars, etc.)
for (BindVariant variant : BindVariant.values()) { for (com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemDefinition def :
if (variant.supportsColor()) { com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemRegistry.getAll()) {
for (ItemColor color : ItemColor.values()) { items.add(DataDrivenBondageItem.createStack(def.id()));
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())
));
} }
// Knives - no color support // Knives - no color support
@@ -699,16 +627,11 @@ public class EntityKidnapperMerchant extends EntityKidnapperElite {
items.add(new ItemStack(ModItems.getKnife(variant))); items.add(new ItemStack(ModItems.getKnife(variant)));
} }
// Complex items // Tools
items.add(new ItemStack(ModItems.CLASSIC_COLLAR.get()));
items.add(new ItemStack(ModItems.SHOCK_COLLAR.get()));
items.add(new ItemStack(ModItems.GPS_COLLAR.get()));
items.add(new ItemStack(ModItems.WHIP.get())); items.add(new ItemStack(ModItems.WHIP.get()));
// BLACKLIST: TASER (too powerful) // BLACKLIST: TASER (too powerful)
// BLACKLIST: LOCKPICK (now in guaranteed utilities) // BLACKLIST: LOCKPICK (now in guaranteed utilities)
// BLACKLIST: MASTER_KEY (too OP - unlocks everything) // 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())); items.add(new ItemStack(ModItems.CLOTHES.get()));
return items; return items;
@@ -754,13 +677,13 @@ public class EntityKidnapperMerchant extends EntityKidnapperElite {
Item i = item.getItem(); Item i = item.getItem();
// Tier 4: GPS collar // Tier 4: GPS collar
if (i == ModItems.GPS_COLLAR.get()) { if (com.tiedup.remake.v2.bondage.CollarHelper.hasGPS(item)) {
return 4; return 4;
} }
// Tier 3: Shock collar, taser, master key // Tier 3: Shock collar, taser, master key
if ( if (
i == ModItems.SHOCK_COLLAR.get() || com.tiedup.remake.v2.bondage.CollarHelper.canShock(item) ||
i == ModItems.TASER.get() || i == ModItems.TASER.get() ||
i == ModItems.MASTER_KEY.get() i == ModItems.MASTER_KEY.get()
) { ) {
@@ -769,11 +692,9 @@ public class EntityKidnapperMerchant extends EntityKidnapperElite {
// Tier 2: Collars, whip, tools, complex items, clothes // Tier 2: Collars, whip, tools, complex items, clothes
if ( if (
i == ModItems.CLASSIC_COLLAR.get() || com.tiedup.remake.v2.bondage.CollarHelper.isCollar(item) ||
i == ModItems.WHIP.get() || i == ModItems.WHIP.get() ||
i == ModItems.LOCKPICK.get() || i == ModItems.LOCKPICK.get() ||
i == ModItems.MEDICAL_GAG.get() ||
i == ModItems.HOOD.get() ||
i instanceof GenericClothes i instanceof GenericClothes
) { ) {
return 2; return 2;

View File

@@ -115,7 +115,8 @@ public class KidnapperCaptureEquipment {
@Nullable @Nullable
public ItemStack getCollarItem() { public ItemStack getCollarItem() {
// Kidnappers always have a shock collar to mark their captives // 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 // HELD ITEM MANAGEMENT

View File

@@ -12,6 +12,8 @@ import org.jetbrains.annotations.Nullable;
* Helper class for selecting themed items for kidnappers. * Helper class for selecting themed items for kidnappers.
* Handles probability-based item selection and color application. * Handles probability-based item selection and color application.
* *
* All bondage items are now created via DataDrivenBondageItem.createStack().
*
* Item selection order (most to least common): * Item selection order (most to least common):
* 1. Bind (arms) - 100% (always) * 1. Bind (arms) - 100% (always)
* 2. Gag - 50% * 2. Gag - 50%
@@ -79,33 +81,10 @@ public class KidnapperItemSelector {
this.blindfold = blindfold; this.blindfold = blindfold;
} }
/** public boolean hasGag() { return !gag.isEmpty(); }
* Check if this selection has a gag. public boolean hasMittens() { return !mittens.isEmpty(); }
*/ public boolean hasEarplugs() { return !earplugs.isEmpty(); }
public boolean hasGag() { public boolean hasBlindfold() { return !blindfold.isEmpty(); }
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();
}
} }
/** /**
@@ -129,16 +108,6 @@ public class KidnapperItemSelector {
return selectItems(false, true); 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( private static double getAdjustedProbability(
double baseProb, double baseProb,
double eliteBonus, double eliteBonus,
@@ -152,9 +121,6 @@ public class KidnapperItemSelector {
return prob; return prob;
} }
/**
* Internal item selection logic.
*/
private static SelectionResult selectItems( private static SelectionResult selectItems(
boolean isElite, boolean isElite,
boolean isArcher boolean isArcher
@@ -163,132 +129,64 @@ public class KidnapperItemSelector {
KidnapperTheme theme = KidnapperTheme.getRandomWeighted(); KidnapperTheme theme = KidnapperTheme.getRandomWeighted();
// 2. Select color (if theme supports it) // 2. Select color (if theme supports it)
// Filter out colors that don't have textures for this theme's bind
ItemColor color = theme.supportsColor() ItemColor color = theme.supportsColor()
? getValidColorForBind(theme.getBind()) ? ItemColor.getRandomStandard()
: null; : null;
// 3. Create bind (always) // 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; ItemStack gag = ItemStack.EMPTY;
double gagProb = getAdjustedProbability( double gagProb = getAdjustedProbability(
PROB_GAG, PROB_GAG, ELITE_GAG_BONUS, ARCHER_GAG_PENALTY, isElite, isArcher
ELITE_GAG_BONUS,
ARCHER_GAG_PENALTY,
isElite,
isArcher
); );
if (RANDOM.nextDouble() < gagProb) { 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; ItemStack mittens = ItemStack.EMPTY;
double mittensProb = getAdjustedProbability( double mittensProb = getAdjustedProbability(
PROB_MITTENS, PROB_MITTENS, ELITE_MITTENS_BONUS, ARCHER_MITTENS_PENALTY, isElite, isArcher
ELITE_MITTENS_BONUS,
ARCHER_MITTENS_PENALTY,
isElite,
isArcher
); );
if (RANDOM.nextDouble() < mittensProb) { if (RANDOM.nextDouble() < mittensProb) {
mittens = createMittens(); mittens = createMittens();
} }
// 6. Roll for earplugs (same for all themes) // 6. Roll for earplugs
ItemStack earplugs = ItemStack.EMPTY; ItemStack earplugs = ItemStack.EMPTY;
double earplugsProb = getAdjustedProbability( double earplugsProb = getAdjustedProbability(
PROB_EARPLUGS, PROB_EARPLUGS, ELITE_EARPLUGS_BONUS, ARCHER_EARPLUGS_PENALTY, isElite, isArcher
ELITE_EARPLUGS_BONUS,
ARCHER_EARPLUGS_PENALTY,
isElite,
isArcher
); );
if (RANDOM.nextDouble() < earplugsProb) { if (RANDOM.nextDouble() < earplugsProb) {
earplugs = createEarplugs(); earplugs = createEarplugs();
} }
// 7. Roll for blindfold (last, most restrictive - randomly selected) // 7. Roll for blindfold
ItemStack blindfold = ItemStack.EMPTY; ItemStack blindfold = ItemStack.EMPTY;
double blindfoldProb = getAdjustedProbability( double blindfoldProb = getAdjustedProbability(
PROB_BLINDFOLD, PROB_BLINDFOLD, ELITE_BLINDFOLD_BONUS, ARCHER_BLINDFOLD_PENALTY, isElite, isArcher
ELITE_BLINDFOLD_BONUS,
ARCHER_BLINDFOLD_PENALTY,
isElite,
isArcher
); );
if (theme.hasBlindfolds() && RANDOM.nextDouble() < blindfoldProb) { if (theme.hasBlindfolds() && RANDOM.nextDouble() < blindfoldProb) {
blindfold = createBlindfold(theme.getRandomBlindfold(), color); blindfold = createItemById(theme.getRandomBlindfoldId(), color);
} }
return new SelectionResult( return new SelectionResult(
theme, theme, color, bind, gag, mittens, earplugs, blindfold
color,
bind,
gag,
mittens,
earplugs,
blindfold
); );
} }
// ITEM CREATION METHODS // 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( public static ItemStack createItemById(String id, @Nullable ItemColor color) {
BindVariant variant,
@Nullable ItemColor color
) {
ItemStack stack = DataDrivenBondageItem.createStack( ItemStack stack = DataDrivenBondageItem.createStack(
new ResourceLocation("tiedup", variant.getRegistryName()) new ResourceLocation("tiedup", id)
); );
if (color != null && variant.supportsColor()) { if (color != null) {
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)
) {
applyColor(stack, color); applyColor(stack, color);
} }
return stack; return stack;
@@ -296,7 +194,6 @@ public class KidnapperItemSelector {
/** /**
* Create mittens ItemStack. * Create mittens ItemStack.
* Mittens don't have color variants.
*/ */
public static ItemStack createMittens() { public static ItemStack createMittens() {
return DataDrivenBondageItem.createStack( return DataDrivenBondageItem.createStack(
@@ -306,7 +203,6 @@ public class KidnapperItemSelector {
/** /**
* Create earplugs ItemStack. * Create earplugs ItemStack.
* Earplugs don't have color variants.
*/ */
public static ItemStack createEarplugs() { public static ItemStack createEarplugs() {
return DataDrivenBondageItem.createStack( return DataDrivenBondageItem.createStack(
@@ -321,7 +217,6 @@ public class KidnapperItemSelector {
/** /**
* Apply color NBT to an ItemStack. * Apply color NBT to an ItemStack.
* Sets both the ItemColor name and CustomModelData for model selection.
*/ */
public static void applyColor(ItemStack stack, ItemColor color) { public static void applyColor(ItemStack stack, ItemColor color) {
if (stack.isEmpty() || color == null) return; if (stack.isEmpty() || color == null) return;
@@ -351,141 +246,9 @@ public class KidnapperItemSelector {
/** /**
* Get the texture suffix for an item's color. * 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) { public static String getColorSuffix(ItemStack stack) {
ItemColor color = getColor(stack); ItemColor color = getColor(stack);
return color != null ? "_" + color.getName() : ""; 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;
}
} }

View File

@@ -161,7 +161,8 @@ public class KidnapperJobManager {
); );
// Put a shock collar on the worker AFTER untie/free // 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 // Add kidnapper as owner so the collar is linked
CollarHelper.addOwner( CollarHelper.addOwner(
shockCollar, shockCollar,

View File

@@ -1,42 +1,33 @@
package com.tiedup.remake.entities; 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; import java.util.Random;
/** /**
* Defines themed item sets for kidnappers. * 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. * Themes are selected randomly with weighted probabilities.
* Higher weight = more common theme. * Higher weight = more common theme.
* *
* Note: Natural themes (SLIME, VINE, WEB) are reserved for monsters. * Note: Natural themes (SLIME, VINE, WEB) are reserved for monsters.
*
* Registry names correspond to data-driven bondage item IDs in DataDrivenItemRegistry.
*/ */
public enum KidnapperTheme { public enum KidnapperTheme {
// === ROPE THEMES (most common) === // === ROPE THEMES (most common) ===
ROPE( ROPE(
BindVariant.ROPES, "ropes",
new GagVariant[] { new String[] { "ropes_gag", "cloth_gag", "cleave_gag" },
GagVariant.ROPES_GAG, new String[] { "classic_blindfold" },
GagVariant.CLOTH_GAG,
GagVariant.CLEAVE_GAG,
},
new BlindfoldVariant[] { BlindfoldVariant.CLASSIC },
true, // supportsColor true, // supportsColor
30 // weight (spawn probability) 30 // weight (spawn probability)
), ),
SHIBARI( SHIBARI(
BindVariant.SHIBARI, "shibari",
new GagVariant[] { new String[] { "ropes_gag", "cloth_gag", "ribbon_gag" },
GagVariant.ROPES_GAG, new String[] { "classic_blindfold" },
GagVariant.CLOTH_GAG,
GagVariant.RIBBON_GAG,
},
new BlindfoldVariant[] { BlindfoldVariant.CLASSIC },
true, true,
15 15
), ),
@@ -44,9 +35,9 @@ public enum KidnapperTheme {
// === TAPE THEME === // === TAPE THEME ===
TAPE( TAPE(
BindVariant.DUCT_TAPE, "duct_tape",
new GagVariant[] { GagVariant.TAPE_GAG, GagVariant.WRAP_GAG }, new String[] { "tape_gag", "wrap_gag" },
new BlindfoldVariant[] { BlindfoldVariant.MASK }, new String[] { "mask_blindfold" },
true, true,
20 20
), ),
@@ -54,13 +45,9 @@ public enum KidnapperTheme {
// === LEATHER/BDSM THEME === // === LEATHER/BDSM THEME ===
LEATHER( LEATHER(
BindVariant.LEATHER_STRAPS, "leather_straps",
new GagVariant[] { new String[] { "ball_gag", "ball_gag_strap", "panel_gag" },
GagVariant.BALL_GAG, new String[] { "mask_blindfold" },
GagVariant.BALL_GAG_STRAP,
GagVariant.PANEL_GAG,
},
new BlindfoldVariant[] { BlindfoldVariant.MASK },
false, false,
15 15
), ),
@@ -68,12 +55,9 @@ public enum KidnapperTheme {
// === CHAIN THEME === // === CHAIN THEME ===
CHAIN( CHAIN(
BindVariant.CHAIN, "chain",
new GagVariant[] { new String[] { "chain_panel_gag", "ball_gag_strap" },
GagVariant.CHAIN_PANEL_GAG, new String[] { "mask_blindfold" },
GagVariant.BALL_GAG_STRAP,
},
new BlindfoldVariant[] { BlindfoldVariant.MASK },
false, false,
10 10
), ),
@@ -81,13 +65,9 @@ public enum KidnapperTheme {
// === MEDICAL THEME === // === MEDICAL THEME ===
MEDICAL( MEDICAL(
BindVariant.MEDICAL_STRAPS, "medical_straps",
new GagVariant[] { new String[] { "tube_gag", "sponge_gag", "ball_gag" },
GagVariant.TUBE_GAG, new String[] { "mask_blindfold" },
GagVariant.SPONGE_GAG,
GagVariant.BALL_GAG,
},
new BlindfoldVariant[] { BlindfoldVariant.MASK },
false, false,
8 8
), ),
@@ -95,9 +75,9 @@ public enum KidnapperTheme {
// === SCI-FI/BEAM THEME === // === SCI-FI/BEAM THEME ===
BEAM( BEAM(
BindVariant.BEAM_CUFFS, "beam_cuffs",
new GagVariant[] { GagVariant.BEAM_PANEL_GAG, GagVariant.LATEX_GAG }, new String[] { "beam_panel_gag", "latex_gag" },
new BlindfoldVariant[] { BlindfoldVariant.MASK }, new String[] { "mask_blindfold" },
false, false,
5 5
), ),
@@ -105,9 +85,9 @@ public enum KidnapperTheme {
// === LATEX THEME (rare) === // === LATEX THEME (rare) ===
LATEX( LATEX(
BindVariant.LATEX_SACK, "latex_sack",
new GagVariant[] { GagVariant.LATEX_GAG, GagVariant.TUBE_GAG }, new String[] { "latex_gag", "tube_gag" },
new BlindfoldVariant[] { BlindfoldVariant.MASK }, new String[] { "mask_blindfold" },
false, false,
3 3
), ),
@@ -115,13 +95,9 @@ public enum KidnapperTheme {
// === ASYLUM THEME (rare) === // === ASYLUM THEME (rare) ===
ASYLUM( ASYLUM(
BindVariant.STRAITJACKET, "straitjacket",
new GagVariant[] { new String[] { "bite_gag", "sponge_gag", "ball_gag" },
GagVariant.BITE_GAG, new String[] { "mask_blindfold" },
GagVariant.SPONGE_GAG,
GagVariant.BALL_GAG,
},
new BlindfoldVariant[] { BlindfoldVariant.MASK },
false, false,
5 5
), ),
@@ -129,9 +105,9 @@ public enum KidnapperTheme {
// === RIBBON THEME (cute/playful) === // === RIBBON THEME (cute/playful) ===
RIBBON( RIBBON(
BindVariant.RIBBON, "ribbon",
new GagVariant[] { GagVariant.RIBBON_GAG, GagVariant.CLOTH_GAG }, new String[] { "ribbon_gag", "cloth_gag" },
new BlindfoldVariant[] { BlindfoldVariant.CLASSIC }, new String[] { "classic_blindfold" },
false, false,
8 8
), ),
@@ -139,54 +115,54 @@ public enum KidnapperTheme {
// === WRAP THEME === // === WRAP THEME ===
WRAP( WRAP(
BindVariant.WRAP, "wrap",
new GagVariant[] { GagVariant.WRAP_GAG, GagVariant.TAPE_GAG }, new String[] { "wrap_gag", "tape_gag" },
new BlindfoldVariant[] { BlindfoldVariant.MASK }, new String[] { "mask_blindfold" },
false, false,
5 5
); );
private static final Random RANDOM = new Random(); private static final Random RANDOM = new Random();
private final BindVariant bind; private final String bindId;
private final GagVariant[] gags; private final String[] gagIds;
private final BlindfoldVariant[] blindfolds; private final String[] blindfoldIds;
private final boolean supportsColor; private final boolean supportsColor;
private final int weight; private final int weight;
KidnapperTheme( KidnapperTheme(
BindVariant bind, String bindId,
GagVariant[] gags, String[] gagIds,
BlindfoldVariant[] blindfolds, String[] blindfoldIds,
boolean supportsColor, boolean supportsColor,
int weight int weight
) { ) {
this.bind = bind; this.bindId = bindId;
this.gags = gags; this.gagIds = gagIds;
this.blindfolds = blindfolds; this.blindfoldIds = blindfoldIds;
this.supportsColor = supportsColor; this.supportsColor = supportsColor;
this.weight = weight; this.weight = weight;
} }
/** /**
* Get the primary bind for this theme. * Get the primary bind registry name for this theme.
*/ */
public BindVariant getBind() { public String getBindId() {
return bind; return bindId;
} }
/** /**
* Get compatible gags for this theme (ordered by preference). * Get compatible gag IDs for this theme (ordered by preference).
*/ */
public GagVariant[] getGags() { public String[] getGagIds() {
return gags; return gagIds;
} }
/** /**
* Get compatible blindfolds for this theme. * Get compatible blindfold IDs for this theme.
*/ */
public BlindfoldVariant[] getBlindfolds() { public String[] getBlindfoldIds() {
return blindfolds; return blindfoldIds;
} }
/** /**
@@ -206,41 +182,40 @@ public enum KidnapperTheme {
} }
/** /**
* Get primary gag (first in list). * Get primary gag ID (first in list).
* Used when only one gag is selected.
*/ */
public GagVariant getPrimaryGag() { public String getPrimaryGagId() {
return gags.length > 0 ? gags[0] : GagVariant.BALL_GAG; 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() { public String getRandomGagId() {
if (gags.length == 0) return GagVariant.BALL_GAG; if (gagIds.length == 0) return "ball_gag";
return gags[RANDOM.nextInt(gags.length)]; return gagIds[RANDOM.nextInt(gagIds.length)];
} }
/** /**
* Get primary blindfold (first in list). * Get primary blindfold ID (first in list).
*/ */
public BlindfoldVariant getPrimaryBlindfold() { public String getPrimaryBlindfoldId() {
return blindfolds.length > 0 ? blindfolds[0] : BlindfoldVariant.CLASSIC; 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() { public String getRandomBlindfoldId() {
if (blindfolds.length == 0) return BlindfoldVariant.CLASSIC; if (blindfoldIds.length == 0) return "classic_blindfold";
return blindfolds[RANDOM.nextInt(blindfolds.length)]; return blindfoldIds[RANDOM.nextInt(blindfoldIds.length)];
} }
/** /**
* Check if this theme has any blindfolds. * Check if this theme has any blindfolds.
*/ */
public boolean hasBlindfolds() { public boolean hasBlindfolds() {
return blindfolds.length > 0; return blindfoldIds.length > 0;
} }
/** /**

View File

@@ -229,20 +229,21 @@ public class MaidDeliverCaptiveGoal extends Goal {
kidnappedState.getEquipment(BodyRegionV2.NECK); kidnappedState.getEquipment(BodyRegionV2.NECK);
if ( if (
!collar.isEmpty() && !collar.isEmpty() &&
collar.getItem() instanceof com.tiedup.remake.v2.bondage.CollarHelper.isCollar(collar)
com.tiedup.remake.items.base.ItemCollar collarItem
) { ) {
for (java.util.UUID ownerId : new java.util.ArrayList<>( 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, collar,
buyerEntity.getUUID(), buyerEntity.getUUID(),
buyerEntity.getName().getString() 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); kidnappedState.equip(BodyRegionV2.NECK, collar);
if ( if (

View File

@@ -305,11 +305,8 @@ public class NpcStruggleGoal extends Goal {
ItemStack collar = npc.getEquipment(BodyRegionV2.NECK); ItemStack collar = npc.getEquipment(BodyRegionV2.NECK);
if (collar.isEmpty()) return false; if (collar.isEmpty()) return false;
if ( if (com.tiedup.remake.v2.bondage.CollarHelper.isCollar(collar)) {
collar.getItem() instanceof List<UUID> ownerUUIDs = com.tiedup.remake.v2.bondage.CollarHelper.getOwners(collar);
com.tiedup.remake.items.base.ItemCollar collarItem
) {
List<UUID> ownerUUIDs = collarItem.getOwners(collar);
if (!ownerUUIDs.isEmpty()) { if (!ownerUUIDs.isEmpty()) {
// Check if any owner is nearby // Check if any owner is nearby
List<Player> players = npc List<Player> players = npc
@@ -338,11 +335,8 @@ public class NpcStruggleGoal extends Goal {
ItemStack collar = npc.getEquipment(BodyRegionV2.NECK); ItemStack collar = npc.getEquipment(BodyRegionV2.NECK);
if (collar.isEmpty()) return null; if (collar.isEmpty()) return null;
if ( if (com.tiedup.remake.v2.bondage.CollarHelper.isCollar(collar)) {
collar.getItem() instanceof List<UUID> ownerUUIDs = com.tiedup.remake.v2.bondage.CollarHelper.getOwners(collar);
com.tiedup.remake.items.base.ItemCollar collarItem
) {
List<UUID> ownerUUIDs = collarItem.getOwners(collar);
if (!ownerUUIDs.isEmpty()) { if (!ownerUUIDs.isEmpty()) {
return ownerUUIDs.get(0); return ownerUUIDs.get(0);
} }

View File

@@ -1,7 +1,7 @@
package com.tiedup.remake.entities.armorstand; package com.tiedup.remake.entities.armorstand;
import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.items.base.PoseType; import com.tiedup.remake.items.base.PoseType;
import com.tiedup.remake.v2.bondage.PoseTypeHelper;
import com.tiedup.remake.v2.BodyRegionV2; import com.tiedup.remake.v2.BodyRegionV2;
import java.util.Map; import java.util.Map;
import net.minecraft.core.Rotations; import net.minecraft.core.Rotations;
@@ -185,11 +185,8 @@ public class ArmorStandBondageHelper {
// Save original pose if not already saved // Save original pose if not already saved
saveOriginalPose(stand); saveOriginalPose(stand);
// Get pose type from bind // Get pose type from bind (V2 data-driven)
PoseType poseType = PoseType.STANDARD; PoseType poseType = PoseTypeHelper.getPoseType(bindStack);
if (bindStack.getItem() instanceof ItemBind bind) {
poseType = bind.getPoseType();
}
// Apply pose // Apply pose
applyBondagePose(stand, poseType); applyBondagePose(stand, poseType);

View File

@@ -116,15 +116,13 @@ public class NpcEquipmentManager {
public boolean hasGaggingEffect() { public boolean hasGaggingEffect() {
ItemStack gag = getCurrentGag(); ItemStack gag = getCurrentGag();
if (gag.isEmpty()) return false; if (gag.isEmpty()) return false;
if (DataDrivenBondageItem.getComponent(gag, ComponentType.GAGGING, GaggingComponent.class) != null) return true; return DataDrivenBondageItem.getComponent(gag, ComponentType.GAGGING, GaggingComponent.class) != null;
return gag.getItem() instanceof com.tiedup.remake.items.base.IHasGaggingEffect;
} }
public boolean hasBlindingEffect() { public boolean hasBlindingEffect() {
ItemStack blindfold = getCurrentBlindfold(); ItemStack blindfold = getCurrentBlindfold();
if (blindfold.isEmpty()) return false; if (blindfold.isEmpty()) return false;
if (DataDrivenBondageItem.getComponent(blindfold, ComponentType.BLINDING, BlindingComponent.class) != null) return true; return DataDrivenBondageItem.getComponent(blindfold, ComponentType.BLINDING, BlindingComponent.class) != null;
return blindfold.getItem() instanceof com.tiedup.remake.items.base.IHasBlindingEffect;
} }
public boolean hasKnives() { public boolean hasKnives() {

View File

@@ -361,19 +361,19 @@ public class KidnapperAppearance {
this.itemSelection = new KidnapperItemSelector.SelectionResult( this.itemSelection = new KidnapperItemSelector.SelectionResult(
this.currentTheme, this.currentTheme,
this.themeColor, this.themeColor,
KidnapperItemSelector.createBind( KidnapperItemSelector.createItemById(
this.currentTheme.getBind(), this.currentTheme.getBindId(),
this.themeColor this.themeColor
), ),
KidnapperItemSelector.createGag( KidnapperItemSelector.createItemById(
this.currentTheme.getPrimaryGag(), this.currentTheme.getPrimaryGagId(),
this.themeColor this.themeColor
), ),
KidnapperItemSelector.createMittens(), KidnapperItemSelector.createMittens(),
KidnapperItemSelector.createEarplugs(), KidnapperItemSelector.createEarplugs(),
this.currentTheme.hasBlindfolds() this.currentTheme.hasBlindfolds()
? KidnapperItemSelector.createBlindfold( ? KidnapperItemSelector.createItemById(
this.currentTheme.getPrimaryBlindfold(), this.currentTheme.getPrimaryBlindfoldId(),
this.themeColor this.themeColor
) )
: ItemStack.EMPTY : ItemStack.EMPTY

View File

@@ -119,8 +119,8 @@ public class MasterPetManager {
if (state == null) return; if (state == null) return;
// Create a choke collar for pet play // Create a choke collar for pet play
ItemStack chokeCollar = new ItemStack( ItemStack chokeCollar = com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem.createStack(
com.tiedup.remake.items.ModItems.CHOKE_COLLAR.get() new net.minecraft.resources.ResourceLocation("tiedup", "choke_collar")
); );
// Configure for pet play BEFORE equipping // Configure for pet play BEFORE equipping

View File

@@ -4,7 +4,6 @@ import com.tiedup.remake.core.SettingsAccessor;
import com.tiedup.remake.core.SystemMessageManager; import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.core.TiedUpMod; import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.dialogue.GagTalkManager; import com.tiedup.remake.dialogue.GagTalkManager;
import com.tiedup.remake.items.base.ItemGag;
import com.tiedup.remake.state.IBondageState; import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.util.GagMaterial; import com.tiedup.remake.util.GagMaterial;
import com.tiedup.remake.v2.bondage.component.ComponentType; import com.tiedup.remake.v2.bondage.component.ComponentType;
@@ -57,11 +56,10 @@ public class ChatEventHandler {
BodyRegionV2.MOUTH BodyRegionV2.MOUTH
); );
// V2: check gagging component, V1 fallback: instanceof ItemGag // V2: check gagging component
GaggingComponent gaggingComp = DataDrivenBondageItem.getComponent( GaggingComponent gaggingComp = DataDrivenBondageItem.getComponent(
gagStack, ComponentType.GAGGING, GaggingComponent.class); gagStack, ComponentType.GAGGING, GaggingComponent.class);
boolean isGagItem = gaggingComp != null boolean isGagItem = gaggingComp != null;
|| gagStack.getItem() instanceof ItemGag;
if (!gagStack.isEmpty() && isGagItem) { if (!gagStack.isEmpty() && isGagItem) {
String originalMessage = event.getRawText(); String originalMessage = event.getRawText();
@@ -70,9 +68,7 @@ public class ChatEventHandler {
if (gaggingComp != null) { if (gaggingComp != null) {
material = gaggingComp.getMaterial(); material = gaggingComp.getMaterial();
} }
if (material == null && gagStack.getItem() instanceof ItemGag gagItem) { // material stays null if no component; GagTalkManager handles null → CLOTH fallback
material = gagItem.getGagMaterial();
}
// 1. Process the message through our GagTalkManager V2 // 1. Process the message through our GagTalkManager V2
Component muffledMessage = GagTalkManager.processGagMessage( Component muffledMessage = GagTalkManager.processGagMessage(

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -3,8 +3,8 @@ package com.tiedup.remake.items;
import com.tiedup.remake.core.TiedUpMod; import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.base.IKnife; import com.tiedup.remake.items.base.IKnife;
import com.tiedup.remake.items.base.ILockable; 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.items.base.KnifeVariant;
import com.tiedup.remake.v2.bondage.BindModeHelper;
import com.tiedup.remake.network.sync.SyncManager; import com.tiedup.remake.network.sync.SyncManager;
import com.tiedup.remake.state.IBondageState; import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.state.PlayerBindState; import com.tiedup.remake.state.PlayerBindState;
@@ -306,10 +306,7 @@ public class GenericKnife extends Item implements IKnife {
player, player,
BodyRegionV2.ARMS BodyRegionV2.ARMS
); );
if ( if (bindStack.isEmpty() || !BindModeHelper.isBindItem(bindStack)) {
bindStack.isEmpty() ||
!(bindStack.getItem() instanceof ItemBind bind)
) {
player.stopUsingItem(); player.stopUsingItem();
return; return;
} }

View File

@@ -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();
}
}

View File

@@ -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.
*
* <p>Special feature: Can be put in "choke mode" which applies a drowning effect.</p>
* <p>Used by Masters for punishment. The effect simulates choking by reducing air supply,
* which triggers drowning damage if left active for too long.</p>
*
* <p><b>Mechanics:</b></p>
* <ul>
* <li>When choking is active, the wearer's air supply decreases rapidly</li>
* <li>This creates the drowning effect (damage and bubble particles)</li>
* <li>Masters should deactivate the choke before the pet dies</li>
* <li>The choke is controlled by the Master's punishment system</li>
* </ul>
*
* @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<Component> 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;
}
}

View File

@@ -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
);
}
}

View File

@@ -5,8 +5,8 @@ import com.tiedup.remake.cells.CellRegistryV2;
import com.tiedup.remake.core.SystemMessageManager; import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.core.TiedUpMod; import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.EntityDamsel; import com.tiedup.remake.entities.EntityDamsel;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.network.ModNetwork; import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.network.personality.PacketOpenCommandWandScreen; import com.tiedup.remake.network.personality.PacketOpenCommandWandScreen;
import com.tiedup.remake.personality.HomeType; import com.tiedup.remake.personality.HomeType;
import com.tiedup.remake.personality.JobExperience; import com.tiedup.remake.personality.JobExperience;
@@ -349,11 +349,11 @@ public class ItemCommandWand extends Item {
// Get collar and verify ownership // Get collar and verify ownership
ItemStack collar = damsel.getEquipment(BodyRegionV2.NECK); ItemStack collar = damsel.getEquipment(BodyRegionV2.NECK);
if (!(collar.getItem() instanceof ItemCollar collarItem)) { if (!CollarHelper.isCollar(collar)) {
return InteractionResult.PASS; return InteractionResult.PASS;
} }
if (!collarItem.getOwners(collar).contains(player.getUUID())) { if (!CollarHelper.isOwner(collar, player)) {
SystemMessageManager.sendToPlayer( SystemMessageManager.sendToPlayer(
player, player,
SystemMessageManager.MessageCategory.ERROR, SystemMessageManager.MessageCategory.ERROR,

View File

@@ -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.
*
* <p>Mechanics:</p>
* <ul>
* <li><b>Safe Zones:</b> Can store multiple coordinates (SafeSpots) where the wearer is allowed to be.</li>
* <li><b>Auto-Shock:</b> If the wearer is outside ALL active safe zones, they are shocked at intervals.</li>
* <li><b>Master Warning:</b> Masters receive an alert message when a safe zone violation is detected.</li>
* <li><b>Public Tracking:</b> If enabled, allows anyone with a Locator to see distance and direction.</li>
* </ul>
*/
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<Component> 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<SafeSpot> 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<SafeSpot> getSafeSpots(ItemStack stack) {
List<SafeSpot> 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
);
}
}
}

View File

@@ -1,8 +1,8 @@
package com.tiedup.remake.items; package com.tiedup.remake.items;
import com.tiedup.remake.core.SystemMessageManager; import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.items.base.ItemOwnerTarget; import com.tiedup.remake.items.base.ItemOwnerTarget;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.state.IBondageState; import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.util.KidnappedHelper; import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.v2.BodyRegionV2; import com.tiedup.remake.v2.BodyRegionV2;
@@ -91,13 +91,10 @@ public class ItemGpsLocator extends ItemOwnerTarget {
ItemStack collarStack = targetState.getEquipment( ItemStack collarStack = targetState.getEquipment(
BodyRegionV2.NECK BodyRegionV2.NECK
); );
if (CollarHelper.hasGPS(collarStack)) {
if ( if (
collarStack.getItem() instanceof CollarHelper.isOwner(collarStack, player) ||
ItemGpsCollar collarItem CollarHelper.hasPublicTracking(collarStack)
) {
if (
collarItem.isOwner(collarStack, player) ||
collarItem.hasPublicTracking(collarStack)
) { ) {
// Check if same dimension // Check if same dimension
boolean sameDimension = player boolean sameDimension = player

View File

@@ -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";
}
}

View File

@@ -259,13 +259,10 @@ public class ItemKey extends ItemOwnerTarget {
ItemStack collarStack = targetState.getEquipment(BodyRegionV2.NECK); ItemStack collarStack = targetState.getEquipment(BodyRegionV2.NECK);
if (collarStack.isEmpty()) return; if (collarStack.isEmpty()) return;
if ( if (com.tiedup.remake.v2.bondage.CollarHelper.isCollar(collarStack)) {
collarStack.getItem() instanceof
com.tiedup.remake.items.base.ItemCollar collar
) {
// Add player as owner to the collar (if not already) // Add player as owner to the collar (if not already)
if (!collar.getOwners(collarStack).contains(player.getUUID())) { if (!com.tiedup.remake.v2.bondage.CollarHelper.isOwner(collarStack, player)) {
collar.addOwner(collarStack, player); com.tiedup.remake.v2.bondage.CollarHelper.addOwner(collarStack, player);
// Update the collar in the target's inventory // Update the collar in the target's inventory
targetState.equip(BodyRegionV2.NECK, collarStack); targetState.equip(BodyRegionV2.NECK, collarStack);

View File

@@ -332,15 +332,12 @@ public class ItemLockpick extends Item {
); );
if (collar.isEmpty()) return; if (collar.isEmpty()) return;
if ( if (com.tiedup.remake.v2.bondage.CollarHelper.canShock(collar)) {
collar.getItem() instanceof
com.tiedup.remake.items.ItemShockCollar shockCollar
) {
// Shock the player // Shock the player
state.shockKidnapped(" (Failed lockpick attempt)", 2.0f); state.shockKidnapped(" (Failed lockpick attempt)", 2.0f);
// Notify owners // Notify owners
notifyOwnersLockpickAttempt(player, collar, shockCollar); notifyOwnersLockpickAttempt(player, collar);
TiedUpMod.LOGGER.info( TiedUpMod.LOGGER.info(
"[LOCKPICK] {} was shocked for failed lockpick attempt", "[LOCKPICK] {} was shocked for failed lockpick attempt",
@@ -354,8 +351,7 @@ public class ItemLockpick extends Item {
*/ */
private static void notifyOwnersLockpickAttempt( private static void notifyOwnersLockpickAttempt(
Player player, Player player,
ItemStack collar, ItemStack collar
com.tiedup.remake.items.ItemShockCollar shockCollar
) { ) {
if (player.getServer() == null) return; if (player.getServer() == null) return;
@@ -367,7 +363,7 @@ public class ItemLockpick extends Item {
).withStyle(ChatFormatting.GOLD) ).withStyle(ChatFormatting.GOLD)
); );
List<UUID> owners = shockCollar.getOwners(collar); List<UUID> owners = com.tiedup.remake.v2.bondage.CollarHelper.getOwners(collar);
for (UUID ownerId : owners) { for (UUID ownerId : owners) {
ServerPlayer owner = player ServerPlayer owner = player
.getServer() .getServer()

View File

@@ -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";
}
}

View File

@@ -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.
*
* <p>Mechanics:</p>
* <ul>
* <li><b>Remote Shocking:</b> Can be triggered by anyone holding a linked Shocker Controller.</li>
* <li><b>Struggle Penalty:</b> If locked, has a chance to shock the wearer during struggle attempts, interrupting them.</li>
* <li><b>Public Mode:</b> Can be set to public mode, allowing anyone to shock the wearer even if they aren't the owner.</li>
* </ul>
*/
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<Component> 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<ItemStack> 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);
}
}

View File

@@ -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.
*
*
* <p>Mechanics:</p>
* <ul>
* <li><b>Self-Triggering:</b> Has an internal timer stored in NBT that shocks the entity when it reaches 0.</li>
* <li><b>Unstruggable:</b> By default, cannot be escaped via struggle mechanics (requires key).</li>
* </ul>
*/
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;
}
}

View File

@@ -3,8 +3,8 @@ package com.tiedup.remake.items;
import com.tiedup.remake.core.ModSounds; import com.tiedup.remake.core.ModSounds;
import com.tiedup.remake.core.SystemMessageManager; import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.core.TiedUpMod; import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.items.base.ItemOwnerTarget; import com.tiedup.remake.items.base.ItemOwnerTarget;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.state.IRestrainable; import com.tiedup.remake.state.IRestrainable;
import com.tiedup.remake.util.KidnappedHelper; import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.v2.BodyRegionV2; import com.tiedup.remake.v2.BodyRegionV2;
@@ -77,12 +77,11 @@ public class ItemShockerController extends ItemOwnerTarget {
BodyRegionV2.NECK BodyRegionV2.NECK
); );
if ( if (
collar.getItem() instanceof CollarHelper.isCollar(collar) &&
ItemCollar collarItem && CollarHelper.hasNickname(collar)
collarItem.hasNickname(collar)
) { ) {
displayName = displayName =
collarItem.getNickname(collar) + CollarHelper.getNickname(collar) +
" (" + " (" +
displayName + displayName +
")"; ")";
@@ -330,12 +329,10 @@ public class ItemShockerController extends ItemOwnerTarget {
IRestrainable state = KidnappedHelper.getKidnappedState(entity); IRestrainable state = KidnappedHelper.getKidnappedState(entity);
if (state != null && state.hasCollar()) { if (state != null && state.hasCollar()) {
ItemStack collarStack = state.getEquipment(BodyRegionV2.NECK); ItemStack collarStack = state.getEquipment(BodyRegionV2.NECK);
if (CollarHelper.canShock(collarStack)) {
if ( if (
collarStack.getItem() instanceof ItemShockCollar collarItem CollarHelper.isOwner(collarStack, ownerId) ||
) { CollarHelper.isPublicShock(collarStack)
if (
collarItem.getOwners(collarStack).contains(ownerId) ||
collarItem.isPublic(collarStack)
) { ) {
targets.add(entity); targets.add(entity);
} }

View File

@@ -2,9 +2,11 @@ package com.tiedup.remake.items;
import com.tiedup.remake.blocks.ModBlocks; import com.tiedup.remake.blocks.ModBlocks;
import com.tiedup.remake.core.TiedUpMod; import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.KidnapperItemSelector;
import com.tiedup.remake.items.base.*; import com.tiedup.remake.items.base.*;
import com.tiedup.remake.v2.V2Items; 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.core.registries.Registries;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.world.item.CreativeModeTab; import net.minecraft.world.item.CreativeModeTab;
@@ -16,7 +18,7 @@ import net.minecraftforge.registries.RegistryObject;
* Creative Mode Tabs Registration * Creative Mode Tabs Registration
* Defines the creative inventory tabs where TiedUp items will appear. * 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 @SuppressWarnings("null") // Minecraft API guarantees non-null returns
public class ModCreativeTabs { public class ModCreativeTabs {
@@ -28,126 +30,27 @@ public class ModCreativeTabs {
CREATIVE_MODE_TABS.register("tiedup_tab", () -> CREATIVE_MODE_TABS.register("tiedup_tab", () ->
CreativeModeTab.builder() CreativeModeTab.builder()
.title(Component.translatable("itemGroup.tiedup")) .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) -> { .displayItems((parameters, output) -> {
// ========== BINDS (from enum) ========== // ========== DATA-DRIVEN BONDAGE ITEMS ==========
for (BindVariant variant : BindVariant.values()) { // All binds, gags, blindfolds, earplugs, mittens, collars,
// Add base item // hood, medical gag, etc. are now data-driven
output.accept(ModItems.getBind(variant)); for (DataDrivenItemDefinition def : DataDrivenItemRegistry.getAll()) {
// 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( output.accept(
KidnapperItemSelector.createBind( DataDrivenBondageItem.createStack(def.id())
variant,
color
)
); );
} }
}
}
}
// ========== 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 ========== // ========== CLOTHES ==========
output.accept(ModItems.CLOTHES.get()); 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) ========== // ========== KNIVES (from enum) ==========
for (KnifeVariant variant : KnifeVariant.values()) { for (KnifeVariant variant : KnifeVariant.values()) {
output.accept(ModItems.getKnife(variant)); output.accept(ModItems.getKnife(variant));
@@ -196,15 +99,6 @@ public class ModCreativeTabs {
com.tiedup.remake.v2.bondage.V2BondageItems.V2_HANDCUFFS.get() 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 ========== // ========== FURNITURE PLACER ITEMS ==========
for (com.tiedup.remake.v2.furniture.FurnitureDefinition def : com.tiedup.remake.v2.furniture.FurnitureRegistry.getAll()) { for (com.tiedup.remake.v2.furniture.FurnitureDefinition def : com.tiedup.remake.v2.furniture.FurnitureRegistry.getAll()) {
output.accept( output.accept(

View File

@@ -3,7 +3,6 @@ package com.tiedup.remake.items;
import com.tiedup.remake.core.TiedUpMod; import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.ModEntities; import com.tiedup.remake.entities.ModEntities;
import com.tiedup.remake.items.base.*; import com.tiedup.remake.items.base.*;
import com.tiedup.remake.items.bondage3d.gags.ItemBallGag3D;
import com.tiedup.remake.items.clothes.GenericClothes; import com.tiedup.remake.items.clothes.GenericClothes;
import java.util.EnumMap; import java.util.EnumMap;
import java.util.Map; import java.util.Map;
@@ -17,14 +16,9 @@ import net.minecraftforge.registries.RegistryObject;
* Mod Items Registration * Mod Items Registration
* Handles registration of all TiedUp items using DeferredRegister. * Handles registration of all TiedUp items using DeferredRegister.
* *
* Refactored with Factory Pattern: * V1 bondage items (binds, gags, blindfolds, earplugs, mittens, collars, hood, medical gag)
* - Binds, Gags, Blindfolds, Earplugs, Knives use EnumMaps and factory methods * have been removed. All bondage items are now data-driven via DataDrivenItemRegistry.
* - Complex items (collars, whip, chloroform, etc.) remain individual registrations * Only non-bondage items and knives remain here.
*
* 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
*/ */
public class ModItems { public class ModItems {
@@ -34,42 +28,7 @@ public class ModItems {
TiedUpMod.MOD_ID TiedUpMod.MOD_ID
); );
// ========== FACTORY-BASED ITEMS ========== // ========== KNIVES (still V1 — not bondage items) ==========
/**
* All bind items (15 variants via BindVariant enum)
*/
public static final Map<BindVariant, RegistryObject<Item>> 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<GagVariant, RegistryObject<Item>> 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<Item> BALL_GAG_3D = ITEMS.register(
"ball_gag_3d",
ItemBallGag3D::new
);
/**
* All blindfold items (2 variants via BlindfoldVariant enum)
*/
public static final Map<BlindfoldVariant, RegistryObject<Item>> BLINDFOLDS =
registerAllBlindfolds();
/**
* All earplugs items (1 variant via EarplugsVariant enum)
*/
public static final Map<EarplugsVariant, RegistryObject<Item>> EARPLUGS =
registerAllEarplugs();
/** /**
* All knife items (3 variants via KnifeVariant enum) * All knife items (3 variants via KnifeVariant enum)
@@ -77,12 +36,6 @@ public class ModItems {
public static final Map<KnifeVariant, RegistryObject<Item>> KNIVES = public static final Map<KnifeVariant, RegistryObject<Item>> KNIVES =
registerAllKnives(); registerAllKnives();
/**
* All mittens items (1 variant via MittensVariant enum)
*/
public static final Map<MittensVariant, RegistryObject<Item>> MITTENS =
registerAllMittens();
/** /**
* Clothes item - uses dynamic textures from URLs. * Clothes item - uses dynamic textures from URLs.
* Users can create presets via anvil naming. * Users can create presets via anvil naming.
@@ -92,48 +45,8 @@ public class ModItems {
GenericClothes::new GenericClothes::new
); );
// ========== COMPLEX ITEMS (individual registrations) ========== // ========== TOOLS ==========
// Medical gag - combo item with IHasBlindingEffect
public static final RegistryObject<Item> MEDICAL_GAG = ITEMS.register(
"medical_gag",
ItemMedicalGag::new
);
// Hood - combo item
public static final RegistryObject<Item> HOOD = ITEMS.register(
"hood",
ItemHood::new
);
// Collars - complex logic
public static final RegistryObject<Item> CLASSIC_COLLAR = ITEMS.register(
"classic_collar",
ItemClassicCollar::new
);
public static final RegistryObject<Item> SHOCK_COLLAR = ITEMS.register(
"shock_collar",
ItemShockCollar::new
);
public static final RegistryObject<Item> SHOCK_COLLAR_AUTO = ITEMS.register(
"shock_collar_auto",
ItemShockCollarAuto::new
);
public static final RegistryObject<Item> GPS_COLLAR = ITEMS.register(
"gps_collar",
ItemGpsCollar::new
);
// Choke Collar - Pet play collar used by Masters
public static final RegistryObject<Item> CHOKE_COLLAR = ITEMS.register(
"choke_collar",
ItemChokeCollar::new
);
// Tools with complex behavior
public static final RegistryObject<Item> WHIP = ITEMS.register( public static final RegistryObject<Item> WHIP = ITEMS.register(
"whip", "whip",
ItemWhip::new ItemWhip::new
@@ -213,13 +126,11 @@ public class ModItems {
// ========== CELL SYSTEM ITEMS ========== // ========== CELL SYSTEM ITEMS ==========
// Admin Wand - Structure marker placement and Cell Core management
public static final RegistryObject<Item> ADMIN_WAND = ITEMS.register( public static final RegistryObject<Item> ADMIN_WAND = ITEMS.register(
"admin_wand", "admin_wand",
ItemAdminWand::new ItemAdminWand::new
); );
// Cell Key - Universal key for iron bar doors
public static final RegistryObject<Item> CELL_KEY = ITEMS.register( public static final RegistryObject<Item> CELL_KEY = ITEMS.register(
"cell_key", "cell_key",
ItemCellKey::new ItemCellKey::new
@@ -227,7 +138,6 @@ public class ModItems {
// ========== SLAVE TRADER SYSTEM ========== // ========== SLAVE TRADER SYSTEM ==========
// Token - Access pass for kidnapper camps
public static final RegistryObject<Item> TOKEN = ITEMS.register( public static final RegistryObject<Item> TOKEN = ITEMS.register(
"token", "token",
ItemToken::new ItemToken::new
@@ -252,72 +162,6 @@ public class ModItems {
// ========== FACTORY METHODS ========== // ========== FACTORY METHODS ==========
private static Map<BindVariant, RegistryObject<Item>> registerAllBinds() {
Map<BindVariant, RegistryObject<Item>> 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<GagVariant, RegistryObject<Item>> registerAllGags() {
Map<GagVariant, RegistryObject<Item>> 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<Item>
> registerAllBlindfolds() {
Map<BlindfoldVariant, RegistryObject<Item>> 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<Item>
> registerAllEarplugs() {
Map<EarplugsVariant, RegistryObject<Item>> 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<KnifeVariant, RegistryObject<Item>> registerAllKnives() { private static Map<KnifeVariant, RegistryObject<Item>> registerAllKnives() {
Map<KnifeVariant, RegistryObject<Item>> map = new EnumMap<>( Map<KnifeVariant, RegistryObject<Item>> map = new EnumMap<>(
KnifeVariant.class KnifeVariant.class
@@ -333,62 +177,8 @@ public class ModItems {
return map; return map;
} }
private static Map<
MittensVariant,
RegistryObject<Item>
> registerAllMittens() {
Map<MittensVariant, RegistryObject<Item>> map = new EnumMap<>(
MittensVariant.class
);
for (MittensVariant variant : MittensVariant.values()) {
map.put(
variant,
ITEMS.register(variant.getRegistryName(), () ->
new GenericMittens(variant)
)
);
}
return map;
}
// ========== HELPER ACCESSORS ========== // ========== 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. * Get a knife item by variant.
* @param variant The knife variant * @param variant The knife variant
@@ -397,13 +187,4 @@ public class ModItems {
public static Item getKnife(KnifeVariant variant) { public static Item getKnife(KnifeVariant variant) {
return KNIVES.get(variant).get(); 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();
}
} }

View File

@@ -1,5 +1,8 @@
package com.tiedup.remake.items.base; 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.nbt.CompoundTag;
import net.minecraft.util.Mth; import net.minecraft.util.Mth;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
@@ -56,9 +59,12 @@ public class AdjustmentHelper {
return tag.getFloat(NBT_ADJUSTMENT_Y); return tag.getFloat(NBT_ADJUSTMENT_Y);
} }
// Fallback to item's default adjustment // Fallback to item's default adjustment from V2 AdjustableComponent
if (stack.getItem() instanceof IAdjustable adj) { AdjustableComponent comp = DataDrivenBondageItem.getComponent(
return adj.getDefaultAdjustment(); stack, ComponentType.ADJUSTABLE, AdjustableComponent.class
);
if (comp != null) {
return comp.getDefaultValue();
} }
return DEFAULT_VALUE; return DEFAULT_VALUE;
@@ -127,16 +133,15 @@ public class AdjustmentHelper {
* Check if an ItemStack's item supports adjustment. * Check if an ItemStack's item supports adjustment.
* *
* @param stack The ItemStack to check * @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) { public static boolean isAdjustable(ItemStack stack) {
if (stack.isEmpty()) { if (stack.isEmpty()) {
return false; return false;
} }
if (stack.getItem() instanceof IAdjustable adj) { return DataDrivenBondageItem.getComponent(
return adj.canBeAdjusted(); stack, ComponentType.ADJUSTABLE, AdjustableComponent.class
} ) != null;
return false;
} }
/** /**

View File

@@ -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.
*
* <p><b>Issue #12 fix:</b> 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;
}
}

View File

@@ -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.
*
* <p><b>Issue #12 fix:</b> 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;
}
}

View File

@@ -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.
*
* <p><b>Issue #12 fix:</b> 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;
}
}

View File

@@ -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.
*
* <p>Note: ItemMedicalGag is NOT included here because it implements
* IHasBlindingEffect (combo item with special behavior).
*
* <p><b>Issue #12 fix:</b> 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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.
*
* <p><b>Issue #12 fix:</b> 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;
}
}

View File

@@ -1,33 +0,0 @@
package com.tiedup.remake.items.base;
/**
* Marker interface for items that have a blinding visual effect.
*
* <p>Items implementing this interface will:
* <ul>
* <li>Apply a screen overlay when worn (client-side)</li>
* <li>Reduce the player's visibility</li>
* <li>Potentially disable certain UI elements</li>
* </ul>
*
* <h2>Usage</h2>
* <pre>{@code
* if (blindfold.getItem() instanceof IHasBlindingEffect) {
* // Apply blinding overlay
* renderBlindingOverlay();
* }
* }</pre>
*
* <h2>Implementations</h2>
* <ul>
* <li>{@link ItemBlindfold} - All blindfold items</li>
* </ul>
*
* <p>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
}

View File

@@ -1,33 +0,0 @@
package com.tiedup.remake.items.base;
/**
* Marker interface for items that have a gagging (speech muffling) effect.
*
* <p>Items implementing this interface will:
* <ul>
* <li>Convert chat messages to "mmpphh" sounds</li>
* <li>Play gagged speech sounds</li>
* <li>Potentially block certain chat commands</li>
* </ul>
*
* <h2>Usage</h2>
* <pre>{@code
* if (gag.getItem() instanceof IHasGaggingEffect) {
* // Convert chat message to gagged speech
* message = GagTalkConverter.convert(message);
* }
* }</pre>
*
* <h2>Implementations</h2>
* <ul>
* <li>{@link ItemGag} - Ball gags, tape gags, cloth gags, etc.</li>
* </ul>
*
* <p>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
}

View File

@@ -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.
*
* <p>Implements {@link IHasResistance} for the struggle/escape system.
* <p>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<String, String> 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<ItemStack> 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<Component> 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
}

View File

@@ -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<Component> 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
}

File diff suppressed because it is too large Load Diff

View File

@@ -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<Component> 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
}

View File

@@ -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<Component> 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
}

View File

@@ -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
}

View File

@@ -195,11 +195,11 @@ public abstract class ItemOwnerTarget extends Item {
BodyRegionV2.NECK BodyRegionV2.NECK
); );
if ( if (
collar.getItem() instanceof ItemCollar collarItem && com.tiedup.remake.v2.bondage.CollarHelper.isCollar(collar) &&
collarItem.hasNickname(collar) com.tiedup.remake.v2.bondage.CollarHelper.hasNickname(collar)
) { ) {
displayName = displayName =
collarItem.getNickname(collar) + com.tiedup.remake.v2.bondage.CollarHelper.getNickname(collar) +
" (" + " (" +
displayName + displayName +
")"; ")";

View File

@@ -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.
*
* <p>Mittens system - blocks hand interactions when equipped.
*
* <p><b>Issue #12 fix:</b> 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;
}
}

View File

@@ -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;
}
}

View File

@@ -166,11 +166,8 @@ public class PacketTighten {
// Check if sender owns collar // Check if sender owns collar
if (!hasPermission && closestState.hasCollar()) { if (!hasPermission && closestState.hasCollar()) {
var collarStack = closestState.getEquipment(BodyRegionV2.NECK); var collarStack = closestState.getEquipment(BodyRegionV2.NECK);
if ( if (com.tiedup.remake.v2.bondage.CollarHelper.isCollar(collarStack)) {
collarStack.getItem() instanceof if (com.tiedup.remake.v2.bondage.CollarHelper.isOwner(collarStack, tightener)) {
com.tiedup.remake.items.base.ItemCollar collar
) {
if (collar.isOwner(collarStack, tightener)) {
hasPermission = true; hasPermission = true;
} }
} }

View File

@@ -2,7 +2,6 @@ package com.tiedup.remake.network.item;
import com.tiedup.remake.core.TiedUpMod; import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.base.AdjustmentHelper; 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.network.sync.SyncManager;
import com.tiedup.remake.state.PlayerBindState; import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.v2.BodyRegionV2; import com.tiedup.remake.v2.BodyRegionV2;
@@ -152,7 +151,7 @@ public class PacketAdjustItem {
return; return;
} }
if (!(stack.getItem() instanceof IAdjustable)) { if (!AdjustmentHelper.isAdjustable(stack)) {
TiedUpMod.LOGGER.warn( TiedUpMod.LOGGER.warn(
"[PACKET] PacketAdjustItem: Item {} is not adjustable", "[PACKET] PacketAdjustItem: Item {} is not adjustable",
stack.getItem() stack.getItem()

View File

@@ -3,7 +3,6 @@ package com.tiedup.remake.network.item;
import com.tiedup.remake.core.SystemMessageManager; import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.core.TiedUpMod; import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.base.AdjustmentHelper; 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.network.sync.SyncManager;
import com.tiedup.remake.state.IBondageState; import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.state.PlayerBindState; import com.tiedup.remake.state.PlayerBindState;
@@ -195,7 +194,7 @@ public class PacketAdjustRemote {
return; return;
} }
if (!(stack.getItem() instanceof IAdjustable)) { if (!AdjustmentHelper.isAdjustable(stack)) {
TiedUpMod.LOGGER.warn( TiedUpMod.LOGGER.warn(
"[PACKET] PacketAdjustRemote: Item {} is not adjustable", "[PACKET] PacketAdjustRemote: Item {} is not adjustable",
stack.getItem() stack.getItem()
@@ -255,11 +254,8 @@ public class PacketAdjustRemote {
ItemStack collarStack = kidnapped.getEquipment( ItemStack collarStack = kidnapped.getEquipment(
BodyRegionV2.NECK BodyRegionV2.NECK
); );
if ( if (com.tiedup.remake.v2.bondage.CollarHelper.isCollar(collarStack)) {
collarStack.getItem() instanceof if (com.tiedup.remake.v2.bondage.CollarHelper.isOwner(collarStack, sender)) {
com.tiedup.remake.items.base.ItemCollar collar
) {
if (collar.isOwner(collarStack, sender)) {
return kidnapped; return kidnapped;
} }
} }

View File

@@ -454,9 +454,7 @@ public class PacketLockpickAttempt {
); );
if (collar.isEmpty()) return; if (collar.isEmpty()) return;
if ( if (com.tiedup.remake.v2.bondage.CollarHelper.canShock(collar)) {
collar.getItem() instanceof com.tiedup.remake.items.ItemShockCollar
) {
state.shockKidnapped(" (Failed lockpick attempt)", 2.0f); state.shockKidnapped(" (Failed lockpick attempt)", 2.0f);
TiedUpMod.LOGGER.info( TiedUpMod.LOGGER.info(
"[PacketLockpickAttempt] Player {} shocked for failed lockpick", "[PacketLockpickAttempt] Player {} shocked for failed lockpick",

View File

@@ -70,11 +70,8 @@ public class PacketRequestNpcInventory {
// Verify player is owner of collar (or NPC has no collar) // Verify player is owner of collar (or NPC has no collar)
if (damsel.hasCollar()) { if (damsel.hasCollar()) {
ItemStack collar = damsel.getEquipment(BodyRegionV2.NECK); ItemStack collar = damsel.getEquipment(BodyRegionV2.NECK);
if ( if (com.tiedup.remake.v2.bondage.CollarHelper.isCollar(collar)) {
collar.getItem() instanceof if (!com.tiedup.remake.v2.bondage.CollarHelper.isOwner(collar, player)) {
com.tiedup.remake.items.base.ItemCollar collarItem
) {
if (!collarItem.isOwner(collar, player)) {
TiedUpMod.LOGGER.warn( TiedUpMod.LOGGER.warn(
"[PacketRequestNpcInventory] Player {} is not owner of NPC collar", "[PacketRequestNpcInventory] Player {} is not owner of NPC collar",
player.getName().getString() player.getName().getString()

View File

@@ -2,7 +2,7 @@ package com.tiedup.remake.network.selfbondage;
import com.tiedup.remake.core.SettingsAccessor; import com.tiedup.remake.core.SettingsAccessor;
import com.tiedup.remake.core.TiedUpMod; 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.ModNetwork;
import com.tiedup.remake.network.PacketRateLimiter; import com.tiedup.remake.network.PacketRateLimiter;
import com.tiedup.remake.network.action.PacketTying; import com.tiedup.remake.network.action.PacketTying;
@@ -79,141 +79,13 @@ public class PacketSelfBondage {
// V2 bondage items — use tying task with V2 equip // V2 bondage items — use tying task with V2 equip
if (item instanceof IV2BondageItem v2Item) { if (item instanceof IV2BondageItem v2Item) {
handleV2SelfBondage(player, stack, v2Item, state); handleV2SelfBondage(player, stack, v2Item, state);
return;
} }
// V1 routes removed — all bondage items are now V2/data-driven
// 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
}); });
ctx.get().setPacketHandled(true); 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. * Handle self-bondage with a V2 bondage item.
* Uses V2TyingPlayerTask for progress, V2EquipmentHelper for equip. * 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) // Can't equip if arms are fully bound (need hands to put on accessories)
ItemStack currentBind = state.getEquipment(BodyRegionV2.ARMS); ItemStack currentBind = state.getEquipment(BodyRegionV2.ARMS);
if (!currentBind.isEmpty()) { if (!currentBind.isEmpty()) {
if (ItemBind.hasArmsBound(currentBind)) { if (BindModeHelper.hasArmsBound(currentBind)) {
TiedUpMod.LOGGER.debug( TiedUpMod.LOGGER.debug(
"[SelfBondage] {} can't self-{} - arms are bound", "[SelfBondage] {} can't self-{} - arms are bound",
player.getName().getString(), player.getName().getString(),

View File

@@ -163,12 +163,9 @@ public class PacketSlaveItemManage {
ItemStack collarStack = targetState.getEquipment( ItemStack collarStack = targetState.getEquipment(
BodyRegionV2.NECK BodyRegionV2.NECK
); );
if (com.tiedup.remake.v2.bondage.CollarHelper.isCollar(collarStack)) {
if ( if (
collarStack.getItem() instanceof !com.tiedup.remake.v2.bondage.CollarHelper.isOwner(collarStack, sender) &&
com.tiedup.remake.items.base.ItemCollar collar
) {
if (
!collar.isOwner(collarStack, sender) &&
!sender.hasPermissions(2) !sender.hasPermissions(2)
) { ) {
TiedUpMod.LOGGER.debug( TiedUpMod.LOGGER.debug(
@@ -437,15 +434,14 @@ public class PacketSlaveItemManage {
if ( if (
region == BodyRegionV2.ARMS && region == BodyRegionV2.ARMS &&
itemStack.getItem() instanceof itemStack.getItem() instanceof com.tiedup.remake.items.base.IHasResistance resistanceItem
com.tiedup.remake.items.base.ItemBind bind
) { ) {
int currentResistance = bind.getCurrentResistance( int currentResistance = resistanceItem.getCurrentResistance(
itemStack, itemStack,
target target
); );
int lockResistance = lockable.getLockResistance(); // Configurable via ModConfig int lockResistance = lockable.getLockResistance(); // Configurable via ModConfig
bind.setCurrentResistance( resistanceItem.setCurrentResistance(
itemStack, itemStack,
currentResistance + lockResistance currentResistance + lockResistance
); );
@@ -599,8 +595,7 @@ public class PacketSlaveItemManage {
) { ) {
if ( if (
collarStack.isEmpty() || collarStack.isEmpty() ||
!(collarStack.getItem() instanceof !com.tiedup.remake.v2.bondage.CollarHelper.isCollar(collarStack)
com.tiedup.remake.items.base.ItemCollar collar)
) { ) {
TiedUpMod.LOGGER.debug( TiedUpMod.LOGGER.debug(
"[PacketSlaveItemManage] No collar for bondage service toggle" "[PacketSlaveItemManage] No collar for bondage service toggle"
@@ -609,7 +604,7 @@ public class PacketSlaveItemManage {
} }
// Check if cell is configured (required for bondage service) // 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( TiedUpMod.LOGGER.debug(
"[PacketSlaveItemManage] Cannot enable bondage service: no cell configured" "[PacketSlaveItemManage] Cannot enable bondage service: no cell configured"
); );
@@ -623,8 +618,8 @@ public class PacketSlaveItemManage {
} }
// Toggle bondage service // Toggle bondage service
boolean currentState = collar.isBondageServiceEnabled(collarStack); boolean currentState = com.tiedup.remake.v2.bondage.CollarHelper.isBondageServiceEnabled(collarStack);
collar.setBondageServiceEnabled(collarStack, !currentState); com.tiedup.remake.v2.bondage.CollarHelper.setBondageServiceEnabled(collarStack, !currentState);
String newState = !currentState ? "enabled" : "disabled"; String newState = !currentState ? "enabled" : "disabled";
TiedUpMod.LOGGER.info( TiedUpMod.LOGGER.info(

View File

@@ -243,22 +243,23 @@ public class PacketBuyCaptive {
kidnappedState.getEquipment(BodyRegionV2.NECK); kidnappedState.getEquipment(BodyRegionV2.NECK);
if ( if (
!collar.isEmpty() && !collar.isEmpty() &&
collar.getItem() instanceof com.tiedup.remake.v2.bondage.CollarHelper.isCollar(collar)
com.tiedup.remake.items.base.ItemCollar collarItem
) { ) {
// Remove all existing owners from collar NBT // Remove all existing owners from collar NBT
for (UUID ownerId : new java.util.ArrayList<>( 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 // Add buyer as new owner
collarItem.addOwner( com.tiedup.remake.v2.bondage.CollarHelper.addOwner(
collar, collar,
buyer.getUUID(), buyer.getUUID(),
buyer.getName().getString() 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 // Re-apply modified collar to persist NBT changes
kidnappedState.equip(BodyRegionV2.NECK, collar); kidnappedState.equip(BodyRegionV2.NECK, collar);

View File

@@ -757,10 +757,9 @@ public class PrisonerService {
if ( if (
!collar.isEmpty() && !collar.isEmpty() &&
collar.getItem() instanceof com.tiedup.remake.v2.bondage.CollarHelper.isCollar(collar)
com.tiedup.remake.items.base.ItemCollar collarItem
) { ) {
List<UUID> nbtOwners = collarItem.getOwners(collar); List<UUID> nbtOwners = com.tiedup.remake.v2.bondage.CollarHelper.getOwners(collar);
if (!nbtOwners.isEmpty()) { if (!nbtOwners.isEmpty()) {
for (UUID ownerUUID : nbtOwners) { for (UUID ownerUUID : nbtOwners) {
collars.registerCollar(playerId, ownerUUID); collars.registerCollar(playerId, ownerUUID);

View File

@@ -4,12 +4,14 @@ import com.tiedup.remake.core.ModSounds;
import com.tiedup.remake.core.SystemMessageManager; import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.core.SystemMessageManager.MessageCategory; import com.tiedup.remake.core.SystemMessageManager.MessageCategory;
import com.tiedup.remake.core.TiedUpMod; 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.state.hosts.IPlayerBindStateHost;
import com.tiedup.remake.util.GameConstants; import com.tiedup.remake.util.GameConstants;
import com.tiedup.remake.util.time.Timer; import com.tiedup.remake.util.time.Timer;
import com.tiedup.remake.v2.BodyRegionV2; 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.List;
import java.util.UUID; import java.util.UUID;
import net.minecraft.ChatFormatting; import net.minecraft.ChatFormatting;
@@ -128,17 +130,16 @@ public class PlayerShockCollar {
// Flags set inside lock, actions performed outside // Flags set inside lock, actions performed outside
boolean shouldShockAuto = false; boolean shouldShockAuto = false;
boolean shouldShockGPS = false; boolean shouldShockGPS = false;
ItemGpsCollar gpsCollar = null; ItemStack gpsStackCopy = null;
ItemStack gpsStack = null;
synchronized (lockTimerAutoShock) { synchronized (lockTimerAutoShock) {
ItemStack collarStack = getCurrentCollar(); ItemStack collarStack = getCurrentCollar();
if (collarStack.isEmpty()) return; if (collarStack.isEmpty()) return;
// Auto-Shock Collar handling // Auto-Shock Collar handling: collar can shock AND has auto interval > 0
if ( if (CollarHelper.canShock(collarStack) && CollarHelper.getShockInterval(collarStack) > 0) {
collarStack.getItem() instanceof ItemShockCollarAuto collarShock int interval = CollarHelper.getShockInterval(collarStack);
) {
if ( if (
timerAutoShockCollar != null && timerAutoShockCollar != null &&
timerAutoShockCollar.isExpired() timerAutoShockCollar.isExpired()
@@ -151,40 +152,38 @@ public class PlayerShockCollar {
timerAutoShockCollar.isExpired() timerAutoShockCollar.isExpired()
) { ) {
timerAutoShockCollar = new Timer( timerAutoShockCollar = new Timer(
collarShock.getInterval() / interval / GameConstants.TICKS_PER_SECOND,
GameConstants.TICKS_PER_SECOND,
player.level() player.level()
); );
} }
} }
// GPS Collar handling // GPS Collar handling
else if (collarStack.getItem() instanceof ItemGpsCollar gps) { else if (CollarHelper.hasGPS(collarStack)) {
if ( if (
gps.isActive(collarStack) && CollarHelper.isActive(collarStack) &&
(timerAutoShockCollar == null || (timerAutoShockCollar == null ||
timerAutoShockCollar.isExpired()) timerAutoShockCollar.isExpired())
) { ) {
List<ItemGpsCollar.SafeSpot> safeSpots = gps.getSafeSpots( GpsComponent gpsComp = DataDrivenBondageItem.getComponent(
collarStack collarStack, ComponentType.GPS, GpsComponent.class
); );
if (safeSpots != null && !safeSpots.isEmpty()) { int safeZoneRadius = gpsComp != null ? gpsComp.getSafeZoneRadius() : 50;
boolean isSafe = false; // Shock interval from ShockComponent (GPS collars also have shock capability)
for (ItemGpsCollar.SafeSpot spot : safeSpots) { int shockInterval = CollarHelper.getShockInterval(collarStack);
if (spot.isInside(player)) { if (shockInterval <= 0) shockInterval = 100; // Fallback: 5 seconds
isSafe = true;
break; // 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) { if (!isSafe) {
timerAutoShockCollar = new Timer( timerAutoShockCollar = new Timer(
gps.getShockInterval(collarStack) / shockInterval / GameConstants.TICKS_PER_SECOND,
GameConstants.TICKS_PER_SECOND,
player.level() player.level()
); );
shouldShockGPS = true; shouldShockGPS = true;
gpsCollar = gps; gpsStackCopy = collarStack.copy();
gpsStack = collarStack.copy();
}
} }
} }
} }
@@ -195,30 +194,58 @@ public class PlayerShockCollar {
this.shockKidnapped(); this.shockKidnapped();
} }
if (shouldShockGPS && gpsCollar != null) { if (shouldShockGPS && gpsStackCopy != null) {
this.shockKidnapped( this.shockKidnapped(
" Return back to your allowed area!", " Return back to your allowed area!",
GameConstants.DEFAULT_SHOCK_DAMAGE 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. * Sends a global alert to masters when a slave violates their GPS zone.
* Private helper method. * Private helper method.
*/ */
private void warnOwnersGPSViolation(ItemGpsCollar gps, ItemStack stack) { private void warnOwnersGPSViolation(ItemStack stack) {
Player player = host.getPlayer(); Player player = host.getPlayer();
if (player.getServer() == null) return; if (player.getServer() == null) return;
if (!CollarHelper.shouldWarnMasters(stack)) return;
// Format: "ALERT: <player name> is outside the safe zone!" // Format: "ALERT: <player name> is outside the safe zone!"
String alertMessage = String.format( String alertMessage = String.format(
SystemMessageManager.getTemplate(MessageCategory.GPS_OWNER_ALERT), SystemMessageManager.getTemplate(MessageCategory.GPS_OWNER_ALERT),
player.getName().getString() player.getName().getString()
); );
for (UUID ownerId : gps.getOwners(stack)) { for (UUID ownerId : CollarHelper.getOwners(stack)) {
ServerPlayer owner = player ServerPlayer owner = player
.getServer() .getServer()
.getPlayerList() .getPlayerList()

View File

@@ -133,10 +133,9 @@ public class PlayerStateQuery {
BodyRegionV2.MOUTH BodyRegionV2.MOUTH
); );
if (gag.isEmpty()) return false; if (gag.isEmpty()) return false;
return ( return com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem.getComponent(
gag.getItem() instanceof gag, com.tiedup.remake.v2.bondage.component.ComponentType.GAGGING,
com.tiedup.remake.items.base.IHasGaggingEffect com.tiedup.remake.v2.bondage.component.GaggingComponent.class) != null;
);
} }
/** /**
@@ -150,10 +149,9 @@ public class PlayerStateQuery {
BodyRegionV2.EYES BodyRegionV2.EYES
); );
if (blindfold.isEmpty()) return false; if (blindfold.isEmpty()) return false;
return ( return com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem.getComponent(
blindfold.getItem() instanceof blindfold, com.tiedup.remake.v2.bondage.component.ComponentType.BLINDING,
com.tiedup.remake.items.base.IHasBlindingEffect com.tiedup.remake.v2.bondage.component.BlindingComponent.class) != null;
);
} }
/** /**

View File

@@ -199,10 +199,13 @@ public class StruggleAccessory extends StruggleState {
if ( if (
!collar.isEmpty() && !collar.isEmpty() &&
collar.getItem() instanceof com.tiedup.remake.v2.bondage.CollarHelper.canShock(collar)
com.tiedup.remake.items.ItemShockCollar shockCollar
) { ) {
return shockCollar.notifyStruggle(player, collar); // V2 shock collarnotify via IHasResistance
if (collar.getItem() instanceof com.tiedup.remake.items.base.IHasResistance resistance) {
resistance.notifyStruggle(player);
}
return true;
} }
return true; // No collar, proceed normally return true; // No collar, proceed normally
} }

View File

@@ -6,7 +6,6 @@ import com.tiedup.remake.core.SystemMessageManager.MessageCategory;
import com.tiedup.remake.core.TiedUpMod; import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.base.IHasResistance; import com.tiedup.remake.items.base.IHasResistance;
import com.tiedup.remake.items.base.ILockable; 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.IPlayerLeashAccess;
import com.tiedup.remake.state.PlayerBindState; import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.v2.BodyRegionV2; import com.tiedup.remake.v2.BodyRegionV2;
@@ -155,10 +154,6 @@ public class StruggleBinds extends StruggleState {
); );
if (!collar.isEmpty() && CollarHelper.canShock(collar)) { 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 // V2 shock collar — notify via IHasResistance if available
if (collar.getItem() instanceof IHasResistance resistance) { if (collar.getItem() instanceof IHasResistance resistance) {
resistance.notifyStruggle(player); resistance.notifyStruggle(player);
@@ -339,8 +334,6 @@ public class StruggleBinds extends StruggleState {
); );
if (comp != null) { if (comp != null) {
baseResistance = comp.getBaseResistance(); baseResistance = comp.getBaseResistance();
} else if (bindStack.getItem() instanceof ItemBind bind) {
baseResistance = SettingsAccessor.getBindResistance(bind.getItemName());
} else { } else {
baseResistance = 100; baseResistance = 100;
} }

View File

@@ -4,7 +4,6 @@ import com.tiedup.remake.core.SystemMessageManager.MessageCategory;
import com.tiedup.remake.core.TiedUpMod; import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.base.IHasResistance; import com.tiedup.remake.items.base.IHasResistance;
import com.tiedup.remake.items.base.ILockable; import com.tiedup.remake.items.base.ILockable;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.state.PlayerBindState; import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.v2.BodyRegionV2; import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.CollarHelper; import com.tiedup.remake.v2.bondage.CollarHelper;
@@ -139,10 +138,6 @@ public class StruggleCollar extends StruggleState {
ItemStack collar = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK); ItemStack collar = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK);
if (!collar.isEmpty() && CollarHelper.canShock(collar)) { 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 // V2 shock collar — notify via IHasResistance
if (collar.getItem() instanceof IHasResistance resistance) { if (collar.getItem() instanceof IHasResistance resistance) {
resistance.notifyStruggle(player); resistance.notifyStruggle(player);

View File

@@ -1,11 +1,6 @@
package com.tiedup.remake.util; package com.tiedup.remake.util;
import com.tiedup.remake.blocks.entity.IBondageItemHolder; 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.BodyRegionV2;
import com.tiedup.remake.v2.bondage.BindModeHelper; import com.tiedup.remake.v2.bondage.BindModeHelper;
import com.tiedup.remake.v2.bondage.CollarHelper; import com.tiedup.remake.v2.bondage.CollarHelper;
@@ -53,31 +48,30 @@ public final class BondageItemLoaderUtility {
ItemStack stack, ItemStack stack,
Player player 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)); holder.setBind(stack.copyWithCount(1));
if (!player.isCreative()) stack.shrink(1); if (!player.isCreative()) stack.shrink(1);
return true; return true;
} }
if ((stack.getItem() instanceof ItemGag if (DataDrivenBondageItem.getComponent(stack, ComponentType.GAGGING, GaggingComponent.class) != null
|| DataDrivenBondageItem.getComponent(stack, ComponentType.GAGGING, GaggingComponent.class) != null)
&& holder.getGag().isEmpty()) { && holder.getGag().isEmpty()) {
holder.setGag(stack.copyWithCount(1)); holder.setGag(stack.copyWithCount(1));
if (!player.isCreative()) stack.shrink(1); if (!player.isCreative()) stack.shrink(1);
return true; return true;
} }
if ((stack.getItem() instanceof ItemBlindfold || isDataDrivenForRegion(stack, BodyRegionV2.EYES)) if (isDataDrivenForRegion(stack, BodyRegionV2.EYES)
&& holder.getBlindfold().isEmpty()) { && holder.getBlindfold().isEmpty()) {
holder.setBlindfold(stack.copyWithCount(1)); holder.setBlindfold(stack.copyWithCount(1));
if (!player.isCreative()) stack.shrink(1); if (!player.isCreative()) stack.shrink(1);
return true; return true;
} }
if ((stack.getItem() instanceof ItemEarplugs || isDataDrivenForRegion(stack, BodyRegionV2.EARS)) if (isDataDrivenForRegion(stack, BodyRegionV2.EARS)
&& holder.getEarplugs().isEmpty()) { && holder.getEarplugs().isEmpty()) {
holder.setEarplugs(stack.copyWithCount(1)); holder.setEarplugs(stack.copyWithCount(1));
if (!player.isCreative()) stack.shrink(1); if (!player.isCreative()) stack.shrink(1);
return true; return true;
} }
if ((stack.getItem() instanceof ItemCollar || CollarHelper.isCollar(stack)) if (CollarHelper.isCollar(stack)
&& holder.getCollar().isEmpty()) { && holder.getCollar().isEmpty()) {
holder.setCollar(stack.copyWithCount(1)); holder.setCollar(stack.copyWithCount(1));
if (!player.isCreative()) stack.shrink(1); if (!player.isCreative()) stack.shrink(1);
@@ -96,14 +90,6 @@ public final class BondageItemLoaderUtility {
*/ */
public static boolean isLoadableBondageItem(ItemStack stack) { public static boolean isLoadableBondageItem(ItemStack stack) {
if (stack.isEmpty()) return false; 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 // V2 data-driven items: bind, gag, blindfold, earplugs, collar
if (BindModeHelper.isBindItem(stack)) return true; if (BindModeHelper.isBindItem(stack)) return true;
if (DataDrivenBondageItem.getComponent(stack, ComponentType.GAGGING, GaggingComponent.class) != null) return true; if (DataDrivenBondageItem.getComponent(stack, ComponentType.GAGGING, GaggingComponent.class) != null) return true;

View File

@@ -1,6 +1,5 @@
package com.tiedup.remake.v2.bondage; package com.tiedup.remake.v2.bondage;
import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.v2.BodyRegionV2; import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemDefinition; import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemDefinition;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemRegistry; import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemRegistry;
@@ -42,8 +41,8 @@ public final class BindModeHelper {
if (def != null) { if (def != null) {
return def.occupiedRegions().contains(BodyRegionV2.ARMS); return def.occupiedRegions().contains(BodyRegionV2.ARMS);
} }
// V1 fallback // No V2 definition found
return stack.getItem() instanceof ItemBind; return false;
} }
/** /**

View File

@@ -1,10 +1,9 @@
package com.tiedup.remake.v2.bondage; package com.tiedup.remake.v2.bondage;
import com.tiedup.remake.items.ItemChokeCollar; import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.ItemGpsCollar; import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.items.ItemShockCollar; import com.tiedup.remake.entities.ai.kidnapper.KidnapperState;
import com.tiedup.remake.items.ItemShockCollarAuto; import com.tiedup.remake.state.CollarRegistry;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.v2.bondage.component.ChokingComponent; import com.tiedup.remake.v2.bondage.component.ChokingComponent;
import com.tiedup.remake.v2.bondage.component.ComponentType; import com.tiedup.remake.v2.bondage.component.ComponentType;
import com.tiedup.remake.v2.bondage.component.GpsComponent; 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.CompoundTag;
import net.minecraft.nbt.ListTag; import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.Tag; 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.entity.player.Player;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
import net.minecraft.world.phys.AABB;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
/** /**
@@ -29,15 +31,16 @@ public final class CollarHelper {
private CollarHelper() {} private CollarHelper() {}
// Thread-local flag to suppress collar removal alerts during programmatic unequip
private static final ThreadLocal<Boolean> SUPPRESS_REMOVAL_ALERT =
ThreadLocal.withInitial(() -> false);
// ===== DETECTION ===== // ===== 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) { public static boolean isCollar(ItemStack stack) {
if (stack.isEmpty()) return false; if (stack.isEmpty()) return false;
if (DataDrivenBondageItem.getComponent(stack, ComponentType.OWNERSHIP, OwnershipComponent.class) != null) { return DataDrivenBondageItem.getComponent(stack, ComponentType.OWNERSHIP, OwnershipComponent.class) != null;
return true;
}
return stack.getItem() instanceof ItemCollar;
} }
// ===== OWNERSHIP (NBT: "owners") ===== // ===== OWNERSHIP (NBT: "owners") =====
@@ -267,13 +270,10 @@ public final class CollarHelper {
// ===== SHOCK ===== // ===== 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) { public static boolean canShock(ItemStack stack) {
if (stack.isEmpty()) return false; if (stack.isEmpty()) return false;
if (DataDrivenBondageItem.getComponent(stack, ComponentType.SHOCK, ShockComponent.class) != null) { return DataDrivenBondageItem.getComponent(stack, ComponentType.SHOCK, ShockComponent.class) != null;
return true;
}
return stack.getItem() instanceof ItemShockCollar;
} }
public static boolean isPublicShock(ItemStack stack) { public static boolean isPublicShock(ItemStack stack) {
@@ -285,27 +285,21 @@ public final class CollarHelper {
stack.getOrCreateTag().putBoolean("public_mode", publicMode); 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) { public static int getShockInterval(ItemStack stack) {
ShockComponent comp = DataDrivenBondageItem.getComponent( ShockComponent comp = DataDrivenBondageItem.getComponent(
stack, ComponentType.SHOCK, ShockComponent.class stack, ComponentType.SHOCK, ShockComponent.class
); );
if (comp != null) return comp.getAutoInterval(); if (comp != null) return comp.getAutoInterval();
if (stack.getItem() instanceof ItemShockCollarAuto auto) {
return auto.getInterval();
}
return 0; return 0;
} }
// ===== GPS ===== // ===== GPS =====
// True if the collar has GPS capabilities // True if the collar has GPS capabilities (V2 GpsComponent)
public static boolean hasGPS(ItemStack stack) { public static boolean hasGPS(ItemStack stack) {
if (stack.isEmpty()) return false; if (stack.isEmpty()) return false;
if (DataDrivenBondageItem.getComponent(stack, ComponentType.GPS, GpsComponent.class) != null) { return DataDrivenBondageItem.getComponent(stack, ComponentType.GPS, GpsComponent.class) != null;
return true;
}
return stack.getItem() instanceof ItemGpsCollar;
} }
public static boolean hasPublicTracking(ItemStack stack) { public static boolean hasPublicTracking(ItemStack stack) {
@@ -330,13 +324,10 @@ public final class CollarHelper {
// ===== CHOKE ===== // ===== 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) { public static boolean isChokeCollar(ItemStack stack) {
if (stack.isEmpty()) return false; if (stack.isEmpty()) return false;
if (DataDrivenBondageItem.getComponent(stack, ComponentType.CHOKING, ChokingComponent.class) != null) { return DataDrivenBondageItem.getComponent(stack, ComponentType.CHOKING, ChokingComponent.class) != null;
return true;
}
return stack.getItem() instanceof ItemChokeCollar;
} }
public static boolean isChoking(ItemStack stack) { public static boolean isChoking(ItemStack stack) {
@@ -361,11 +352,69 @@ public final class CollarHelper {
// Executes the action with collar removal alerts suppressed // Executes the action with collar removal alerts suppressed
public static void runWithSuppressedAlert(Runnable action) { 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) // True if removal alerts are currently suppressed (ThreadLocal state)
public static boolean isRemovalAlertSuppressed() { 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<EntityKidnapper> 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()
);
}
}
} }
} }

View File

@@ -1,6 +1,5 @@
package com.tiedup.remake.v2.bondage; package com.tiedup.remake.v2.bondage;
import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.items.base.PoseType; import com.tiedup.remake.items.base.PoseType;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemDefinition; import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemDefinition;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemRegistry; import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemRegistry;
@@ -26,10 +25,6 @@ public final class PoseTypeHelper {
return PoseType.STANDARD; return PoseType.STANDARD;
} }
} }
// V1 fallback
if (stack.getItem() instanceof ItemBind bind) {
return bind.getPoseType();
}
return PoseType.STANDARD; return PoseType.STANDARD;
} }
} }

View File

@@ -2,7 +2,6 @@ package com.tiedup.remake.v2.bondage.component;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.tiedup.remake.core.TiedUpMod; import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.state.CollarRegistry; import com.tiedup.remake.state.CollarRegistry;
import com.tiedup.remake.v2.bondage.CollarHelper; import com.tiedup.remake.v2.bondage.CollarHelper;
import java.util.HashSet; import java.util.HashSet;
@@ -71,7 +70,7 @@ public class OwnershipComponent implements IItemComponent {
// onCollarRemoved handles both the alert AND the unregister call internally, // onCollarRemoved handles both the alert AND the unregister call internally,
// so we do NOT call registry.unregisterWearer() separately to avoid double unregister. // so we do NOT call registry.unregisterWearer() separately to avoid double unregister.
if (!CollarHelper.isRemovalAlertSuppressed()) { if (!CollarHelper.isRemovalAlertSuppressed()) {
ItemCollar.onCollarRemoved(entity, true); CollarHelper.onCollarRemoved(entity, true);
} else { } else {
// Suppressed alert path: still need to unregister, just skip the alert // Suppressed alert path: still need to unregister, just skip the alert
try { try {

View File

@@ -2,7 +2,7 @@ package com.tiedup.remake.v2.furniture;
import com.tiedup.remake.core.TiedUpMod; import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.ItemMasterKey; 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.IBondageState;
import com.tiedup.remake.state.PlayerBindState; import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.state.PlayerCaptorManager; import com.tiedup.remake.state.PlayerCaptorManager;
@@ -713,11 +713,10 @@ public class EntityFurniture
); );
if ( if (
collarStack.isEmpty() || collarStack.isEmpty() ||
!(collarStack.getItem() instanceof !CollarHelper.isCollar(collarStack)
ItemCollar collar)
) continue; ) continue;
if ( if (
!collar.isOwner(collarStack, serverPlayer) && !CollarHelper.isOwner(collarStack, serverPlayer) &&
!serverPlayer.hasPermissions(2) !serverPlayer.hasPermissions(2)
) continue; ) continue;