Compare commits
22 Commits
3a1082dc38
...
feature/d0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7bd840705a | ||
|
|
90bc890b95 | ||
|
|
185ac63a44 | ||
|
|
dcc8493e5e | ||
|
|
bfcc20d242 | ||
|
|
3a81bb6e12 | ||
|
|
bb589d44f8 | ||
|
|
456335e0dd | ||
|
|
bb209bcd8e | ||
|
|
1327e3bfc3 | ||
|
|
dbacef66d5 | ||
|
|
231522c68e | ||
|
|
84f4c3a53f | ||
|
|
caeb4469b1 | ||
|
|
3a1f401ccf | ||
|
|
a781dad597 | ||
|
|
750be66d80 | ||
|
|
1b70041c36 | ||
|
|
b8a0d839f5 | ||
|
|
edfc3c6506 | ||
| 3fe3e16e0a | |||
|
|
e17998933c |
@@ -105,7 +105,7 @@ Some dependencies are included as local JARs in `libs/` because they are not ava
|
||||
GPL-3.0 with Commons Clause - see [LICENSE](LICENSE) for details.
|
||||
|
||||
**TL;DR:** Free to use, modify, and distribute. Cannot be sold or put behind a paywall.
|
||||
The 3D models are the **property of their creators**; if their names are listed, please ask them for permission.
|
||||
The 3D models are the **property of their creators**; if their names are listed, please ask them for permission otherwise me.
|
||||
|
||||
## Status
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
7. [Animations](#animations) — item poses, fallback chain, variants, context animations
|
||||
8. [Animation Templates](#animation-templates)
|
||||
9. [Exporting from Blender](#exporting-from-blender)
|
||||
10. [The JSON Definition](#the-json-definition)
|
||||
10. [The JSON Definition](#the-json-definition) — field reference, components, pose priority, movement styles
|
||||
11. [Packaging as a Resource Pack](#packaging-as-a-resource-pack)
|
||||
12. [Common Mistakes](#common-mistakes)
|
||||
13. [Examples](#examples)
|
||||
@@ -762,7 +762,9 @@ The `movement_style` changes how the player physically moves — slower speed, d
|
||||
| `animations` | string/object | No | `"auto"` (default) or explicit name mapping |
|
||||
| `movement_style` | string | No | Movement restriction: `"waddle"`, `"shuffle"`, `"hop"`, or `"crawl"` |
|
||||
| `movement_modifier` | object | No | Override speed/jump for the movement style (requires `movement_style`) |
|
||||
| `creator` | string | No | Author/creator name, shown in the item tooltip |
|
||||
| `animation_bones` | object | Yes | Per-animation bone whitelist (see below) |
|
||||
| `components` | object | No | Gameplay behavior components (see [Components](#components-gameplay-behaviors) below) |
|
||||
|
||||
### animation_bones (required)
|
||||
|
||||
@@ -784,6 +786,87 @@ At runtime, the effective bones for a given animation clip are computed as the *
|
||||
|
||||
This field is **required**. Items without `animation_bones` will be rejected by the parser.
|
||||
|
||||
### Components (Gameplay Behaviors)
|
||||
|
||||
Components add gameplay behaviors to your item without requiring Java code. Each component is a self-contained module you declare in the `"components"` block of your JSON definition.
|
||||
|
||||
**Format:** A JSON object where each key is a component name and each value is the component's configuration (an object, or `true` for defaults).
|
||||
|
||||
```json
|
||||
"components": {
|
||||
"lockable": { "lock_resistance": 200 },
|
||||
"resistance": { "base": 150 },
|
||||
"gagging": { "comprehension": 0.2, "range": 10.0 }
|
||||
}
|
||||
```
|
||||
|
||||
Items without the `"components"` field work normally — components are entirely optional.
|
||||
|
||||
#### Available Components
|
||||
|
||||
| Component | Description | Config Fields |
|
||||
|-----------|-------------|---------------|
|
||||
| `lockable` | Item can be locked with a padlock. Locked items cannot be unequipped. | `lock_resistance` (int, default: 250) — resistance added by the lock for struggle mechanics |
|
||||
| `resistance` | Struggle resistance. Higher = harder to escape. | `base` (int, default: 100) — base resistance value |
|
||||
| `gagging` | Muffles the wearer's speech. | `comprehension` (0.0–1.0, default: 0.2) — how much speech is understandable. `range` (float, default: 10.0) — max hearing distance in blocks |
|
||||
| `blinding` | Applies a blindfold overlay to the wearer's screen. | `overlay` (string, optional) — custom overlay texture path. Omit for default |
|
||||
| `shock` | Item can shock the wearer (manually or automatically). | `damage` (float, default: 2.0) — damage per shock. `auto_interval` (int, default: 0) — ticks between auto-shocks (0 = manual only) |
|
||||
| `gps` | GPS tracking and safe zone enforcement. | `safe_zone_radius` (int, default: 50) — safe zone in blocks (0 = tracking only). `public_tracking` (bool, default: false) — anyone can track, not just owner |
|
||||
| `choking` | Drains air, applies darkness/slowness, deals damage when activated. | `air_drain_per_tick` (int, default: 8) — air drained per tick. `non_lethal_for_master` (bool, default: true) — won't kill if worn by a master's pet |
|
||||
| `adjustable` | Allows Y-offset adjustment via GUI slider. | `default` (float, default: 0.0), `min` (float, default: -4.0), `max` (float, default: 4.0), `step` (float, default: 0.25) — all in pixels (1px = 1/16 block) |
|
||||
|
||||
#### Example: Shock Collar with GPS
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "tiedup:bondage_item",
|
||||
"display_name": "GPS Shock Collar",
|
||||
"model": "mycreator:models/gltf/gps_shock_collar.glb",
|
||||
"regions": ["NECK"],
|
||||
"animation_bones": {
|
||||
"idle": []
|
||||
},
|
||||
"pose_priority": 10,
|
||||
"escape_difficulty": 5,
|
||||
"components": {
|
||||
"lockable": { "lock_resistance": 300 },
|
||||
"resistance": { "base": 150 },
|
||||
"shock": { "damage": 3.0, "auto_interval": 200 },
|
||||
"gps": { "safe_zone_radius": 50 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This collar can be locked (300 resistance to break the lock), has 150 base struggle resistance, shocks every 200 ticks (10 seconds) automatically, and enforces a 50-block safe zone.
|
||||
|
||||
#### Example: Adjustable Blindfold
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "tiedup:bondage_item",
|
||||
"display_name": "Leather Blindfold",
|
||||
"model": "mycreator:models/gltf/blindfold.glb",
|
||||
"regions": ["EYES"],
|
||||
"animation_bones": {
|
||||
"idle": ["head"]
|
||||
},
|
||||
"pose_priority": 10,
|
||||
"escape_difficulty": 2,
|
||||
"components": {
|
||||
"blinding": {},
|
||||
"resistance": { "base": 80 },
|
||||
"adjustable": { "min": -2.0, "max": 2.0, "step": 0.5 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Component Tips
|
||||
|
||||
- **You can combine any components.** A gag with `gagging` + `lockable` + `resistance` + `adjustable` is perfectly valid.
|
||||
- **Omit components you don't need.** A decorative collar with no shock/GPS just omits those components entirely.
|
||||
- **Default values are sensible.** `"lockable": {}` gives you standard lock behavior with default resistance. You only need to specify fields you want to customize.
|
||||
- **Components don't affect rendering.** They are purely gameplay — your GLB model and animations are independent of which components you use.
|
||||
|
||||
### Pose Priority
|
||||
|
||||
When multiple items affect the same bones, the highest `pose_priority` wins.
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.tiedup.remake.v2.bondage.component;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
/**
|
||||
* Component: positional adjustment for data-driven items.
|
||||
* Allows Y offset adjustment via GUI slider.
|
||||
*
|
||||
* JSON config:
|
||||
* {@code "adjustable": {"default": 0.0, "min": -4.0, "max": 4.0, "step": 0.25}}
|
||||
*/
|
||||
public class AdjustableComponent implements IItemComponent {
|
||||
|
||||
private final float defaultValue;
|
||||
private final float min;
|
||||
private final float max;
|
||||
private final float step;
|
||||
|
||||
private AdjustableComponent(float defaultValue, float min, float max, float step) {
|
||||
this.defaultValue = defaultValue;
|
||||
this.min = min;
|
||||
this.max = max;
|
||||
this.step = step;
|
||||
}
|
||||
|
||||
public static IItemComponent fromJson(JsonObject config) {
|
||||
float defaultVal = 0.0f;
|
||||
float min = -4.0f;
|
||||
float max = 4.0f;
|
||||
float step = 0.25f;
|
||||
if (config != null) {
|
||||
if (config.has("default")) defaultVal = config.get("default").getAsFloat();
|
||||
if (config.has("min")) min = config.get("min").getAsFloat();
|
||||
if (config.has("max")) max = config.get("max").getAsFloat();
|
||||
if (config.has("step")) step = config.get("step").getAsFloat();
|
||||
}
|
||||
// Ensure min <= max
|
||||
if (min > max) {
|
||||
float tmp = min;
|
||||
min = max;
|
||||
max = tmp;
|
||||
}
|
||||
step = Math.max(0.01f, step);
|
||||
defaultVal = Math.max(min, Math.min(max, defaultVal));
|
||||
return new AdjustableComponent(defaultVal, min, max, step);
|
||||
}
|
||||
|
||||
/** Default Y offset value. */
|
||||
public float getDefaultValue() {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/** Minimum Y offset. */
|
||||
public float getMin() {
|
||||
return min;
|
||||
}
|
||||
|
||||
/** Maximum Y offset. */
|
||||
public float getMax() {
|
||||
return max;
|
||||
}
|
||||
|
||||
/** Step increment for the adjustment slider. */
|
||||
public float getStep() {
|
||||
return step;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.tiedup.remake.v2.bondage.component;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Component: blinding effect for data-driven items.
|
||||
* Replaces IHasBlindingEffect marker interface.
|
||||
*
|
||||
* JSON config:
|
||||
* {@code "blinding": {}} or {@code "blinding": {"overlay": "tiedup:textures/overlay/custom.png"}}
|
||||
*/
|
||||
public class BlindingComponent implements IItemComponent {
|
||||
|
||||
private final String overlay; // nullable — null means default overlay
|
||||
|
||||
private BlindingComponent(String overlay) {
|
||||
this.overlay = overlay;
|
||||
}
|
||||
|
||||
public static IItemComponent fromJson(JsonObject config) {
|
||||
String overlay = null;
|
||||
if (config != null && config.has("overlay")) {
|
||||
overlay = config.get("overlay").getAsString();
|
||||
}
|
||||
return new BlindingComponent(overlay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom overlay texture path, or null for the mod's default blindfold overlay.
|
||||
*/
|
||||
@Nullable
|
||||
public String getOverlay() {
|
||||
return overlay;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.tiedup.remake.v2.bondage.component;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
/**
|
||||
* Component: choking effect for data-driven items.
|
||||
* When active, drains air, applies darkness/slowness, deals damage.
|
||||
*
|
||||
* JSON config:
|
||||
* {@code "choking": {"air_drain_per_tick": 8, "non_lethal_for_master": true}}
|
||||
*/
|
||||
public class ChokingComponent implements IItemComponent {
|
||||
|
||||
private final int airDrainPerTick;
|
||||
private final boolean nonLethalForMaster;
|
||||
|
||||
private ChokingComponent(int airDrainPerTick, boolean nonLethalForMaster) {
|
||||
this.airDrainPerTick = airDrainPerTick;
|
||||
this.nonLethalForMaster = nonLethalForMaster;
|
||||
}
|
||||
|
||||
public static IItemComponent fromJson(JsonObject config) {
|
||||
int drain = 8;
|
||||
boolean nonLethal = true;
|
||||
if (config != null) {
|
||||
if (config.has("air_drain_per_tick")) {
|
||||
drain = config.get("air_drain_per_tick").getAsInt();
|
||||
}
|
||||
if (config.has("non_lethal_for_master")) {
|
||||
nonLethal = config.get("non_lethal_for_master").getAsBoolean();
|
||||
}
|
||||
}
|
||||
drain = Math.max(1, drain);
|
||||
return new ChokingComponent(drain, nonLethal);
|
||||
}
|
||||
|
||||
/** Air drained per tick (net after vanilla +4 restoration). */
|
||||
public int getAirDrainPerTick() {
|
||||
return airDrainPerTick;
|
||||
}
|
||||
|
||||
/** Whether the choke is non-lethal when used by a master on their pet. */
|
||||
public boolean isNonLethalForMaster() {
|
||||
return nonLethalForMaster;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.tiedup.remake.v2.bondage.component;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.EnumMap;
|
||||
import java.util.Map;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public IItemComponent get(ComponentType type) {
|
||||
return components.get(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;
|
||||
}
|
||||
|
||||
public boolean has(ComponentType type) {
|
||||
return components.containsKey(type);
|
||||
}
|
||||
|
||||
public void onEquipped(ItemStack stack, LivingEntity entity) {
|
||||
for (IItemComponent c : components.values()) {
|
||||
c.onEquipped(stack, entity);
|
||||
}
|
||||
}
|
||||
|
||||
public void onUnequipped(ItemStack stack, LivingEntity entity) {
|
||||
for (IItemComponent c : components.values()) {
|
||||
c.onUnequipped(stack, entity);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean blocksUnequip(ItemStack stack, LivingEntity entity) {
|
||||
for (IItemComponent c : components.values()) {
|
||||
if (c.blocksUnequip(stack, entity)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return components.isEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.tiedup.remake.v2.bondage.component;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public enum ComponentType {
|
||||
LOCKABLE("lockable", LockableComponent::fromJson),
|
||||
RESISTANCE("resistance", ResistanceComponent::fromJson),
|
||||
GAGGING("gagging", GaggingComponent::fromJson),
|
||||
BLINDING("blinding", BlindingComponent::fromJson),
|
||||
SHOCK("shock", ShockComponent::fromJson),
|
||||
GPS("gps", GpsComponent::fromJson),
|
||||
CHOKING("choking", ChokingComponent::fromJson),
|
||||
ADJUSTABLE("adjustable", AdjustableComponent::fromJson);
|
||||
|
||||
private final String jsonKey;
|
||||
private final Function<JsonObject, IItemComponent> factory;
|
||||
|
||||
/** Pre-built lookup map for O(1) fromKey() instead of linear scan. */
|
||||
private static final Map<String, ComponentType> BY_KEY;
|
||||
static {
|
||||
Map<String, ComponentType> map = new HashMap<>();
|
||||
for (ComponentType type : values()) {
|
||||
map.put(type.jsonKey, type);
|
||||
}
|
||||
BY_KEY = Collections.unmodifiableMap(map);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static ComponentType fromKey(String key) {
|
||||
return BY_KEY.get(key);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.tiedup.remake.v2.bondage.component;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
/**
|
||||
* Component: gagging behavior for data-driven items.
|
||||
*
|
||||
* JSON config: {@code "gagging": {"comprehension": 0.2, "range": 10.0}}
|
||||
*/
|
||||
public class GaggingComponent implements IItemComponent {
|
||||
|
||||
private final double comprehension;
|
||||
private final double range;
|
||||
|
||||
private GaggingComponent(double comprehension, double range) {
|
||||
this.comprehension = comprehension;
|
||||
this.range = range;
|
||||
}
|
||||
|
||||
public static IItemComponent fromJson(JsonObject config) {
|
||||
double comprehension = 0.2;
|
||||
double range = 10.0;
|
||||
if (config != null) {
|
||||
if (config.has("comprehension")) {
|
||||
comprehension = config.get("comprehension").getAsDouble();
|
||||
}
|
||||
if (config.has("range")) {
|
||||
range = config.get("range").getAsDouble();
|
||||
}
|
||||
}
|
||||
comprehension = Math.max(0.0, Math.min(1.0, comprehension));
|
||||
range = Math.max(0.0, range);
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.tiedup.remake.v2.bondage.component;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
/**
|
||||
* Component: GPS tracking and safe zone for data-driven items.
|
||||
*
|
||||
* JSON config:
|
||||
* {@code "gps": {"safe_zone_radius": 50, "public_tracking": false}}
|
||||
*/
|
||||
public class GpsComponent implements IItemComponent {
|
||||
|
||||
private final int safeZoneRadius;
|
||||
private final boolean publicTracking;
|
||||
|
||||
private GpsComponent(int safeZoneRadius, boolean publicTracking) {
|
||||
this.safeZoneRadius = safeZoneRadius;
|
||||
this.publicTracking = publicTracking;
|
||||
}
|
||||
|
||||
public static IItemComponent fromJson(JsonObject config) {
|
||||
int radius = 50;
|
||||
boolean publicTracking = false;
|
||||
if (config != null) {
|
||||
if (config.has("safe_zone_radius")) {
|
||||
radius = config.get("safe_zone_radius").getAsInt();
|
||||
}
|
||||
if (config.has("public_tracking")) {
|
||||
publicTracking = config.get("public_tracking").getAsBoolean();
|
||||
}
|
||||
}
|
||||
radius = Math.max(0, radius);
|
||||
return new GpsComponent(radius, publicTracking);
|
||||
}
|
||||
|
||||
/** Safe zone radius in blocks. 0 = no safe zone (tracking only). */
|
||||
public int getSafeZoneRadius() {
|
||||
return safeZoneRadius;
|
||||
}
|
||||
|
||||
/** Whether any player can track (not just the owner). */
|
||||
public boolean isPublicTracking() {
|
||||
return publicTracking;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.tiedup.remake.v2.bondage.component;
|
||||
|
||||
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.
|
||||
*/
|
||||
public interface IItemComponent {
|
||||
|
||||
default void onEquipped(ItemStack stack, LivingEntity entity) {}
|
||||
|
||||
default void onUnequipped(ItemStack stack, LivingEntity entity) {}
|
||||
|
||||
default boolean blocksUnequip(ItemStack stack, LivingEntity entity) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.tiedup.remake.v2.bondage.component;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
/**
|
||||
* Component: lockable behavior for data-driven items.
|
||||
*
|
||||
* <p>Stores the per-item lock resistance parsed from JSON. The lock check
|
||||
* itself is NOT done here because {@code AbstractV2BondageItem.canUnequip()}
|
||||
* already delegates to {@code ILockable.isLocked()} -- duplicating it in
|
||||
* {@code blocksUnequip()} would be redundant.</p>
|
||||
*
|
||||
* <p>Consumers retrieve the per-item lock resistance via
|
||||
* {@link com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem#getItemLockResistance(net.minecraft.world.item.ItemStack)}.</p>
|
||||
*
|
||||
* JSON config:
|
||||
* {@code "lockable": {}} or {@code "lockable": {"lock_resistance": 300}}
|
||||
*/
|
||||
public class LockableComponent implements IItemComponent {
|
||||
|
||||
private final int lockResistance;
|
||||
|
||||
private LockableComponent(int lockResistance) {
|
||||
this.lockResistance = lockResistance;
|
||||
}
|
||||
|
||||
public static IItemComponent fromJson(JsonObject config) {
|
||||
int resistance = 250; // default
|
||||
if (config != null && config.has("lock_resistance")) {
|
||||
resistance = config.get("lock_resistance").getAsInt();
|
||||
}
|
||||
resistance = Math.max(0, resistance);
|
||||
return new LockableComponent(resistance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the per-item lock resistance value parsed from JSON.
|
||||
*
|
||||
* @return lock resistance (always >= 0 after RISK-003 clamping)
|
||||
*/
|
||||
public int getLockResistance() {
|
||||
return lockResistance;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.tiedup.remake.v2.bondage.component;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
/**
|
||||
* Component: struggle resistance for data-driven items.
|
||||
*
|
||||
* JSON config: {@code "resistance": {"base": 150}}
|
||||
*/
|
||||
public class ResistanceComponent implements IItemComponent {
|
||||
|
||||
private final int baseResistance;
|
||||
|
||||
private ResistanceComponent(int baseResistance) {
|
||||
this.baseResistance = baseResistance;
|
||||
}
|
||||
|
||||
public static IItemComponent fromJson(JsonObject config) {
|
||||
int base = 100;
|
||||
if (config != null && config.has("base")) {
|
||||
base = config.get("base").getAsInt();
|
||||
}
|
||||
base = Math.max(0, base);
|
||||
return new ResistanceComponent(base);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the base resistance for this item.
|
||||
*/
|
||||
public int getBaseResistance() {
|
||||
return baseResistance;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.tiedup.remake.v2.bondage.component;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
/**
|
||||
* Component: shock collar behavior for data-driven items.
|
||||
*
|
||||
* JSON config:
|
||||
* {@code "shock": {"damage": 2.0, "auto_interval": 0}}
|
||||
* auto_interval: ticks between auto-shocks (0 = no auto-shock, manual only)
|
||||
*/
|
||||
public class ShockComponent implements IItemComponent {
|
||||
|
||||
private final float damage;
|
||||
private final int autoInterval; // 0 = manual only
|
||||
|
||||
private ShockComponent(float damage, int autoInterval) {
|
||||
this.damage = damage;
|
||||
this.autoInterval = autoInterval;
|
||||
}
|
||||
|
||||
public static IItemComponent fromJson(JsonObject config) {
|
||||
float damage = 2.0f;
|
||||
int autoInterval = 0;
|
||||
if (config != null) {
|
||||
if (config.has("damage")) {
|
||||
damage = config.get("damage").getAsFloat();
|
||||
}
|
||||
if (config.has("auto_interval")) {
|
||||
autoInterval = config.get("auto_interval").getAsInt();
|
||||
}
|
||||
}
|
||||
damage = Math.max(0.0f, damage);
|
||||
autoInterval = Math.max(0, autoInterval);
|
||||
return new ShockComponent(damage, autoInterval);
|
||||
}
|
||||
|
||||
/** Damage dealt per shock. */
|
||||
public float getDamage() {
|
||||
return damage;
|
||||
}
|
||||
|
||||
/** Ticks between auto-shocks. 0 = manual shock only (via controller). */
|
||||
public int getAutoInterval() {
|
||||
return autoInterval;
|
||||
}
|
||||
|
||||
/** Whether this item has auto-shock capability. */
|
||||
public boolean hasAutoShock() {
|
||||
return autoInterval > 0;
|
||||
}
|
||||
}
|
||||
@@ -4,13 +4,23 @@ import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import com.tiedup.remake.v2.bondage.IV2BondageEquipment;
|
||||
import com.tiedup.remake.v2.bondage.V2BondageItems;
|
||||
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
|
||||
import com.tiedup.remake.v2.bondage.component.ComponentHolder;
|
||||
import com.tiedup.remake.v2.bondage.component.ComponentType;
|
||||
import com.tiedup.remake.v2.bondage.component.IItemComponent;
|
||||
import com.tiedup.remake.v2.bondage.component.LockableComponent;
|
||||
import com.tiedup.remake.v2.bondage.component.ResistanceComponent;
|
||||
import com.tiedup.remake.v2.bondage.items.AbstractV2BondageItem;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.TooltipFlag;
|
||||
import net.minecraft.world.level.Level;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
@@ -142,6 +152,21 @@ public class DataDrivenBondageItem extends AbstractV2BondageItem {
|
||||
.entrySet()) {
|
||||
ItemStack stack = entry.getValue();
|
||||
if (stack.getItem() == this) {
|
||||
// Try component first (stack-aware, fixes I-03)
|
||||
ResistanceComponent comp = DataDrivenBondageItem
|
||||
.getComponent(
|
||||
stack,
|
||||
ComponentType.RESISTANCE,
|
||||
ResistanceComponent.class
|
||||
);
|
||||
if (comp != null) {
|
||||
maxDifficulty = Math.max(
|
||||
maxDifficulty,
|
||||
comp.getBaseResistance()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
// Fallback: read from definition directly
|
||||
DataDrivenItemDefinition def =
|
||||
DataDrivenItemRegistry.get(stack);
|
||||
if (def != null) {
|
||||
@@ -177,6 +202,69 @@ public class DataDrivenBondageItem extends AbstractV2BondageItem {
|
||||
);
|
||||
}
|
||||
|
||||
// ===== TOOLTIPS =====
|
||||
|
||||
@Override
|
||||
public void appendHoverText(
|
||||
ItemStack stack,
|
||||
@Nullable Level level,
|
||||
List<Component> tooltip,
|
||||
TooltipFlag flag
|
||||
) {
|
||||
DataDrivenItemDefinition def = DataDrivenItemRegistry.get(stack);
|
||||
if (def != null) {
|
||||
// Creator
|
||||
if (def.creator() != null && !def.creator().isEmpty()) {
|
||||
tooltip.add(
|
||||
Component.translatable(
|
||||
"item.tiedup.tooltip.creator",
|
||||
Component.literal(def.creator())
|
||||
.withStyle(ChatFormatting.WHITE)
|
||||
).withStyle(ChatFormatting.GRAY)
|
||||
);
|
||||
}
|
||||
|
||||
// Regions
|
||||
String regions = def.occupiedRegions().stream()
|
||||
.map(r -> r.name().toLowerCase())
|
||||
.collect(Collectors.joining(", "));
|
||||
tooltip.add(
|
||||
Component.translatable(
|
||||
"item.tiedup.tooltip.regions",
|
||||
regions
|
||||
).withStyle(ChatFormatting.DARK_AQUA)
|
||||
);
|
||||
|
||||
// Movement style
|
||||
if (def.movementStyle() != null) {
|
||||
tooltip.add(
|
||||
Component.translatable(
|
||||
"item.tiedup.tooltip.movement_style",
|
||||
def.movementStyle().name().toLowerCase()
|
||||
).withStyle(ChatFormatting.DARK_PURPLE)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Lock status + escape difficulty (from AbstractV2BondageItem)
|
||||
super.appendHoverText(stack, level, tooltip, flag);
|
||||
|
||||
if (def != null && flag.isAdvanced()) {
|
||||
// Advanced info (F3+H): pose priority + item ID
|
||||
tooltip.add(
|
||||
Component.translatable(
|
||||
"item.tiedup.tooltip.pose_priority",
|
||||
def.posePriority()
|
||||
).withStyle(ChatFormatting.DARK_GRAY)
|
||||
);
|
||||
tooltip.add(
|
||||
Component.literal(
|
||||
def.id().toString()
|
||||
).withStyle(ChatFormatting.DARK_GRAY)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== DISPLAY NAME =====
|
||||
|
||||
@Override
|
||||
@@ -189,6 +277,77 @@ public class DataDrivenBondageItem extends AbstractV2BondageItem {
|
||||
return Component.literal(def.displayName());
|
||||
}
|
||||
|
||||
// ===== COMPONENT LIFECYCLE DELEGATION =====
|
||||
|
||||
@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
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific component from a data-driven item stack.
|
||||
*
|
||||
* @param stack the ItemStack to inspect
|
||||
* @param type the component type to look up
|
||||
* @param clazz the expected component class
|
||||
* @param <T> the component type
|
||||
* @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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get per-item lock resistance from the LockableComponent, if present.
|
||||
*
|
||||
* <p>Returns the component's JSON-configured value if the stack has a
|
||||
* LockableComponent, otherwise falls back to the global config value
|
||||
* from {@link com.tiedup.remake.core.SettingsAccessor#getPadlockResistance}.</p>
|
||||
*
|
||||
* @param stack the ItemStack to inspect
|
||||
* @return lock resistance value for this specific item
|
||||
*/
|
||||
public static int getItemLockResistance(ItemStack stack) {
|
||||
LockableComponent comp = getComponent(
|
||||
stack,
|
||||
ComponentType.LOCKABLE,
|
||||
LockableComponent.class
|
||||
);
|
||||
if (comp != null) {
|
||||
return comp.getLockResistance();
|
||||
}
|
||||
return com.tiedup.remake.core.SettingsAccessor.getPadlockResistance(
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
// ===== FACTORY =====
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.tiedup.remake.v2.bondage.datadriven;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import com.tiedup.remake.v2.bondage.component.ComponentType;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
@@ -81,6 +83,9 @@ public record DataDrivenItemDefinition(
|
||||
@Nullable
|
||||
com.tiedup.remake.v2.bondage.movement.MovementModifier movementModifier,
|
||||
|
||||
/** Optional creator/author name displayed in the item tooltip. */
|
||||
@Nullable String creator,
|
||||
|
||||
/**
|
||||
* Per-animation bone whitelist. Maps animation name (e.g. "idle", "struggle")
|
||||
* to the set of PlayerAnimator bone names this item is allowed to animate.
|
||||
@@ -94,5 +99,19 @@ public record DataDrivenItemDefinition(
|
||||
*
|
||||
* <p>This field is required in the JSON definition. Never null, never empty.</p>
|
||||
*/
|
||||
Map<String, Set<String>> animationBones
|
||||
) {}
|
||||
Map<String, Set<String>> animationBones,
|
||||
|
||||
/** Raw component configs from JSON, keyed by ComponentType. */
|
||||
Map<ComponentType, JsonObject> componentConfigs
|
||||
) {
|
||||
|
||||
/** Compact constructor: default null componentConfigs to empty immutable map. */
|
||||
public DataDrivenItemDefinition {
|
||||
if (componentConfigs == null) componentConfigs = Map.of();
|
||||
}
|
||||
|
||||
/** Check whether this definition declares a given component type. */
|
||||
public boolean hasComponent(ComponentType type) {
|
||||
return componentConfigs.containsKey(type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,14 @@ import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import com.tiedup.remake.v2.bondage.component.ComponentType;
|
||||
import com.tiedup.remake.v2.bondage.movement.MovementModifier;
|
||||
import com.tiedup.remake.v2.bondage.movement.MovementStyle;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumMap;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
@@ -250,6 +252,9 @@ public final class DataDrivenItemParser {
|
||||
);
|
||||
}
|
||||
|
||||
// Optional: creator (author name for tooltip)
|
||||
String creator = getStringOrNull(root, "creator");
|
||||
|
||||
// Required: animation_bones (per-animation bone whitelist)
|
||||
Map<String, Set<String>> animationBones = parseAnimationBones(
|
||||
root,
|
||||
@@ -263,6 +268,39 @@ public final class DataDrivenItemParser {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Optional: components (per-component JSON configs)
|
||||
Map<ComponentType, JsonObject> componentConfigs =
|
||||
new EnumMap<>(ComponentType.class);
|
||||
if (root.has("components")) {
|
||||
JsonObject componentsObj = root.getAsJsonObject("components");
|
||||
for (Map.Entry<String, JsonElement> entry :
|
||||
componentsObj.entrySet()) {
|
||||
ComponentType compType = ComponentType.fromKey(
|
||||
entry.getKey()
|
||||
);
|
||||
if (compType != null) {
|
||||
JsonObject config;
|
||||
if (entry.getValue().isJsonObject()) {
|
||||
config = entry.getValue().getAsJsonObject().deepCopy();
|
||||
} else {
|
||||
LOGGER.warn(
|
||||
"[DataDrivenItemParser] Component '{}' in item '{}' has non-object config, using defaults",
|
||||
entry.getKey(),
|
||||
fileId
|
||||
);
|
||||
config = new JsonObject();
|
||||
}
|
||||
componentConfigs.put(compType, config);
|
||||
} else {
|
||||
LOGGER.warn(
|
||||
"[DataDrivenItemParser] Unknown component type '{}' in item '{}'",
|
||||
entry.getKey(),
|
||||
fileId
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build the item ID from the file path
|
||||
// fileId is like "tiedup:tiedup_items/leather_armbinder.json"
|
||||
// We want "tiedup:leather_armbinder"
|
||||
@@ -298,7 +336,9 @@ public final class DataDrivenItemParser {
|
||||
icon,
|
||||
movementStyle,
|
||||
movementModifier,
|
||||
animationBones
|
||||
creator,
|
||||
animationBones,
|
||||
componentConfigs
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
package com.tiedup.remake.v2.bondage.datadriven;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.tiedup.remake.v2.bondage.component.ComponentHolder;
|
||||
import com.tiedup.remake.v2.bondage.component.ComponentType;
|
||||
import com.tiedup.remake.v2.bondage.component.IItemComponent;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
@@ -13,9 +18,9 @@ import org.jetbrains.annotations.Nullable;
|
||||
* Thread-safe registry for data-driven bondage item definitions.
|
||||
*
|
||||
* <p>Populated by the reload listener that scans {@code tiedup_items/} JSON files.
|
||||
* Uses volatile atomic swap (same pattern as {@link
|
||||
* com.tiedup.remake.client.animation.context.ContextGlbRegistry}) to ensure
|
||||
* the render thread always sees a consistent snapshot.</p>
|
||||
* Uses a single volatile snapshot reference to ensure readers always see a
|
||||
* consistent pair of definitions + component holders. This prevents torn reads
|
||||
* where one map is updated but the other is stale.</p>
|
||||
*
|
||||
* <p>Lookup methods accept either a {@link ResourceLocation} ID directly
|
||||
* or an {@link ItemStack} (reads the {@code tiedup_item_id} NBT tag).</p>
|
||||
@@ -26,13 +31,25 @@ public final class DataDrivenItemRegistry {
|
||||
public static final String NBT_ITEM_ID = "tiedup_item_id";
|
||||
|
||||
/**
|
||||
* Volatile reference to an unmodifiable map. Reload builds a new map
|
||||
* and swaps atomically; consumer threads always see a consistent snapshot.
|
||||
* Immutable snapshot combining definitions and their component holders.
|
||||
* Swapped atomically via a single volatile write to prevent torn reads.
|
||||
*/
|
||||
private static volatile Map<
|
||||
ResourceLocation,
|
||||
DataDrivenItemDefinition
|
||||
> DEFINITIONS = Map.of();
|
||||
private record RegistrySnapshot(
|
||||
Map<ResourceLocation, DataDrivenItemDefinition> definitions,
|
||||
Map<ResourceLocation, ComponentHolder> holders
|
||||
) {
|
||||
static final RegistrySnapshot EMPTY = new RegistrySnapshot(Map.of(), Map.of());
|
||||
}
|
||||
|
||||
/**
|
||||
* Single volatile reference to the current registry state.
|
||||
* All read methods capture this reference ONCE at the start to ensure
|
||||
* consistency within a single call.
|
||||
*/
|
||||
private static volatile RegistrySnapshot SNAPSHOT = RegistrySnapshot.EMPTY;
|
||||
|
||||
/** Guards read-then-write sequences in {@link #reload} and {@link #mergeAll}. */
|
||||
private static final Object RELOAD_LOCK = new Object();
|
||||
|
||||
private DataDrivenItemRegistry() {}
|
||||
|
||||
@@ -45,7 +62,11 @@ public final class DataDrivenItemRegistry {
|
||||
public static void reload(
|
||||
Map<ResourceLocation, DataDrivenItemDefinition> newDefs
|
||||
) {
|
||||
DEFINITIONS = Collections.unmodifiableMap(new HashMap<>(newDefs));
|
||||
synchronized (RELOAD_LOCK) {
|
||||
Map<ResourceLocation, DataDrivenItemDefinition> defs =
|
||||
Collections.unmodifiableMap(new HashMap<>(newDefs));
|
||||
SNAPSHOT = new RegistrySnapshot(defs, buildComponentHolders(defs));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,18 +75,23 @@ public final class DataDrivenItemRegistry {
|
||||
* <p>On an integrated server, both the client (assets/) and server (data/) reload
|
||||
* listeners populate this registry. Using {@link #reload} would cause the second
|
||||
* listener to overwrite the first's definitions. This method builds a new map
|
||||
* from the existing snapshot + the new entries, then swaps atomically.</p>
|
||||
* from the existing snapshot + the new entries, then swaps atomically as a
|
||||
* single snapshot to prevent torn reads.</p>
|
||||
*
|
||||
* @param newDefs the definitions to merge (will overwrite existing entries with same key)
|
||||
*/
|
||||
public static void mergeAll(
|
||||
Map<ResourceLocation, DataDrivenItemDefinition> newDefs
|
||||
) {
|
||||
Map<ResourceLocation, DataDrivenItemDefinition> merged = new HashMap<>(
|
||||
DEFINITIONS
|
||||
);
|
||||
merged.putAll(newDefs);
|
||||
DEFINITIONS = Collections.unmodifiableMap(merged);
|
||||
synchronized (RELOAD_LOCK) {
|
||||
Map<ResourceLocation, DataDrivenItemDefinition> merged = new HashMap<>(
|
||||
SNAPSHOT.definitions
|
||||
);
|
||||
merged.putAll(newDefs);
|
||||
Map<ResourceLocation, DataDrivenItemDefinition> defs =
|
||||
Collections.unmodifiableMap(merged);
|
||||
SNAPSHOT = new RegistrySnapshot(defs, buildComponentHolders(defs));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -76,7 +102,8 @@ public final class DataDrivenItemRegistry {
|
||||
*/
|
||||
@Nullable
|
||||
public static DataDrivenItemDefinition get(ResourceLocation id) {
|
||||
return DEFINITIONS.get(id);
|
||||
RegistrySnapshot snap = SNAPSHOT;
|
||||
return snap.definitions.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,7 +121,8 @@ public final class DataDrivenItemRegistry {
|
||||
tag.getString(NBT_ITEM_ID)
|
||||
);
|
||||
if (id == null) return null;
|
||||
return DEFINITIONS.get(id);
|
||||
RegistrySnapshot snap = SNAPSHOT;
|
||||
return snap.definitions.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -103,13 +131,85 @@ public final class DataDrivenItemRegistry {
|
||||
* @return unmodifiable collection of all definitions
|
||||
*/
|
||||
public static Collection<DataDrivenItemDefinition> getAll() {
|
||||
return DEFINITIONS.values();
|
||||
RegistrySnapshot snap = SNAPSHOT;
|
||||
return snap.definitions.values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all definitions. Called on world unload or for testing.
|
||||
*/
|
||||
public static void clear() {
|
||||
DEFINITIONS = Map.of();
|
||||
SNAPSHOT = RegistrySnapshot.EMPTY;
|
||||
}
|
||||
|
||||
// ===== COMPONENT HOLDERS =====
|
||||
|
||||
/**
|
||||
* Get the ComponentHolder for a data-driven item stack.
|
||||
*
|
||||
* <p>Captures the snapshot reference once to ensure consistent reads
|
||||
* between the definition lookup and the holder lookup.</p>
|
||||
*
|
||||
* @param stack the ItemStack to inspect
|
||||
* @return the holder, or null if the stack is not data-driven or has no definition
|
||||
*/
|
||||
@Nullable
|
||||
public static ComponentHolder getComponents(ItemStack stack) {
|
||||
if (stack.isEmpty()) return null;
|
||||
CompoundTag tag = stack.getTag();
|
||||
if (tag == null || !tag.contains(NBT_ITEM_ID)) return null;
|
||||
ResourceLocation id = ResourceLocation.tryParse(
|
||||
tag.getString(NBT_ITEM_ID)
|
||||
);
|
||||
if (id == null) return null;
|
||||
// Capture snapshot once to ensure definition and holder come from
|
||||
// the same atomic snapshot, preventing torn reads.
|
||||
RegistrySnapshot snap = SNAPSHOT;
|
||||
DataDrivenItemDefinition def = snap.definitions.get(id);
|
||||
if (def == null) return null;
|
||||
return snap.holders.get(def.id());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ComponentHolder for a data-driven item by its definition ID.
|
||||
*
|
||||
* @param id the definition ID
|
||||
* @return the holder, or null if not found
|
||||
*/
|
||||
@Nullable
|
||||
public static ComponentHolder getComponents(ResourceLocation id) {
|
||||
RegistrySnapshot snap = SNAPSHOT;
|
||||
return snap.holders.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build component holders from a definitions map.
|
||||
* Each definition's raw componentConfigs are instantiated via
|
||||
* {@link ComponentType#create(JsonObject)}.
|
||||
*/
|
||||
private static Map<ResourceLocation, ComponentHolder> buildComponentHolders(
|
||||
Map<ResourceLocation, DataDrivenItemDefinition> definitions
|
||||
) {
|
||||
Map<ResourceLocation, ComponentHolder> holders = new HashMap<>();
|
||||
for (Map.Entry<ResourceLocation, DataDrivenItemDefinition> entry :
|
||||
definitions.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(),
|
||||
components.isEmpty()
|
||||
? ComponentHolder.EMPTY
|
||||
: new ComponentHolder(components)
|
||||
);
|
||||
}
|
||||
return Collections.unmodifiableMap(holders);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,6 +229,10 @@
|
||||
"item.tiedup.tooltip.lockable": "Lockable (has padlock)",
|
||||
"item.tiedup.tooltip.jammed": "Jammed (lockpick blocked)",
|
||||
"item.tiedup.tooltip.escape_difficulty": "Escape Difficulty: %s",
|
||||
"item.tiedup.tooltip.creator": "By %s",
|
||||
"item.tiedup.tooltip.regions": "Regions: %s",
|
||||
"item.tiedup.tooltip.movement_style": "Movement: %s",
|
||||
"item.tiedup.tooltip.pose_priority": "Pose Priority: %s",
|
||||
|
||||
"item.tiedup.v2_handcuffs": "Handcuffs",
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"type": "tiedup:bondage_item",
|
||||
"display_name": "Data-Driven Handcuffs",
|
||||
"creator": "TiedUp! Team",
|
||||
"model": "tiedup:models/gltf/v2/handcuffs/cuffs_prototype.glb",
|
||||
"regions": ["ARMS"],
|
||||
"pose_priority": 30,
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"type": "tiedup:bondage_item",
|
||||
"display_name": "Component Test Gag",
|
||||
"model": "tiedup:models/gltf/v2/handcuffs/cuffs_prototype.glb",
|
||||
"regions": ["MOUTH"],
|
||||
"pose_priority": 10,
|
||||
"escape_difficulty": 3,
|
||||
"lockable": true,
|
||||
"animation_bones": {
|
||||
"idle": []
|
||||
},
|
||||
"components": {
|
||||
"lockable": {
|
||||
"lock_resistance": 200
|
||||
},
|
||||
"resistance": {
|
||||
"base": 80
|
||||
},
|
||||
"gagging": {
|
||||
"comprehension": 0.15,
|
||||
"range": 8.0
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user