feature/d01-component-system #5

Merged
NotEvil merged 20 commits from feature/d01-component-system into develop 2026-04-14 00:54:17 +00:00
6 changed files with 252 additions and 1 deletions
Showing only changes of commit 185ac63a44 - Show all commits

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,12 @@ import org.jetbrains.annotations.Nullable;
public enum ComponentType {
LOCKABLE("lockable", LockableComponent::fromJson),
RESISTANCE("resistance", ResistanceComponent::fromJson),
GAGGING("gagging", GaggingComponent::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;

View File

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

View File

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