feat(D-01/C): consumer migration — 85 files migrated to V2 helpers

Phase 1 (state): PlayerBindState, PlayerCaptorManager, PlayerEquipment,
  PlayerDataRetrieval, PlayerLifecycle, PlayerShockCollar, StruggleAccessory
Phase 2 (client): AnimationTickHandler, NpcAnimationTickHandler, 5 render
  handlers, DamselModel, 3 client mixins, SelfBondageInputHandler,
  SlaveManagementScreen, ActionPanel, SlaveEntryWidget, ModKeybindings
Phase 3 (entities): 28 entity/AI files migrated to CollarHelper,
  BindModeHelper, PoseTypeHelper, createStack()
Phase 4 (network): PacketSlaveAction, PacketMasterEquip,
  PacketAssignCellToCollar, PacketNpcCommand, PacketFurnitureForcemount
Phase 5 (events): RestraintTaskTickHandler, PetPlayRestrictionHandler,
  PlayerEnslavementHandler, ChatEventHandler, LaborAttackPunishmentHandler
Phase 6 (commands): BondageSubCommand, CollarCommand, NPCCommand,
  KidnapSetCommand
Phase 7 (compat): MCAKidnappedAdapter, MCA mixins
Phase 8 (misc): GagTalkManager, PetRequestManager, HangingCagePiece,
  BondageItemBlockEntity, TrappedChestBlockEntity, DispenserBehaviors,
  BondageItemLoaderUtility, RestraintApplicator, StruggleSessionManager,
  MovementStyleResolver, CampLifecycleManager

Some files retain dual V1/V2 checks (instanceof V1 || V2Helper) for
coexistence — V1-only branches removed in Branch D.
This commit is contained in:
NotEvil
2026-04-15 00:16:50 +02:00
parent 9f2b06b711
commit 449178f57b
85 changed files with 2885 additions and 777 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,155 @@
# D-01 Branch E : Resistance & Lock System Rework
> **Prérequis :** Branch D (V1 cleanup) mergée.
> **Branche :** `feature/d01-branch-e-resistance`
> **Objectif :** Redesign complet du système de résistance/lock.
---
## Nouveau modèle
### Principes
1. **La résistance vient de l'item** — définie dans le JSON via `ResistanceComponent`, point final.
2. **Le lock est binaire** — on/off. Pas de "lock resistance" séparée. Le lock *active* la nécessité de struggle pour les items non-ARMS.
3. **ARMS = toujours actif** — un bind aux bras nécessite toujours un struggle pour s'en libérer soi-même, locké ou non.
4. **Non-ARMS + pas locké = libre** — un gag/blindfold/collar non-locké peut être retiré librement (par soi-même ou un autre joueur).
5. **Non-ARMS + locké = struggle requis** — le lock active la résistance de l'item.
6. **Un autre joueur peut aider** — retirer un item non-locké sur un autre joueur ne nécessite pas de struggle (aide).
### Matrice de struggle
| Région | Locké ? | Self-remove | Autre joueur remove |
|--------|---------|-------------|---------------------|
| ARMS | Non | Struggle (résistance item) | Libre (aide) |
| ARMS | Oui | Struggle (résistance item) | Struggle (résistance item) |
| Non-ARMS | Non | Libre | Libre |
| Non-ARMS | Oui | Struggle (résistance item) | Struggle (résistance item) |
### Items organiques (slime, vine, web, tape)
Ces items sont "lockés par nature" — pas de padlock possible mais impossible à retirer sans struggle.
**Option :** Nouveau composant `BuiltInLockComponent` dans le JSON :
```json
"components": {
"resistance": {"id": "slime"},
"built_in_lock": {}
}
```
`BuiltInLockComponent` :
- `blocksUnequip()` retourne `true` (comme un lock, mais sans padlock)
- `ILockable.canAttachPadlock()` retourne `false` (déjà le cas pour les organiques)
- L'item se comporte comme un ARMS bind : toujours struggle required
**Alternative :** Flag `"always_locked": true` sur la definition JSON. Plus simple, pas besoin de nouveau composant.
---
## Problèmes actuels que ce rework corrige
### P1. Singleton MAX scan
`DataDrivenBondageItem.getBaseResistance(LivingEntity)` retourne le MAX de tous les items data-driven équipés. Un gag résistance 50 hérite de la résistance 200 du chain bind.
**Fix :** Initialiser `currentResistance` dans le NBT à l'equip depuis `ResistanceComponent.getBaseResistance()`. Plus jamais de fallback au MAX scan runtime. Le `getBaseResistance(LivingEntity)` du singleton devient un no-op/fallback qui n'est plus utilisé par le struggle.
### P2. isItemLocked() dead code
`StruggleState.struggle()` ne call jamais `isItemLocked()`. Le x10 penalty n'est jamais appliqué.
**Fix :** Supprimer le concept de "locked penalty". Avec le nouveau modèle, le lock **active** le struggle, il ne le ralentit pas. Si l'item est locké, il faut struggle avec la résistance complète de l'item. Si non locké (et non-ARMS), pas de struggle du tout.
### P3. Lock resistance / item resistance déconnectés
`ILockable.getLockResistance()` vs `IHasResistance.getBaseResistance()` sont deux systèmes indépendants.
**Fix :** Supprimer `ILockable.getLockResistance()` / `getCurrentLockResistance()` / `setCurrentLockResistance()` / `initializeLockResistance()` / `clearLockResistance()`. La résistance du lockpick minigame utilise directement la résistance de l'item (ou un multiplicateur fixe).
### P4. Dice-roll ignore le lock
**Fix :** Avec le nouveau modèle, le dice-roll ne change pas. C'est `canStruggle()` qui gate l'accès :
```java
// StruggleBinds.canStruggle()
// ARMS: toujours struggle-able (self)
return true;
// StruggleCollar/StruggleAccessory.canStruggle()
// Non-ARMS: seulement si locké
return isLocked(stack) || hasBuiltInLock(stack);
```
---
## Bugs pré-existants à corriger dans cette branche
### B1. V1 ItemCollar.onUnequipped() — suppressed path skip unregister
Quand `isRemovalAlertSuppressed()` est true, `ItemCollar.onUnequipped()` return early SANS appeler `CollarRegistry.unregisterWearer()`. Entrées fantômes persistées.
**Fichier :** `items/base/ItemCollar.java` lignes 1382-1395
**Fix :** Ajouter `unregisterWearer()` dans le branch suppressed.
### B2. DataDrivenItemRegistry.clear() pas synchronisé
`clear()` écrit `SNAPSHOT = EMPTY` sans acquérir `RELOAD_LOCK`. Race avec `mergeAll()`.
**Fichier :** `v2/bondage/datadriven/DataDrivenItemRegistry.java` ligne 142
**Fix :** Synchroniser sur `RELOAD_LOCK`.
### B3. V2TyingPlayerTask.heldStack reference stale
Le held item peut être remplacé entre début et fin du tying → item dupliqué.
**Fichier :** `tasks/V2TyingPlayerTask.java` ligne 80
**Fix :** Valider `heldStack` non-vide et matching avant equip dans `onComplete()`.
---
## Tâches
### E1. Initialiser currentResistance à l'equip
Dans `DataDrivenBondageItem.onEquipped()` et les hooks V1 `onEquipped()` :
- Lire `ResistanceComponent.getBaseResistance()` (ou `IHasResistance.getBaseResistance()` pour V1)
- Écrire immédiatement dans le NBT via `setCurrentResistance(stack, base)`
- Élimine le MAX scan comme source d'initialisation
### E2. Refactor canStruggle() — nouveau modèle
- `StruggleBinds.canStruggle()` : ARMS → toujours true (self) si item existe
- Nouveau `StruggleAccessory` (ou refactor de StruggleCollar) : non-ARMS → true seulement si locké ou built-in lock
- Supprimer `isItemLocked()` penalty (dead code de toute façon)
### E3. "Aide" — remove non-locké par un autre joueur
Modifier `AbstractV2BondageItem.interactLivingEntity()` :
- Si clic sur un joueur qui porte l'item ET item non-locké ET clicker n'a pas l'item en main → retirer l'item (aide)
- Ou via un packet dédié (clic droit main vide sur joueur attaché)
### E4. BuiltInLockComponent ou flag `always_locked`
Pour les items organiques qui ne peuvent pas avoir de padlock mais nécessitent un struggle.
### E5. Cleanup ILockable — supprimer lock resistance
Supprimer : `getLockResistance()`, `getCurrentLockResistance()`, `setCurrentLockResistance()`, `initializeLockResistance()`, `clearLockResistance()`.
Le lockpick minigame utilise la résistance de l'item directement (ou un multiplicateur config).
### E6. Fix bugs pré-existants (B1, B2, B3)
---
## Vérification
- [ ] V2 bind résistance 50 + V2 gag résistance 80 : chacun a sa propre résistance (pas MAX)
- [ ] Gag non-locké → retirable sans struggle
- [ ] Gag locké → struggle avec résistance du gag
- [ ] Bind ARMS non-locké → self-struggle requis, autre joueur peut aider (libre)
- [ ] Bind ARMS locké → self-struggle requis, autre joueur aussi struggle
- [ ] Slime bind (built-in lock) → struggle obligatoire, pas de padlock possible
- [ ] `currentResistance` initialisé dans NBT dès l'equip
- [ ] CollarRegistry clean après removals légitimes
- [ ] Pas de duplication d'item via tying task

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

@@ -6,6 +6,14 @@ import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.items.base.ItemEarplugs;
import com.tiedup.remake.items.base.ItemGag;
import com.tiedup.remake.items.clothes.GenericClothes;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.BindModeHelper;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.v2.bondage.component.ComponentType;
import com.tiedup.remake.v2.bondage.component.GaggingComponent;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemDefinition;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemRegistry;
import javax.annotation.Nullable;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
@@ -191,55 +199,45 @@ public abstract class BondageItemBlockEntity
@Override
public void readBondageData(CompoundTag tag) {
// Read bind with type validation
// Read bind with type validation (V1 ItemBind or V2 ARMS-region item)
if (tag.contains("bind")) {
ItemStack bindStack = ItemStack.of(tag.getCompound("bind"));
if (
!bindStack.isEmpty() && bindStack.getItem() instanceof ItemBind
) {
if (!bindStack.isEmpty() && (bindStack.getItem() instanceof ItemBind || BindModeHelper.isBindItem(bindStack))) {
this.bind = bindStack;
}
}
// Read gag with type validation
// Read gag with type validation (V1 ItemGag or V2 GAGGING component)
if (tag.contains("gag")) {
ItemStack gagStack = ItemStack.of(tag.getCompound("gag"));
if (!gagStack.isEmpty() && gagStack.getItem() instanceof ItemGag) {
if (!gagStack.isEmpty() && (gagStack.getItem() instanceof ItemGag
|| DataDrivenBondageItem.getComponent(gagStack, ComponentType.GAGGING, GaggingComponent.class) != null)) {
this.gag = gagStack;
}
}
// Read blindfold with type validation
// Read blindfold with type validation (V1 ItemBlindfold or V2 EYES-region item)
if (tag.contains("blindfold")) {
ItemStack blindfoldStack = ItemStack.of(
tag.getCompound("blindfold")
);
if (
!blindfoldStack.isEmpty() &&
blindfoldStack.getItem() instanceof ItemBlindfold
) {
ItemStack blindfoldStack = ItemStack.of(tag.getCompound("blindfold"));
if (!blindfoldStack.isEmpty() && (blindfoldStack.getItem() instanceof ItemBlindfold
|| isDataDrivenForRegion(blindfoldStack, BodyRegionV2.EYES))) {
this.blindfold = blindfoldStack;
}
}
// Read earplugs with type validation
// Read earplugs with type validation (V1 ItemEarplugs or V2 EARS-region item)
if (tag.contains("earplugs")) {
ItemStack earplugsStack = ItemStack.of(tag.getCompound("earplugs"));
if (
!earplugsStack.isEmpty() &&
earplugsStack.getItem() instanceof ItemEarplugs
) {
if (!earplugsStack.isEmpty() && (earplugsStack.getItem() instanceof ItemEarplugs
|| isDataDrivenForRegion(earplugsStack, BodyRegionV2.EARS))) {
this.earplugs = earplugsStack;
}
}
// Read collar with type validation
// Read collar with type validation (V1 ItemCollar or V2 collar)
if (tag.contains("collar")) {
ItemStack collarStack = ItemStack.of(tag.getCompound("collar"));
if (
!collarStack.isEmpty() &&
collarStack.getItem() instanceof ItemCollar
) {
if (!collarStack.isEmpty() && (collarStack.getItem() instanceof ItemCollar || CollarHelper.isCollar(collarStack))) {
this.collar = collarStack;
}
}
@@ -279,6 +277,14 @@ public abstract class BondageItemBlockEntity
return tag;
}
// V2 HELPERS
/** Check if a stack is a data-driven item occupying the given body region. */
private static boolean isDataDrivenForRegion(ItemStack stack, BodyRegionV2 region) {
DataDrivenItemDefinition def = DataDrivenItemRegistry.get(stack);
return def != null && def.occupiedRegions().contains(region);
}
// NETWORK SYNC
/**

View File

@@ -1,7 +1,19 @@
package com.tiedup.remake.blocks.entity;
import com.tiedup.remake.items.base.*;
import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.items.base.ItemBlindfold;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.items.base.ItemEarplugs;
import com.tiedup.remake.items.base.ItemGag;
import com.tiedup.remake.items.clothes.GenericClothes;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.BindModeHelper;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.v2.bondage.component.ComponentType;
import com.tiedup.remake.v2.bondage.component.GaggingComponent;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemDefinition;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemRegistry;
import javax.annotation.Nullable;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
@@ -45,7 +57,7 @@ public class TrappedChestBlockEntity
@Override
public void setBind(ItemStack stack) {
if (stack.isEmpty() || stack.getItem() instanceof ItemBind) {
if (stack.isEmpty() || stack.getItem() instanceof ItemBind || BindModeHelper.isBindItem(stack)) {
this.bind = stack;
setChangedAndSync();
}
@@ -58,7 +70,8 @@ public class TrappedChestBlockEntity
@Override
public void setGag(ItemStack stack) {
if (stack.isEmpty() || stack.getItem() instanceof ItemGag) {
if (stack.isEmpty() || stack.getItem() instanceof ItemGag
|| DataDrivenBondageItem.getComponent(stack, ComponentType.GAGGING, GaggingComponent.class) != null) {
this.gag = stack;
setChangedAndSync();
}
@@ -71,7 +84,8 @@ public class TrappedChestBlockEntity
@Override
public void setBlindfold(ItemStack stack) {
if (stack.isEmpty() || stack.getItem() instanceof ItemBlindfold) {
if (stack.isEmpty() || stack.getItem() instanceof ItemBlindfold
|| isDataDrivenForRegion(stack, BodyRegionV2.EYES)) {
this.blindfold = stack;
setChangedAndSync();
}
@@ -84,7 +98,8 @@ public class TrappedChestBlockEntity
@Override
public void setEarplugs(ItemStack stack) {
if (stack.isEmpty() || stack.getItem() instanceof ItemEarplugs) {
if (stack.isEmpty() || stack.getItem() instanceof ItemEarplugs
|| isDataDrivenForRegion(stack, BodyRegionV2.EARS)) {
this.earplugs = stack;
setChangedAndSync();
}
@@ -97,7 +112,7 @@ public class TrappedChestBlockEntity
@Override
public void setCollar(ItemStack stack) {
if (stack.isEmpty() || stack.getItem() instanceof ItemCollar) {
if (stack.isEmpty() || stack.getItem() instanceof ItemCollar || CollarHelper.isCollar(stack)) {
this.collar = stack;
setChangedAndSync();
}
@@ -183,6 +198,14 @@ public class TrappedChestBlockEntity
writeBondageData(tag);
}
// V2 HELPERS
/** Check if a stack is a data-driven item occupying the given body region. */
private static boolean isDataDrivenForRegion(ItemStack stack, BodyRegionV2 region) {
DataDrivenItemDefinition def = DataDrivenItemRegistry.get(stack);
return def != null && def.occupiedRegions().contains(region);
}
// NETWORK SYNC
/**

View File

@@ -2,8 +2,8 @@ package com.tiedup.remake.cells;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.base.ILockable;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.prison.PrisonerManager;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.state.IRestrainable;
import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.v2.BodyRegionV2;
@@ -231,7 +231,7 @@ public final class CampLifecycleManager {
}
// Suppress collar removal alerts - this is a legitimate release (camp death)
ItemCollar.runWithSuppressedAlert(() -> {
CollarHelper.runWithSuppressedAlert(() -> {
// Unlock collar if owned by the dead camp/trader
unlockCollarIfOwnedBy(prisoner, state, traderUUID);
@@ -285,8 +285,8 @@ public final class CampLifecycleManager {
return;
}
if (collar.getItem() instanceof ItemCollar collarItem) {
List<UUID> owners = collarItem.getOwners(collar);
if (CollarHelper.isCollar(collar)) {
List<UUID> owners = CollarHelper.getOwners(collar);
// If the dead trader/camp is an owner, unlock the collar
if (owners.contains(ownerUUID)) {

View File

@@ -3,8 +3,8 @@ package com.tiedup.remake.client;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.GenericBind;
import com.tiedup.remake.items.base.BindVariant;
import com.tiedup.remake.items.base.PoseType;
import com.tiedup.remake.v2.bondage.PoseTypeHelper;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
@@ -141,12 +141,7 @@ public class FirstPersonMittensRenderer {
net.minecraft.world.item.ItemStack bindStack =
V2EquipmentHelper.getInRegion(player, BodyRegionV2.ARMS);
if (bindStack.isEmpty()) return false;
if (bindStack.getItem() instanceof GenericBind bind) {
BindVariant variant = bind.getVariant();
return (
variant == BindVariant.WRAP || variant == BindVariant.LATEX_SACK
);
}
return false;
PoseType poseType = PoseTypeHelper.getPoseType(bindStack);
return poseType == PoseType.WRAP || poseType == PoseType.LATEX_SACK;
}
}

View File

@@ -6,7 +6,7 @@ import com.tiedup.remake.client.gui.screens.UnifiedBondageScreen;
import com.tiedup.remake.core.ModConfig;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.base.ILockable;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.network.action.PacketForceSeatModifier;
import com.tiedup.remake.network.action.PacketStruggle;
@@ -428,11 +428,8 @@ public class ModKeybindings {
target,
BodyRegionV2.NECK
);
if (
!collarStack.isEmpty() &&
collarStack.getItem() instanceof ItemCollar collar
) {
return collar.isOwner(collarStack, player);
if (!collarStack.isEmpty() && CollarHelper.isCollar(collarStack)) {
return CollarHelper.isOwner(collarStack, player);
}
return false;
}

View File

@@ -1,8 +1,8 @@
package com.tiedup.remake.client.animation.render;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.items.base.PoseType;
import com.tiedup.remake.v2.bondage.PoseTypeHelper;
import com.tiedup.remake.state.HumanChairHelper;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.v2.BodyRegionV2;
@@ -93,14 +93,11 @@ public class DogPoseRenderHandler {
}
ItemStack bindForPose = state.getEquipment(BodyRegionV2.ARMS);
if (
bindForPose.isEmpty() ||
!(bindForPose.getItem() instanceof ItemBind itemBind)
) {
if (bindForPose.isEmpty()) {
return;
}
PoseType bindPoseType = itemBind.getPoseType();
PoseType bindPoseType = PoseTypeHelper.getPoseType(bindForPose);
// Check for humanChairMode NBT override
bindPoseType = HumanChairHelper.resolveEffectivePose(
bindPoseType,

View File

@@ -2,8 +2,8 @@ package com.tiedup.remake.client.animation.render;
import com.tiedup.remake.client.state.PetBedClientState;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.items.base.PoseType;
import com.tiedup.remake.v2.bondage.PoseTypeHelper;
import com.tiedup.remake.state.HumanChairHelper;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.v2.BodyRegionV2;
@@ -83,11 +83,9 @@ public class PetBedRenderHandler {
PlayerBindState state = PlayerBindState.getInstance(player);
if (state == null) return false;
ItemStack bind = state.getEquipment(BodyRegionV2.ARMS);
if (
bind.isEmpty() || !(bind.getItem() instanceof ItemBind itemBind)
) return false;
if (bind.isEmpty()) return false;
PoseType pose = HumanChairHelper.resolveEffectivePose(
itemBind.getPoseType(),
PoseTypeHelper.getPoseType(bind),
bind
);
return pose == PoseType.DOG || pose == PoseType.HUMAN_CHAIR;

View File

@@ -2,8 +2,8 @@ package com.tiedup.remake.client.animation.render;
import com.tiedup.remake.client.renderer.layers.ClothesRenderHelper;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.items.base.PoseType;
import com.tiedup.remake.v2.bondage.PoseTypeHelper;
import com.tiedup.remake.items.clothes.ClothesProperties;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.v2.BodyRegionV2;
@@ -71,10 +71,8 @@ public class PlayerArmHideEventHandler {
// === HIDE ARMS (wrap/latex_sack poses) ===
if (state.hasArmsBound()) {
ItemStack bind = state.getEquipment(BodyRegionV2.ARMS);
if (
!bind.isEmpty() && bind.getItem() instanceof ItemBind itemBind
) {
PoseType poseType = itemBind.getPoseType();
if (!bind.isEmpty()) {
PoseType poseType = PoseTypeHelper.getPoseType(bind);
// Only hide arms for wrap/sack poses (arms are covered by the item)
if (

View File

@@ -14,8 +14,9 @@ import com.tiedup.remake.client.gltf.GltfAnimationApplier;
import com.tiedup.remake.client.state.ClothesClientCache;
import com.tiedup.remake.client.state.MovementStyleClientState;
import com.tiedup.remake.client.state.PetBedClientState;
import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.items.base.PoseType;
import com.tiedup.remake.v2.bondage.BindModeHelper;
import com.tiedup.remake.v2.bondage.PoseTypeHelper;
import com.tiedup.remake.state.HumanChairHelper;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.v2.BodyRegionV2;
@@ -249,14 +250,10 @@ public class AnimationTickHandler {
PlayerBindState state
) {
ItemStack bind = state.getEquipment(BodyRegionV2.ARMS);
PoseType poseType = PoseType.STANDARD;
PoseType poseType = PoseTypeHelper.getPoseType(bind);
if (bind.getItem() instanceof ItemBind itemBind) {
poseType = itemBind.getPoseType();
// Human chair mode: override DOG pose to HUMAN_CHAIR (straight limbs)
poseType = HumanChairHelper.resolveEffectivePose(poseType, bind);
}
// Human chair mode: override DOG pose to HUMAN_CHAIR (straight limbs)
poseType = HumanChairHelper.resolveEffectivePose(poseType, bind);
// Derive bound state from V2 regions (works client-side, synced via capability)
boolean armsBound = V2EquipmentHelper.isRegionOccupied(
@@ -268,10 +265,10 @@ public class AnimationTickHandler {
BodyRegionV2.LEGS
);
// V1 fallback: if no V2 regions are set but player is tied, derive from ItemBind NBT
if (!armsBound && !legsBound && bind.getItem() instanceof ItemBind) {
armsBound = ItemBind.hasArmsBound(bind);
legsBound = ItemBind.hasLegsBound(bind);
// V1 fallback: if no V2 regions are set but player is tied, derive from bind mode NBT
if (!armsBound && !legsBound && BindModeHelper.isBindItem(bind)) {
armsBound = BindModeHelper.hasArmsBound(bind);
legsBound = BindModeHelper.hasLegsBound(bind);
}
boolean isStruggling = state.isStruggling();

View File

@@ -9,8 +9,9 @@ import com.tiedup.remake.client.gltf.GltfAnimationApplier;
import com.tiedup.remake.entities.AbstractTiedUpNpc;
import com.tiedup.remake.entities.EntityMaster;
import com.tiedup.remake.entities.ai.master.MasterState;
import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.items.base.PoseType;
import com.tiedup.remake.v2.bondage.BindModeHelper;
import com.tiedup.remake.v2.bondage.PoseTypeHelper;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.IV2BondageEquipment;
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
@@ -161,13 +162,8 @@ public class NpcAnimationTickHandler {
net.minecraft.world.item.ItemStack bind = entity.getEquipment(
BodyRegionV2.ARMS
);
PoseType poseType = PoseType.STANDARD;
boolean hasBind = false;
if (bind.getItem() instanceof ItemBind itemBind) {
poseType = itemBind.getPoseType();
hasBind = true;
}
PoseType poseType = PoseTypeHelper.getPoseType(bind);
boolean hasBind = BindModeHelper.isBindItem(bind);
// Derive bound state from V2 regions (AbstractTiedUpNpc implements IV2EquipmentHolder)
boolean armsBound = V2EquipmentHelper.isRegionOccupied(
@@ -179,10 +175,10 @@ public class NpcAnimationTickHandler {
BodyRegionV2.LEGS
);
// V1 fallback: if no V2 regions set but NPC has a bind, derive from ItemBind NBT
if (!armsBound && !legsBound && bind.getItem() instanceof ItemBind) {
armsBound = ItemBind.hasArmsBound(bind);
legsBound = ItemBind.hasLegsBound(bind);
// V1 fallback: if no V2 regions set but NPC has a bind, derive from bind mode NBT
if (!armsBound && !legsBound && BindModeHelper.isBindItem(bind)) {
armsBound = BindModeHelper.hasArmsBound(bind);
legsBound = BindModeHelper.hasLegsBound(bind);
}
boolean isStruggling = entity.isStruggling();

View File

@@ -1,8 +1,8 @@
package com.tiedup.remake.client.events;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.ModItems;
import com.tiedup.remake.items.base.BindVariant;
import com.tiedup.remake.items.base.PoseType;
import com.tiedup.remake.v2.bondage.PoseTypeHelper;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.v2.BodyRegionV2;
@@ -138,7 +138,7 @@ public class LeashProxyClientHandler {
ItemStack bind = state.getEquipment(BodyRegionV2.ARMS);
if (
!bind.isEmpty() &&
bind.getItem() == ModItems.getBind(BindVariant.DOGBINDER)
PoseTypeHelper.getPoseType(bind) == PoseType.DOG
) {
return DOGWALK_Y_OFFSET;
}

View File

@@ -1,13 +1,20 @@
package com.tiedup.remake.client.events;
import com.tiedup.remake.items.base.*;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.network.selfbondage.PacketSelfBondage;
import com.tiedup.remake.v2.bondage.BindModeHelper;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.v2.bondage.IV2BondageItem;
import com.tiedup.remake.v2.bondage.component.BlindingComponent;
import com.tiedup.remake.v2.bondage.component.ComponentType;
import com.tiedup.remake.v2.bondage.component.GaggingComponent;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemDefinition;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemRegistry;
import net.minecraft.client.Minecraft;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
@@ -70,7 +77,7 @@ public class SelfBondageInputHandler {
if (!event.getLevel().isClientSide()) return;
ItemStack stack = event.getItemStack();
if (isSelfBondageItem(stack.getItem())) {
if (isSelfBondageItem(stack)) {
event.setCanceled(true);
startSelfBondage();
}
@@ -87,11 +94,11 @@ public class SelfBondageInputHandler {
InteractionHand hand = InteractionHand.MAIN_HAND;
ItemStack stack = player.getMainHandItem();
if (!isSelfBondageItem(stack.getItem())) {
if (!isSelfBondageItem(stack)) {
stack = player.getOffhandItem();
hand = InteractionHand.OFF_HAND;
if (!isSelfBondageItem(stack.getItem())) {
if (!isSelfBondageItem(stack)) {
return; // No bondage item in either hand
}
}
@@ -130,7 +137,7 @@ public class SelfBondageInputHandler {
// Check if still holding bondage item in the active hand
ItemStack stack = player.getItemInHand(activeHand);
if (!isSelfBondageItem(stack.getItem())) {
if (!isSelfBondageItem(stack)) {
stopSelfBondage();
return;
}
@@ -153,27 +160,31 @@ public class SelfBondageInputHandler {
}
/**
* Check if an item supports self-bondage.
* Check if a stack supports self-bondage.
* Collar is explicitly excluded.
*/
private static boolean isSelfBondageItem(Item item) {
// Collar cannot be self-equipped (V1 collar guard)
if (item instanceof ItemCollar) {
private static boolean isSelfBondageItem(ItemStack stack) {
if (stack.isEmpty()) return false;
// Collar cannot be self-equipped (V1 collar guard + V2 ownership component)
if (stack.getItem() instanceof ItemCollar || CollarHelper.isCollar(stack)) {
return false;
}
// V2 bondage items support self-bondage (left-click hold with tying duration)
if (item instanceof IV2BondageItem) {
if (stack.getItem() instanceof IV2BondageItem) {
return true;
}
// V1 bondage items (legacy)
return (
item instanceof ItemBind ||
item instanceof ItemGag ||
item instanceof ItemBlindfold ||
item instanceof ItemMittens ||
item instanceof ItemEarplugs
);
// V2 data-driven items: check if it occupies any bondage region
DataDrivenItemDefinition def = DataDrivenItemRegistry.get(stack);
if (def != null) {
return true;
}
// V1 fallback: bind items
return BindModeHelper.isBindItem(stack)
|| DataDrivenBondageItem.getComponent(stack, ComponentType.GAGGING, GaggingComponent.class) != null
|| DataDrivenBondageItem.getComponent(stack, ComponentType.BLINDING, BlindingComponent.class) != null;
}
}

View File

@@ -3,7 +3,7 @@ package com.tiedup.remake.client.gui.screens;
import com.tiedup.remake.client.gui.util.GuiColors;
import com.tiedup.remake.client.gui.util.GuiLayoutConstants;
import com.tiedup.remake.client.gui.widgets.SlaveEntryWidget;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.network.slave.PacketSlaveAction;
import com.tiedup.remake.state.IBondageState;
@@ -145,8 +145,8 @@ public class SlaveManagementScreen extends BaseScreen {
ItemStack collarStack = kidnapped.getEquipment(
BodyRegionV2.NECK
);
if (collarStack.getItem() instanceof ItemCollar collar) {
if (collar.isOwner(collarStack, player)) {
if (CollarHelper.isCollar(collarStack)) {
if (CollarHelper.isOwner(collarStack, player)) {
addSlaveEntry(kidnapped);
addedUUIDs.add(entity.getUUID());
}

View File

@@ -7,7 +7,7 @@ import com.tiedup.remake.items.ItemLockpick;
import com.tiedup.remake.items.ModItems;
import com.tiedup.remake.items.base.IHasResistance;
import com.tiedup.remake.items.base.ILockable;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.network.action.PacketSetKnifeCutTarget;
import com.tiedup.remake.network.minigame.PacketLockpickMiniGameStart;
@@ -474,10 +474,10 @@ public class ActionPanel extends AbstractWidget {
// Bondage Service toggle (NECK collar only, prison configured)
if (
selectedRegion == BodyRegionV2.NECK &&
selectedItem.getItem() instanceof ItemCollar collar
CollarHelper.isCollar(selectedItem)
) {
if (collar.hasCellAssigned(selectedItem)) {
boolean svcEnabled = collar.isBondageServiceEnabled(
if (CollarHelper.hasCellAssigned(selectedItem)) {
boolean svcEnabled = CollarHelper.isBondageServiceEnabled(
selectedItem
);
String svcKey = svcEnabled

View File

@@ -5,7 +5,7 @@ import static com.tiedup.remake.client.gui.util.GuiLayoutConstants.*;
import com.google.common.collect.ImmutableList;
import com.tiedup.remake.client.gui.util.GuiColors;
import com.tiedup.remake.items.ItemGpsCollar;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.v2.BodyRegionV2;
import java.util.ArrayList;
@@ -560,19 +560,13 @@ public class SlaveEntryWidget
private boolean hasShockCollar() {
if (!slave.hasCollar()) return false;
ItemStack collar = slave.getEquipment(BodyRegionV2.NECK);
return (
collar.getItem() instanceof ItemCollar itemCollar &&
itemCollar.canShock()
);
return CollarHelper.canShock(collar);
}
private boolean hasGPSCollar() {
if (!slave.hasCollar()) return false;
ItemStack collar = slave.getEquipment(BodyRegionV2.NECK);
return (
collar.getItem() instanceof ItemCollar itemCollar &&
itemCollar.hasGPS()
);
return CollarHelper.hasGPS(collar);
}
private boolean isInAnySafeZone(
@@ -580,7 +574,7 @@ public class SlaveEntryWidget
ItemStack collarStack,
LivingEntity entity
) {
if (!gps.isActive(collarStack)) return true;
if (!CollarHelper.isActive(collarStack)) return true;
var safeSpots = gps.getSafeSpots(collarStack);
if (safeSpots.isEmpty()) return true;

View File

@@ -9,8 +9,9 @@ import com.tiedup.remake.entities.AbstractTiedUpNpc;
import com.tiedup.remake.entities.EntityKidnapperArcher;
import com.tiedup.remake.entities.EntityMaster;
import com.tiedup.remake.entities.ai.master.MasterState;
import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.items.base.PoseType;
import com.tiedup.remake.v2.bondage.BindModeHelper;
import com.tiedup.remake.v2.bondage.PoseTypeHelper;
import com.tiedup.remake.items.clothes.GenericClothes;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
@@ -230,11 +231,7 @@ public class DamselModel
if (inPose) {
ItemStack bind = entity.getEquipment(BodyRegionV2.ARMS);
PoseType poseType = PoseType.STANDARD;
if (bind.getItem() instanceof ItemBind itemBind) {
poseType = itemBind.getPoseType();
}
PoseType poseType = PoseTypeHelper.getPoseType(bind);
// Hide arms for wrap/latex_sack poses
if (poseType == PoseType.WRAP || poseType == PoseType.LATEX_SACK) {
@@ -252,9 +249,7 @@ public class DamselModel
PoseType currentPoseType = PoseType.STANDARD;
if (inPose) {
ItemStack bindForPoseType = entity.getEquipment(BodyRegionV2.ARMS);
if (bindForPoseType.getItem() instanceof ItemBind itemBindForType) {
currentPoseType = itemBindForType.getPoseType();
}
currentPoseType = PoseTypeHelper.getPoseType(bindForPoseType);
}
// Check if this is a Master in human chair mode (head should look around freely)
@@ -306,11 +301,7 @@ public class DamselModel
// Animation not yet active (1-frame delay) - apply static pose as fallback
// This ensures immediate visual feedback when bind is applied
ItemStack bind = entity.getEquipment(BodyRegionV2.ARMS);
PoseType fallbackPoseType = PoseType.STANDARD;
if (bind.getItem() instanceof ItemBind itemBind) {
fallbackPoseType = itemBind.getPoseType();
}
PoseType fallbackPoseType = PoseTypeHelper.getPoseType(bind);
// Derive bound state from V2 regions (AbstractTiedUpNpc implements IV2EquipmentHolder)
boolean armsBound = V2EquipmentHelper.isRegionOccupied(
@@ -323,10 +314,10 @@ public class DamselModel
);
if (
!armsBound && !legsBound && bind.getItem() instanceof ItemBind
!armsBound && !legsBound && BindModeHelper.isBindItem(bind)
) {
armsBound = ItemBind.hasArmsBound(bind);
legsBound = ItemBind.hasLegsBound(bind);
armsBound = BindModeHelper.hasArmsBound(bind);
legsBound = BindModeHelper.hasLegsBound(bind);
}
// Apply static pose directly to model parts

View File

@@ -7,8 +7,8 @@ import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.tiedup.remake.cells.CellDataV2;
import com.tiedup.remake.cells.CellRegistryV2;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.util.teleport.Position;
import com.tiedup.remake.util.teleport.TeleportHelper;
import com.tiedup.remake.v2.BodyRegionV2;
@@ -160,8 +160,8 @@ public class CollarCommand {
}
if (source.getEntity() instanceof ServerPlayer executor) {
if (collar.getItem() instanceof ItemCollar collarItem) {
collarItem.addOwner(collar, executor);
if (CollarHelper.isCollar(collar)) {
CollarHelper.addOwner(collar, executor);
source.sendSuccess(
() ->
Component.literal(
@@ -195,8 +195,8 @@ public class CollarCommand {
}
if (source.getEntity() instanceof ServerPlayer executor) {
if (collar.getItem() instanceof ItemCollar collarItem) {
collarItem.removeOwner(collar, executor.getUUID());
if (CollarHelper.isCollar(collar)) {
CollarHelper.removeOwner(collar, executor.getUUID());
source.sendSuccess(
() ->
Component.literal(
@@ -230,8 +230,8 @@ public class CollarCommand {
return 0;
}
if (collar.getItem() instanceof ItemCollar collarItem) {
collarItem.setNickname(collar, name);
if (CollarHelper.isCollar(collar)) {
CollarHelper.setNickname(collar, name);
source.sendSuccess(
() ->
Component.literal(
@@ -261,8 +261,8 @@ public class CollarCommand {
return 0;
}
if (collar.getItem() instanceof ItemCollar collarItem) {
collarItem.addOwner(collar, owner);
if (CollarHelper.isCollar(collar)) {
CollarHelper.addOwner(collar, owner);
source.sendSuccess(
() ->
Component.literal(
@@ -296,8 +296,8 @@ public class CollarCommand {
return 0;
}
if (collar.getItem() instanceof ItemCollar collarItem) {
collarItem.removeOwner(collar, owner.getUUID());
if (CollarHelper.isCollar(collar)) {
CollarHelper.removeOwner(collar, owner.getUUID());
source.sendSuccess(
() ->
Component.literal(
@@ -348,8 +348,8 @@ public class CollarCommand {
return 0;
}
if (collar.getItem() instanceof ItemCollar collarItem) {
collarItem.setCellId(collar, cell.getId());
if (CollarHelper.isCollar(collar)) {
CollarHelper.setCellId(collar, cell.getId());
source.sendSuccess(
() ->
Component.literal(
@@ -388,8 +388,8 @@ public class CollarCommand {
return 0;
}
if (collar.getItem() instanceof ItemCollar collarItem) {
if (!collarItem.hasCellAssigned(collar)) {
if (CollarHelper.isCollar(collar)) {
if (!CollarHelper.hasCellAssigned(collar)) {
source.sendFailure(
Component.literal("No cell assigned to collar")
);
@@ -397,7 +397,7 @@ public class CollarCommand {
}
// Get cell position and teleport
java.util.UUID cellId = collarItem.getCellId(collar);
java.util.UUID cellId = CollarHelper.getCellId(collar);
ServerLevel serverLevel = source.getLevel();
CellDataV2 cell = CellRegistryV2.get(serverLevel).getCell(cellId);
@@ -449,7 +449,7 @@ public class CollarCommand {
return 0;
}
if (collar.getItem() instanceof ItemCollar collarItem) {
if (CollarHelper.isCollar(collar)) {
source.sendSuccess(
() ->
Component.literal(
@@ -460,24 +460,24 @@ public class CollarCommand {
false
);
String nickname = collarItem.getNickname(collar);
String nickname = CollarHelper.getNickname(collar);
source.sendSuccess(
() ->
Component.literal(
"§7Nickname: §f" +
(nickname.isEmpty() ? "None" : nickname)
(nickname == null || nickname.isEmpty() ? "None" : nickname)
),
false
);
source.sendSuccess(
() ->
Component.literal(
"§7Has Owner: §f" + collarItem.hasOwner(collar)
"§7Has Owner: §f" + CollarHelper.hasOwner(collar)
),
false
);
// Cell assignment
java.util.UUID cellId = collarItem.getCellId(collar);
java.util.UUID cellId = CollarHelper.getCellId(collar);
if (cellId != null) {
ServerLevel serverLevel = source.getLevel();
CellRegistryV2 registry = CellRegistryV2.get(serverLevel);
@@ -510,10 +510,12 @@ public class CollarCommand {
);
}
boolean locked = collar.getItem() instanceof com.tiedup.remake.items.base.ILockable lockable
&& lockable.isLocked(collar);
source.sendSuccess(
() ->
Component.literal(
"§7Locked: §f" + collarItem.isLocked(collar)
"§7Locked: §f" + locked
),
false
);

View File

@@ -4,12 +4,10 @@ import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.tiedup.remake.items.ModItems;
import com.tiedup.remake.items.base.BindVariant;
import com.tiedup.remake.items.base.BlindfoldVariant;
import com.tiedup.remake.items.base.EarplugsVariant;
import com.tiedup.remake.items.base.GagVariant;
import com.tiedup.remake.items.base.KnifeVariant;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
import java.util.Optional;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.network.chat.Component;
@@ -82,42 +80,18 @@ public class KidnapSetCommand {
int given = 0;
// Binds
given += giveItem(
player,
new ItemStack(ModItems.getBind(BindVariant.ROPES), 8)
);
given += giveItem(
player,
new ItemStack(ModItems.getBind(BindVariant.CHAIN), 4)
);
given += giveItem(
player,
new ItemStack(ModItems.getBind(BindVariant.LEATHER_STRAPS), 4)
);
given += giveDataDrivenItems(player, "ropes", 8);
given += giveDataDrivenItems(player, "chain", 4);
given += giveDataDrivenItems(player, "leather_straps", 4);
// Gags
given += giveItem(
player,
new ItemStack(ModItems.getGag(GagVariant.CLOTH_GAG), 4)
);
given += giveItem(
player,
new ItemStack(ModItems.getGag(GagVariant.BALL_GAG), 4)
);
given += giveItem(
player,
new ItemStack(ModItems.getGag(GagVariant.TAPE_GAG), 4)
);
given += giveDataDrivenItems(player, "cloth_gag", 4);
given += giveDataDrivenItems(player, "ball_gag", 4);
given += giveDataDrivenItems(player, "tape_gag", 4);
// Blindfolds
given += giveItem(
player,
new ItemStack(ModItems.getBlindfold(BlindfoldVariant.CLASSIC), 4)
);
given += giveItem(
player,
new ItemStack(ModItems.getBlindfold(BlindfoldVariant.MASK), 2)
);
given += giveDataDrivenItems(player, "classic_blindfold", 4);
given += giveDataDrivenItems(player, "blindfold_mask", 2);
// Collars
given += giveItem(
@@ -155,10 +129,7 @@ public class KidnapSetCommand {
given += giveItem(player, new ItemStack(ModItems.MASTER_KEY.get(), 1));
// Earplugs
given += giveItem(
player,
new ItemStack(ModItems.getEarplugs(EarplugsVariant.CLASSIC), 4)
);
given += giveDataDrivenItems(player, "classic_earplugs", 4);
// Rope arrows
given += giveItem(player, new ItemStack(ModItems.ROPE_ARROW.get(), 16));
@@ -182,6 +153,18 @@ public class KidnapSetCommand {
return finalGiven;
}
private static int giveDataDrivenItems(ServerPlayer player, String itemName, int count) {
int given = 0;
for (int i = 0; i < count; i++) {
ItemStack stack = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", itemName));
if (!stack.isEmpty()) {
giveItem(player, stack);
given++;
}
}
return given;
}
private static int giveItem(ServerPlayer player, ItemStack stack) {
if (!player.getInventory().add(stack)) {
// Drop on ground if inventory full

View File

@@ -7,11 +7,9 @@ import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.tiedup.remake.entities.*;
import com.tiedup.remake.entities.skins.EliteKidnapperSkinManager;
import com.tiedup.remake.items.ModItems;
import com.tiedup.remake.items.base.BindVariant;
import com.tiedup.remake.items.base.BlindfoldVariant;
import com.tiedup.remake.items.base.EarplugsVariant;
import com.tiedup.remake.items.base.GagVariant;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
import net.minecraft.resources.ResourceLocation;
import com.tiedup.remake.v2.BodyRegionV2;
import java.util.List;
import java.util.Optional;
@@ -507,7 +505,7 @@ public class NPCCommand {
npc.equip(
BodyRegionV2.ARMS,
new ItemStack(ModItems.getBind(BindVariant.ROPES))
DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "ropes"))
);
context
.getSource()
@@ -538,7 +536,7 @@ public class NPCCommand {
npc.equip(
BodyRegionV2.MOUTH,
new ItemStack(ModItems.getGag(GagVariant.CLOTH_GAG))
DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "cloth_gag"))
);
context
.getSource()
@@ -571,7 +569,7 @@ public class NPCCommand {
npc.equip(
BodyRegionV2.EYES,
new ItemStack(ModItems.getBlindfold(BlindfoldVariant.CLASSIC))
DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "classic_blindfold"))
);
context
.getSource()
@@ -656,10 +654,10 @@ public class NPCCommand {
com.tiedup.remake.entities.AbstractTiedUpNpc npcEntity
) {
npcEntity.applyBondage(
new ItemStack(ModItems.getBind(BindVariant.ROPES)),
new ItemStack(ModItems.getGag(GagVariant.CLOTH_GAG)),
new ItemStack(ModItems.getBlindfold(BlindfoldVariant.CLASSIC)),
new ItemStack(ModItems.getEarplugs(EarplugsVariant.CLASSIC)),
DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "ropes")),
DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "cloth_gag")),
DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "classic_blindfold")),
DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "classic_earplugs")),
new ItemStack(ModItems.CLASSIC_COLLAR.get()),
ItemStack.EMPTY // No clothes
);

View File

@@ -9,12 +9,10 @@ import com.tiedup.remake.commands.CommandHelper;
import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.items.ModItems;
import com.tiedup.remake.items.base.AdjustmentHelper;
import com.tiedup.remake.items.base.BindVariant;
import com.tiedup.remake.items.base.BlindfoldVariant;
import com.tiedup.remake.items.base.EarplugsVariant;
import com.tiedup.remake.items.base.GagVariant;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
import net.minecraft.resources.ResourceLocation;
import com.tiedup.remake.network.sync.PacketSyncBindState;
import com.tiedup.remake.state.PlayerBindState;
import net.minecraft.commands.CommandSourceStack;
@@ -256,7 +254,7 @@ public class BondageSubCommand {
return 0;
}
ItemStack ropes = new ItemStack(ModItems.getBind(BindVariant.ROPES));
ItemStack ropes = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "ropes"));
state.putBindOn(ropes);
CommandHelper.syncPlayerState(targetPlayer, state);
@@ -387,7 +385,7 @@ public class BondageSubCommand {
return 0;
}
ItemStack gag = new ItemStack(ModItems.getGag(GagVariant.CLOTH_GAG));
ItemStack gag = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "cloth_gag"));
state.putGagOn(gag);
CommandHelper.syncPlayerState(targetPlayer, state);
@@ -440,9 +438,7 @@ public class BondageSubCommand {
return 0;
}
ItemStack blindfold = new ItemStack(
ModItems.getBlindfold(BlindfoldVariant.CLASSIC)
);
ItemStack blindfold = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "classic_blindfold"));
state.putBlindfoldOn(blindfold);
CommandHelper.syncPlayerState(targetPlayer, state);
@@ -500,8 +496,7 @@ public class BondageSubCommand {
ItemStack collar = new ItemStack(ModItems.CLASSIC_COLLAR.get());
if (context.getSource().getEntity() instanceof ServerPlayer executor) {
ItemCollar collarItem = (ItemCollar) collar.getItem();
collarItem.addOwner(collar, executor);
CollarHelper.addOwner(collar, executor);
}
state.putCollarOn(collar);
@@ -1021,9 +1016,7 @@ public class BondageSubCommand {
return 0;
}
ItemStack earplugs = new ItemStack(
ModItems.getEarplugs(EarplugsVariant.CLASSIC)
);
ItemStack earplugs = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "classic_earplugs"));
state.putEarplugsOn(earplugs);
CommandHelper.syncPlayerState(targetPlayer, state);
@@ -1067,25 +1060,19 @@ public class BondageSubCommand {
int applied = 0;
if (!state.isTiedUp()) {
ItemStack ropes = new ItemStack(
ModItems.getBind(BindVariant.ROPES)
);
ItemStack ropes = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "ropes"));
state.putBindOn(ropes);
applied++;
}
if (!state.isGagged()) {
ItemStack gag = new ItemStack(
ModItems.getGag(GagVariant.CLOTH_GAG)
);
ItemStack gag = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "cloth_gag"));
state.putGagOn(gag);
applied++;
}
if (!state.isBlindfolded()) {
ItemStack blindfold = new ItemStack(
ModItems.getBlindfold(BlindfoldVariant.CLASSIC)
);
ItemStack blindfold = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "classic_blindfold"));
state.putBlindfoldOn(blindfold);
applied++;
}
@@ -1095,17 +1082,14 @@ public class BondageSubCommand {
if (
context.getSource().getEntity() instanceof ServerPlayer executor
) {
ItemCollar collarItem = (ItemCollar) collar.getItem();
collarItem.addOwner(collar, executor);
CollarHelper.addOwner(collar, executor);
}
state.putCollarOn(collar);
applied++;
}
if (!state.hasEarplugs()) {
ItemStack earplugs = new ItemStack(
ModItems.getEarplugs(EarplugsVariant.CLASSIC)
);
ItemStack earplugs = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "classic_earplugs"));
state.putEarplugsOn(earplugs);
applied++;
}
@@ -1167,21 +1151,15 @@ public class BondageSubCommand {
// First fully restrain
if (!state.isTiedUp()) {
ItemStack ropes = new ItemStack(
ModItems.getBind(BindVariant.ROPES)
);
ItemStack ropes = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "ropes"));
state.putBindOn(ropes);
}
if (!state.isGagged()) {
ItemStack gag = new ItemStack(
ModItems.getGag(GagVariant.CLOTH_GAG)
);
ItemStack gag = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "cloth_gag"));
state.putGagOn(gag);
}
if (!state.isBlindfolded()) {
ItemStack blindfold = new ItemStack(
ModItems.getBlindfold(BlindfoldVariant.CLASSIC)
);
ItemStack blindfold = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "classic_blindfold"));
state.putBlindfoldOn(blindfold);
}
if (!state.hasCollar()) {
@@ -1189,15 +1167,12 @@ public class BondageSubCommand {
if (
context.getSource().getEntity() instanceof ServerPlayer executor
) {
ItemCollar collarItem = (ItemCollar) collar.getItem();
collarItem.addOwner(collar, executor);
CollarHelper.addOwner(collar, executor);
}
state.putCollarOn(collar);
}
if (!state.hasEarplugs()) {
ItemStack earplugs = new ItemStack(
ModItems.getEarplugs(EarplugsVariant.CLASSIC)
);
ItemStack earplugs = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "classic_earplugs"));
state.putEarplugsOn(earplugs);
}

View File

@@ -2,8 +2,10 @@ package com.tiedup.remake.compat.mca.capability;
import com.tiedup.remake.compat.mca.MCABondageManager;
import com.tiedup.remake.compat.mca.MCACompat;
import com.tiedup.remake.items.base.IHasBlindingEffect;
import com.tiedup.remake.items.base.IHasGaggingEffect;
import com.tiedup.remake.v2.bondage.component.BlindingComponent;
import com.tiedup.remake.v2.bondage.component.ComponentType;
import com.tiedup.remake.v2.bondage.component.GaggingComponent;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
import com.tiedup.remake.items.base.IHasResistance;
import com.tiedup.remake.items.base.ILockable;
import com.tiedup.remake.items.base.ItemBind;
@@ -277,16 +279,17 @@ public class MCAKidnappedAdapter implements IRestrainable {
@Override
public boolean hasGaggingEffect() {
ItemStack gag = cap.getGag();
return !gag.isEmpty() && gag.getItem() instanceof IHasGaggingEffect;
if (gag.isEmpty()) return false;
if (DataDrivenBondageItem.getComponent(gag, ComponentType.GAGGING, GaggingComponent.class) != null) return true;
return gag.getItem() instanceof com.tiedup.remake.items.base.IHasGaggingEffect;
}
@Override
public boolean hasBlindingEffect() {
ItemStack blindfold = cap.getBlindfold();
return (
!blindfold.isEmpty() &&
blindfold.getItem() instanceof IHasBlindingEffect
);
if (blindfold.isEmpty()) return false;
if (DataDrivenBondageItem.getComponent(blindfold, ComponentType.BLINDING, BlindingComponent.class) != null) return true;
return blindfold.getItem() instanceof com.tiedup.remake.items.base.IHasBlindingEffect;
}
@Override

View File

@@ -5,6 +5,9 @@ import static com.tiedup.remake.util.GameConstants.*;
import com.tiedup.remake.dialogue.EmotionalContext.EmotionType;
import com.tiedup.remake.items.base.ItemGag;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.v2.bondage.component.ComponentType;
import com.tiedup.remake.v2.bondage.component.GaggingComponent;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
import com.tiedup.remake.util.GagMaterial;
import com.tiedup.remake.util.PhoneticMapper;
import com.tiedup.remake.util.SyllableAnalyzer;
@@ -58,7 +61,12 @@ public class GagTalkManager {
) {
LivingEntity entity = kidnapped.asLivingEntity();
GagMaterial material = GagMaterial.CLOTH;
if (gagStack.getItem() instanceof ItemGag gag) {
// V2: check data-driven GaggingComponent first
GaggingComponent gaggingComp = DataDrivenBondageItem.getComponent(
gagStack, ComponentType.GAGGING, GaggingComponent.class);
if (gaggingComp != null && gaggingComp.getMaterial() != null) {
material = gaggingComp.getMaterial();
} else if (gagStack.getItem() instanceof ItemGag gag) {
material = gag.getGagMaterial();
}
@@ -514,8 +522,15 @@ public class GagTalkManager {
}
GagMaterial material = GagMaterial.CLOTH;
if (gagStack != null && gagStack.getItem() instanceof ItemGag gag) {
material = gag.getGagMaterial();
if (gagStack != null && !gagStack.isEmpty()) {
// V2: check data-driven GaggingComponent first
GaggingComponent comp = DataDrivenBondageItem.getComponent(
gagStack, ComponentType.GAGGING, GaggingComponent.class);
if (comp != null && comp.getMaterial() != null) {
material = comp.getMaterial();
} else if (gagStack.getItem() instanceof ItemGag gag) {
material = gag.getGagMaterial();
}
}
StringBuilder muffled = new StringBuilder();

View File

@@ -5,9 +5,9 @@ import com.tiedup.remake.dialogue.DialogueBridge;
import com.tiedup.remake.entities.EntityMaster;
import com.tiedup.remake.entities.ai.master.MasterPlaceBlockGoal;
import com.tiedup.remake.entities.ai.master.MasterState;
import com.tiedup.remake.items.ModItems;
import com.tiedup.remake.items.base.BindVariant;
import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
import net.minecraft.resources.ResourceLocation;
import com.tiedup.remake.network.master.PacketOpenPetRequestMenu;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.v2.BodyRegionV2;
@@ -171,9 +171,7 @@ public class PetRequestManager {
// Put dogbind on player (if not already tied)
if (!state.isTiedUp()) {
ItemStack dogbind = new ItemStack(
ModItems.getBind(BindVariant.DOGBINDER)
);
ItemStack dogbind = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "dogbinder"));
state.equip(BodyRegionV2.ARMS, dogbind);
TiedUpMod.LOGGER.debug(
"[PetRequestManager] Equipped dogbind on {} for walk",
@@ -228,7 +226,7 @@ public class PetRequestManager {
}
// Master equips armbinder on pet (classic pet play restraint)
ItemStack bind = new ItemStack(ModItems.getBind(BindVariant.ARMBINDER));
ItemStack bind = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "armbinder"));
state.equip(BodyRegionV2.ARMS, bind);
DialogueBridge.talkTo(master, pet, "petplay.tie_accept");

View File

@@ -4,7 +4,12 @@ import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.damsel.components.*;
import com.tiedup.remake.entities.skins.Gender;
import com.tiedup.remake.items.base.ILockable;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.v2.bondage.PoseTypeHelper;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
import com.tiedup.remake.v2.bondage.component.ComponentType;
import com.tiedup.remake.v2.bondage.component.GaggingComponent;
import com.tiedup.remake.v2.bondage.component.BlindingComponent;
import com.tiedup.remake.state.ICaptor;
import com.tiedup.remake.state.IRestrainable;
import com.tiedup.remake.state.IRestrainableEntity;
@@ -455,16 +460,8 @@ public abstract class AbstractTiedUpNpc
*/
public boolean isDogPose() {
ItemStack bind = this.getEquipment(BodyRegionV2.ARMS);
if (
bind.getItem() instanceof
com.tiedup.remake.items.base.ItemBind itemBind
) {
return (
itemBind.getPoseType() ==
com.tiedup.remake.items.base.PoseType.DOG
);
}
return false;
if (bind.isEmpty()) return false;
return PoseTypeHelper.getPoseType(bind) == com.tiedup.remake.items.base.PoseType.DOG;
}
/**
@@ -679,10 +676,8 @@ public abstract class AbstractTiedUpNpc
// Exception: collar owner can leash even if not tied
if (this.hasCollar()) {
ItemStack collar = this.getEquipment(BodyRegionV2.NECK);
if (collar.getItem() instanceof ItemCollar collarItem) {
if (collarItem.getOwners(collar).contains(player.getUUID())) {
return true;
}
if (CollarHelper.isOwner(collar, player)) {
return true;
}
}
@@ -801,20 +796,16 @@ public abstract class AbstractTiedUpNpc
public boolean hasGaggingEffect() {
ItemStack gag = this.getEquipment(BodyRegionV2.MOUTH);
if (gag.isEmpty()) return false;
return (
gag.getItem() instanceof
com.tiedup.remake.items.base.IHasGaggingEffect
);
if (DataDrivenBondageItem.getComponent(gag, ComponentType.GAGGING, GaggingComponent.class) != null) return true;
return gag.getItem() instanceof com.tiedup.remake.items.base.IHasGaggingEffect;
}
@Override
public boolean hasBlindingEffect() {
ItemStack blindfold = this.getEquipment(BodyRegionV2.EYES);
if (blindfold.isEmpty()) return false;
return (
blindfold.getItem() instanceof
com.tiedup.remake.items.base.IHasBlindingEffect
);
if (DataDrivenBondageItem.getComponent(blindfold, ComponentType.BLINDING, BlindingComponent.class) != null) return true;
return blindfold.getItem() instanceof com.tiedup.remake.items.base.IHasBlindingEffect;
}
@Override
@@ -990,9 +981,9 @@ public abstract class AbstractTiedUpNpc
ItemStack collar = this.getEquipment(BodyRegionV2.NECK);
if (collar.isEmpty()) return false;
if (!(collar.getItem() instanceof ItemCollar itemCollar)) return false;
if (!CollarHelper.isCollar(collar)) return false;
java.util.UUID cellId = itemCollar.getCellId(collar);
java.util.UUID cellId = CollarHelper.getCellId(collar);
if (cellId == null) return false;
// Get cell position from registry

View File

@@ -1,10 +1,9 @@
package com.tiedup.remake.entities;
import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.items.ModItems;
import com.tiedup.remake.items.base.BindVariant;
import com.tiedup.remake.items.base.GagVariant;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
import net.minecraft.resources.ResourceLocation;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.util.MessageDispatcher;
import com.tiedup.remake.util.teleport.Position;
@@ -53,13 +52,10 @@ public class BondageServiceHandler {
if (!npc.hasCollar()) return false;
ItemStack collar = npc.getEquipment(BodyRegionV2.NECK);
if (collar.getItem() instanceof ItemCollar itemCollar) {
return (
itemCollar.hasCellAssigned(collar) &&
itemCollar.isBondageServiceEnabled(collar)
);
}
return false;
return (
CollarHelper.hasCellAssigned(collar) &&
CollarHelper.isBondageServiceEnabled(collar)
);
}
/**
@@ -70,11 +66,9 @@ public class BondageServiceHandler {
public String getMessage() {
if (npc.hasCollar()) {
ItemStack collar = npc.getEquipment(BodyRegionV2.NECK);
if (collar.getItem() instanceof ItemCollar itemCollar) {
String message = itemCollar.getServiceSentence(collar);
if (message != null && !message.isEmpty()) {
return message;
}
String message = CollarHelper.getServiceSentence(collar);
if (message != null && !message.isEmpty()) {
return message;
}
}
return DEFAULT_MESSAGE;
@@ -119,9 +113,9 @@ public class BondageServiceHandler {
*/
private void capturePlayer(Player player) {
ItemStack collar = npc.getEquipment(BodyRegionV2.NECK);
if (!(collar.getItem() instanceof ItemCollar itemCollar)) return;
if (!CollarHelper.isCollar(collar)) return;
java.util.UUID cellId = itemCollar.getCellId(collar);
java.util.UUID cellId = CollarHelper.getCellId(collar);
if (cellId == null) return;
// Get cell position from registry
@@ -141,7 +135,7 @@ public class BondageServiceHandler {
);
// Warn masters if configured
warnOwners(player, itemCollar, collar);
warnOwners(player, collar);
// Get player's kidnapped state
PlayerBindState state = PlayerBindState.getInstance(player);
@@ -149,18 +143,18 @@ public class BondageServiceHandler {
// Apply bondage
state.equip(
BodyRegionV2.ARMS,
new ItemStack(ModItems.getBind(BindVariant.ROPES))
DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "ropes"))
);
state.equip(
BodyRegionV2.MOUTH,
new ItemStack(ModItems.getGag(GagVariant.BALL_GAG))
DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "ball_gag"))
);
// Teleport to cell
state.teleportToPosition(cellPosition);
// Tie to pole if configured on collar
if (itemCollar.shouldTieToPole(collar)) {
if (CollarHelper.shouldTieToPole(collar)) {
state.tieToClosestPole(3);
}
}
@@ -178,10 +172,9 @@ public class BondageServiceHandler {
*/
private void warnOwners(
Player capturedPlayer,
ItemCollar itemCollar,
ItemStack collarStack
) {
if (!itemCollar.shouldWarnMasters(collarStack)) {
if (!CollarHelper.shouldWarnMasters(collarStack)) {
return;
}
@@ -191,7 +184,7 @@ public class BondageServiceHandler {
capturedPlayer.getName().getString() +
" via bondage service!";
for (UUID ownerUUID : itemCollar.getOwners(collarStack)) {
for (UUID ownerUUID : CollarHelper.getOwners(collarStack)) {
Player owner = npc.level().getPlayerByUUID(ownerUUID);
if (owner != null) {
SystemMessageManager.sendChatToPlayer(

View File

@@ -5,7 +5,7 @@ import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.damsel.components.*;
import com.tiedup.remake.entities.skins.DamselSkinManager;
import com.tiedup.remake.entities.skins.Gender;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.state.ICaptor;
import com.tiedup.remake.v2.BodyRegionV2;
import java.util.UUID;
@@ -527,8 +527,8 @@ public class EntityDamsel
if (!this.hasCollar()) return false;
ItemStack collar = this.getEquipment(BodyRegionV2.NECK);
if (!(collar.getItem() instanceof ItemCollar collarItem)) return false;
if (!collarItem.getOwners(collar).contains(commander.getUUID())) {
if (!CollarHelper.isCollar(collar)) return false;
if (!CollarHelper.isOwner(collar, commander.getUUID())) {
if (!this.isGagged()) {
com.tiedup.remake.dialogue.EntityDialogueManager.talkByDialogueId(
this,
@@ -653,8 +653,8 @@ public class EntityDamsel
return net.minecraft.world.InteractionResult.FAIL;
}
ItemStack collar = this.getEquipment(BodyRegionV2.NECK);
if (collar.getItem() instanceof ItemCollar collarItem) {
if (!collarItem.isOwner(collar, player)) {
if (CollarHelper.isCollar(collar)) {
if (!CollarHelper.isOwner(collar, player)) {
if (
player instanceof
net.minecraft.server.level.ServerPlayer sp
@@ -693,9 +693,9 @@ public class EntityDamsel
this.hasCollar()
) {
ItemStack collar = this.getEquipment(BodyRegionV2.NECK);
if (collar.getItem() instanceof ItemCollar collarItem) {
if (CollarHelper.isCollar(collar)) {
if (
collarItem.isOwner(collar, player) &&
CollarHelper.isOwner(collar, player) &&
player instanceof
net.minecraft.server.level.ServerPlayer serverPlayer
) {
@@ -822,8 +822,8 @@ public class EntityDamsel
public String getTargetRelation(Player player) {
if (hasCollar()) {
ItemStack collar = getEquipment(BodyRegionV2.NECK);
if (collar.getItem() instanceof ItemCollar collarItem) {
if (collarItem.isOwner(collar, player)) {
if (CollarHelper.isCollar(collar)) {
if (CollarHelper.isOwner(collar, player)) {
return "master";
}
}

View File

@@ -12,7 +12,7 @@ import com.tiedup.remake.entities.kidnapper.components.KidnapperAggressionSystem
import com.tiedup.remake.entities.skins.Gender;
import com.tiedup.remake.entities.skins.KidnapperSkinManager;
import com.tiedup.remake.items.ModItems;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.personality.PersonalityType;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.state.ICaptor;
@@ -1367,10 +1367,7 @@ public class EntityKidnapper
if (!this.hasCollar()) return false;
ItemStack collar = this.getEquipment(BodyRegionV2.NECK);
if (collar.getItem() instanceof ItemCollar collarItem) {
return collarItem.isOwner(collar, player);
}
return false;
return CollarHelper.isOwner(collar, player);
}
/** Damage reduction multiplier against monsters (50% damage taken) */

View File

@@ -18,6 +18,7 @@ import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.state.ICaptor;
import com.tiedup.remake.util.MessageDispatcher;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
@@ -615,78 +616,82 @@ public class EntityKidnapperMerchant extends EntityKidnapperElite {
private List<ItemStack> collectAllModItems() {
List<ItemStack> items = new ArrayList<>();
// All binds (15)
// Items with colors get multiple variants (one per color)
// All binds — iterate V1 variants, create V2 stacks
for (BindVariant variant : BindVariant.values()) {
if (variant.supportsColor()) {
// Add one item per color (16 standard colors)
for (ItemColor color : ItemColor.values()) {
if (
color != ItemColor.CAUTION && color != ItemColor.CLEAR
) {
ItemStack stack = new ItemStack(
ModItems.getBind(variant)
ItemStack stack = DataDrivenBondageItem.createStack(
new net.minecraft.resources.ResourceLocation("tiedup", variant.getRegistryName())
);
KidnapperItemSelector.applyColor(stack, color);
items.add(stack);
}
}
} else {
// No color variants
items.add(new ItemStack(ModItems.getBind(variant)));
items.add(DataDrivenBondageItem.createStack(
new net.minecraft.resources.ResourceLocation("tiedup", variant.getRegistryName())
));
}
}
// All gags (19)
// All gags
for (GagVariant variant : GagVariant.values()) {
if (variant.supportsColor()) {
// Add one item per color
for (ItemColor color : ItemColor.values()) {
// TAPE_GAG can use caution/clear, others only standard colors
if (
variant == GagVariant.TAPE_GAG ||
(color != ItemColor.CAUTION && color != ItemColor.CLEAR)
) {
ItemStack stack = new ItemStack(
ModItems.getGag(variant)
ItemStack stack = DataDrivenBondageItem.createStack(
new net.minecraft.resources.ResourceLocation("tiedup", variant.getRegistryName())
);
KidnapperItemSelector.applyColor(stack, color);
items.add(stack);
}
}
} else {
items.add(new ItemStack(ModItems.getGag(variant)));
items.add(DataDrivenBondageItem.createStack(
new net.minecraft.resources.ResourceLocation("tiedup", variant.getRegistryName())
));
}
}
// All blindfolds (2) - BOTH support colors
// All blindfolds
for (BlindfoldVariant variant : BlindfoldVariant.values()) {
if (variant.supportsColor()) {
// Add one item per color (16 standard colors)
for (ItemColor color : ItemColor.values()) {
if (
color != ItemColor.CAUTION && color != ItemColor.CLEAR
) {
ItemStack stack = new ItemStack(
ModItems.getBlindfold(variant)
ItemStack stack = DataDrivenBondageItem.createStack(
new net.minecraft.resources.ResourceLocation("tiedup", variant.getRegistryName())
);
KidnapperItemSelector.applyColor(stack, color);
items.add(stack);
}
}
} else {
items.add(new ItemStack(ModItems.getBlindfold(variant)));
items.add(DataDrivenBondageItem.createStack(
new net.minecraft.resources.ResourceLocation("tiedup", variant.getRegistryName())
));
}
}
// Earplugs - no color support
for (EarplugsVariant variant : EarplugsVariant.values()) {
items.add(new ItemStack(ModItems.getEarplugs(variant)));
items.add(DataDrivenBondageItem.createStack(
new net.minecraft.resources.ResourceLocation("tiedup", variant.getRegistryName())
));
}
// Mittens - no color support
for (MittensVariant variant : MittensVariant.values()) {
items.add(new ItemStack(ModItems.getMittens(variant)));
items.add(DataDrivenBondageItem.createStack(
new net.minecraft.resources.ResourceLocation("tiedup", variant.getRegistryName())
));
}
// Knives - no color support

View File

@@ -94,10 +94,8 @@ public class EntityRopeArrow extends AbstractArrow {
int roll = this.random.nextInt(100) + 1;
if (roll <= bindChance) {
// Success! Bind the target
ItemStack ropeItem = new ItemStack(
ModItems.getBind(
com.tiedup.remake.items.base.BindVariant.ROPES
)
ItemStack ropeItem = com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem.createStack(
new net.minecraft.resources.ResourceLocation("tiedup", "ropes")
);
targetState.equip(BodyRegionV2.ARMS, ropeItem);

View File

@@ -1,7 +1,8 @@
package com.tiedup.remake.entities;
import com.tiedup.remake.items.ModItems;
import com.tiedup.remake.items.base.BindVariant;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
import net.minecraft.resources.ResourceLocation;
import com.tiedup.remake.v2.bondage.IV2BondageItem;
import net.minecraft.world.entity.EquipmentSlot;
import net.minecraft.world.item.ItemStack;
@@ -46,7 +47,7 @@ public class KidnapperCaptureEquipment {
) {
return mainHand;
}
return new ItemStack(ModItems.getBind(BindVariant.ROPES));
return DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "ropes"));
}
/**

View File

@@ -1,11 +1,9 @@
package com.tiedup.remake.entities;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.util.teleport.Position;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.CollarHelper;
import java.util.List;
import java.util.UUID;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import org.jetbrains.annotations.Nullable;
@@ -31,21 +29,7 @@ public class KidnapperCollarConfig {
this.kidnapper = kidnapper;
}
// COLLAR ITEM ACCESS
/**
* Get the collar item if equipped.
* @return ItemCollar or null if no collar or not an ItemCollar
*/
@Nullable
public ItemCollar getCollarItem() {
ItemStack collar = kidnapper.getEquipment(BodyRegionV2.NECK);
if (collar.isEmpty()) return null;
if (collar.getItem() instanceof ItemCollar itemCollar) {
return itemCollar;
}
return null;
}
// COLLAR STACK ACCESS
/**
* Get the collar ItemStack.
@@ -55,30 +39,29 @@ public class KidnapperCollarConfig {
return kidnapper.getEquipment(BodyRegionV2.NECK);
}
/**
* Check if the kidnapper has a valid collar.
*/
private boolean hasValidCollar() {
return kidnapper.hasCollar() && CollarHelper.isCollar(getCollarStack());
}
// KIDNAPPING MODE
/**
* Check if kidnapping mode is enabled via collar.
*/
public boolean isKidnappingModeEnabled() {
if (!kidnapper.hasCollar()) return false;
ItemCollar itemCollar = getCollarItem();
if (itemCollar == null) return false;
return itemCollar.isKidnappingModeEnabled(getCollarStack());
if (!hasValidCollar()) return false;
return CollarHelper.isKidnappingModeEnabled(getCollarStack());
}
/**
* Check if kidnapping mode is fully ready (enabled + prison set).
*/
public boolean isKidnappingModeReady() {
if (!kidnapper.hasCollar()) return false;
ItemCollar itemCollar = getCollarItem();
if (itemCollar == null) return false;
return itemCollar.isKidnappingModeReady(getCollarStack());
if (!hasValidCollar()) return false;
return CollarHelper.isKidnappingModeReady(getCollarStack());
}
// POSITION GETTERS
@@ -88,20 +71,16 @@ public class KidnapperCollarConfig {
*/
@Nullable
public java.util.UUID getCellId() {
ItemCollar itemCollar = getCollarItem();
if (itemCollar == null) return null;
return itemCollar.getCellId(getCollarStack());
if (!hasValidCollar()) return null;
return CollarHelper.getCellId(getCollarStack());
}
/**
* Check if collar has a cell assigned.
*/
public boolean hasCellAssigned() {
ItemCollar itemCollar = getCollarItem();
if (itemCollar == null) return false;
return itemCollar.hasCellAssigned(getCollarStack());
if (!hasValidCollar()) return false;
return CollarHelper.hasCellAssigned(getCollarStack());
}
// BEHAVIOR FLAGS
@@ -110,20 +89,16 @@ public class KidnapperCollarConfig {
* Check if should warn masters after capturing slave.
*/
public boolean shouldWarnMasters() {
ItemCollar itemCollar = getCollarItem();
if (itemCollar == null) return false;
return itemCollar.shouldWarnMasters(getCollarStack());
if (!hasValidCollar()) return false;
return CollarHelper.shouldWarnMasters(getCollarStack());
}
/**
* Check if should tie slave to pole in prison.
*/
public boolean shouldTieToPole() {
ItemCollar itemCollar = getCollarItem();
if (itemCollar == null) return false;
return itemCollar.shouldTieToPole(getCollarStack());
if (!hasValidCollar()) return false;
return CollarHelper.shouldTieToPole(getCollarStack());
}
// BLACKLIST/WHITELIST
@@ -139,19 +114,18 @@ public class KidnapperCollarConfig {
* </ul>
*/
public boolean isValidKidnappingTarget(Player player) {
ItemCollar itemCollar = getCollarItem();
if (itemCollar == null) return true; // No collar config = everyone is valid
if (!hasValidCollar()) return true; // No collar config = everyone is valid
ItemStack collarStack = getCollarStack();
UUID playerUUID = player.getUUID();
// If whitelist exists and is not empty, player MUST be on it
List<UUID> whitelist = itemCollar.getWhitelist(collarStack);
List<UUID> whitelist = CollarHelper.getWhitelist(collarStack);
if (!whitelist.isEmpty()) {
return whitelist.contains(playerUUID);
}
// Otherwise, check blacklist (blacklisted = not a valid target)
return !itemCollar.isBlacklisted(collarStack, playerUUID);
return !CollarHelper.isBlacklisted(collarStack, playerUUID);
}
}

View File

@@ -1,8 +1,9 @@
package com.tiedup.remake.entities;
import com.tiedup.remake.items.ModItems;
import com.tiedup.remake.items.base.*;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
import java.util.Random;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.item.ItemStack;
import org.jetbrains.annotations.Nullable;
@@ -242,7 +243,9 @@ public class KidnapperItemSelector {
BindVariant variant,
@Nullable ItemColor color
) {
ItemStack stack = new ItemStack(ModItems.getBind(variant));
ItemStack stack = DataDrivenBondageItem.createStack(
new ResourceLocation("tiedup", variant.getRegistryName())
);
if (color != null && variant.supportsColor()) {
applyColor(stack, color);
}
@@ -257,7 +260,9 @@ public class KidnapperItemSelector {
GagVariant variant,
@Nullable ItemColor color
) {
ItemStack stack = new ItemStack(ModItems.getGag(variant));
ItemStack stack = DataDrivenBondageItem.createStack(
new ResourceLocation("tiedup", variant.getRegistryName())
);
if (
color != null &&
variant.supportsColor() &&
@@ -276,7 +281,9 @@ public class KidnapperItemSelector {
BlindfoldVariant variant,
@Nullable ItemColor color
) {
ItemStack stack = new ItemStack(ModItems.getBlindfold(variant));
ItemStack stack = DataDrivenBondageItem.createStack(
new ResourceLocation("tiedup", variant.getRegistryName())
);
if (
color != null &&
variant.supportsColor() &&
@@ -292,7 +299,9 @@ public class KidnapperItemSelector {
* Mittens don't have color variants.
*/
public static ItemStack createMittens() {
return new ItemStack(ModItems.getMittens(MittensVariant.LEATHER));
return DataDrivenBondageItem.createStack(
new ResourceLocation("tiedup", "leather_mittens")
);
}
/**
@@ -300,7 +309,9 @@ public class KidnapperItemSelector {
* Earplugs don't have color variants.
*/
public static ItemStack createEarplugs() {
return new ItemStack(ModItems.getEarplugs(EarplugsVariant.CLASSIC));
return DataDrivenBondageItem.createStack(
new ResourceLocation("tiedup", "classic_earplugs")
);
}
// COLOR METHODS

View File

@@ -2,7 +2,7 @@ package com.tiedup.remake.entities;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.ModItems;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.state.CollarRegistry;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.util.tasks.ItemTask;
@@ -162,15 +162,15 @@ public class KidnapperJobManager {
// Put a shock collar on the worker AFTER untie/free
ItemStack shockCollar = new ItemStack(ModItems.SHOCK_COLLAR_AUTO.get());
if (shockCollar.getItem() instanceof ItemCollar collarItem) {
// Add kidnapper as owner so the collar is linked
collarItem.addOwner(
shockCollar,
kidnapper.getUUID(),
kidnapper.getNpcName()
);
// Lock the collar so they can't remove it
shockCollar = collarItem.setLocked(shockCollar, true);
// Add kidnapper as owner so the collar is linked
CollarHelper.addOwner(
shockCollar,
kidnapper.getUUID(),
kidnapper.getNpcName()
);
// Lock the collar so they can't remove it
if (shockCollar.getItem() instanceof com.tiedup.remake.items.base.ILockable lockable) {
shockCollar = lockable.setLocked(shockCollar, true);
}
captive.equip(BodyRegionV2.NECK, shockCollar);

View File

@@ -1,7 +1,6 @@
package com.tiedup.remake.entities;
import com.tiedup.remake.items.ModItems;
import com.tiedup.remake.items.base.BindVariant;
import com.tiedup.remake.v2.bondage.PoseTypeHelper;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.v2.BodyRegionV2;
@@ -108,7 +107,7 @@ public final class LeashProxyEntity extends Turtle {
ItemStack bind = state.getEquipment(BodyRegionV2.ARMS);
if (
!bind.isEmpty() &&
bind.getItem() == ModItems.getBind(BindVariant.DOGBINDER)
PoseTypeHelper.getPoseType(bind) == com.tiedup.remake.items.base.PoseType.DOG
) {
yOffset = 0.35D; // Lower for 4-legged dogwalk pose (back/hip level)
}

View File

@@ -11,7 +11,7 @@ import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.entities.EntityKidnapper.CaptivePriority;
import com.tiedup.remake.entities.ai.StuckDetector;
import com.tiedup.remake.entities.ai.WaypointNavigator;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.prison.PrisonerManager;
import com.tiedup.remake.prison.PrisonerRecord;
import com.tiedup.remake.prison.PrisonerState;
@@ -260,7 +260,7 @@ public class KidnapperBringToCellGoal extends Goal {
);
IRestrainable captive = this.kidnapper.getCaptive();
if (captive != null) {
ItemCollar.runWithSuppressedAlert(() ->
CollarHelper.runWithSuppressedAlert(() ->
captive.free(false)
);
this.kidnapper.removeCaptive(captive, false);
@@ -1079,9 +1079,9 @@ public class KidnapperBringToCellGoal extends Goal {
ItemStack collar = captive.getEquipment(BodyRegionV2.NECK);
if (
!collar.isEmpty() &&
collar.getItem() instanceof ItemCollar collarItem
CollarHelper.isCollar(collar)
) {
collarItem.setCellId(collar, this.targetCell.getId());
CollarHelper.setCellId(collar, this.targetCell.getId());
}
TiedUpMod.LOGGER.info(

View File

@@ -8,7 +8,7 @@ import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.entities.ai.StuckDetector;
import com.tiedup.remake.items.ItemKey;
import com.tiedup.remake.items.ModItems;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.network.action.PacketTying;
import com.tiedup.remake.prison.PrisonerManager;
@@ -849,11 +849,8 @@ public class KidnapperCaptureGoal extends Goal {
// If already has collar, check ownership
if (state.hasCollar()) {
ItemStack existingCollar = state.getEquipment(BodyRegionV2.NECK);
if (
existingCollar.getItem() instanceof
com.tiedup.remake.items.base.ItemCollar collarItem
) {
java.util.List<java.util.UUID> owners = collarItem.getOwners(
if (CollarHelper.isCollar(existingCollar)) {
java.util.List<java.util.UUID> owners = CollarHelper.getOwners(
existingCollar
);
if (!owners.contains(this.kidnapper.getUUID())) {
@@ -885,9 +882,9 @@ public class KidnapperCaptureGoal extends Goal {
for (java.util.UUID oldOwner : new java.util.ArrayList<>(
owners
)) {
collarItem.removeOwner(existingCollar, oldOwner);
CollarHelper.removeOwner(existingCollar, oldOwner);
}
collarItem.addOwner(
CollarHelper.addOwner(
existingCollar,
this.kidnapper.getUUID(),
this.kidnapper.getNpcName()
@@ -929,9 +926,9 @@ public class KidnapperCaptureGoal extends Goal {
if (this.captureProgress >= COLLAR_APPLY_TIME) {
// Create a copy of the collar and configure it
ItemStack collarCopy = collar.copy();
if (collarCopy.getItem() instanceof ItemCollar collarItem) {
if (CollarHelper.isCollar(collarCopy)) {
// Add kidnapper as owner
collarItem.addOwner(
CollarHelper.addOwner(
collarCopy,
this.kidnapper.getUUID(),
this.kidnapper.getNpcName()
@@ -941,12 +938,17 @@ public class KidnapperCaptureGoal extends Goal {
ItemStack keyStack = new ItemStack(ModItems.COLLAR_KEY.get());
if (keyStack.getItem() instanceof ItemKey keyItem) {
UUID keyUUID = keyItem.getKeyUUID(keyStack);
collarItem.setLockedByKeyUUID(collarCopy, keyUUID);
// Lock via ILockable interface (collar must implement it)
if (collarCopy.getItem() instanceof com.tiedup.remake.items.base.ILockable lockable) {
lockable.setLockedByKeyUUID(collarCopy, keyUUID);
}
// Store the key on the kidnapper for potential drop on death
this.kidnapper.addCollarKey(keyStack);
} else {
// Fallback: just lock without a key
collarItem.setLocked(collarCopy, true);
if (collarCopy.getItem() instanceof com.tiedup.remake.items.base.ILockable lockable) {
lockable.setLocked(collarCopy, true);
}
}
}

View File

@@ -8,8 +8,8 @@ import com.tiedup.remake.entities.AbstractTiedUpNpc;
import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.entities.ai.StuckDetector;
import com.tiedup.remake.entities.ai.WaypointNavigator;
import com.tiedup.remake.items.ModItems;
import com.tiedup.remake.items.base.BindVariant;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
import net.minecraft.resources.ResourceLocation;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.util.KidnapperAIHelper;
@@ -547,9 +547,7 @@ public class KidnapperWalkPrisonerGoal extends Goal {
);
// 3. Change bind to DOGBINDER
ItemStack dogBinder = new ItemStack(
ModItems.getBind(BindVariant.DOGBINDER)
);
ItemStack dogBinder = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "dogbinder"));
this.walkedPrisoner.equip(BodyRegionV2.ARMS, dogBinder);
TiedUpMod.LOGGER.debug(
@@ -738,7 +736,7 @@ public class KidnapperWalkPrisonerGoal extends Goal {
if (currentBind.isEmpty() || !prisoner.isTiedUp()) {
// They freed themselves - put dogbinder back on
ItemStack dogBinder = new ItemStack(
ModItems.getBind(BindVariant.DOGBINDER)
DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "dogbinder")).getItem()
);
prisoner.equip(BodyRegionV2.ARMS, dogBinder);
}

View File

@@ -338,10 +338,8 @@ public class MaidReturnGoal extends Goal {
// Fallback: use basic rope if no snapshot
cap.equip(
BodyRegionV2.ARMS,
new ItemStack(
com.tiedup.remake.items.ModItems.getBind(
com.tiedup.remake.items.base.BindVariant.ROPES
)
com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem.createStack(
new net.minecraft.resources.ResourceLocation("tiedup", "ropes")
)
);
}

View File

@@ -3,8 +3,8 @@ package com.tiedup.remake.entities.ai.master;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.dialogue.DialogueBridge;
import com.tiedup.remake.entities.EntityMaster;
import com.tiedup.remake.items.ModItems;
import com.tiedup.remake.items.base.BindVariant;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
import net.minecraft.resources.ResourceLocation;
import com.tiedup.remake.state.HumanChairHelper;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.v2.BodyRegionV2;
@@ -394,8 +394,8 @@ public class MasterHumanChairGoal extends Goal {
// Apply invisible dogbind for the pose animation
if (!bindState.isTiedUp()) {
ItemStack dogbind = new ItemStack(
ModItems.getBind(BindVariant.DOGBINDER)
ItemStack dogbind = DataDrivenBondageItem.createStack(
new ResourceLocation("tiedup", "dogbinder")
);
CompoundTag tag = dogbind.getOrCreateTag();
tag.putBoolean(NBT_HUMAN_CHAIR_BIND, true);

View File

@@ -3,12 +3,9 @@ package com.tiedup.remake.entities.ai.master;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.dialogue.DialogueBridge;
import com.tiedup.remake.entities.EntityMaster;
import com.tiedup.remake.items.ItemChokeCollar;
import com.tiedup.remake.items.ModItems;
import com.tiedup.remake.items.base.BindVariant;
import com.tiedup.remake.items.base.BlindfoldVariant;
import com.tiedup.remake.items.base.GagVariant;
import com.tiedup.remake.items.base.MittensVariant;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
import net.minecraft.resources.ResourceLocation;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.IV2BondageEquipment;
@@ -261,7 +258,7 @@ public class MasterPunishGoal extends Goal {
// CHOKE: only if pet has choke collar
if (bindState != null && bindState.hasCollar()) {
ItemStack collar = bindState.getEquipment(BodyRegionV2.NECK);
if (collar.getItem() instanceof ItemChokeCollar) {
if (CollarHelper.isChokeCollar(collar)) {
available.add(PunishmentType.CHOKE_COLLAR);
}
}
@@ -393,8 +390,8 @@ public class MasterPunishGoal extends Goal {
PlayerBindState bindState = PlayerBindState.getInstance(pet);
if (bindState != null && bindState.hasCollar()) {
ItemStack collar = bindState.getEquipment(BodyRegionV2.NECK);
if (collar.getItem() instanceof ItemChokeCollar chokeCollar) {
chokeCollar.setChoking(collar, true);
if (CollarHelper.isChokeCollar(collar)) {
CollarHelper.setChoking(collar, true);
this.activeChokeCollar = collar;
this.chokeActiveTimer = 1;
@@ -457,12 +454,14 @@ public class MasterPunishGoal extends Goal {
*/
private ItemStack createAccessory(BodyRegionV2 region) {
return switch (region) {
case EYES -> new ItemStack(
ModItems.getBlindfold(BlindfoldVariant.CLASSIC)
case EYES -> DataDrivenBondageItem.createStack(
new ResourceLocation("tiedup", "classic_blindfold")
);
case MOUTH -> new ItemStack(ModItems.getGag(GagVariant.BALL_GAG));
case HANDS -> new ItemStack(
ModItems.getMittens(MittensVariant.LEATHER)
case MOUTH -> DataDrivenBondageItem.createStack(
new ResourceLocation("tiedup", "ball_gag")
);
case HANDS -> DataDrivenBondageItem.createStack(
new ResourceLocation("tiedup", "leather_mittens")
);
default -> ItemStack.EMPTY;
};
@@ -472,8 +471,8 @@ public class MasterPunishGoal extends Goal {
* Apply armbinder as punishment.
*/
private void applyTighten(ServerPlayer pet) {
ItemStack armbinder = new ItemStack(
ModItems.getBind(BindVariant.ARMBINDER)
ItemStack armbinder = DataDrivenBondageItem.createStack(
new ResourceLocation("tiedup", "armbinder")
);
// Mark as temporary
@@ -545,9 +544,9 @@ public class MasterPunishGoal extends Goal {
private void deactivateChoke() {
if (
!activeChokeCollar.isEmpty() &&
activeChokeCollar.getItem() instanceof ItemChokeCollar chokeCollar
CollarHelper.isChokeCollar(activeChokeCollar)
) {
chokeCollar.setChoking(activeChokeCollar, false);
CollarHelper.setChoking(activeChokeCollar, false);
}
this.activeChokeCollar = ItemStack.EMPTY;
this.chokeActiveTimer = 0;

View File

@@ -3,11 +3,8 @@ package com.tiedup.remake.entities.ai.master;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.dialogue.DialogueBridge;
import com.tiedup.remake.entities.EntityMaster;
import com.tiedup.remake.items.ModItems;
import com.tiedup.remake.items.base.BindVariant;
import com.tiedup.remake.items.base.BlindfoldVariant;
import com.tiedup.remake.items.base.GagVariant;
import com.tiedup.remake.items.base.MittensVariant;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
import net.minecraft.resources.ResourceLocation;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.IV2BondageEquipment;
@@ -17,7 +14,6 @@ import java.util.List;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.ai.goal.Goal;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
/**
@@ -249,15 +245,18 @@ public class MasterRandomEventGoal extends Goal {
* Create a random accessory item for the given body region.
*/
private ItemStack createRandomAccessory(BodyRegionV2 region) {
Item item = switch (region) {
case EYES -> ModItems.getBlindfold(BlindfoldVariant.CLASSIC);
case MOUTH -> ModItems.getGag(GagVariant.BALL_GAG);
case HANDS -> ModItems.getMittens(MittensVariant.LEATHER);
default -> null;
return switch (region) {
case EYES -> DataDrivenBondageItem.createStack(
new ResourceLocation("tiedup", "classic_blindfold")
);
case MOUTH -> DataDrivenBondageItem.createStack(
new ResourceLocation("tiedup", "ball_gag")
);
case HANDS -> DataDrivenBondageItem.createStack(
new ResourceLocation("tiedup", "leather_mittens")
);
default -> ItemStack.EMPTY;
};
if (item == null) return ItemStack.EMPTY;
return new ItemStack(item);
}
/**
@@ -304,8 +303,8 @@ public class MasterRandomEventGoal extends Goal {
// Put pet in dogbind if not already tied
PlayerBindState bindState = PlayerBindState.getInstance(pet);
if (bindState != null && !bindState.isTiedUp()) {
ItemStack dogbind = new ItemStack(
ModItems.getBind(BindVariant.DOGBINDER)
ItemStack dogbind = DataDrivenBondageItem.createStack(
new ResourceLocation("tiedup", "dogbinder")
);
bindState.equip(BodyRegionV2.ARMS, dogbind);
}

View File

@@ -3,7 +3,7 @@ package com.tiedup.remake.entities.ai.master;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.dialogue.DialogueBridge;
import com.tiedup.remake.entities.EntityMaster;
import com.tiedup.remake.items.ItemChokeCollar;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.util.MessageDispatcher;
import com.tiedup.remake.v2.BodyRegionV2;
@@ -441,8 +441,8 @@ public class MasterTaskWatchGoal extends Goal {
if (bindState != null && bindState.hasCollar()) {
ItemStack collar = bindState.getEquipment(BodyRegionV2.NECK);
if (collar.getItem() instanceof ItemChokeCollar chokeCollar) {
chokeCollar.setChoking(collar, true);
if (CollarHelper.isChokeCollar(collar)) {
CollarHelper.setChoking(collar, true);
this.activeChokeCollar = collar;
this.chokeTimer = CHOKE_DURATION;
@@ -475,9 +475,9 @@ public class MasterTaskWatchGoal extends Goal {
private void deactivateChoke() {
if (
!activeChokeCollar.isEmpty() &&
activeChokeCollar.getItem() instanceof ItemChokeCollar chokeCollar
CollarHelper.isChokeCollar(activeChokeCollar)
) {
chokeCollar.setChoking(activeChokeCollar, false);
CollarHelper.setChoking(activeChokeCollar, false);
TiedUpMod.LOGGER.debug(
"[MasterTaskWatchGoal] {} deactivated choke",
master.getNpcName()

View File

@@ -1,7 +1,7 @@
package com.tiedup.remake.entities.ai.personality;
import com.tiedup.remake.entities.EntityDamsel;
import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.v2.bondage.BindModeHelper;
import com.tiedup.remake.personality.NpcCommand;
import com.tiedup.remake.personality.ToolMode;
import com.tiedup.remake.v2.BodyRegionV2;
@@ -506,7 +506,7 @@ public class NpcFollowCommandGoal extends Goal {
if (dist <= ATTACK_RANGE) {
// Try to capture using bind item
ItemStack bindItem = npc.getMainHandItem();
if (bindItem.getItem() instanceof ItemBind) {
if (BindModeHelper.isBindItem(bindItem)) {
// Apply bind to target
captureTarget.equip(BodyRegionV2.ARMS, bindItem.copy());

View File

@@ -1,8 +1,8 @@
package com.tiedup.remake.entities.ai.personality;
import com.tiedup.remake.entities.EntityDamsel;
import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.v2.bondage.BindModeHelper;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.personality.NpcCommand;
import com.tiedup.remake.personality.PersonalityState;
import com.tiedup.remake.v2.BodyRegionV2;
@@ -426,7 +426,7 @@ public class NpcGuardCommandGoal extends Goal {
NonNullList<ItemStack> inventory = npc.getNpcInventory();
for (int i = 0; i < inventory.size(); i++) {
ItemStack stack = inventory.get(i);
if (stack.getItem() instanceof ItemBind) {
if (BindModeHelper.isBindItem(stack)) {
// Apply bind to slave
slave.equip(BodyRegionV2.ARMS, stack.copy());
stack.shrink(1);
@@ -486,8 +486,8 @@ public class NpcGuardCommandGoal extends Goal {
private UUID getCollarOwnerUUID(EntityDamsel slave) {
if (!slave.hasCollar()) return null;
ItemStack collar = slave.getEquipment(BodyRegionV2.NECK);
if (collar.getItem() instanceof ItemCollar collarItem) {
java.util.List<UUID> owners = collarItem.getOwners(collar);
if (CollarHelper.isCollar(collar)) {
java.util.List<UUID> owners = CollarHelper.getOwners(collar);
if (!owners.isEmpty()) {
return owners.get(0);
}

View File

@@ -3,7 +3,7 @@ package com.tiedup.remake.entities.damsel.components;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.AbstractTiedUpNpc;
import com.tiedup.remake.entities.BondageServiceHandler;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.state.ICaptor;
import com.tiedup.remake.state.IRestrainable;
import com.tiedup.remake.state.IRestrainableEntity;
@@ -565,9 +565,9 @@ public class DamselBondageManager implements IRestrainable {
ItemStack collar = getEquipment(BodyRegionV2.NECK);
if (collar.isEmpty()) return false;
if (!(collar.getItem() instanceof ItemCollar itemCollar)) return false;
if (!CollarHelper.isCollar(collar)) return false;
UUID cellId = itemCollar.getCellId(collar);
UUID cellId = CollarHelper.getCellId(collar);
if (cellId == null) return false;
// Get cell position from registry

View File

@@ -2,7 +2,7 @@ package com.tiedup.remake.entities.damsel.components;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.dialogue.EntityDialogueManager;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.personality.*;
import com.tiedup.remake.v2.BodyRegionV2;
import java.util.*;
@@ -180,8 +180,8 @@ public class DamselPersonalitySystem {
UUID masterUUID = null;
if (context.hasCollar()) {
ItemStack collar = context.getEquipment(BodyRegionV2.NECK);
if (collar.getItem() instanceof ItemCollar collarItem) {
List<UUID> owners = collarItem.getOwners(collar);
if (CollarHelper.isCollar(collar)) {
List<UUID> owners = CollarHelper.getOwners(collar);
if (!owners.isEmpty()) {
masterUUID = owners.get(0);
}

View File

@@ -4,7 +4,11 @@ import com.tiedup.remake.core.ModConfig;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.AbstractTiedUpNpc;
import com.tiedup.remake.items.base.ILockable;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
import com.tiedup.remake.v2.bondage.component.ComponentType;
import com.tiedup.remake.v2.bondage.component.GaggingComponent;
import com.tiedup.remake.v2.bondage.component.BlindingComponent;
import com.tiedup.remake.state.IRestrainableEntity;
import com.tiedup.remake.util.RestraintEffectUtils;
import com.tiedup.remake.util.TiedUpSounds;
@@ -112,19 +116,15 @@ public class NpcEquipmentManager {
public boolean hasGaggingEffect() {
ItemStack gag = getCurrentGag();
if (gag.isEmpty()) return false;
return (
gag.getItem() instanceof
com.tiedup.remake.items.base.IHasGaggingEffect
);
if (DataDrivenBondageItem.getComponent(gag, ComponentType.GAGGING, GaggingComponent.class) != null) return true;
return gag.getItem() instanceof com.tiedup.remake.items.base.IHasGaggingEffect;
}
public boolean hasBlindingEffect() {
ItemStack blindfold = getCurrentBlindfold();
if (blindfold.isEmpty()) return false;
return (
blindfold.getItem() instanceof
com.tiedup.remake.items.base.IHasBlindingEffect
);
if (DataDrivenBondageItem.getComponent(blindfold, ComponentType.BLINDING, BlindingComponent.class) != null) return true;
return blindfold.getItem() instanceof com.tiedup.remake.items.base.IHasBlindingEffect;
}
public boolean hasKnives() {
@@ -768,8 +768,8 @@ public class NpcEquipmentManager {
public boolean isCollarOwner(Player player) {
if (!hasCollar()) return true;
ItemStack collar = getCurrentCollar();
if (!(collar.getItem() instanceof ItemCollar collarItem)) return true;
return collarItem.isOwner(collar, player);
if (!CollarHelper.isCollar(collar)) return true;
return CollarHelper.isOwner(collar, player);
}
// COERCION

View File

@@ -6,7 +6,7 @@ import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.dialogue.EntityDialogueManager;
import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.entities.ai.kidnapper.KidnapperState;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.prison.PrisonerManager;
import com.tiedup.remake.prison.PrisonerRecord;
import com.tiedup.remake.prison.PrisonerState;
@@ -189,9 +189,9 @@ public class KidnapperCaptiveManager {
// If target has collar, verify we own it
if (target.hasCollar()) {
ItemStack collar = target.getEquipment(BodyRegionV2.NECK);
if (collar.getItem() instanceof ItemCollar collarItem) {
if (CollarHelper.isCollar(collar)) {
// Check if THIS kidnapper is owner
if (!collarItem.getOwners(collar).contains(host.getUUID())) {
if (!CollarHelper.getOwners(collar).contains(host.getUUID())) {
TiedUpMod.LOGGER.debug(
"[KidnapperCaptiveManager] {} can't capture {} - collar owned by someone else",
host.getNpcName(),

View File

@@ -7,7 +7,7 @@ import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.entities.EntityKidnapperElite;
import com.tiedup.remake.entities.ai.kidnapper.KidnapperState;
import com.tiedup.remake.items.ModItems;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.prison.PrisonerManager;
import com.tiedup.remake.prison.PrisonerRecord;
import com.tiedup.remake.state.IBondageState;
@@ -206,8 +206,8 @@ public class KidnapperTargetSelector {
// Self-collared entities (exploit) are treated as uncolllared — kidnappers ignore self-collars
if (state.hasCollar()) {
ItemStack collar = state.getEquipment(BodyRegionV2.NECK);
if (collar.getItem() instanceof ItemCollar collarItem) {
java.util.List<UUID> owners = collarItem.getOwners(collar);
if (CollarHelper.isCollar(collar)) {
java.util.List<UUID> owners = CollarHelper.getOwners(collar);
// Filter out self-collar (owner == wearer = exploit)
java.util.List<UUID> realOwners = owners
.stream()
@@ -312,8 +312,8 @@ public class KidnapperTargetSelector {
// Other kidnappers' collars are fair game — kidnapper can steal from kidnapper
if (state != null && state.hasCollar()) {
ItemStack collar = state.getEquipment(BodyRegionV2.NECK);
if (collar.getItem() instanceof ItemCollar collarItem) {
java.util.List<UUID> owners = collarItem.getOwners(collar);
if (CollarHelper.isCollar(collar)) {
java.util.List<UUID> owners = CollarHelper.getOwners(collar);
if (!owners.isEmpty() && !owners.contains(host.getUUID())) {
// Check if any owner is a DIFFERENT player (not self-collared, not a kidnapper)
if (host.level() instanceof ServerLevel sl) {

View File

@@ -4,7 +4,7 @@ import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.EntityMaster;
import com.tiedup.remake.entities.ai.master.MasterRandomEventGoal;
import com.tiedup.remake.entities.ai.master.MasterState;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.minigame.StruggleSessionManager;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.util.KidnappedHelper;
@@ -126,22 +126,27 @@ public class MasterPetManager {
// Configure for pet play BEFORE equipping
if (
chokeCollar.getItem() instanceof
com.tiedup.remake.items.ItemChokeCollar collar
com.tiedup.remake.items.base.IHasResistance resistable
) {
collar.setCurrentResistance(chokeCollar, PET_COLLAR_RESISTANCE);
collar.setLocked(chokeCollar, true);
collar.setLockable(chokeCollar, false); // Cannot be lockpicked
collar.setCanBeStruggledOut(chokeCollar, true); // Can struggle, but very hard
// Add this Master as owner
collar.addOwner(chokeCollar, master.getUUID(), master.getNpcName());
// Set NBT flag for pet play mode
chokeCollar.getOrCreateTag().putBoolean("petPlayMode", true);
chokeCollar
.getOrCreateTag()
.putUUID("masterUUID", master.getUUID());
resistable.setCurrentResistance(chokeCollar, PET_COLLAR_RESISTANCE);
resistable.setCanBeStruggledOut(chokeCollar, true); // Can struggle, but very hard
}
if (
chokeCollar.getItem() instanceof
com.tiedup.remake.items.base.ILockable lockable
) {
lockable.setLocked(chokeCollar, true);
lockable.setLockable(chokeCollar, false); // Cannot be lockpicked
}
// Add this Master as owner
CollarHelper.addOwner(chokeCollar, master.getUUID(), master.getNpcName());
// Set NBT flag for pet play mode
CollarHelper.setPetPlayMode(chokeCollar, true);
chokeCollar
.getOrCreateTag()
.putUUID("masterUUID", master.getUUID());
// Replace any existing collar (force removal) with the choke collar
state.replaceEquipment(BodyRegionV2.NECK, chokeCollar, true);
@@ -186,9 +191,9 @@ public class MasterPetManager {
}
// Unlock the collar
if (collarStack.getItem() instanceof ItemCollar collar) {
collar.setLocked(collarStack, false);
collar.setLockable(collarStack, true);
if (collarStack.getItem() instanceof com.tiedup.remake.items.base.ILockable lockable) {
lockable.setLocked(collarStack, false);
lockable.setLockable(collarStack, true);
}
}
@@ -242,8 +247,8 @@ public class MasterPetManager {
);
if (collarStack.isEmpty()) return;
if (collarStack.getItem() instanceof ItemCollar collar) {
collar.setCurrentResistance(collarStack, PET_COLLAR_RESISTANCE);
if (collarStack.getItem() instanceof com.tiedup.remake.items.base.IHasResistance resistable) {
resistable.setCurrentResistance(collarStack, PET_COLLAR_RESISTANCE);
TiedUpMod.LOGGER.debug(
"[MasterPetManager] Reset collar resistance for {}",
pet.getName().getString()

View File

@@ -4,7 +4,7 @@ import com.tiedup.remake.cells.CampLifecycleManager;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.EntityMaid;
import com.tiedup.remake.entities.EntitySlaveTrader;
import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.v2.bondage.BindModeHelper;
import java.util.UUID;
import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.Component;
@@ -40,7 +40,7 @@ public class CampNpcProtectionHandler {
// Check if player is holding restraint item
ItemStack heldItem = player.getItemInHand(event.getHand());
if (!(heldItem.getItem() instanceof ItemBind)) return;
if (!BindModeHelper.isBindItem(heldItem)) return;
// Check if target is trader or maid with active camp
UUID campId = null;

View File

@@ -3,8 +3,8 @@ package com.tiedup.remake.events.captivity;
import com.tiedup.remake.core.SettingsAccessor;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.LeashProxyEntity;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.state.IPlayerLeashAccess;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.util.KidnappedHelper;
@@ -146,9 +146,9 @@ public class PlayerEnslavementHandler {
ItemStack collar = slaveKidnappedState.getEquipment(
BodyRegionV2.NECK
);
if (collar.getItem() instanceof ItemCollar collarItem) {
if (CollarHelper.isCollar(collar)) {
if (
collarItem
CollarHelper
.getOwners(collar)
.contains(master.getUUID())
) {

View File

@@ -4,8 +4,8 @@ import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.entities.EntityMaid;
import com.tiedup.remake.entities.EntitySlaveTrader;
import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.prison.LaborRecord;
import com.tiedup.remake.v2.bondage.BindModeHelper;
import com.tiedup.remake.prison.PrisonerManager;
import com.tiedup.remake.prison.PrisonerRecord;
import com.tiedup.remake.prison.PrisonerState;
@@ -145,7 +145,7 @@ public class LaborAttackPunishmentHandler {
// Check if player is holding a restraint item (rope, chain, etc.)
ItemStack heldItem = serverPlayer.getItemInHand(hand);
if (!(heldItem.getItem() instanceof ItemBind)) {
if (!BindModeHelper.isBindItem(heldItem)) {
return; // Not a restraint item
}

View File

@@ -2,8 +2,8 @@ package com.tiedup.remake.events.restriction;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.EntityMaster;
import com.tiedup.remake.items.ItemChokeCollar;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.blocks.PetBedManager;
import net.minecraft.core.BlockPos;
@@ -284,8 +284,8 @@ public class PetPlayRestrictionHandler {
if (bindState == null || !bindState.hasCollar()) return;
ItemStack collar = bindState.getEquipment(BodyRegionV2.NECK);
if (collar.getItem() instanceof ItemChokeCollar chokeCollar) {
if (chokeCollar.isChoking(collar)) {
if (CollarHelper.isChokeCollar(collar)) {
if (CollarHelper.isChoking(collar)) {
// Apply ChokeEffect (short duration, re-applied each active tick)
if (
!player.hasEffect(

View File

@@ -3,8 +3,8 @@ package com.tiedup.remake.events.restriction;
import com.tiedup.remake.core.SettingsAccessor;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.minigame.StruggleSessionManager;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.network.personality.PacketSlaveBeingFreed;
import com.tiedup.remake.state.IBondageState;
@@ -642,14 +642,11 @@ public class RestraintTaskTickHandler {
if (!(slave.level() instanceof ServerLevel serverLevel)) return;
ItemStack collar = slave.getEquipment(BodyRegionV2.NECK);
if (
collar.isEmpty() ||
!(collar.getItem() instanceof ItemCollar collarItem)
) {
if (collar.isEmpty() || !CollarHelper.isCollar(collar)) {
return;
}
List<UUID> owners = collarItem.getOwners(collar);
List<UUID> owners = CollarHelper.getOwners(collar);
if (owners.isEmpty()) return;
// Create alert packet

View File

@@ -7,6 +7,9 @@ import com.tiedup.remake.dialogue.GagTalkManager;
import com.tiedup.remake.items.base.ItemGag;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.util.GagMaterial;
import com.tiedup.remake.v2.bondage.component.ComponentType;
import com.tiedup.remake.v2.bondage.component.GaggingComponent;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.util.TiedUpUtils;
import com.tiedup.remake.v2.BodyRegionV2;
@@ -54,12 +57,22 @@ public class ChatEventHandler {
BodyRegionV2.MOUTH
);
if (
!gagStack.isEmpty() &&
gagStack.getItem() instanceof ItemGag gagItem
) {
// V2: check gagging component, V1 fallback: instanceof ItemGag
GaggingComponent gaggingComp = DataDrivenBondageItem.getComponent(
gagStack, ComponentType.GAGGING, GaggingComponent.class);
boolean isGagItem = gaggingComp != null
|| gagStack.getItem() instanceof ItemGag;
if (!gagStack.isEmpty() && isGagItem) {
String originalMessage = event.getRawText();
GagMaterial material = gagItem.getGagMaterial();
// V2: get material from component, V1 fallback: from ItemGag
GagMaterial material = null;
if (gaggingComp != null) {
material = gaggingComp.getMaterial();
}
if (material == null && gagStack.getItem() instanceof ItemGag gagItem) {
material = gagItem.getGagMaterial();
}
// 1. Process the message through our GagTalkManager V2
Component muffledMessage = GagTalkManager.processGagMessage(
@@ -83,7 +96,9 @@ public class ChatEventHandler {
.append("> ")
.append(muffledMessage);
double range = material.getTalkRange();
double range = material != null
? material.getTalkRange()
: (gaggingComp != null ? gaggingComp.getRange() : 10.0);
List<ServerPlayer> nearbyPlayers =
TiedUpUtils.getPlayersAround(

View File

@@ -1,9 +1,8 @@
package com.tiedup.remake.minigame;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.ItemShockCollar;
import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.items.base.IHasResistance;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.minigame.ContinuousStruggleMiniGameState.TickResult;
import com.tiedup.remake.minigame.ContinuousStruggleMiniGameState.UpdateType;
import com.tiedup.remake.network.ModNetwork;
@@ -544,11 +543,11 @@ public class StruggleSessionManager {
if (collar.isEmpty()) return false;
// Only shock collars can trigger during struggle
if (!(collar.getItem() instanceof ItemShockCollar)) return false;
if (!CollarHelper.canShock(collar)) return false;
// Must be locked
if (collar.getItem() instanceof ItemCollar collarItem) {
return collarItem.isLocked(collar);
if (collar.getItem() instanceof com.tiedup.remake.items.base.ILockable lockable) {
return lockable.isLocked(collar);
}
return false;
}
@@ -643,14 +642,12 @@ public class StruggleSessionManager {
player,
BodyRegionV2.ARMS
);
if (
bindStack.isEmpty() ||
!(bindStack.getItem() instanceof ItemBind bind)
) {
if (bindStack.isEmpty()) {
return;
}
bind.setCurrentResistance(bindStack, session.getCurrentResistance());
if (bindStack.getItem() instanceof IHasResistance resistanceItem) {
resistanceItem.setCurrentResistance(bindStack, session.getCurrentResistance());
}
}
/**

View File

@@ -4,7 +4,7 @@ import com.tiedup.remake.compat.mca.MCACompat;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.ItemKey;
import com.tiedup.remake.items.ItemMasterKey;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.IV2BondageItem;
@@ -182,8 +182,8 @@ public abstract class MixinMCAVillagerInteraction {
// Can leash if player is a collar owner
if (state.hasCollar()) {
ItemStack collar = state.getEquipment(BodyRegionV2.NECK);
if (collar.getItem() instanceof ItemCollar collarItem) {
return collarItem.getOwners(collar).contains(player.getUUID());
if (CollarHelper.isCollar(collar)) {
return CollarHelper.isOwner(collar, player);
}
}

View File

@@ -1,7 +1,7 @@
package com.tiedup.remake.mixin;
import com.tiedup.remake.compat.mca.MCACompat;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.v2.BodyRegionV2;
import net.minecraft.world.entity.LivingEntity;
@@ -71,8 +71,8 @@ public class MixinMCAVillagerLeash {
// Can be leashed if player is collar owner
if (state.hasCollar()) {
ItemStack collar = state.getEquipment(BodyRegionV2.NECK);
if (collar.getItem() instanceof ItemCollar collarItem) {
if (collarItem.getOwners(collar).contains(player.getUUID())) {
if (CollarHelper.isCollar(collar)) {
if (CollarHelper.isOwner(collar, player)) {
cir.setReturnValue(true);
return;
}

View File

@@ -1,7 +1,7 @@
package com.tiedup.remake.mixin.client;
import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.items.base.PoseType;
import com.tiedup.remake.v2.bondage.PoseTypeHelper;
import com.tiedup.remake.state.HumanChairHelper;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.v2.BodyRegionV2;
@@ -57,11 +57,11 @@ public abstract class MixinCamera {
}
ItemStack bind = state.getEquipment(BodyRegionV2.ARMS);
if (bind.isEmpty() || !(bind.getItem() instanceof ItemBind itemBind)) {
if (bind.isEmpty()) {
return;
}
if (itemBind.getPoseType() != PoseType.DOG) {
if (PoseTypeHelper.getPoseType(bind) != PoseType.DOG) {
return;
}

View File

@@ -2,8 +2,8 @@ package com.tiedup.remake.mixin.client;
import com.tiedup.remake.client.animation.render.DogPoseRenderHandler;
import com.tiedup.remake.client.animation.util.DogPoseHelper;
import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.items.base.PoseType;
import com.tiedup.remake.v2.bondage.PoseTypeHelper;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.v2.BodyRegionV2;
import net.minecraft.client.model.PlayerModel;
@@ -45,11 +45,11 @@ public class MixinPlayerModel {
}
ItemStack bind = state.getEquipment(BodyRegionV2.ARMS);
if (bind.isEmpty() || !(bind.getItem() instanceof ItemBind itemBind)) {
if (bind.isEmpty()) {
return;
}
if (itemBind.getPoseType() != PoseType.DOG) {
if (PoseTypeHelper.getPoseType(bind) != PoseType.DOG) {
return;
}

View File

@@ -4,8 +4,9 @@ import com.tiedup.remake.client.animation.BondageAnimationManager;
import com.tiedup.remake.client.animation.StaticPoseApplier;
import com.tiedup.remake.client.animation.util.AnimationIdBuilder;
import com.tiedup.remake.compat.mca.MCACompat;
import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.items.base.PoseType;
import com.tiedup.remake.v2.bondage.BindModeHelper;
import com.tiedup.remake.v2.bondage.PoseTypeHelper;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
@@ -78,11 +79,7 @@ public class MixinVillagerEntityBaseModelMCA<T extends LivingEntity> {
// Get pose info from bind item
ItemStack bind = state.getEquipment(BodyRegionV2.ARMS);
PoseType poseType = PoseType.STANDARD;
if (bind.getItem() instanceof ItemBind itemBind) {
poseType = itemBind.getPoseType();
}
PoseType poseType = PoseTypeHelper.getPoseType(bind);
// Derive bound state from V2 regions, fallback to V1 bind mode NBT
boolean armsBound = V2EquipmentHelper.isRegionOccupied(
@@ -94,9 +91,9 @@ public class MixinVillagerEntityBaseModelMCA<T extends LivingEntity> {
BodyRegionV2.LEGS
);
if (!armsBound && !legsBound && bind.getItem() instanceof ItemBind) {
armsBound = ItemBind.hasArmsBound(bind);
legsBound = ItemBind.hasLegsBound(bind);
if (!armsBound && !legsBound && BindModeHelper.isBindItem(bind)) {
armsBound = BindModeHelper.hasArmsBound(bind);
legsBound = BindModeHelper.hasLegsBound(bind);
}
// MCA doesn't track struggling state - use false for now

View File

@@ -5,8 +5,8 @@ import com.tiedup.remake.cells.CellRegistryV2;
import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.EntityDamsel;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.network.PacketRateLimiter;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.network.sync.SyncManager;
import com.tiedup.remake.personality.PersonalityState;
import com.tiedup.remake.state.IBondageState;
@@ -14,9 +14,9 @@ import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.v2.BodyRegionV2;
import java.util.UUID;
import java.util.function.Supplier;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
@@ -96,7 +96,7 @@ public class PacketAssignCellToCollar {
}
ItemStack collarStack = state.getEquipment(BodyRegionV2.NECK);
if (!(collarStack.getItem() instanceof ItemCollar collar)) {
if (!CollarHelper.isCollar(collarStack)) {
TiedUpMod.LOGGER.debug(
"[PacketAssignCellToCollar] Invalid collar item"
);
@@ -105,7 +105,7 @@ public class PacketAssignCellToCollar {
// Security: Verify sender owns the collar (or is admin)
if (
!collar.isOwner(collarStack, sender) &&
!CollarHelper.isOwner(collarStack, sender) &&
!sender.hasPermissions(2)
) {
TiedUpMod.LOGGER.debug(
@@ -139,7 +139,15 @@ public class PacketAssignCellToCollar {
}
// Set the cell ID on the collar
collar.setCellId(collarStack, msg.cellId);
if (msg.cellId != null) {
CollarHelper.setCellId(collarStack, msg.cellId);
} else {
// Clear cell assignment
CompoundTag collarTag = collarStack.getTag();
if (collarTag != null) {
collarTag.remove("cellId");
}
}
// Sync PersonalityState for damsels
if (target instanceof EntityDamsel damsel) {

View File

@@ -7,8 +7,8 @@ import com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory;
import com.tiedup.remake.entities.EntityDamsel;
import com.tiedup.remake.items.ItemCommandWand;
import com.tiedup.remake.items.ModItems;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.network.PacketRateLimiter;
import com.tiedup.remake.personality.JobExperience;
import com.tiedup.remake.personality.NpcCommand;
@@ -390,11 +390,11 @@ public class PacketNpcCommand {
}
ItemStack collar = damsel.getEquipment(BodyRegionV2.NECK);
if (!(collar.getItem() instanceof ItemCollar collarItem)) {
if (!CollarHelper.isCollar(collar)) {
return false;
}
if (!collarItem.getOwners(collar).contains(sender.getUUID())) {
if (!CollarHelper.isOwner(collar, sender)) {
SystemMessageManager.sendToPlayer(
sender,
SystemMessageManager.MessageCategory.ERROR,

View File

@@ -1,7 +1,7 @@
package com.tiedup.remake.network.slave;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.network.PacketRateLimiter;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.v2.BodyRegionV2;
@@ -75,9 +75,9 @@ public class PacketMasterEquip {
);
if (targetState == null || !targetState.hasCollar()) return;
ItemStack collarStack = targetState.getEquipment(BodyRegionV2.NECK);
if (collarStack.getItem() instanceof ItemCollar collar) {
if (CollarHelper.isCollar(collarStack)) {
if (
!collar.isOwner(collarStack, sender) &&
!CollarHelper.isOwner(collarStack, sender) &&
!sender.hasPermissions(2)
) return;
}

View File

@@ -2,10 +2,8 @@ package com.tiedup.remake.network.slave;
import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.ItemGpsCollar;
import com.tiedup.remake.items.ItemShockCollar;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.state.IRestrainable;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.state.PlayerCaptorManager;
@@ -128,8 +126,8 @@ public class PacketSlaveAction {
ItemStack collarStack = kidnapped.getEquipment(
BodyRegionV2.NECK
);
if (collarStack.getItem() instanceof ItemCollar collar) {
if (collar.isOwner(collarStack, sender)) {
if (CollarHelper.isCollar(collarStack)) {
if (CollarHelper.isOwner(collarStack, sender)) {
targetCaptive = kidnapped;
break;
}
@@ -219,10 +217,7 @@ public class PacketSlaveAction {
}
ItemStack collarStack = target.getEquipment(BodyRegionV2.NECK);
if (
!(collarStack.getItem() instanceof ItemCollar collar) ||
!collar.canShock()
) {
if (!CollarHelper.canShock(collarStack)) {
SystemMessageManager.sendToPlayer(
sender,
SystemMessageManager.MessageCategory.ERROR,
@@ -232,14 +227,8 @@ public class PacketSlaveAction {
}
// Check if sender is owner of the collar or collar is public
// FIX: Always check permissions for ANY collar that can shock, not just ItemShockCollar
boolean isOwner = collar.isOwner(collarStack, sender);
boolean isPublic = false;
// ItemShockCollar has additional "public mode" that allows anyone to shock
if (collarStack.getItem() instanceof ItemShockCollar shockCollar) {
isPublic = shockCollar.isPublic(collarStack);
}
boolean isOwner = CollarHelper.isOwner(collarStack, sender);
boolean isPublic = CollarHelper.isPublicShock(collarStack);
if (!isOwner && !isPublic) {
SystemMessageManager.sendToPlayer(
@@ -285,10 +274,7 @@ public class PacketSlaveAction {
}
ItemStack collarStack = target.getEquipment(BodyRegionV2.NECK);
if (
!(collarStack.getItem() instanceof ItemCollar collar) ||
!collar.hasGPS()
) {
if (!CollarHelper.hasGPS(collarStack)) {
SystemMessageManager.sendToPlayer(
sender,
SystemMessageManager.MessageCategory.ERROR,
@@ -298,18 +284,16 @@ public class PacketSlaveAction {
}
// Check permissions
if (collarStack.getItem() instanceof ItemGpsCollar gpsCollar) {
boolean isOwner = collar.isOwner(collarStack, sender);
boolean isPublic = gpsCollar.hasPublicTracking(collarStack);
boolean isOwner = CollarHelper.isOwner(collarStack, sender);
boolean isPublic = CollarHelper.hasPublicTracking(collarStack);
if (!isOwner && !isPublic) {
SystemMessageManager.sendToPlayer(
sender,
SystemMessageManager.MessageCategory.ERROR,
"You don't have permission to track " + name + "!"
);
return;
}
if (!isOwner && !isPublic) {
SystemMessageManager.sendToPlayer(
sender,
SystemMessageManager.MessageCategory.ERROR,
"You don't have permission to track " + name + "!"
);
return;
}
// Check same dimension
@@ -367,8 +351,8 @@ public class PacketSlaveAction {
} else {
// For collar-owned entities, just remove collar ownership
ItemStack collarStack = target.getEquipment(BodyRegionV2.NECK);
if (collarStack.getItem() instanceof ItemCollar collar) {
collar.removeOwner(collarStack, sender.getUUID());
if (CollarHelper.isCollar(collarStack)) {
CollarHelper.removeOwner(collarStack, sender.getUUID());
SystemMessageManager.sendToPlayer(
sender,
"Released collar control of " + name + "!",

View File

@@ -1,6 +1,6 @@
package com.tiedup.remake.personality;
import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.v2.bondage.BindModeHelper;
import net.minecraft.world.item.AxeItem;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.PickaxeItem;
@@ -48,7 +48,7 @@ public enum ToolMode {
if (item instanceof AxeItem) {
return WOODCUTTING;
}
if (item instanceof ItemBind) {
if (BindModeHelper.isBindItem(mainHand)) {
return CAPTURE;
}

View File

@@ -1,8 +1,8 @@
package com.tiedup.remake.state;
import com.tiedup.remake.items.base.ILockable;
import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.BindModeHelper;
import java.util.function.Supplier;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
@@ -97,7 +97,7 @@ public interface IBondageState extends ICapturable {
if (!isTiedUp()) return false;
ItemStack bind = getEquipment(BodyRegionV2.ARMS);
if (bind.isEmpty()) return false;
return ItemBind.hasArmsBound(bind);
return BindModeHelper.hasArmsBound(bind);
}
/**
@@ -111,7 +111,7 @@ public interface IBondageState extends ICapturable {
if (!isTiedUp()) return false;
ItemStack bind = getEquipment(BodyRegionV2.ARMS);
if (bind.isEmpty()) return false;
return ItemBind.hasLegsBound(bind);
return BindModeHelper.hasLegsBound(bind);
}
/**
@@ -121,10 +121,10 @@ public interface IBondageState extends ICapturable {
* @return "full", "arms", or "legs"
*/
default String getBindModeId() {
if (!isTiedUp()) return ItemBind.BIND_MODE_FULL;
if (!isTiedUp()) return BindModeHelper.MODE_FULL;
ItemStack bind = getEquipment(BodyRegionV2.ARMS);
if (bind.isEmpty()) return ItemBind.BIND_MODE_FULL;
return ItemBind.getBindModeId(bind);
if (bind.isEmpty()) return BindModeHelper.MODE_FULL;
return BindModeHelper.getBindModeId(bind);
}
/**

View File

@@ -3,8 +3,8 @@ package com.tiedup.remake.state;
import com.tiedup.remake.core.ModSounds;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.LeashProxyEntity;
import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.items.base.ILockable;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.network.sync.SyncManager;
import com.tiedup.remake.state.components.PlayerCaptivity;
import com.tiedup.remake.state.components.PlayerClothesPermission;
@@ -737,8 +737,8 @@ public class PlayerBindState implements IRestrainable, IPlayerBindStateHost {
ItemStack collar = getCurrentCollar();
return (
!collar.isEmpty() &&
collar.getItem() instanceof ItemCollar collarItem &&
collarItem.isLocked(collar)
collar.getItem() instanceof ILockable lockable &&
lockable.isLocked(collar)
);
}
@@ -1211,9 +1211,9 @@ public class PlayerBindState implements IRestrainable, IPlayerBindStateHost {
ItemStack collar = getEquipment(BodyRegionV2.NECK);
if (
!collar.isEmpty() &&
collar.getItem() instanceof ItemCollar collarItem
collar.getItem() instanceof ILockable lockable
) {
collarItem.setLocked(collar, false);
lockable.setLocked(collar, false);
}
// Drop all items

View File

@@ -1,8 +1,8 @@
package com.tiedup.remake.state;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.CollarHelper;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
@@ -144,9 +144,9 @@ public class PlayerCaptorManager implements ICaptor {
// Check if target has collar with this captor as owner
if (target.hasCollar()) {
ItemStack collar = target.getEquipment(BodyRegionV2.NECK);
if (collar.getItem() instanceof ItemCollar collarItem) {
if (CollarHelper.isCollar(collar)) {
if (
collarItem.getOwners(collar).contains(this.captor.getUUID())
CollarHelper.getOwners(collar).contains(this.captor.getUUID())
) {
return true;
}

View File

@@ -1,7 +1,7 @@
package com.tiedup.remake.state.components;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.state.hosts.IPlayerBindStateHost;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
import net.minecraft.world.entity.LivingEntity;
@@ -87,12 +87,9 @@ public class PlayerDataRetrieval {
Player player = host.getPlayer();
ItemStack collar = getCurrentCollar();
if (
!collar.isEmpty() &&
collar.getItem() instanceof ItemCollar collarItem
) {
if (!collar.isEmpty() && CollarHelper.isCollar(collar)) {
// Try to get nickname from collar NBT
String nickname = collarItem.getNickname(collar);
String nickname = CollarHelper.getNickname(collar);
if (nickname != null && !nickname.isEmpty()) {
return nickname;
}
@@ -109,8 +106,8 @@ public class PlayerDataRetrieval {
ItemStack collar = getCurrentCollar();
if (collar.isEmpty()) return false;
if (collar.getItem() instanceof ItemCollar collarItem) {
String nickname = collarItem.getNickname(collar);
if (CollarHelper.isCollar(collar)) {
String nickname = CollarHelper.getNickname(collar);
return nickname != null && !nickname.isEmpty();
}
return false;

View File

@@ -1,8 +1,6 @@
package com.tiedup.remake.state.components;
import com.tiedup.remake.items.base.ILockable;
import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.state.hosts.IPlayerBindStateHost;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.IV2BondageEquipment;
@@ -154,9 +152,9 @@ public class PlayerEquipment {
);
if (collar.isEmpty()) return ItemStack.EMPTY;
if (collar.getItem() instanceof ItemCollar collarItem) {
if (!force && collarItem.isLocked(collar)) return ItemStack.EMPTY;
collarItem.setLocked(collar, false);
if (collar.getItem() instanceof ILockable lockable) {
if (!force && lockable.isLocked(collar)) return ItemStack.EMPTY;
lockable.setLocked(collar, false);
}
return V2EquipmentHelper.unequipFromRegion(player, BodyRegionV2.NECK);
}

View File

@@ -3,8 +3,8 @@ package com.tiedup.remake.state.components;
import com.tiedup.remake.cells.CampOwnership;
import com.tiedup.remake.cells.CellRegistryV2;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.state.hosts.IPlayerBindStateHost;
import com.tiedup.remake.v2.bondage.BindModeHelper;
import com.tiedup.remake.v2.BodyRegionV2;
import java.util.UUID;
import net.minecraft.server.level.ServerLevel;
@@ -148,7 +148,7 @@ public class PlayerLifecycle {
*/
private boolean hasLegsBound(ItemStack bind) {
if (bind.isEmpty()) return false;
if (!(bind.getItem() instanceof ItemBind)) return false;
return ItemBind.hasLegsBound(bind);
if (!BindModeHelper.isBindItem(bind)) return false;
return BindModeHelper.hasLegsBound(bind);
}
}

View File

@@ -1,7 +1,19 @@
package com.tiedup.remake.util;
import com.tiedup.remake.blocks.entity.IBondageItemHolder;
import com.tiedup.remake.items.base.*;
import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.items.base.ItemBlindfold;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.items.base.ItemEarplugs;
import com.tiedup.remake.items.base.ItemGag;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.BindModeHelper;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.v2.bondage.component.ComponentType;
import com.tiedup.remake.v2.bondage.component.GaggingComponent;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemDefinition;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemRegistry;
import java.util.List;
import net.minecraft.ChatFormatting;
import net.minecraft.nbt.CompoundTag;
@@ -41,36 +53,32 @@ public final class BondageItemLoaderUtility {
ItemStack stack,
Player player
) {
if (stack.getItem() instanceof ItemBind && holder.getBind().isEmpty()) {
if ((stack.getItem() instanceof ItemBind || BindModeHelper.isBindItem(stack)) && holder.getBind().isEmpty()) {
holder.setBind(stack.copyWithCount(1));
if (!player.isCreative()) stack.shrink(1);
return true;
}
if (stack.getItem() instanceof ItemGag && holder.getGag().isEmpty()) {
if ((stack.getItem() instanceof ItemGag
|| DataDrivenBondageItem.getComponent(stack, ComponentType.GAGGING, GaggingComponent.class) != null)
&& holder.getGag().isEmpty()) {
holder.setGag(stack.copyWithCount(1));
if (!player.isCreative()) stack.shrink(1);
return true;
}
if (
stack.getItem() instanceof ItemBlindfold &&
holder.getBlindfold().isEmpty()
) {
if ((stack.getItem() instanceof ItemBlindfold || isDataDrivenForRegion(stack, BodyRegionV2.EYES))
&& holder.getBlindfold().isEmpty()) {
holder.setBlindfold(stack.copyWithCount(1));
if (!player.isCreative()) stack.shrink(1);
return true;
}
if (
stack.getItem() instanceof ItemEarplugs &&
holder.getEarplugs().isEmpty()
) {
if ((stack.getItem() instanceof ItemEarplugs || isDataDrivenForRegion(stack, BodyRegionV2.EARS))
&& holder.getEarplugs().isEmpty()) {
holder.setEarplugs(stack.copyWithCount(1));
if (!player.isCreative()) stack.shrink(1);
return true;
}
if (
stack.getItem() instanceof ItemCollar &&
holder.getCollar().isEmpty()
) {
if ((stack.getItem() instanceof ItemCollar || CollarHelper.isCollar(stack))
&& holder.getCollar().isEmpty()) {
holder.setCollar(stack.copyWithCount(1));
if (!player.isCreative()) stack.shrink(1);
return true;
@@ -87,13 +95,28 @@ public final class BondageItemLoaderUtility {
* @return true if the item can be loaded into a bondage item holder
*/
public static boolean isLoadableBondageItem(ItemStack stack) {
return (
(stack.getItem() instanceof ItemBind) ||
(stack.getItem() instanceof ItemGag) ||
(stack.getItem() instanceof ItemBlindfold) ||
(stack.getItem() instanceof ItemEarplugs) ||
(stack.getItem() instanceof ItemCollar)
);
if (stack.isEmpty()) return false;
// V1 item types
if (stack.getItem() instanceof ItemBind
|| stack.getItem() instanceof ItemGag
|| stack.getItem() instanceof ItemBlindfold
|| stack.getItem() instanceof ItemEarplugs
|| stack.getItem() instanceof ItemCollar) {
return true;
}
// V2 data-driven items: bind, gag, blindfold, earplugs, collar
if (BindModeHelper.isBindItem(stack)) return true;
if (DataDrivenBondageItem.getComponent(stack, ComponentType.GAGGING, GaggingComponent.class) != null) return true;
if (isDataDrivenForRegion(stack, BodyRegionV2.EYES)) return true;
if (isDataDrivenForRegion(stack, BodyRegionV2.EARS)) return true;
if (CollarHelper.isCollar(stack)) return true;
return false;
}
/** Check if a stack is a data-driven item occupying the given body region. */
private static boolean isDataDrivenForRegion(ItemStack stack, BodyRegionV2 region) {
DataDrivenItemDefinition def = DataDrivenItemRegistry.get(stack);
return def != null && def.occupiedRegions().contains(region);
}
/**

View File

@@ -2,9 +2,8 @@ package com.tiedup.remake.util;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.base.IHasResistance;
import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.v2.BodyRegionV2;
import java.util.UUID;
import net.minecraft.ChatFormatting;
@@ -60,7 +59,7 @@ public final class RestraintApplicator {
}
// Tighten existing bind - reset resistance to max
if (currentBind.getItem() instanceof ItemBind bindItem) {
if (currentBind.getItem() instanceof IHasResistance bindItem) {
int maxResistance = bindItem.getBaseResistance(target);
state.setCurrentBindResistance(maxResistance);
return true;
@@ -286,11 +285,8 @@ public final class RestraintApplicator {
ItemStack collarCopy = collar.copy();
// Add owner if provided
if (
ownerUUID != null &&
collarCopy.getItem() instanceof ItemCollar collarItem
) {
collarItem.addOwner(
if (ownerUUID != null && CollarHelper.isCollar(collarCopy)) {
CollarHelper.addOwner(
collarCopy,
ownerUUID,
ownerName != null ? ownerName : "Unknown"

View File

@@ -1,7 +1,8 @@
package com.tiedup.remake.v2.bondage.movement;
import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.items.base.PoseType;
import com.tiedup.remake.v2.bondage.BindModeHelper;
import com.tiedup.remake.v2.bondage.PoseTypeHelper;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemDefinition;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemRegistry;
@@ -19,7 +20,7 @@ import org.jetbrains.annotations.Nullable;
* default speed/jump values. Modifiers from lower-severity items are ignored.</p>
*
* <h3>V1 Compatibility (H6 fix)</h3>
* <p>V1 items ({@link ItemBind}) stored in V2 capability
* <p>V1 items (ItemBind) stored in V2 capability
* do not have data-driven definitions. This resolver provides a fallback that
* maps V1 bind mode + pose type to a {@link MovementStyle} with speed values matching
* the original V1 behavior, preventing double stacking between the legacy
@@ -122,7 +123,7 @@ public final class MovementStyleResolver {
// ==================== V1 Fallback ====================
/**
* Attempt to derive a movement style from a V1 {@link ItemBind} item.
* Attempt to derive a movement style from a V1 bind item.
*
* <p>Only items with legs bound produce a movement style. The mapping preserves
* the original V1 speed values:</p>
@@ -137,15 +138,15 @@ public final class MovementStyleResolver {
*/
@Nullable
private static V1Fallback resolveV1Fallback(ItemStack stack) {
if (!(stack.getItem() instanceof ItemBind bindItem)) {
if (!BindModeHelper.isBindItem(stack)) {
return null;
}
if (!ItemBind.hasLegsBound(stack)) {
if (!BindModeHelper.hasLegsBound(stack)) {
return null;
}
PoseType poseType = bindItem.getPoseType();
PoseType poseType = PoseTypeHelper.getPoseType(stack);
return switch (poseType) {
case WRAP, LATEX_SACK -> new V1Fallback(

View File

@@ -1,8 +1,8 @@
package com.tiedup.remake.v2.furniture.network;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.network.PacketRateLimiter;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.v2.BodyRegionV2;
@@ -28,7 +28,7 @@ import net.minecraftforge.network.NetworkEvent;
* Client-to-server packet: master forces a captive onto a furniture seat.
*
* <p>The sender must own the captive's collar (verified via
* {@link ItemCollar#isOwner(ItemStack, net.minecraft.world.entity.player.Player)}),
* {@link CollarHelper#isOwner(ItemStack, net.minecraft.world.entity.player.Player)}),
* the captive must be alive and within 5 blocks of both sender and furniture,
* and the furniture must have an available seat.</p>
*
@@ -132,10 +132,7 @@ public class PacketFurnitureForcemount {
}
ItemStack collarStack = captiveState.getEquipment(BodyRegionV2.NECK);
if (
collarStack.isEmpty() ||
!(collarStack.getItem() instanceof ItemCollar collar)
) {
if (!CollarHelper.isCollar(collarStack)) {
TiedUpMod.LOGGER.debug(
"[PacketFurnitureForcemount] Invalid collar item on captive"
);
@@ -143,7 +140,7 @@ public class PacketFurnitureForcemount {
}
// Collar must be owned by sender (or sender has admin permission)
if (!collar.isOwner(collarStack, sender) && !sender.hasPermissions(2)) {
if (!CollarHelper.isOwner(collarStack, sender) && !sender.hasPermissions(2)) {
TiedUpMod.LOGGER.debug(
"[PacketFurnitureForcemount] {} is not the collar owner of {}",
sender.getName().getString(),

View File

@@ -3,13 +3,15 @@ package com.tiedup.remake.worldgen;
import com.tiedup.remake.blocks.ModBlocks;
import com.tiedup.remake.blocks.entity.TrappedChestBlockEntity;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.ModItems;
import com.tiedup.remake.items.base.BindVariant;
import com.tiedup.remake.items.base.BlindfoldVariant;
import com.tiedup.remake.items.base.GagVariant;
import com.tiedup.remake.v2.V2Blocks;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemDefinition;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemRegistry;
import com.tiedup.remake.v2.blocks.PetCageBlock;
import com.tiedup.remake.v2.blocks.PetCagePartBlock;
import java.util.List;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
@@ -540,34 +542,35 @@ public class HangingCagePiece extends StructurePiece {
BlockEntity be = level.getBlockEntity(chestPos);
if (be instanceof TrappedChestBlockEntity trappedChest) {
// Random bind
BindVariant[] bindVariants = BindVariant.values();
BindVariant chosenBind = bindVariants[random.nextInt(
bindVariants.length
)];
ItemStack bindStack = new ItemStack(ModItems.getBind(chosenBind));
trappedChest.setBind(bindStack);
// Random gag (50% chance)
if (random.nextFloat() < 0.50f) {
GagVariant[] gagVariants = GagVariant.values();
GagVariant chosenGag = gagVariants[random.nextInt(
gagVariants.length
)];
ItemStack gagStack = new ItemStack(ModItems.getGag(chosenGag));
trappedChest.setGag(gagStack);
// Random bind from data-driven ARMS items
List<DataDrivenItemDefinition> binds = DataDrivenItemRegistry.getAll().stream()
.filter(d -> d.occupiedRegions().contains(BodyRegionV2.ARMS))
.collect(Collectors.toList());
if (!binds.isEmpty()) {
DataDrivenItemDefinition chosenBind = binds.get(random.nextInt(binds.size()));
trappedChest.setBind(DataDrivenBondageItem.createStack(chosenBind.id()));
}
// Random blindfold (30% chance)
// Random gag from data-driven MOUTH items (50% chance)
if (random.nextFloat() < 0.50f) {
List<DataDrivenItemDefinition> gags = DataDrivenItemRegistry.getAll().stream()
.filter(d -> d.occupiedRegions().contains(BodyRegionV2.MOUTH))
.collect(Collectors.toList());
if (!gags.isEmpty()) {
DataDrivenItemDefinition chosenGag = gags.get(random.nextInt(gags.size()));
trappedChest.setGag(DataDrivenBondageItem.createStack(chosenGag.id()));
}
}
// Random blindfold from data-driven EYES items (30% chance)
if (random.nextFloat() < 0.30f) {
BlindfoldVariant[] bfVariants = BlindfoldVariant.values();
BlindfoldVariant chosenBf = bfVariants[random.nextInt(
bfVariants.length
)];
ItemStack bfStack = new ItemStack(
ModItems.getBlindfold(chosenBf)
);
trappedChest.setBlindfold(bfStack);
List<DataDrivenItemDefinition> blindfolds = DataDrivenItemRegistry.getAll().stream()
.filter(d -> d.occupiedRegions().contains(BodyRegionV2.EYES))
.collect(Collectors.toList());
if (!blindfolds.isEmpty()) {
DataDrivenItemDefinition chosenBf = blindfolds.get(random.nextInt(blindfolds.size()));
trappedChest.setBlindfold(DataDrivenBondageItem.createStack(chosenBf.id()));
}
}
}
}
@@ -629,22 +632,26 @@ public class HangingCagePiece extends StructurePiece {
entityTag.putUUID("UUID", java.util.UUID.randomUUID());
// Random bind item — the damsel spawns already restrained
BindVariant[] variants = BindVariant.values();
BindVariant chosenBind = variants[random.nextInt(variants.length)];
ItemStack bindStack = new ItemStack(ModItems.getBind(chosenBind));
bindStack.getOrCreateTag().putString("bindMode", "full");
entityTag.put("Bind", bindStack.save(new CompoundTag()));
List<DataDrivenItemDefinition> bindDefs = DataDrivenItemRegistry.getAll().stream()
.filter(d -> d.occupiedRegions().contains(BodyRegionV2.ARMS))
.collect(Collectors.toList());
if (!bindDefs.isEmpty()) {
DataDrivenItemDefinition chosenBind = bindDefs.get(random.nextInt(bindDefs.size()));
ItemStack bindStack = DataDrivenBondageItem.createStack(chosenBind.id());
bindStack.getOrCreateTag().putString("bindMode", "full");
entityTag.put("Bind", bindStack.save(new CompoundTag()));
// Add directly to chunk's pending entity list
ChunkAccess chunk = level.getChunk(masterPos);
if (chunk instanceof ProtoChunk protoChunk) {
protoChunk.addEntity(entityTag);
TiedUpMod.LOGGER.info(
"[HangingCage] Scheduled {} damsel with {} at {}",
shiny ? "shiny" : "regular",
chosenBind.getRegistryName(),
masterPos.toShortString()
);
// Add directly to chunk's pending entity list
ChunkAccess chunk = level.getChunk(masterPos);
if (chunk instanceof ProtoChunk protoChunk) {
protoChunk.addEntity(entityTag);
TiedUpMod.LOGGER.info(
"[HangingCage] Scheduled {} damsel with {} at {}",
shiny ? "shiny" : "regular",
chosenBind.id(),
masterPos.toShortString()
);
}
}
}
}