fix(D-01/D): checkup cleanup — 5 issues resolved
1. LockableComponent: remove duplicate "Lockable" tooltip line (ILockable.appendLockTooltip already handles lock status display) 2. ILockable/IHasResistance Javadoc: update @link refs from deleted V1 classes to V2 AbstractV2BondageItem/DataDrivenBondageItem 3. SettingsAccessor Javadoc: remove stale BindVariant @link references 4. DataDrivenBondageItem: update NECK block comment (remove branch ref) 5. Delete empty bondage3d/gags/ directory
This commit is contained in:
869
docs/superpowers/plans/2026-04-13-d01-phase1-component-system.md
Normal file
869
docs/superpowers/plans/2026-04-13-d01-phase1-component-system.md
Normal 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."
|
||||
```
|
||||
Reference in New Issue
Block a user