diff --git a/src/main/java/kamkeel/npcs/controllers/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/AbilityController.java index f226a505c..1da844451 100644 --- a/src/main/java/kamkeel/npcs/controllers/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/AbilityController.java @@ -4,6 +4,7 @@ import kamkeel.npcs.controllers.data.ability.*; 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.type.AbilityCharge; import kamkeel.npcs.controllers.data.ability.type.AbilityCutter; import kamkeel.npcs.controllers.data.ability.type.AbilityDash; @@ -152,6 +153,7 @@ public class AbilityController implements IAbilityHandler { private void registerBuiltinConditionTypes() { registerCondition(ConditionHPThreshold::new); registerCondition(ConditionHitCount::new); + registerCondition(ConditionItem::new); } // ═══════════════════════════════════════════════════════════════════════════ diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/AbilityCondition.java b/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/AbilityCondition.java index 95d38bf9b..6fc7b8a12 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/AbilityCondition.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/AbilityCondition.java @@ -14,6 +14,8 @@ import java.util.List; import java.util.function.Supplier; public abstract class AbilityCondition { + public static final int MAX_CONDITIONS = 5; + protected String typeId = ""; protected String name = ""; protected UserType userType = UserType.BOTH; @@ -69,7 +71,7 @@ public abstract class AbilityCondition { nbt.setString("typeId", getTypeId()); nbt.setString("name", getName()); nbt.setInteger("userType", getUserType().ordinal()); - nbt.setInteger("filter", getUserType().ordinal()); + nbt.setInteger("filter", getFilter().ordinal()); writeTypeNBT(nbt); return nbt; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionItem.java b/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionItem.java new file mode 100644 index 000000000..14b751d29 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionItem.java @@ -0,0 +1,201 @@ +package kamkeel.npcs.controllers.data.ability.conditions; + +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.client.gui.builder.FieldDef; +import noppes.npcs.entity.EntityNPCInterface; + +import java.util.List; + +public class ConditionItem extends AbilityCondition { + + public enum UsageType { + ARMOR, HOLDING, AMMO, OFFHAND; + + public static UsageType fromOrdinal(int ordinal) { + UsageType[] values = values(); + return (ordinal >= 0 && ordinal < values.length) ? values[ordinal] : HOLDING; + } + } + + public enum ArmorSlot { + BOOTS, LEGS, CHEST, HELMET; + + public static ArmorSlot fromOrdinal(int ordinal) { + ArmorSlot[] values = values(); + return (ordinal >= 0 && ordinal < values.length) ? values[ordinal] : BOOTS; + } + } + + // Shared + private UsageType usageType = UsageType.HOLDING; + private String itemName = ""; + + // ARMOR only + private ArmorSlot armorSlot = ArmorSlot.BOOTS; + private boolean fullArmorSet = false; + + // AMMO + PLAYER only + private int requiredCount = 1; + + public ConditionItem() { + this.typeId = "condition.cnpc.item"; + this.name = "condition.item"; + } + + @Override + public boolean check(EntityLivingBase caster, EntityLivingBase target) { + switch (getFilter()) { + case CASTER: + return checkEntity(caster); + case TARGET: + return target != null && checkEntity(target); + case BOTH: + return checkEntity(caster) && (target != null && checkEntity(target)); + default: + return checkEntity(caster); + } + } + + private boolean checkEntity(EntityLivingBase entity) { + switch (usageType) { + case HOLDING: + return checkHolding(entity); + case ARMOR: + return checkArmor(entity); + case AMMO: + return checkAmmo(entity); + case OFFHAND: + return checkOffhand(entity); + default: + return false; + } + } + + private boolean checkHolding(EntityLivingBase entity) { + ItemStack held = entity.getHeldItem(); + return matchesItem(held); + } + + private boolean checkArmor(EntityLivingBase entity) { + // MC getEquipmentInSlot: 1=boots, 2=legs, 3=chest, 4=helmet + int slot = armorSlot.ordinal() + 1; // BOOTS=0, LEGS=1, CHEST=2, HELMET=3 + return matchesItem(entity.getEquipmentInSlot(slot)); + } + + private boolean checkFullArmor(EntityLivingBase entity) { + for (int i = 0; i < 4; i++) { + int slot = i + 1; + if (!matchesItem(i, entity.getEquipmentInSlot(slot))) + return false; + } + + return true; + } + + private boolean checkAmmo(EntityLivingBase entity) { + if (entity instanceof EntityPlayer) { + return checkPlayerInventoryCount((EntityPlayer) entity); + } + if (entity instanceof EntityNPCInterface) { + return checkNPCProjectile((EntityNPCInterface) entity); + } + + return false; + } + + private boolean checkOffhand(EntityLivingBase entity) { + // No offhand for players + if (entity instanceof EntityPlayer) return false; + + ItemStack item = ((EntityNPCInterface) entity).getOffHand(); + return matchesItem(item); + } + + private boolean checkPlayerInventoryCount(EntityPlayer player) { + int count = 0; + for (ItemStack stack : player.inventory.mainInventory) { + if (matchesItem(stack)) { + count += stack.stackSize; + } + } + return count >= requiredCount; + } + + private boolean checkNPCProjectile(EntityNPCInterface npc) { + ItemStack projectile = npc.inventory.getProjectile(); + return matchesItem(projectile); + } + + private boolean matchesItem(ItemStack stack) { + return matchesItem(0, stack); + } + + private boolean matchesItem(int index, ItemStack stack) { + if (stack == null || itemName == null || itemName.isEmpty()) return false; + String registryName = net.minecraft.item.Item.itemRegistry.getNameForObject(stack.getItem()); + return itemName.equals(registryName); + } + + @Override + public void getConditionDefinitions(List defs) { + defs.add(FieldDef.enumField("condition.usage_type", UsageType.class, + this::getUsageType, this::setUsageType)); + + defs.add(FieldDef.stringField("condition.item_name", + this::getItemName, this::setItemName)); +// .hover("condition.hover.item_name")); + + + + defs.add(FieldDef.enumField("condition.armor_slot", ArmorSlot.class, + this::getArmorSlot, this::setArmorSlot) + .visibleWhen(() -> usageType == UsageType.ARMOR)); +// .hover("condition.hover.armor_slots")); + + defs.add(FieldDef.intField("condition.item_count", + this::getRequiredCount, this::setRequiredCount) + .range(1, 64) + .visibleWhen(() -> usageType == UsageType.AMMO && userType.allowsPlayer())); +// .hover("condition.hover.required_count")); + } + + @Override + public void writeTypeNBT(NBTTagCompound nbt) { + nbt.setString("itemName", itemName); + nbt.setInteger("usageType", usageType.ordinal()); + nbt.setInteger("armorSlot", armorSlot.ordinal()); + nbt.setInteger("requiredCount", requiredCount); + } + + @Override + public void readTypeNBT(NBTTagCompound nbt) { + itemName = nbt.getString("itemName"); + usageType = UsageType.fromOrdinal(nbt.getInteger("usageType")); + armorSlot = ArmorSlot.fromOrdinal(nbt.getInteger("armorSlot")); + requiredCount = nbt.getInteger("requiredCount"); + } + + + public UsageType getUsageType() { return usageType; } + public void setUsageType(UsageType usageType) { this.usageType = usageType; } + + public String getItemName() { return itemName; } + public void setItemName(String itemName) { this.itemName = itemName; } + + public ArmorSlot getArmorSlot() { return armorSlot; } + public void setArmorSlot(ArmorSlot armorSlot) { this.armorSlot = armorSlot; } + + public boolean isFullArmorSet() { + return fullArmorSet; + } + + public void setFullArmorSet(boolean fullArmorSet) { + this.fullArmorSet = fullArmorSet; + } + + public int getRequiredCount() { return requiredCount; } + public void setRequiredCount(int requiredCount) { this.requiredCount = Math.max(1, requiredCount); } +} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java index b611e4e6b..a3cee0443 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java @@ -20,6 +20,8 @@ import noppes.npcs.client.gui.util.SubGuiInterface; import java.util.ArrayList; import java.util.List; +import static kamkeel.npcs.controllers.data.ability.conditions.AbilityCondition.MAX_CONDITIONS; + public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldListener, ISubGuiListener { private static final int DECLARATIVE_ID_START = 1000; @@ -42,6 +44,12 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi private static final int L_LABEL_X = 5; private static final int ROW_H = 24; + // Condition buttons + private static final int COND_BASE = 50; + private static final int COND_STRIDE = 10; + private static final int COND_END = COND_BASE + MAX_CONDITIONS * COND_STRIDE; // 50 + N*10 + private static final int BTN_ADD_COND = COND_END; + private final Ability ability; private final IAbilityConfigCallback callback; private int activeTab = TAB_GENERAL; @@ -234,17 +242,17 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi sw.addLabel(new GuiNpcLabel(labelCounter, "ability.conditions", L_LABEL_X, y + 2, 0xFFFF55)); y += 15; - for (int i = 0; i < conditions.size() && i < 3; i++) { + for (int i = 0; i < conditions.size() && i < MAX_CONDITIONS; i++) { AbilityCondition cond = conditions.get(i); String condName = getConditionDisplayName(cond); - sw.addButton(new GuiNpcButton(50 + i * 10, L_LABEL_X, y, 140, 20, condName)); - sw.addButton(new GuiNpcButton(51 + i * 10, L_LABEL_X + 145, y, 40, 20, "gui.edit")); - sw.addButton(new GuiNpcButton(52 + i * 10, L_LABEL_X + 190, y, 20, 20, "X")); + sw.addButton(new GuiNpcButton(COND_BASE + i * COND_STRIDE, L_LABEL_X, y, 140, 20, condName)); + 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; } - if (conditions.size() < 3) { - sw.addButton(new GuiNpcButton(80, L_LABEL_X, y, 50, 20, "gui.add")); + if (conditions.size() < MAX_CONDITIONS) { + sw.addButton(new GuiNpcButton(BTN_ADD_COND, L_LABEL_X, y, 50, 20, "gui.add"));; y += ROW_H; } @@ -312,9 +320,9 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi } private boolean handleConditionButton(int id) { - if (id >= 50 && id < 80) { - int condIndex = (id - 50) / 10; - int action = (id - 50) % 10; + if (id >= COND_BASE && id < COND_END) { + int condIndex = (id - COND_BASE) / COND_STRIDE; + int action = (id - COND_BASE) % COND_STRIDE; if (action == 0 || action == 1) { if (condIndex < conditions.size()) { editingConditionIndex = condIndex; @@ -328,8 +336,8 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi } return true; } - if (id == 80) { - if (conditions.size() < 3) { + if (id == BTN_ADD_COND) { + if (conditions.size() < MAX_CONDITIONS) { editingConditionIndex = conditions.size(); setSubGui(new SubGuiConditionEdit(null)); } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiChainedAbilityConfig.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiChainedAbilityConfig.java index 9783bb9a7..d1946e2e2 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiChainedAbilityConfig.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiChainedAbilityConfig.java @@ -32,6 +32,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +import static kamkeel.npcs.controllers.data.ability.conditions.AbilityCondition.MAX_CONDITIONS; /** * SubGui for editing a {@link ChainedAbility}. @@ -70,9 +71,9 @@ public class SubGuiChainedAbilityConfig extends SubGuiInterface implements IText // Actual ID = COND_BASE + i * COND_STRIDE + offset // offset 0 = name/click, 1 = edit, 2 = delete private static final int COND_BASE = 50; - private static final int COND_END = 80; private static final int COND_STRIDE = 10; - private static final int BTN_ADD_COND = 80; + private static final int COND_END = COND_BASE + MAX_CONDITIONS * COND_STRIDE; // 50 + N*10 + private static final int BTN_ADD_COND = COND_END; // ── Layout constants ────────────────────────────────────────────────────── private static final int L_LABEL_X = 5; @@ -427,7 +428,7 @@ public class SubGuiChainedAbilityConfig extends SubGuiInterface implements IText sw.addLabel(new GuiNpcLabel(labelCounter, "ability.conditions", L_LABEL_X, y + 2, 0xFFFF55)); y += 15; - for (int i = 0; i < conditions.size() && i < 3; i++) { + 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)); @@ -436,7 +437,7 @@ public class SubGuiChainedAbilityConfig extends SubGuiInterface implements IText y += 22; } - if (conditions.size() < 3) { + if (conditions.size() < MAX_CONDITIONS) { sw.addButton(new GuiNpcButton(BTN_ADD_COND, L_LABEL_X, y, 50, 20, "gui.add")); y += ROW_H; } @@ -615,7 +616,7 @@ public class SubGuiChainedAbilityConfig extends SubGuiInterface implements IText return true; } if (id == BTN_ADD_COND) { - if (conditions.size() < 3) { + if (conditions.size() < MAX_CONDITIONS) { editingConditionIndex = conditions.size(); setSubGui(new SubGuiConditionEdit(null)); } diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index c6d745706..d32b62471 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -2083,15 +2083,27 @@ condition.filter=Filter Type condition.filter.target=Target condition.filter.caster=Caster condition.filter.both=Both -condition.hit_count=Hit Count + +# Condition HP Threshold condition.hp_threshold=HP Threshold condition.hp_above=HP Above condition.hp_below=HP Below condition.threshold=Threshold condition.threshold_type=Threshold Type + +# Condition Hit Count +condition.hit_count=Hit Count condition.required_hits=Required Hits condition.within_ticks=Within (ticks) +# Condition Item +condition.item=Has Item +condition.usage_type=Usage Type +condition.armor_slot=Armor Slot +condition.full_armor=Full Armor +condition.item_count=Item Count +condition.item_name=Item Name + # Condition Hover Tooltips condition.hover.type=Condition type that must be met for ability to activate condition.hover.hpAbove=Activates when NPC health is above this percentage