Fixed modifier application

This commit is contained in:
Indyuce 2022-08-18 12:48:50 +02:00
parent 4706dd0ac0
commit dbd1936609
11 changed files with 88 additions and 73 deletions

View File

@ -42,24 +42,25 @@ public class Type {
// Hand Accessories // Hand Accessories
public static final Type CATALYST = new Type(TypeSet.OFFHAND, "CATALYST", false, EquipmentSlot.MAIN_HAND); public static final Type CATALYST = new Type(TypeSet.OFFHAND, "CATALYST", false, EquipmentSlot.MAIN_HAND);
public static final Type OFF_CATALYST = new Type(TypeSet.OFFHAND, "OFF_CATALYST", false, EquipmentSlot.OTHER); public static final Type OFF_CATALYST = new Type(TypeSet.OFFHAND, "OFF_CATALYST", false, EquipmentSlot.OFF_HAND);
public static final Type BOTH_CATALYST = new Type(TypeSet.OFFHAND, "BOTH_CATALYST", false, EquipmentSlot.MAIN_HAND); public static final Type MAIN_CATALYST = new Type(TypeSet.OFFHAND, "MAIN_CATALYST", false, EquipmentSlot.MAIN_HAND);
// Any // Any
public static final Type ORNAMENT = new Type(TypeSet.EXTRA, "ORNAMENT", false, EquipmentSlot.ANY); public static final Type ORNAMENT = new Type(TypeSet.EXTRA, "ORNAMENT", false, EquipmentSlot.OTHER);
// Extra // Extra
public static final Type ARMOR = new Type(TypeSet.EXTRA, "ARMOR", false, EquipmentSlot.ARMOR); public static final Type ARMOR = new Type(TypeSet.EXTRA, "ARMOR", false, EquipmentSlot.ARMOR);
public static final Type TOOL = new Type(TypeSet.EXTRA, "TOOL", false, EquipmentSlot.MAIN_HAND); public static final Type TOOL = new Type(TypeSet.EXTRA, "TOOL", false, EquipmentSlot.MAIN_HAND);
public static final Type CONSUMABLE = new Type(TypeSet.EXTRA, "CONSUMABLE", false, EquipmentSlot.MAIN_HAND); public static final Type CONSUMABLE = new Type(TypeSet.EXTRA, "CONSUMABLE", false, EquipmentSlot.MAIN_HAND);
public static final Type MISCELLANEOUS = new Type(TypeSet.EXTRA, "MISCELLANEOUS", false, EquipmentSlot.MAIN_HAND); public static final Type MISCELLANEOUS = new Type(TypeSet.EXTRA, "MISCELLANEOUS", false, EquipmentSlot.MAIN_HAND);
public static final Type GEM_STONE = new Type(TypeSet.EXTRA, "GEM_STONE", false, EquipmentSlot.OTHER); public static final Type GEM_STONE = new Type(TypeSet.EXTRA, "GEM_STONE", false, null);
public static final Type SKIN = new Type(TypeSet.EXTRA, "SKIN", false, EquipmentSlot.OTHER); public static final Type SKIN = new Type(TypeSet.EXTRA, "SKIN", false, null);
public static final Type ACCESSORY = new Type(TypeSet.EXTRA, "ACCESSORY", false, EquipmentSlot.ACCESSORY); public static final Type ACCESSORY = new Type(TypeSet.EXTRA, "ACCESSORY", false, EquipmentSlot.ACCESSORY);
public static final Type BLOCK = new Type(TypeSet.EXTRA, "BLOCK", false, EquipmentSlot.OTHER); public static final Type BLOCK = new Type(TypeSet.EXTRA, "BLOCK", false, null);
private final String id; private final String id;
private final TypeSet set; private final TypeSet set;
@Nullable
private final EquipmentSlot equipType; private final EquipmentSlot equipType;
private final boolean weapon; private final boolean weapon;
@ -155,6 +156,7 @@ public class Type {
return name; return name;
} }
@Nullable
public EquipmentSlot getEquipmentType() { public EquipmentSlot getEquipmentType() {
return equipType; return equipType;
} }
@ -163,10 +165,18 @@ public class Type {
return item.clone(); return item.clone();
} }
/**
* @deprecated Use {@link #getSupertype()}
*/
@Deprecated
public boolean isSubtype() { public boolean isSubtype() {
return parent != null; return parent != null;
} }
/**
* @deprecated Use {@link #getSupertype()}
*/
@Deprecated
public Type getParent() { public Type getParent() {
return parent; return parent;
} }
@ -175,15 +185,15 @@ public class Type {
* @return Does it display as four rows in /mmoitems browse? * @return Does it display as four rows in /mmoitems browse?
*/ */
public boolean isFourGUIMode() { public boolean isFourGUIMode() {
return equipType == EquipmentSlot.ARMOR; return getSupertype().equipType == EquipmentSlot.ARMOR;
} }
/** /**
* @return Should its stats apply in offhand * @return If an item can be equipped in both hands
*/ */
public boolean isOffhandItem() { public boolean isHandItem() {
final Type supertype = getSupertype(); final @NotNull Type supertype = getSupertype();
return supertype.equals(OFF_CATALYST) || supertype.equals(BOTH_CATALYST); return supertype.equipType == EquipmentSlot.MAIN_HAND && !supertype.equals(MAIN_CATALYST);
} }
/** /**
@ -199,11 +209,8 @@ public class Type {
*/ */
public Type getSupertype() { public Type getSupertype() {
Type parentMost = this; Type parentMost = this;
while (parentMost.parent != null)
while (parentMost.isSubtype()) { parentMost = parentMost.parent;
parentMost = parentMost.getParent();
}
return parentMost; return parentMost;
} }
@ -212,11 +219,12 @@ public class Type {
* or if this type is a subtype of the given type. * or if this type is a subtype of the given type.
*/ */
public boolean corresponds(Type type) { public boolean corresponds(Type type) {
return equals(type) || (isSubtype() && getParent().equals(type)); return getSupertype().equals(type);
} }
@Deprecated
public boolean corresponds(TypeSet set) { public boolean corresponds(TypeSet set) {
return getItemSet() == set; return getSupertype().getItemSet() == set;
} }
/** /**
@ -298,8 +306,8 @@ public class Type {
* @param id The type id * @param id The type id
* @return The type or null if it couldn't be found * @return The type or null if it couldn't be found
*/ */
public static @Nullable @Nullable
Type get(@Nullable String id) { public static Type get(@Nullable String id) {
if (id == null) { if (id == null) {
return null; return null;
} }

View File

@ -26,7 +26,7 @@ public class Weapon extends UseItem {
@Override @Override
public boolean checkItemRequirements() { public boolean checkItemRequirements() {
if (playerData.areHandsFull()) { if (playerData.isEncumbered()) {
Message.HANDS_TOO_CHARGED.format(ChatColor.RED).send(getPlayer()); Message.HANDS_TOO_CHARGED.format(ChatColor.RED).send(getPlayer());
return false; return false;
} }

View File

@ -4,7 +4,7 @@ import io.lumine.mythic.lib.api.item.NBTItem;
import io.lumine.mythic.lib.api.player.EquipmentSlot; import io.lumine.mythic.lib.api.player.EquipmentSlot;
import io.lumine.mythic.lib.player.PlayerMetadata; import io.lumine.mythic.lib.player.PlayerMetadata;
import io.lumine.mythic.lib.skill.trigger.TriggerType; import io.lumine.mythic.lib.skill.trigger.TriggerType;
import io.lumine.mythic.lib.util.ProjectileTrigger; import io.lumine.mythic.lib.util.CustomProjectile;
import net.Indyuce.mmoitems.ItemStats; import net.Indyuce.mmoitems.ItemStats;
import net.Indyuce.mmoitems.MMOItems; import net.Indyuce.mmoitems.MMOItems;
import org.bukkit.GameMode; import org.bukkit.GameMode;
@ -41,6 +41,6 @@ public class Crossbow extends UntargetedWeapon {
// Trigger abilities // Trigger abilities
stats.getData().triggerSkills(TriggerType.SHOOT_BOW, arrow); stats.getData().triggerSkills(TriggerType.SHOOT_BOW, arrow);
new ProjectileTrigger(stats.getData(), ProjectileTrigger.ProjectileType.ARROW, arrow, slot); new CustomProjectile(stats.getData(), CustomProjectile.ProjectileType.ARROW, arrow, slot);
} }
} }

View File

@ -67,7 +67,7 @@ public class MMOItemTemplate extends PostLoadObject implements ItemReference {
protected void whenPostLoaded(ConfigurationSection config) { protected void whenPostLoaded(ConfigurationSection config) {
FriendlyFeedbackProvider ffp = new FriendlyFeedbackProvider(FFPMMOItems.get()); FriendlyFeedbackProvider ffp = new FriendlyFeedbackProvider(FFPMMOItems.get());
ffp.activatePrefix(true, getType().toString() + " " + getId()); ffp.activatePrefix(true, getType().getId() + " " + getId());
if (config.contains("option")) if (config.contains("option"))
for (TemplateOption option : TemplateOption.values()) for (TemplateOption option : TemplateOption.values())

View File

@ -53,7 +53,7 @@ public class PlayerData {
private final Map<PotionEffectType, PotionEffect> permanentEffects = new HashMap<>(); private final Map<PotionEffectType, PotionEffect> permanentEffects = new HashMap<>();
private final Set<ParticleRunnable> itemParticles = new HashSet<>(); private final Set<ParticleRunnable> itemParticles = new HashSet<>();
private ParticleRunnable overridingItemParticles = null; private ParticleRunnable overridingItemParticles = null;
private boolean handsFull = false; private boolean encumbered = false;
@Nullable @Nullable
private SetBonuses setBonuses = null; private SetBonuses setBonuses = null;
private final PlayerStats stats; private final PlayerStats stats;
@ -118,25 +118,28 @@ public class PlayerData {
overridingItemParticles.cancel(); overridingItemParticles.cancel();
} }
@Deprecated
public boolean areHandsFull() {
return isEncumbered();
}
/** /**
* @return If the player hands are full i.e if the player is holding * @return If the player hands are full i.e if the player is holding
* two items in their hands, one being two handed * two items in their hands, one being two handed
*/ */
public boolean areHandsFull() { public boolean isEncumbered() {
if (!mmoData.isOnline())
return false;
// Get the mainhand and offhand items. // Get the mainhand and offhand items.
NBTItem main = MythicLib.plugin.getVersion().getWrapper().getNBTItem(getPlayer().getInventory().getItemInMainHand()); final NBTItem main = MythicLib.plugin.getVersion().getWrapper().getNBTItem(getPlayer().getInventory().getItemInMainHand());
NBTItem off = MythicLib.plugin.getVersion().getWrapper().getNBTItem(getPlayer().getInventory().getItemInOffHand()); final NBTItem off = MythicLib.plugin.getVersion().getWrapper().getNBTItem(getPlayer().getInventory().getItemInOffHand());
// Is either hand two-handed? // Is either hand two-handed?
boolean mainhand_twohanded = main.getBoolean(ItemStats.TWO_HANDED.getNBTPath()); final boolean mainhand_twohanded = main.getBoolean(ItemStats.TWO_HANDED.getNBTPath());
boolean offhand_twohanded = off.getBoolean(ItemStats.TWO_HANDED.getNBTPath()); final boolean offhand_twohanded = off.getBoolean(ItemStats.TWO_HANDED.getNBTPath());
// Is either hand encumbering: Not NULL, not AIR, and not Handworn // Is either hand encumbering: Not NULL, not AIR, and not Handworn
boolean mainhand_encumbering = (main.getItem() != null && main.getItem().getType() != Material.AIR && !main.getBoolean(ItemStats.HANDWORN.getNBTPath())); final boolean mainhand_encumbering = (main.getItem() != null && main.getItem().getType() != Material.AIR && !main.getBoolean(ItemStats.HANDWORN.getNBTPath()));
boolean offhand_encumbering = (off.getItem() != null && off.getItem().getType() != Material.AIR && !off.getBoolean(ItemStats.HANDWORN.getNBTPath())); final boolean offhand_encumbering = (off.getItem() != null && off.getItem().getType() != Material.AIR && !off.getBoolean(ItemStats.HANDWORN.getNBTPath()));
// Will it encumber? // Will it encumber?
return (mainhand_twohanded && offhand_encumbering) || (mainhand_encumbering && offhand_twohanded); return (mainhand_twohanded && offhand_encumbering) || (mainhand_encumbering && offhand_twohanded);
@ -177,10 +180,10 @@ public class PlayerData {
permissions.clear(); permissions.clear();
/* /*
* Updates the full-hands boolean, this way it can be cached and used in * Updates the encumbered boolean, this way it can be
* the updateEffects() method * cached and used in the updateEffects() method
*/ */
handsFull = areHandsFull(); encumbered = isEncumbered();
// Find all the items the player can actually use // Find all the items the player can actually use
for (EquippedItem item : MMOItems.plugin.getInventory().getInventory(getPlayer())) { for (EquippedItem item : MMOItems.plugin.getInventory().getInventory(getPlayer())) {
@ -203,10 +206,19 @@ public class PlayerData {
Bukkit.getPluginManager().callEvent(new RefreshInventoryEvent(inventory.getEquipped(), getPlayer(), this)); Bukkit.getPluginManager().callEvent(new RefreshInventoryEvent(inventory.getEquipped(), getPlayer(), this));
for (EquippedItem equipped : inventory.getEquipped()) { for (EquippedItem equipped : inventory.getEquipped()) {
VolatileMMOItem item = equipped.getCached(); final VolatileMMOItem item = equipped.getCached();
// Stats which don't apply from off hand // Abilities
if (equipped.getSlot() == EquipmentSlot.OFF_HAND && equipped.getCached().getType().getEquipmentType() != EquipmentSlot.OFF_HAND) if (item.hasData(ItemStats.ABILITIES))
for (AbilityData abilityData : ((AbilityListData) item.getData(ItemStats.ABILITIES)).getAbilities()) {
ModifierSource modSource = equipped.getCached().getType() == null ? ModifierSource.OTHER : equipped.getCached().getType().getItemSet().getModifierSource();
mmoData.getPassiveSkillMap().addModifier(new PassiveSkill("MMOItemsItem", abilityData, equipped.getSlot(), modSource));
}
// Modifier application rules
final ModifierSource source = item.getType().getItemSet().getModifierSource();
final EquipmentSlot equipmentSlot = equipped.getSlot();
if (source.isWeapon() && equipmentSlot == EquipmentSlot.MAIN_HAND.getOppositeHand())
continue; continue;
// Apply permanent potion effects // Apply permanent potion effects
@ -227,16 +239,8 @@ public class PlayerData {
itemParticles.add(particleData.start(this)); itemParticles.add(particleData.start(this));
} }
// Abilities
if (item.hasData(ItemStats.ABILITIES) && (MMOItems.plugin.getConfig().getBoolean("abilities-bypass-encumbering") || !handsFull))
for (AbilityData abilityData : ((AbilityListData) item.getData(ItemStats.ABILITIES)).getAbilities()) {
ModifierSource modSource = equipped.getCached().getType() == null ? ModifierSource.OTHER : equipped.getCached().getType().getItemSet().getModifierSource();
mmoData.getPassiveSkillMap().addModifier(new PassiveSkill("MMOItemsItem", abilityData, equipped.getSlot(), modSource));
}
// Apply permissions if Vault exists // Apply permissions if Vault exists
if (MMOItems.plugin.hasPermissions() && item.hasData(ItemStats.GRANTED_PERMISSIONS)) { if (MMOItems.plugin.hasPermissions() && item.hasData(ItemStats.GRANTED_PERMISSIONS)) {
permissions.addAll(((StringListData) item.getData(ItemStats.GRANTED_PERMISSIONS)).getList()); permissions.addAll(((StringListData) item.getData(ItemStats.GRANTED_PERMISSIONS)).getList());
Permission perms = MMOItems.plugin.getVault().getPermissions(); Permission perms = MMOItems.plugin.getVault().getPermissions();
permissions.forEach(perm -> { permissions.forEach(perm -> {
@ -311,7 +315,7 @@ public class PlayerData {
permanentEffects.values().forEach(effect -> getPlayer().addPotionEffect(effect)); permanentEffects.values().forEach(effect -> getPlayer().addPotionEffect(effect));
// Two handed slowness // Two handed slowness
if (handsFull) if (encumbered)
getPlayer().addPotionEffect(new PotionEffect(PotionEffectType.SLOW, 40, 1, true, false)); getPlayer().addPotionEffect(new PotionEffect(PotionEffectType.SLOW, 40, 1, true, false));
} }

View File

@ -70,12 +70,11 @@ public class PlayerStats {
double value = item.getNBT().getStat(stat.getId()); double value = item.getNBT().getStat(stat.getId());
if (value != 0) { if (value != 0) {
final Type type = item.getCached().getType();
Type type = item.getCached().getType(); final ModifierSource source = type == null ? ModifierSource.OTHER : type.getItemSet().getModifierSource();
ModifierSource source = type == null ? ModifierSource.OTHER : type.getItemSet().getModifierSource();
// Apply hand weapon stat offset // Apply hand weapon stat offset
if (item.getSlot() == EquipmentSlot.MAIN_HAND && stat instanceof AttackWeaponStat) if (source.isWeapon() && stat instanceof AttackWeaponStat)
value -= ((AttackWeaponStat) stat).getOffset(playerData); value -= ((AttackWeaponStat) stat).getOffset(playerData);
packet.addModifier(new StatModifier("MMOItem-" + index++, stat.getId(), value, ModifierType.FLAT, item.getSlot(), source)); packet.addModifier(new StatModifier("MMOItem-" + index++, stat.getId(), value, ModifierType.FLAT, item.getSlot(), source));

View File

@ -55,8 +55,12 @@ public class EquippedItem {
/** /**
* The slot this equipped item is defined to be, will this <code>Type</code> * The slot this equipped item is defined to be, will this <code>Type</code>
* actually add its stats to the player when held here? * actually add register its modifiers to the player when held here?
* <p></p> * <p>
* There's a difference between registering modifiers and applying it stats.
* For instance, modifiers from both hands are registered if the placement is
* legal but might not be taken into account during stat calculation!
* <p>
* An <code>OFF_CATALYST</code> may only add in the <code>OFFHAND</code>, and such. * An <code>OFF_CATALYST</code> may only add in the <code>OFFHAND</code>, and such.
*/ */
public boolean isPlacementLegal() { public boolean isPlacementLegal() {
@ -70,12 +74,12 @@ public class EquippedItem {
return false; return false;
// Equips anywhere // Equips anywhere
if (slot == EquipmentSlot.ANY) if (slot == EquipmentSlot.OTHER || type.getEquipmentType() == EquipmentSlot.OTHER)
return true; return true;
// Does it work in offhand // Hand items
if (slot == EquipmentSlot.OFF_HAND) if (type.isHandItem())
return type.isOffhandItem(); return slot.isHand();
return slot == type.getEquipmentType(); return slot == type.getEquipmentType();
} }

View File

@ -36,8 +36,8 @@ public class OrnamentPlayerInventory implements PlayerInventory, Listener {
// Ornaments // Ornaments
for (ItemStack item : player.getInventory().getContents()) { for (ItemStack item : player.getInventory().getContents()) {
NBTItem nbtItem; NBTItem nbtItem;
if (item != null && (nbtItem = MythicLib.plugin.getVersion().getWrapper().getNBTItem(item)).hasType() && Type.get(nbtItem.getType()).getEquipmentType() == EquipmentSlot.ANY) if (item != null && (nbtItem = MythicLib.plugin.getVersion().getWrapper().getNBTItem(item)).hasType() && Type.get(nbtItem.getType()).getSupertype().equals(Type.ORNAMENT))
list.add(new EquippedItem(nbtItem, EquipmentSlot.ANY)); list.add(new EquippedItem(nbtItem, EquipmentSlot.OTHER));
} }
return list; return list;
@ -47,7 +47,7 @@ public class OrnamentPlayerInventory implements PlayerInventory, Listener {
public void a(EntityPickupItemEvent event) { public void a(EntityPickupItemEvent event) {
if (event.getEntityType() == EntityType.PLAYER) { if (event.getEntityType() == EntityType.PLAYER) {
NBTItem nbt = NBTItem.get(event.getItem().getItemStack()); NBTItem nbt = NBTItem.get(event.getItem().getItemStack());
if (nbt.hasType() && Type.get(nbt.getType()).getEquipmentType() == EquipmentSlot.ANY) if (nbt.hasType() && Type.get(nbt.getType()).getSupertype().equals(Type.ORNAMENT))
PlayerData.get((Player) event.getEntity()).updateInventory(); PlayerData.get((Player) event.getEntity()).updateInventory();
} }
} }
@ -55,7 +55,7 @@ public class OrnamentPlayerInventory implements PlayerInventory, Listener {
@EventHandler(ignoreCancelled = true) @EventHandler(ignoreCancelled = true)
public void b(PlayerDropItemEvent event) { public void b(PlayerDropItemEvent event) {
NBTItem nbt = NBTItem.get(event.getItemDrop().getItemStack()); NBTItem nbt = NBTItem.get(event.getItemDrop().getItemStack());
if (nbt.hasType() && Type.get(nbt.getType()).getEquipmentType() == EquipmentSlot.ANY) if (nbt.hasType() && Type.get(nbt.getType()).getSupertype().equals(Type.ORNAMENT))
PlayerData.get(event.getPlayer()).updateInventory(); PlayerData.get(event.getPlayer()).updateInventory();
} }
} }

View File

@ -286,7 +286,7 @@ public class ItemUse implements Listener {
} }
// Have to get hand manually because 1.15 and below does not have event.getHand() // Have to get hand manually because 1.15 and below does not have event.getHand()
ItemStack itemInMainHand = playerData.getPlayer().getInventory().getItemInMainHand(); final ItemStack itemInMainHand = playerData.getPlayer().getInventory().getItemInMainHand();
final EquipmentSlot bowSlot = itemInMainHand.isSimilar(event.getBow()) ? EquipmentSlot.MAIN_HAND : EquipmentSlot.OFF_HAND; final EquipmentSlot bowSlot = itemInMainHand.isSimilar(event.getBow()) ? EquipmentSlot.MAIN_HAND : EquipmentSlot.OFF_HAND;
MMOItems.plugin.getEntities().registerCustomProjectile(item, playerData.getStats().newTemporary(bowSlot), event.getProjectile(), event.getForce()); MMOItems.plugin.getEntities().registerCustomProjectile(item, playerData.getStats().newTemporary(bowSlot), event.getProjectile(), event.getForce());
} }

View File

@ -138,13 +138,13 @@ public class PlayerListener implements Listener {
if (!(event.getEntity() instanceof Trident) || !(event.getEntity().getShooter() instanceof Player)) if (!(event.getEntity() instanceof Trident) || !(event.getEntity().getShooter() instanceof Player))
return; return;
InteractItem item = new InteractItem((Player) event.getEntity().getShooter(), Material.TRIDENT); final InteractItem item = new InteractItem((Player) event.getEntity().getShooter(), Material.TRIDENT);
if (!item.hasItem()) if (!item.hasItem())
return; return;
NBTItem nbtItem = MythicLib.plugin.getVersion().getWrapper().getNBTItem(item.getItem()); final NBTItem nbtItem = MythicLib.plugin.getVersion().getWrapper().getNBTItem(item.getItem());
Type type = Type.get(nbtItem.getType()); final Type type = Type.get(nbtItem.getType());
PlayerData playerData = PlayerData.get((Player) event.getEntity().getShooter()); final PlayerData playerData = PlayerData.get((Player) event.getEntity().getShooter());
if (type != null) { if (type != null) {
final Weapon weapon = new Weapon(playerData, nbtItem); final Weapon weapon = new Weapon(playerData, nbtItem);

View File

@ -161,10 +161,10 @@ LUTE:
- '{range}&8- &7Lvl Range: &e#range#' - '{range}&8- &7Lvl Range: &e#range#'
- '{tier}&8- &7Item Tier: #prefix##tier#' - '{tier}&8- &7Item Tier: #prefix##tier#'
# Applies stats in mainhand only # Applies stats in both hands
CATALYST: CATALYST:
display: PRISMARINE_SHARD display: PRISMARINE_SHARD
name: 'Catalyst (Mainhand)' name: 'Catalyst'
unident-item: unident-item:
name: '&f#prefix#Unidentified Catalyst' name: '&f#prefix#Unidentified Catalyst'
lore: lore:
@ -189,10 +189,10 @@ OFF_CATALYST:
- '{range}&8- &7Lvl Range: &e#range#' - '{range}&8- &7Lvl Range: &e#range#'
- '{tier}&8- &7Item Tier: #prefix##tier#' - '{tier}&8- &7Item Tier: #prefix##tier#'
# Applies stats in both hands # Applies stats in mainhand only
BOTH_CATALYST: MAIN_CATALYST:
display: PRISMARINE_CRYSTALS display: PRISMARINE_CRYSTALS
name: 'Catalyst' name: 'Catalyst (Mainhand)'
unident-item: unident-item:
name: '&f#prefix#Unidentified Offhand Catalyst' name: '&f#prefix#Unidentified Offhand Catalyst'
lore: lore: