feat(D-01/A): config-driven components + tooltip hook (A1, A2, A3)

- ResistanceComponent: resistanceId delegates to SettingsAccessor at runtime,
  fallback to hardcoded base for backward compat
- GaggingComponent: material field delegates to GagMaterial enum from ModConfig,
  explicit comprehension/range overrides take priority
- IItemComponent: add default appendTooltip() method
- ComponentHolder: iterate components for tooltip contribution
- 6 components implement appendTooltip (lockable, resistance, gagging, shock,
  gps, choking)
- DataDrivenBondageItem: call holder.appendTooltip() in appendHoverText()
This commit is contained in:
NotEvil
2026-04-14 15:05:48 +02:00
parent fa4c332a10
commit b81d3eed95
9 changed files with 196 additions and 23 deletions

View File

@@ -1,6 +1,13 @@
package com.tiedup.remake.v2.bondage.component; package com.tiedup.remake.v2.bondage.component;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import java.util.List;
import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.Component;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.level.Level;
import org.jetbrains.annotations.Nullable;
/** /**
* Component: choking effect for data-driven items. * Component: choking effect for data-driven items.
@@ -43,4 +50,9 @@ public class ChokingComponent implements IItemComponent {
public boolean isNonLethalForMaster() { public boolean isNonLethalForMaster() {
return nonLethalForMaster; return nonLethalForMaster;
} }
@Override
public void appendTooltip(ItemStack stack, @Nullable Level level, List<Component> tooltip, TooltipFlag flag) {
tooltip.add(Component.translatable("item.tiedup.tooltip.choking").withStyle(ChatFormatting.DARK_PURPLE));
}
} }

View File

@@ -2,9 +2,13 @@ package com.tiedup.remake.v2.bondage.component;
import java.util.Collections; import java.util.Collections;
import java.util.EnumMap; import java.util.EnumMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.level.Level;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
public final class ComponentHolder { public final class ComponentHolder {
@@ -58,6 +62,12 @@ public final class ComponentHolder {
return false; return false;
} }
public void appendTooltip(ItemStack stack, @Nullable Level level, List<Component> tooltip, TooltipFlag flag) {
for (IItemComponent c : components.values()) {
c.appendTooltip(stack, level, tooltip, flag);
}
}
public boolean isEmpty() { public boolean isEmpty() {
return components.isEmpty(); return components.isEmpty();
} }

View File

@@ -1,45 +1,94 @@
package com.tiedup.remake.v2.bondage.component; package com.tiedup.remake.v2.bondage.component;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.util.GagMaterial;
import java.util.List;
import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.Component;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.level.Level;
import org.jetbrains.annotations.Nullable;
/** /**
* Component: gagging behavior for data-driven items. * Component: gagging behavior for data-driven items.
* *
* JSON config: {@code "gagging": {"comprehension": 0.2, "range": 10.0}} * <p>Config-driven: {@code "gagging": {"material": "ball"}} delegates to
* {@link GagMaterial} for comprehension/range from ModConfig at runtime.</p>
*
* <p>Override: {@code "gagging": {"comprehension": 0.15, "range": 8.0}} uses
* explicit values that take priority over the material lookup.</p>
*/ */
public class GaggingComponent implements IItemComponent { public class GaggingComponent implements IItemComponent {
private final double comprehension; private final @Nullable String material;
private final double range; private final double comprehensionOverride;
private final double rangeOverride;
private GaggingComponent(double comprehension, double range) { private GaggingComponent(@Nullable String material, double comprehensionOverride, double rangeOverride) {
this.comprehension = comprehension; this.material = material;
this.range = range; this.comprehensionOverride = comprehensionOverride;
this.rangeOverride = rangeOverride;
} }
public static IItemComponent fromJson(JsonObject config) { public static IItemComponent fromJson(JsonObject config) {
double comprehension = 0.2; String material = null;
double range = 10.0; double comprehension = -1;
double range = -1;
if (config != null) { if (config != null) {
if (config.has("material")) {
material = config.get("material").getAsString();
}
if (config.has("comprehension")) { if (config.has("comprehension")) {
comprehension = config.get("comprehension").getAsDouble(); comprehension = Math.max(0.0, Math.min(1.0, config.get("comprehension").getAsDouble()));
} }
if (config.has("range")) { if (config.has("range")) {
range = config.get("range").getAsDouble(); range = Math.max(0.0, config.get("range").getAsDouble());
} }
} }
comprehension = Math.max(0.0, Math.min(1.0, comprehension)); return new GaggingComponent(material, comprehension, range);
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). */ /** How much of the gagged speech is comprehensible (0.0 = nothing, 1.0 = full). */
public double getComprehension() { public double getComprehension() {
return comprehension; if (comprehensionOverride >= 0) return comprehensionOverride;
GagMaterial gag = getMaterial();
if (gag != null) return gag.getComprehension();
return 0.2;
} }
/** Maximum range in blocks where muffled speech can be heard. */ /** Maximum range in blocks where muffled speech can be heard. */
public double getRange() { public double getRange() {
return range; if (rangeOverride >= 0) return rangeOverride;
GagMaterial gag = getMaterial();
if (gag != null) return gag.getTalkRange();
return 10.0;
}
/** The gag material enum, or null if not configured or invalid. */
public @Nullable GagMaterial getMaterial() {
if (material == null) return null;
try {
return GagMaterial.valueOf(material.toUpperCase());
} catch (IllegalArgumentException e) {
TiedUpMod.LOGGER.warn("[GaggingComponent] Unknown gag material: {}", material);
return null;
}
}
/** The raw material string from JSON, or null. */
public @Nullable String getMaterialName() {
return material;
}
@Override
public void appendTooltip(ItemStack stack, @Nullable Level level, List<Component> tooltip, TooltipFlag flag) {
if (material != null) {
tooltip.add(Component.translatable("item.tiedup.tooltip.gag_material", material)
.withStyle(ChatFormatting.RED));
} else {
tooltip.add(Component.translatable("item.tiedup.tooltip.gagging").withStyle(ChatFormatting.RED));
}
} }
} }

View File

@@ -1,6 +1,13 @@
package com.tiedup.remake.v2.bondage.component; package com.tiedup.remake.v2.bondage.component;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import java.util.List;
import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.Component;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.level.Level;
import org.jetbrains.annotations.Nullable;
/** /**
* Component: GPS tracking and safe zone for data-driven items. * Component: GPS tracking and safe zone for data-driven items.
@@ -42,4 +49,14 @@ public class GpsComponent implements IItemComponent {
public boolean isPublicTracking() { public boolean isPublicTracking() {
return publicTracking; return publicTracking;
} }
@Override
public void appendTooltip(ItemStack stack, @Nullable Level level, List<Component> tooltip, TooltipFlag flag) {
tooltip.add(Component.translatable("item.tiedup.tooltip.gps_tracking")
.withStyle(ChatFormatting.AQUA));
if (safeZoneRadius > 0) {
tooltip.add(Component.translatable("item.tiedup.tooltip.gps_zone_radius", safeZoneRadius)
.withStyle(ChatFormatting.DARK_AQUA));
}
}
} }

View File

@@ -1,7 +1,12 @@
package com.tiedup.remake.v2.bondage.component; package com.tiedup.remake.v2.bondage.component;
import java.util.List;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.level.Level;
import org.jetbrains.annotations.Nullable;
/** /**
* A reusable behavior module for data-driven bondage items. * A reusable behavior module for data-driven bondage items.
@@ -16,4 +21,6 @@ public interface IItemComponent {
default boolean blocksUnequip(ItemStack stack, LivingEntity entity) { default boolean blocksUnequip(ItemStack stack, LivingEntity entity) {
return false; return false;
} }
default void appendTooltip(ItemStack stack, @Nullable Level level, List<Component> tooltip, TooltipFlag flag) {}
} }

View File

@@ -1,6 +1,13 @@
package com.tiedup.remake.v2.bondage.component; package com.tiedup.remake.v2.bondage.component;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import java.util.List;
import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.Component;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.level.Level;
import org.jetbrains.annotations.Nullable;
/** /**
* Component: lockable behavior for data-driven items. * Component: lockable behavior for data-driven items.
@@ -41,4 +48,13 @@ public class LockableComponent implements IItemComponent {
public int getLockResistance() { public int getLockResistance() {
return lockResistance; return lockResistance;
} }
@Override
public void appendTooltip(ItemStack stack, @Nullable Level level, List<Component> tooltip, TooltipFlag flag) {
tooltip.add(Component.translatable("item.tiedup.tooltip.lockable").withStyle(ChatFormatting.GOLD));
if (flag.isAdvanced()) {
tooltip.add(Component.translatable("item.tiedup.tooltip.lock_resistance", lockResistance)
.withStyle(ChatFormatting.DARK_GRAY));
}
}
} }

View File

@@ -1,33 +1,70 @@
package com.tiedup.remake.v2.bondage.component; package com.tiedup.remake.v2.bondage.component;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.tiedup.remake.core.SettingsAccessor;
import java.util.List;
import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.Component;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.level.Level;
import org.jetbrains.annotations.Nullable;
/** /**
* Component: struggle resistance for data-driven items. * Component: struggle resistance for data-driven items.
* *
* JSON config: {@code "resistance": {"base": 150}} * <p>Config-driven: {@code "resistance": {"id": "rope"}} delegates to
* {@link SettingsAccessor#getBindResistance(String)} at runtime.</p>
*
* <p>Legacy/override: {@code "resistance": {"base": 150}} uses a hardcoded value.</p>
*/ */
public class ResistanceComponent implements IItemComponent { public class ResistanceComponent implements IItemComponent {
private final int baseResistance; private final @Nullable String resistanceId;
private final int fallbackBase;
private ResistanceComponent(int baseResistance) { private ResistanceComponent(@Nullable String resistanceId, int fallbackBase) {
this.baseResistance = baseResistance; this.resistanceId = resistanceId;
this.fallbackBase = fallbackBase;
} }
public static IItemComponent fromJson(JsonObject config) { public static IItemComponent fromJson(JsonObject config) {
String id = null;
int base = 100; int base = 100;
if (config != null && config.has("base")) { if (config != null) {
base = config.get("base").getAsInt(); if (config.has("id")) {
id = config.get("id").getAsString();
}
if (config.has("base")) {
base = config.get("base").getAsInt();
}
} }
base = Math.max(0, base); base = Math.max(0, base);
return new ResistanceComponent(base); return new ResistanceComponent(id, base);
} }
/** /**
* Get the base resistance for this item. * Get the base resistance for this item.
* If a {@code resistanceId} is configured, delegates to server config at runtime.
* Otherwise returns the hardcoded fallback value.
*/ */
public int getBaseResistance() { public int getBaseResistance() {
return baseResistance; if (resistanceId != null) {
return SettingsAccessor.getBindResistance(resistanceId);
}
return fallbackBase;
}
/** The config key used for runtime resistance lookup, or null if hardcoded. */
public @Nullable String getResistanceId() {
return resistanceId;
}
@Override
public void appendTooltip(ItemStack stack, @Nullable Level level, List<Component> tooltip, TooltipFlag flag) {
if (flag.isAdvanced()) {
tooltip.add(Component.translatable("item.tiedup.tooltip.resistance", getBaseResistance())
.withStyle(ChatFormatting.DARK_GRAY));
}
} }
} }

View File

@@ -1,6 +1,13 @@
package com.tiedup.remake.v2.bondage.component; package com.tiedup.remake.v2.bondage.component;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import java.util.List;
import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.Component;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.level.Level;
import org.jetbrains.annotations.Nullable;
/** /**
* Component: shock collar behavior for data-driven items. * Component: shock collar behavior for data-driven items.
@@ -49,4 +56,16 @@ public class ShockComponent implements IItemComponent {
public boolean hasAutoShock() { public boolean hasAutoShock() {
return autoInterval > 0; return autoInterval > 0;
} }
@Override
public void appendTooltip(ItemStack stack, @Nullable Level level, List<Component> tooltip, TooltipFlag flag) {
if (hasAutoShock()) {
float seconds = autoInterval / 20.0f;
tooltip.add(Component.translatable("item.tiedup.tooltip.shock_auto", String.format("%.1f", seconds))
.withStyle(ChatFormatting.DARK_RED));
} else {
tooltip.add(Component.translatable("item.tiedup.tooltip.shock_manual")
.withStyle(ChatFormatting.DARK_RED));
}
}
} }

View File

@@ -246,6 +246,12 @@ public class DataDrivenBondageItem extends AbstractV2BondageItem {
} }
} }
// Component tooltips
ComponentHolder holder = DataDrivenItemRegistry.getComponents(stack);
if (holder != null) {
holder.appendTooltip(stack, level, tooltip, flag);
}
// Lock status + escape difficulty (from AbstractV2BondageItem) // Lock status + escape difficulty (from AbstractV2BondageItem)
super.appendHoverText(stack, level, tooltip, flag); super.appendHoverText(stack, level, tooltip, flag);