diff --git a/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/item/builder/ItemStackBuilder.kt b/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/item/builder/ItemStackBuilder.kt index f7d4de03b..462f3f171 100644 --- a/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/item/builder/ItemStackBuilder.kt +++ b/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/item/builder/ItemStackBuilder.kt @@ -4,26 +4,31 @@ import io.github.pylonmc.pylon.core.datatypes.PylonSerializers import io.github.pylonmc.pylon.core.i18n.PylonTranslator.Companion.translate import io.github.pylonmc.pylon.core.item.PylonItemSchema import io.github.pylonmc.pylon.core.util.editData +import io.github.pylonmc.pylon.core.util.editDataOrDefault +import io.github.pylonmc.pylon.core.util.editDataOrSet import io.github.pylonmc.pylon.core.util.fromMiniMessage import io.papermc.paper.datacomponent.DataComponentBuilder import io.papermc.paper.datacomponent.DataComponentType import io.papermc.paper.datacomponent.DataComponentTypes import io.papermc.paper.datacomponent.item.CustomModelData +import io.papermc.paper.datacomponent.item.ItemAttributeModifiers import io.papermc.paper.datacomponent.item.ItemLore import net.kyori.adventure.text.Component import net.kyori.adventure.text.ComponentLike import org.apache.commons.lang3.LocaleUtils import org.bukkit.Material import org.bukkit.NamespacedKey +import org.bukkit.attribute.Attribute +import org.bukkit.attribute.AttributeModifier import org.bukkit.inventory.ItemStack import org.bukkit.inventory.meta.ItemMeta import org.bukkit.persistence.PersistentDataContainer import xyz.xenondevs.invui.item.ItemProvider import java.util.function.Consumer +import kotlin.collections.forEach @Suppress("UnstableApiUsage") -open class ItemStackBuilder private constructor(val stack: ItemStack) : ItemProvider { - +open class ItemStackBuilder internal constructor(val stack: ItemStack) : ItemProvider { fun amount(amount: Int) = apply { stack.amount = amount } @@ -66,6 +71,14 @@ open class ItemStackBuilder private constructor(val stack: ItemStack) : ItemProv stack.editData(type, block) } + fun editDataOrDefault(type: DataComponentType.Valued, block: (T) -> T) = apply { + stack.editDataOrDefault(type, block) + } + + fun editDataOrSet(type: DataComponentType.Valued, block: (T?) -> T) = apply { + stack.editDataOrSet(type, block) + } + fun name(name: Component) = set(DataComponentTypes.ITEM_NAME, name) fun name(name: String) = name(fromMiniMessage(name)) @@ -87,6 +100,20 @@ open class ItemStackBuilder private constructor(val stack: ItemStack) : ItemProv fun defaultTranslatableLore(key: NamespacedKey) = lore(Component.translatable(loreKey(key), "")) + @JvmOverloads + fun addAttributeModifier( + attribute: Attribute, + modifier: AttributeModifier, + replaceExisting: Boolean = true + ) = apply { + editDataOrSet(DataComponentTypes.ATTRIBUTE_MODIFIERS) { modifiers -> + val copying = modifiers?.modifiers()?.filter { !replaceExisting || it.modifier().key != modifier.key } + ItemAttributeModifiers.itemAttributes().copy(copying) + .addModifier(attribute, modifier) + .build() + } + } + fun build(): ItemStack = stack.clone() override fun get(lang: String?): ItemStack { @@ -102,6 +129,9 @@ open class ItemStackBuilder private constructor(val stack: ItemStack) : ItemProv companion object { + val baseAttackDamage = NamespacedKey.minecraft("base_attack_damage") + val baseAttackSpeed = NamespacedKey.minecraft("base_attack_speed") + fun nameKey(key: NamespacedKey) = "pylon.${key.namespace}.item.${key.key}.name" @@ -123,20 +153,20 @@ open class ItemStackBuilder private constructor(val stack: ItemStack) : ItemProv * with the item's ID set to [key] */ @JvmStatic - fun pylonItem(material: Material, key: NamespacedKey): ItemStackBuilder { - return of(material) + fun pylonItem(material: Material, key: NamespacedKey): PylonItemStackBuilder { + return PylonItemStackBuilder(ItemStack(material), key) .editPdc { pdc -> pdc.set(PylonItemSchema.pylonItemKeyKey, PylonSerializers.NAMESPACED_KEY, key) } .set(DataComponentTypes.CUSTOM_MODEL_DATA, CustomModelData.customModelData().addString(key.toString())) .defaultTranslatableName(key) - .defaultTranslatableLore(key) + .defaultTranslatableLore(key) as PylonItemStackBuilder } /** * Returns an [ItemStackBuilder] with name and lore set to the default translation keys, and with the item's ID set to [key] */ @JvmStatic - fun pylonItem(stack: ItemStack, key: NamespacedKey): ItemStackBuilder { - return of(stack) + fun pylonItem(stack: ItemStack, key: NamespacedKey): PylonItemStackBuilder { + return PylonItemStackBuilder(stack, key) .editPdc { it.set(PylonItemSchema.pylonItemKeyKey, PylonSerializers.NAMESPACED_KEY, key) } .let { // Adds the pylon item key as the FIRST string in custom model data, but preserve any pre-existing data @@ -149,7 +179,14 @@ open class ItemStackBuilder private constructor(val stack: ItemStack) : ItemProv it.set(DataComponentTypes.CUSTOM_MODEL_DATA, modelData) } .defaultTranslatableName(key) - .defaultTranslatableLore(key) + .defaultTranslatableLore(key) as PylonItemStackBuilder + } + + fun ItemAttributeModifiers.Builder.copy(modifiers: List?) : ItemAttributeModifiers.Builder { + modifiers?.forEach { entry -> + this.addModifier(entry.attribute(), entry.modifier(), entry.group, entry.display()) + } + return this } } } diff --git a/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/item/builder/PylonItemStackBuilder.kt b/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/item/builder/PylonItemStackBuilder.kt new file mode 100644 index 000000000..992fbd66a --- /dev/null +++ b/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/item/builder/PylonItemStackBuilder.kt @@ -0,0 +1,134 @@ +package io.github.pylonmc.pylon.core.item.builder + +import io.github.pylonmc.pylon.core.config.Settings +import io.github.pylonmc.pylon.core.config.adapter.ConfigAdapter +import io.papermc.paper.datacomponent.DataComponentTypes +import io.papermc.paper.datacomponent.item.ItemAttributeModifiers +import io.papermc.paper.datacomponent.item.Tool +import io.papermc.paper.datacomponent.item.UseCooldown +import io.papermc.paper.datacomponent.item.Weapon +import io.papermc.paper.registry.keys.tags.BlockTypeTagKeys +import io.papermc.paper.registry.set.RegistryKeySet +import net.kyori.adventure.util.TriState +import org.bukkit.NamespacedKey +import org.bukkit.Registry +import org.bukkit.attribute.Attribute +import org.bukkit.attribute.AttributeModifier +import org.bukkit.block.BlockType +import org.bukkit.inventory.EquipmentSlotGroup +import org.bukkit.inventory.ItemStack + +@Suppress("UnstableApiUsage") +class PylonItemStackBuilder : ItemStackBuilder { + private val itemKey: NamespacedKey + + internal constructor(stack: ItemStack, itemKey: NamespacedKey) : super(stack) { + this.itemKey = itemKey + } + + @JvmOverloads + fun helmet( + armor: Double = Settings.get(itemKey).getOrThrow("armor", ConfigAdapter.DOUBLE), + armorToughness: Double = Settings.get(itemKey).getOrThrow("armor-toughness", ConfigAdapter.DOUBLE) + ) = armor(EquipmentSlotGroup.HEAD, armor, armorToughness) + + @JvmOverloads + fun chestPlate( + armor: Double = Settings.get(itemKey).getOrThrow("armor", ConfigAdapter.DOUBLE), + armorToughness: Double = Settings.get(itemKey).getOrThrow("armor-toughness", ConfigAdapter.DOUBLE) + ) = armor(EquipmentSlotGroup.CHEST, armor, armorToughness) + + @JvmOverloads + fun leggings( + armor: Double = Settings.get(itemKey).getOrThrow("armor", ConfigAdapter.DOUBLE), + armorToughness: Double = Settings.get(itemKey).getOrThrow("armor-toughness", ConfigAdapter.DOUBLE) + ) = armor(EquipmentSlotGroup.LEGS, armor, armorToughness) + + @JvmOverloads + fun boots( + armor: Double = Settings.get(itemKey).getOrThrow("armor", ConfigAdapter.DOUBLE), + armorToughness: Double = Settings.get(itemKey).getOrThrow("armor-toughness", ConfigAdapter.DOUBLE) + ) = armor(EquipmentSlotGroup.FEET, armor, armorToughness) + + @JvmOverloads + fun armor( + slot: EquipmentSlotGroup, + armor: Double = Settings.get(itemKey).getOrThrow("armor", ConfigAdapter.DOUBLE), + armorToughness: Double = Settings.get(itemKey).getOrThrow("armor-toughness", ConfigAdapter.DOUBLE) + ) = apply { + editDataOrSet(DataComponentTypes.ATTRIBUTE_MODIFIERS) { modifiers -> + val copying = modifiers?.modifiers()?.filter { it.modifier().key.namespace != "minecraft" || !it.modifier().key.key.contains("armor.") } + ItemAttributeModifiers.itemAttributes().copy(copying) + .addModifier(Attribute.ARMOR, AttributeModifier(itemKey, armor, AttributeModifier.Operation.ADD_NUMBER, slot)) + .addModifier(Attribute.ARMOR_TOUGHNESS, AttributeModifier(itemKey, armorToughness, AttributeModifier.Operation.ADD_NUMBER, slot)) + .build() + } + } + + @JvmOverloads + fun axe( + miningSpeed: Float = Settings.get(itemKey).getOrThrow("mining-speed", ConfigAdapter.FLOAT), + miningDurabilityDamage: Int = Settings.get(itemKey).getOrThrow("mining-durability-damage", ConfigAdapter.INT) + ) = tool(Registry.BLOCK.getTag(BlockTypeTagKeys.MINEABLE_AXE), miningSpeed, miningDurabilityDamage) + + @JvmOverloads + fun pickaxe( + miningSpeed: Float = Settings.get(itemKey).getOrThrow("mining-speed", ConfigAdapter.FLOAT), + miningDurabilityDamage: Int = Settings.get(itemKey).getOrThrow("mining-durability-damage", ConfigAdapter.INT) + ) = tool(Registry.BLOCK.getTag(BlockTypeTagKeys.MINEABLE_PICKAXE), miningSpeed, miningDurabilityDamage) + + @JvmOverloads + fun shovel( + miningSpeed: Float = Settings.get(itemKey).getOrThrow("mining-speed", ConfigAdapter.FLOAT), + miningDurabilityDamage: Int = Settings.get(itemKey).getOrThrow("mining-durability-damage", ConfigAdapter.INT) + ) = tool(Registry.BLOCK.getTag(BlockTypeTagKeys.MINEABLE_SHOVEL), miningSpeed, miningDurabilityDamage) + + @JvmOverloads + fun hoe( + miningSpeed: Float = Settings.get(itemKey).getOrThrow("mining-speed", ConfigAdapter.FLOAT), + miningDurabilityDamage: Int = Settings.get(itemKey).getOrThrow("mining-durability-damage", ConfigAdapter.INT) + ) = tool(Registry.BLOCK.getTag(BlockTypeTagKeys.MINEABLE_HOE), miningSpeed, miningDurabilityDamage) + + @JvmOverloads + fun tool( + blocks: RegistryKeySet, + miningSpeed: Float = Settings.get(itemKey).getOrThrow("mining-speed", ConfigAdapter.FLOAT), + miningDurabilityDamage: Int = Settings.get(itemKey).getOrThrow("mining-durability-damage", ConfigAdapter.INT) + ) = apply { + set(DataComponentTypes.TOOL, Tool.tool() + .defaultMiningSpeed(miningSpeed) + .damagePerBlock(miningDurabilityDamage) + .addRule(Tool.rule(blocks, miningSpeed, TriState.TRUE))) + } + + fun noTool() = unset(DataComponentTypes.TOOL) as PylonItemStackBuilder + + @JvmOverloads + fun weapon( + disablesShield: Boolean = false, + attackDamage: Double = Settings.get(itemKey).getOrThrow("attack-damage", ConfigAdapter.DOUBLE), + attackSpeed: Double = Settings.get(itemKey).getOrThrow("attack-speed", ConfigAdapter.DOUBLE), + attackDurabilityDamage: Int = Settings.get(itemKey).getOrThrow("attack-durability-damage", ConfigAdapter.INT), + disableShieldSeconds: Float? = null + ) = apply { + addAttributeModifier(Attribute.ATTACK_DAMAGE, AttributeModifier(baseAttackDamage, -1.0 + attackDamage, AttributeModifier.Operation.ADD_NUMBER, EquipmentSlotGroup.MAINHAND)) + addAttributeModifier(Attribute.ATTACK_SPEED, AttributeModifier(baseAttackSpeed, -4.0 + attackSpeed, AttributeModifier.Operation.ADD_NUMBER, EquipmentSlotGroup.MAINHAND)) + set(DataComponentTypes.WEAPON, Weapon.weapon() + .itemDamagePerAttack(attackDurabilityDamage) + .disableBlockingForSeconds(if (disablesShield) disableShieldSeconds ?: Settings.get(itemKey).getOrThrow("disable-shield-seconds", ConfigAdapter.FLOAT) else 0f)) + } + + @JvmOverloads + fun attackKnockback(knockback: Double = Settings.get(itemKey).getOrThrow("attack-knockback", ConfigAdapter.DOUBLE)) = + addAttributeModifier(Attribute.ATTACK_KNOCKBACK, AttributeModifier(itemKey, knockback, AttributeModifier.Operation.ADD_NUMBER, EquipmentSlotGroup.MAINHAND)) as PylonItemStackBuilder + + @JvmOverloads + fun durability( + durability: Int = Settings.get(itemKey).getOrThrow("durability", ConfigAdapter.INT) + ) = set(DataComponentTypes.MAX_DAMAGE, durability) as PylonItemStackBuilder + + @JvmOverloads + fun useCooldown( + cooldownTicks: Int = Settings.get(itemKey).getOrThrow("cooldown-ticks", ConfigAdapter.INT) + ) = set(DataComponentTypes.USE_COOLDOWN, UseCooldown.useCooldown(cooldownTicks / 20.0f).cooldownGroup(itemKey)) as PylonItemStackBuilder +} \ No newline at end of file diff --git a/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/util/PylonUtils.kt b/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/util/PylonUtils.kt index b30090e69..81c719ebe 100644 --- a/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/util/PylonUtils.kt +++ b/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/util/PylonUtils.kt @@ -287,6 +287,27 @@ inline fun ItemStack.editData( return this } +@JvmSynthetic +@Suppress("UnstableApiUsage") +inline fun ItemStack.editDataOrDefault( + type: DataComponentType.Valued, + block: (T) -> T +): ItemStack { + val data = getData(type) ?: this.type.getDefaultData(type) ?: return this + setData(type, block(data)) + return this +} + +@JvmSynthetic +@Suppress("UnstableApiUsage") +inline fun ItemStack.editDataOrSet( + type: DataComponentType.Valued, + block: (T?) -> T +): ItemStack { + setData(type, block(getData(type))) + return this +} + /** * Wrapper around [PersistentDataContainer.set] that allows nullable values to be passed *