This commit is contained in:
Kam committed 2026-02-21 03:04:19 -05:00
commit 185f06822a
11 files changed
+367 -112

No files matched your search

@@ -5,6 +5,7 @@ import kamkeel.npcs.controllers.data.ability.conditions.AbilityCondition;
import kamkeel.npcs.controllers.data.ability.conditions.ConditionHPThreshold;
import kamkeel.npcs.controllers.data.ability.conditions.ConditionHitCount;
import kamkeel.npcs.controllers.data.ability.conditions.ConditionItem;
import kamkeel.npcs.controllers.data.ability.conditions.ConditionQuestCompleted;
import kamkeel.npcs.controllers.data.ability.type.AbilityCharge;
import kamkeel.npcs.controllers.data.ability.type.AbilityCutter;
import kamkeel.npcs.controllers.data.ability.type.AbilityDash;
@@ -154,6 +155,7 @@ public class AbilityController implements IAbilityHandler {
registerCondition(ConditionHPThreshold::new);
registerCondition(ConditionHitCount::new);
registerCondition(ConditionItem::new);
registerCondition(ConditionQuestCompleted::new);
}
// ═══════════════════════════════════════════════════════════════════════════
@@ -6,6 +6,7 @@ import kamkeel.npcs.controllers.AbilityController;
import kamkeel.npcs.controllers.data.ability.UserType;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.StatCollector;
import noppes.npcs.LogWriter;
import noppes.npcs.client.gui.builder.FieldDef;
@@ -40,6 +41,14 @@ public abstract class AbilityCondition {
return false;
}
/**
* Returns true if all required fields are filled in.
* Used to disable the Done button in SubGuiConditionEdit when incomplete.
*/
public boolean isConfigured() {
return true;
}
public UserType getUserType() {
return userType;
}
@@ -63,10 +72,19 @@ public abstract class AbilityCondition {
@SideOnly(Side.CLIENT)
public abstract void getConditionDefinitions(List<FieldDef> defs);
/**
* Returns a human-readable summary of this condition's configuration.
* Used for tooltips when hovering over condition buttons.
*/
@SideOnly(Side.CLIENT)
public abstract String getConditionSummary();
@SideOnly(Side.CLIENT)
public final List<FieldDef> getAllDefinitions() {
List<FieldDef> defs = new ArrayList<>();
defs.add(FieldDef.labelField("ability.validFor", () ->
"\u00A7e" + StatCollector.translateToLocal("ability.userType." + getUserType().name())));
defs.add(FieldDef.enumField("condition.filter", ConditionFilter.class, this::getFilter, this::setFilter));
getConditionDefinitions(defs);
return defs;
@@ -1,60 +1,8 @@
package kamkeel.npcs.controllers.data.ability.conditions;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import noppes.npcs.client.gui.builder.FieldDef;
import java.util.List;
public class ConditionHPThreshold extends AbilityCondition {
private float threshold = 0.5f; // 50%
private boolean percent = true;
private ThresholdType thresholdType = ThresholdType.ABOVE;
public enum ThresholdType {
ABOVE {
@Override
public boolean test(float value, float threshold) {
return value > threshold;
}
},
BELOW {
@Override
public boolean test(float value, float threshold) {
return value < threshold;
}
},
EQUAL {
@Override
public boolean test(float value, float threshold) {
return value == threshold;
}
};
public abstract boolean test(float value, float threshold);
public static ThresholdType fromOrdinal(int ordinal) {
ThresholdType[] values = values();
if (ordinal >= 0 && ordinal < values.length) {
return values[ordinal];
}
return ABOVE;
}
@Override
public String toString() {
switch (this) {
case ABOVE:
return "condition.hp_above";
case BELOW:
return "condition.hp_below";
case EQUAL:
return "condition.hp_equal";
default:
return name();
}
}
}
public class ConditionHPThreshold extends ConditionThreshold {
public ConditionHPThreshold() {
this.typeId = "condition.cnpc.hp_threshold";
@@ -62,50 +10,17 @@ public class ConditionHPThreshold extends AbilityCondition {
}
@Override
protected boolean checkEntity(EntityLivingBase entity) {
float entityHP = isPercent() ? entity.getHealth() / entity.getMaxHealth() : entity.getHealth();
return thresholdType.test(entityHP, getThreshold());
protected float getEntityValue(EntityLivingBase entity) {
return entity.getHealth();
}
@Override
public void getConditionDefinitions(List<FieldDef> defs) {
defs.add(FieldDef.floatField("condition.threshold", this::getThreshold, this::setThreshold).min(0));
defs.add(FieldDef.boolField("condition.percent", this::isPercent, this::setIsPercent));
defs.add(FieldDef.enumField("condition.threshold_type", ThresholdType.class,
this::getThresholdType, this::setThresholdType));
protected float getEntityMaxValue(EntityLivingBase entity) {
return entity.getMaxHealth();
}
@Override
public void writeTypeNBT(NBTTagCompound nbt) {
nbt.setFloat("threshold", threshold);
}
@Override
public void readTypeNBT(NBTTagCompound nbt) {
threshold = nbt.getFloat("threshold");
}
public float getThreshold() {
return threshold;
}
public void setThreshold(float threshold) {
this.threshold = Math.max(0, threshold);
}
public ThresholdType getThresholdType() {
return thresholdType;
}
public void setThresholdType(ThresholdType thresholdType) {
this.thresholdType = thresholdType;
}
public boolean isPercent() {
return percent;
}
public void setIsPercent(boolean percent) {
this.percent = percent;
protected String getStatName() {
return "HP";
}
}
@@ -1,8 +1,8 @@
package kamkeel.npcs.controllers.data.ability.conditions;
import kamkeel.npcs.controllers.data.ability.UserType;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.StatCollector;
import noppes.npcs.client.gui.builder.FieldDef;
import noppes.npcs.entity.EntityNPCInterface;
@@ -15,7 +15,6 @@ public class ConditionHitCount extends AbilityCondition {
public ConditionHitCount() {
this.typeId = "condition.cnpc.hit_count";
this.name = "condition.hit_count";
this.userType = UserType.NPC_ONLY;
}
@Override
@@ -59,6 +58,12 @@ public class ConditionHitCount extends AbilityCondition {
defs.add(FieldDef.intField("condition.within_ticks", this::getWithinTicks, this::setWithinTicks).min(0));
}
@Override
public String getConditionSummary() {
String filterLabel = StatCollector.translateToLocal("condition.filter." + getFilter().name().toLowerCase());
return "[" + filterLabel + "] Hit " + requiredHits + " times in " + withinTicks + " ticks";
}
@Override
public void writeTypeNBT(NBTTagCompound nbt) {
nbt.setInteger("requiredHits", requiredHits);
@@ -4,6 +4,7 @@ import net.minecraft.entity.EntityLivingBase;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.StatCollector;
import noppes.npcs.client.gui.builder.FieldDef;
import noppes.npcs.entity.EntityNPCInterface;
@@ -149,6 +150,14 @@ public class ConditionItem extends AbilityCondition {
// .hover("condition.hover.required_count"));
}
@Override
public String getConditionSummary() {
String filterLabel = StatCollector.translateToLocal("condition.filter." + getFilter().name().toLowerCase());
String usage = usageType.name();
String item = itemName.isEmpty() ? "None" : itemName;
return "[" + filterLabel + "] " + usage + ": " + item;
}
@Override
public void writeTypeNBT(NBTTagCompound nbt) {
nbt.setString("itemName", itemName);
@@ -166,6 +175,11 @@ public class ConditionItem extends AbilityCondition {
}
@Override
public boolean isConfigured() {
return itemName != null && !itemName.isEmpty();
}
public UsageType getUsageType() { return usageType; }
public void setUsageType(UsageType usageType) { this.usageType = usageType; }
@@ -0,0 +1,86 @@
package kamkeel.npcs.controllers.data.ability.conditions;
import kamkeel.npcs.controllers.data.ability.UserType;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.StatCollector;
import noppes.npcs.client.gui.builder.FieldDef;
import noppes.npcs.client.gui.select.GuiQuestSelection;
import noppes.npcs.client.gui.util.SubGuiInterface;
import noppes.npcs.controllers.QuestController;
import noppes.npcs.controllers.data.PlayerData;
import noppes.npcs.controllers.data.Quest;
import java.util.List;
public class ConditionQuestCompleted extends AbilityCondition {
private int questId = -1;
public ConditionQuestCompleted() {
this.typeId = "condition.cnpc.quest_completed";
this.name = "condition.quest_completed";
this.userType = UserType.PLAYER_ONLY;
}
@Override
protected boolean checkEntity(EntityLivingBase entity) {
if (!(entity instanceof EntityPlayer)) return false;
if (questId < 0) return false;
EntityPlayer player = (EntityPlayer) entity;
return PlayerData.get(player).questData.hasFinishedQuest(questId);
}
@Override
public void getConditionDefinitions(List<FieldDef> defs) {
defs.add(FieldDef.subGuiField("condition.select_quest",
() -> new GuiQuestSelection(questId),
(SubGuiInterface gui) -> {
GuiQuestSelection sel = (GuiQuestSelection) gui;
if (sel.selectedQuest != null) {
questId = sel.selectedQuest.id;
}
})
.buttonLabel(() -> {
if (questId < 0) return "None";
Quest q = QuestController.Instance.quests.get(questId);
return q != null ? q.title : "ID:" + questId;
})
.clearable(() -> questId = -1));
}
@Override
public String getConditionSummary() {
String filterLabel = StatCollector.translateToLocal("condition.filter." + getFilter().name().toLowerCase());
String questName = "None";
if (questId >= 0) {
Quest q = QuestController.Instance.quests.get(questId);
questName = q != null ? q.title : "ID:" + questId;
}
return "[" + filterLabel + "] Quest: " + questName;
}
@Override
public void writeTypeNBT(NBTTagCompound nbt) {
nbt.setInteger("questId", questId);
}
@Override
public void readTypeNBT(NBTTagCompound nbt) {
questId = nbt.getInteger("questId");
}
@Override
public boolean isConfigured() {
return questId >= 0;
}
public int getQuestId() {
return questId;
}
public void setQuestId(int questId) {
this.questId = questId;
}
}
@@ -0,0 +1,171 @@
package kamkeel.npcs.controllers.data.ability.conditions;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.StatCollector;
import noppes.npcs.client.gui.builder.FieldDef;
import java.util.List;
/**
* Abstract base for threshold-based conditions (HP, Ki, etc.).
* Provides shared percent/flat toggle, ThresholdType enum, and FieldDef generation.
*/
public abstract class ConditionThreshold extends AbilityCondition {
public enum ThresholdType {
ABOVE {
@Override
public boolean test(float value, float threshold) {
return value >= threshold;
}
},
BELOW {
@Override
public boolean test(float value, float threshold) {
return value <= threshold;
}
},
EQUAL {
@Override
public boolean test(float value, float threshold) {
return value == threshold;
}
};
public abstract boolean test(float value, float threshold);
public static ThresholdType fromOrdinal(int ordinal) {
ThresholdType[] values = values();
if (ordinal >= 0 && ordinal < values.length) {
return values[ordinal];
}
return ABOVE;
}
@Override
public String toString() {
switch (this) {
case ABOVE:
return "condition.above";
case BELOW:
return "condition.below";
case EQUAL:
return "condition.equal";
default:
return name();
}
}
}
protected float thresholdFlat = 100f;
protected float thresholdPercent = 0.5f;
protected boolean percent = true;
protected ThresholdType thresholdType = ThresholdType.ABOVE;
/**
* Returns the raw stat value for the entity (e.g. current HP, current Ki).
*/
protected abstract float getEntityValue(EntityLivingBase entity);
/**
* Returns the maximum stat value for the entity (e.g. max HP, max Ki).
* Used to compute the percentage ratio.
*/
protected abstract float getEntityMaxValue(EntityLivingBase entity);
/**
* Returns the display name for the stat being checked (e.g. "HP", "Ki").
* Used in getConditionSummary().
*/
protected abstract String getStatName();
@Override
protected boolean checkEntity(EntityLivingBase entity) {
float value;
float threshold;
if (percent) {
float max = getEntityMaxValue(entity);
value = max > 0 ? getEntityValue(entity) / max : 0;
threshold = thresholdPercent;
} else {
value = getEntityValue(entity);
threshold = thresholdFlat;
}
return thresholdType.test(value, threshold);
}
@Override
public void getConditionDefinitions(List<FieldDef> defs) {
defs.add(FieldDef.floatField("condition.threshold_flat", this::getThresholdFlat, this::setThresholdFlat)
.min(0).visibleWhen(() -> !isPercent()));
defs.add(FieldDef.floatField("condition.threshold_percent", this::getThresholdPercent, this::setThresholdPercent)
.min(0).max(1).visibleWhen(this::isPercent));
defs.add(FieldDef.boolField("condition.percent", this::isPercent, this::setIsPercent));
defs.add(FieldDef.enumField("condition.threshold_type", ThresholdType.class,
this::getThresholdType, this::setThresholdType));
}
@Override
public void writeTypeNBT(NBTTagCompound nbt) {
nbt.setFloat("thresholdFlat", thresholdFlat);
nbt.setFloat("thresholdPercent", thresholdPercent);
nbt.setBoolean("percent", percent);
nbt.setInteger("thresholdType", thresholdType.ordinal());
}
@Override
public void readTypeNBT(NBTTagCompound nbt) {
thresholdFlat = nbt.getFloat("thresholdFlat");
thresholdPercent = nbt.getFloat("thresholdPercent");
percent = nbt.getBoolean("percent");
thresholdType = ThresholdType.fromOrdinal(nbt.getInteger("thresholdType"));
}
@Override
public String getConditionSummary() {
String filterLabel = StatCollector.translateToLocal("condition.filter." + getFilter().name().toLowerCase());
String typeName = getStatName();
String typeLabel = StatCollector.translateToLocal(thresholdType.toString());
String value = percent
? String.format("%.0f%%", thresholdPercent * 100)
: String.format("%.0f", thresholdFlat);
return "[" + filterLabel + "] " + typeName + " " + typeLabel + " " + value;
}
// ═══════════════════════════════════════════════════════════════════
// GETTERS / SETTERS
// ═══════════════════════════════════════════════════════════════════
public float getThresholdFlat() {
return thresholdFlat;
}
public void setThresholdFlat(float value) {
this.thresholdFlat = Math.max(0, value);
}
public float getThresholdPercent() {
return thresholdPercent;
}
public void setThresholdPercent(float value) {
this.thresholdPercent = Math.max(0, Math.min(1, value));
}
public boolean isPercent() {
return percent;
}
public void setIsPercent(boolean percent) {
this.percent = percent;
}
public ThresholdType getThresholdType() {
return thresholdType;
}
public void setThresholdType(ThresholdType type) {
this.thresholdType = type;
}
}
@@ -245,7 +245,9 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi
for (int i = 0; i < conditions.size() && i < MAX_CONDITIONS; i++) {
AbilityCondition cond = conditions.get(i);
String condName = getConditionDisplayName(cond);
sw.addButton(new GuiNpcButton(COND_BASE + i * COND_STRIDE, L_LABEL_X, y, 140, 20, condName));
GuiNpcButton condBtn = new GuiNpcButton(COND_BASE + i * COND_STRIDE, L_LABEL_X, y, 140, 20, condName);
condBtn.hoverableText = cond.getConditionSummary();
sw.addButton(condBtn);
sw.addButton(new GuiNpcButton(COND_BASE + i * COND_STRIDE + 1, L_LABEL_X + 145, y, 40, 20, "gui.edit"));
sw.addButton(new GuiNpcButton(COND_BASE + i * COND_STRIDE + 2, L_LABEL_X + 190, y, 20, 20, "X"));
y += 22;
@@ -31,6 +31,7 @@ public class SubGuiConditionEdit extends SubGuiInterface implements ITextfieldLi
private static final int BTN_NAMESPACE = 1;
private static final int BTN_CANCEL = 10;
private static final int BTN_DONE = 11;
private static final int TF_SEARCH = 12;
// Left panel
private static final int LEFT_WIDTH = 110;
@@ -39,6 +40,8 @@ public class SubGuiConditionEdit extends SubGuiInterface implements ITextfieldLi
// Right panel
private static final int RIGHT_GAP = 5;
private String searchText = "";
private AbilityCondition condition;
private AbilityCondition result;
private String selectedTypeId;
@@ -158,11 +161,11 @@ public class SubGuiConditionEdit extends SubGuiInterface implements ITextfieldLi
GuiNpcTextField.unfocus();
super.initGui();
int y = guiTop + 5;
int leftX = guiLeft + LEFT_MARGIN;
int btnY = guiTop + ySize - 26;
// Namespace filter button (full width at top)
addButton(new GuiNpcButton(BTN_NAMESPACE, guiLeft + LEFT_MARGIN, y, xSize - LEFT_MARGIN * 2, 20, getNamespaceFilterLabel()));
y += 24;
// Namespace filter button (above left scroll only)
addButton(new GuiNpcButton(BTN_NAMESPACE, leftX, guiTop + 5, LEFT_WIDTH, 20, getNamespaceFilterLabel()));
// Build filtered type list
filteredTypeIds = getFilteredTypes();
@@ -171,22 +174,28 @@ public class SubGuiConditionEdit extends SubGuiInterface implements ITextfieldLi
for (String typeId : filteredTypeIds) {
Supplier<AbilityCondition> factory = AbilityController.Instance.getConditionType(typeId);
String displayName = factory != null ? I18n.format(factory.get().getName()) : typeId;
if (!searchText.isEmpty() && !displayName.toLowerCase().contains(searchText.toLowerCase())) continue;
scrollList.add(displayName);
displayNameToTypeId.put(displayName, typeId);
}
// Left panel: condition type scroll list
int scrollHeight = ySize - 58;
scroll = new GuiCustomScroll(this, 0);
scroll.guiLeft = guiLeft + LEFT_MARGIN;
scroll.guiTop = y;
// Left panel: condition type scroll list (between namespace button and search bar)
int scrollTop = guiTop + 29;
int scrollBottom = btnY - 24;
int scrollHeight = scrollBottom - scrollTop;
if (scroll == null) {
scroll = new GuiCustomScroll(this, 0);
}
scroll.guiLeft = leftX;
scroll.guiTop = scrollTop;
scroll.setSize(LEFT_WIDTH, scrollHeight);
scroll.setUnsortedList(scrollList);
// Pre-select current condition type in scroll
if (selectedTypeId != null) {
for (int i = 0; i < filteredTypeIds.length; i++) {
if (filteredTypeIds[i].equals(selectedTypeId)) {
for (int i = 0; i < scrollList.size(); i++) {
String typeId = displayNameToTypeId.get(scrollList.get(i));
if (typeId != null && typeId.equals(selectedTypeId)) {
scroll.selected = i;
break;
}
@@ -194,9 +203,16 @@ public class SubGuiConditionEdit extends SubGuiInterface implements ITextfieldLi
}
addScroll(scroll);
// Right panel: condition config via FieldDef
int rightX = guiLeft + LEFT_MARGIN + LEFT_WIDTH + RIGHT_GAP;
// Search text field (below left scroll)
GuiNpcTextField searchField = new GuiNpcTextField(TF_SEARCH, this, fontRendererObj, leftX, scrollBottom + 2, LEFT_WIDTH, 20, searchText);
searchField.setHoverText("gui.search");
addTextField(searchField);
// Right panel: condition config via FieldDef (full height from top to buttons)
int rightX = leftX + LEFT_WIDTH + RIGHT_GAP;
int rightW = xSize - LEFT_MARGIN * 2 - LEFT_WIDTH - RIGHT_GAP;
int rightTop = guiTop + 5;
int rightHeight = btnY - rightTop - 4;
List<FieldDef> fields = new ArrayList<>();
if (condition != null) {
@@ -205,13 +221,17 @@ public class SubGuiConditionEdit extends SubGuiInterface implements ITextfieldLi
builder = new AbilityFieldBuilder(this, fontRendererObj);
builder.startIds(DECLARATIVE_ID_START, CLEAR_ID_START, LABEL_ID_START);
builder.contentRight(rightW);
builder.startY(5);
builder.buildScrollWindow(fields, rightX, y, rightW, scrollHeight);
builder.buildScrollWindow(fields, rightX, rightTop, rightW, rightHeight);
// Bottom buttons
int btnY = guiTop + ySize - 26;
addButton(new GuiNpcButton(BTN_CANCEL, guiLeft + LEFT_MARGIN, btnY, 60, 20, "gui.cancel"));
addButton(new GuiNpcButton(BTN_DONE, guiLeft + xSize - LEFT_MARGIN - 60, btnY, 60, 20, "gui.done"));
addButton(new GuiNpcButton(BTN_CANCEL, leftX, btnY, 60, 20, "gui.cancel"));
GuiNpcButton doneBtn = new GuiNpcButton(BTN_DONE, guiLeft + xSize - LEFT_MARGIN - 60, btnY, 60, 20, "gui.done");
if (condition == null || !condition.isConfigured()) {
doneBtn.setEnabled(false);
}
addButton(doneBtn);
}
@Override
@@ -263,6 +283,14 @@ public class SubGuiConditionEdit extends SubGuiInterface implements ITextfieldLi
@Override
public void unFocused(GuiNpcTextField textField) {
if (textField.id == TF_SEARCH) {
String newText = textField.getText();
if (!newText.equals(searchText)) {
searchText = newText;
initGui();
}
return;
}
if (textField.id < DECLARATIVE_ID_START) return;
if (builder != null && builder.handleTextFieldEvent(textField.id, textField)) {
initGui();
@@ -344,6 +344,9 @@ public class GuiFieldBuilder {
String translated = StatCollector.translateToLocal(def.getLabel());
int labelW = fontRenderer.getStringWidth(translated);
int fieldX = colLLabel + labelW + labelPadding;
// Clamp field width so it doesn't extend under the scrollbar
int maxW = contentRight - fieldX - 15;
if (maxW > 0 && fieldW > maxW) fieldW = maxW;
renderFieldAt(def, colLLabel, fieldX, fieldW, y);
}