Skip to content

Commit

Permalink
[FEATURE] Weapon/Armor Leveling System (#23) (#49)
Browse files Browse the repository at this point in the history
* [feat] Initial work on weapon leveling & item attributes

* Revert some changes
- Revert build.gradle
- Revert gradle.properties
- Remove asserts from ItemStackMixin.java
- Revert some lines in ItemStackMixin.java

* [feat] XP increases on kill
- Weapon XP increases by a value specified in the config when a mob is killed
- Moved XP from an attribute to NBT

* [feat] Rudimentary item levelling

* [feat] Rudimentary armour levelling

* Changes to tooltip colouring

* [feat] Improve levelling config

* [fix] Add back accidentally removed translation key

* [feat] Implement percent damage reduction

* [feat] Items and weapons in loot tables can generate with a level
- Range they can generate in is different based on dimension
- Non-Vanilla dimensions follow the overworld range

* [fix] Remove TODO commment

---------

Co-authored-by: naomi <[email protected]>
  • Loading branch information
RedstoneWizard08 and naomieow authored Dec 13, 2024
1 parent a3ae2a3 commit b691a03
Show file tree
Hide file tree
Showing 13 changed files with 522 additions and 25 deletions.
85 changes: 66 additions & 19 deletions src/main/java/com/bibireden/playerex/mixin/ItemStackMixin.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,7 @@
import net.minecraft.world.InteractionResultHolder;
import net.minecraft.world.entity.EquipmentSlot;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.ai.attributes.Attribute;
import net.minecraft.world.entity.ai.attributes.AttributeInstance;
import net.minecraft.world.entity.ai.attributes.AttributeModifier;
import net.minecraft.world.entity.ai.attributes.Attributes;
import net.minecraft.world.entity.ai.attributes.*;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
Expand All @@ -39,22 +36,22 @@
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import org.spongepowered.asm.mixin.injection.callback.LocalCapture;

import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.*;
import java.util.function.Consumer;

@Mixin(ItemStack.class)
abstract class ItemStackMixin {
@Shadow
public abstract boolean hurt(int amount, RandomSource random, @Nullable ServerPlayer serverPlayer);

@Shadow
public abstract CompoundTag getOrCreateTag();

@Inject(method = "use(Lnet/minecraft/world/level/Level;Lnet/minecraft/world/entity/player/Player;Lnet/minecraft/world/InteractionHand;)Lnet/minecraft/world/InteractionResultHolder;", at = @At(value = "HEAD"), cancellable = true)
public void preventUse(Level level, Player player, InteractionHand usedHand, CallbackInfoReturnable<InteractionResultHolder<ItemStack>> cir) {
if (!PlayerEX.CONFIG.getFeatureSettings().getItemBreakingEnabled()) return;

ItemStack stack = (ItemStack)(Object)this;
ItemStack stack = (ItemStack) (Object) this;
if (PlayerEXUtil.isBroken(stack)) {
cir.setReturnValue(InteractionResultHolder.fail(stack));
}
Expand All @@ -64,7 +61,7 @@ public void preventUse(Level level, Player player, InteractionHand usedHand, Cal
public void preventUseOnBlock(UseOnContext context, CallbackInfoReturnable<InteractionResult> cir) {
if (!PlayerEX.CONFIG.getFeatureSettings().getItemBreakingEnabled()) return;

ItemStack stack = (ItemStack)(Object)this;
ItemStack stack = (ItemStack) (Object) this;
if (PlayerEXUtil.isBroken(stack)) {
cir.setReturnValue(InteractionResult.FAIL);
}
Expand All @@ -74,7 +71,7 @@ public void preventUseOnBlock(UseOnContext context, CallbackInfoReturnable<Inter
public void preventDamage(int amount, RandomSource random, ServerPlayer user, CallbackInfoReturnable<Boolean> cir) {
if (!PlayerEX.CONFIG.getFeatureSettings().getItemBreakingEnabled()) return;

ItemStack stack = (ItemStack)(Object)this;
ItemStack stack = (ItemStack) (Object) this;
if (PlayerEXUtil.isBroken(stack)) {
cir.setReturnValue(true);
}
Expand All @@ -83,10 +80,10 @@ public void preventDamage(int amount, RandomSource random, ServerPlayer user, Ca
@Inject(method = "hurtAndBreak(ILnet/minecraft/world/entity/LivingEntity;Ljava/util/function/Consumer;)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/item/ItemStack;shrink(I)V"), cancellable = true)
public <T extends LivingEntity> void preventBreak(int amount, T entity, Consumer<T> onBroken, CallbackInfo ci) {
if (!PlayerEX.CONFIG.getFeatureSettings().getItemBreakingEnabled()) return;
ItemStack stack = (ItemStack)(Object)this;
if (stack.getItem().builtInRegistryHolder().is(PlayerEXTags.UNBREAKABLE_ITEMS)) {
ItemStack stack = (ItemStack) (Object) this;
if (stack.getItemHolder().is(PlayerEXTags.UNBREAKABLE_ITEMS)) {
if (!PlayerEXUtil.isBroken(stack)) {
CompoundTag tag = stack.getTag();
CompoundTag tag = stack.getOrCreateTag();
tag.putBoolean("broken", true);
stack.setTag(tag);
}
Expand All @@ -98,20 +95,26 @@ public <T extends LivingEntity> void preventBreak(int amount, T entity, Consumer
public void removeBrokenOnRepair(int damage, CallbackInfo ci) {
if (!PlayerEX.CONFIG.getFeatureSettings().getItemBreakingEnabled()) return;

ItemStack stack = (ItemStack)(Object)this;
ItemStack stack = (ItemStack) (Object) this;
if (PlayerEXUtil.isBroken(stack) && damage < stack.getDamageValue()) {
CompoundTag tag = stack.getTag();
CompoundTag tag = stack.getOrCreateTag();
tag.putBoolean("broken", false);
stack.setTag(tag);
}
}

@Inject(method = "getAttributeModifiers(Lnet/minecraft/world/entity/EquipmentSlot;)Lcom/google/common/collect/Multimap;", at = @At(value = "RETURN"), cancellable = true)
public void preventArmour(EquipmentSlot slot, CallbackInfoReturnable<Multimap<Attribute, AttributeModifier>> cir) {
public void modifyAttributeModifiers(EquipmentSlot slot, CallbackInfoReturnable<Multimap<Attribute, AttributeModifier>> cir) {
if (!PlayerEX.CONFIG.getFeatureSettings().getItemBreakingEnabled()) return;

ItemStack stack = (ItemStack)(Object)this;
ItemStack stack = (ItemStack) (Object) this;
HashMultimap<Attribute, AttributeModifier> hashmap = HashMultimap.create(cir.getReturnValue());
if (PlayerEX.CONFIG.getWeaponLevelingSettings().getEnabled() && PlayerEXUtil.isWeapon(stack)) {
PlayerEXUtil.addToModifier(hashmap, Attributes.ATTACK_DAMAGE, getLevel() * PlayerEX.CONFIG.getWeaponLevelingSettings().getDamagePerLevel());
}
if (PlayerEX.CONFIG.getArmorLevelingSettings().getEnabled() && PlayerEXUtil.isArmor(stack)) {
PlayerEXUtil.addToModifier(hashmap, Attributes.ARMOR, getLevel() * PlayerEX.CONFIG.getArmorLevelingSettings().getArmorPerLevel());
}
if (PlayerEXUtil.isBroken(stack)) {
PlayerEXUtil.removeModifier(hashmap, Attributes.ARMOR);
PlayerEXUtil.removeModifier(hashmap, Attributes.ARMOR_TOUGHNESS);
Expand Down Expand Up @@ -166,7 +169,9 @@ public void preventArmour(EquipmentSlot slot, CallbackInfoReturnable<Multimap<At
}

@ModifyVariable(method = "getTooltipLines", at = @At(value = "STORE", ordinal = 1), ordinal = 1)
private double playerex$modifyAdditionAttributeKnockback(double original) { return original / 10.0; }
private double playerex$modifyAdditionAttributeKnockback(double original) {
return original / 10.0;
}

// todo: not sure about the implementation(s) here...
@Inject(method = "getTooltipLines", at = @At(value = "INVOKE", target = "Ljava/util/List;add(Ljava/lang/Object;)Z", ordinal = 7, shift = At.Shift.AFTER))
Expand Down Expand Up @@ -237,4 +242,46 @@ public void preventArmour(EquipmentSlot slot, CallbackInfoReturnable<Multimap<At
);
}
}

@Unique
private int getLevel() {
ItemStack itemStack = (ItemStack) (Object) this;
return itemStack.getOrCreateTag().getInt("Level");
}

@Unique
private int getXp() {
ItemStack itemStack = (ItemStack) (Object) this;
return itemStack.getOrCreateTag().getInt("Experience");
}

@Unique
private double getReduction() {
return Math.min(getLevel() * PlayerEX.CONFIG.getArmorLevelingSettings().getReductionPerLevel(), PlayerEX.CONFIG.getArmorLevelingSettings().getMaxReduction());
}

@Inject(method = "getTooltipLines", at = @At(value = "INVOKE", target = "Ljava/util/List;add(Ljava/lang/Object;)Z", ordinal = 0, shift = At.Shift.AFTER))
private void playerex$insertLevelTooltip(
Player player, TooltipFlag context,
CallbackInfoReturnable<List<Component>> info,
@Local List<Component> list
) {
ItemStack itemStack = (ItemStack) (Object) this;
if (PlayerEXUtil.isLevelable(itemStack)) {
list.add(Component.translatable("playerex.item.level", getLevel(), PlayerEXUtil.getMaxLevel(itemStack)));
list.add(Component.translatable("playerex.item.experience", getXp(), PlayerEXUtil.getRequiredXpForNextLevel(itemStack)));
}
}

@Inject(method = "getTooltipLines", at = @At(value = "INVOKE", target = "Ljava/util/List;add(Ljava/lang/Object;)Z", ordinal = 6, shift = At.Shift.AFTER))
private void playerex$insertReductionTooltip(
Player player, TooltipFlag context,
CallbackInfoReturnable<List<Component>> info,
@Local List<Component> list
) {
ItemStack itemStack = (ItemStack) (Object) this;
if (PlayerEXUtil.isArmor(itemStack)) {
list.add(Component.translatable("playerex.item.reduction", String.format("%.2f", getReduction())));
}
}
}
16 changes: 16 additions & 0 deletions src/main/java/com/bibireden/playerex/mixin/LivingEntityMixin.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import net.minecraft.world.InteractionHand;
import net.minecraft.world.damagesource.DamageSource;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.ItemStack;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
Expand Down Expand Up @@ -66,4 +67,19 @@ public void preventAttack(InteractionHand hand, CallbackInfo ci) {
private boolean playerex$damage(boolean original, DamageSource source, float damage) {
return LivingEntityEvents.SHOULD_DAMAGE.invoker().shouldDamage((LivingEntity) (Object) this, source, damage);
}

@ModifyReturnValue(
method = "getDamageAfterArmorAbsorb(Lnet/minecraft/world/damagesource/DamageSource;F)F",
at = @At("RETURN")
)
public float applyDamageReduction(float original) {
LivingEntity self = (LivingEntity) (Object) this;
var total = 0d;
for (ItemStack slot : self.getArmorSlots()) {
total += Math.min(slot.getOrCreateTag().getInt("Level") * PlayerEX.CONFIG.getArmorLevelingSettings().getReductionPerLevel(), PlayerEX.CONFIG.getArmorLevelingSettings().getMaxReduction());
}
// 400 because 4 slots then percentage to decimal
total /= 400;
return (float) (original * (1 - total));
}
}
10 changes: 9 additions & 1 deletion src/main/kotlin/com/bibireden/playerex/PlayerEX.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,21 @@ import com.bibireden.playerex.api.event.LivingEntityEvents
import com.bibireden.playerex.api.event.PlayerEXSoundEvents
import com.bibireden.playerex.api.event.PlayerEntityEvents
import com.bibireden.playerex.config.PlayerEXConfig
import com.bibireden.playerex.config.PlayerEXConfigModel
import com.bibireden.playerex.config.PlayerEXConfigModel.Lifecycle
import com.bibireden.playerex.ext.component
import com.bibireden.playerex.factory.*
import com.bibireden.playerex.networking.NetworkingChannels
import com.bibireden.playerex.networking.NetworkingPackets
import com.bibireden.playerex.networking.registerServerbound
import com.bibireden.playerex.networking.types.UpdatePacketType
import com.bibireden.playerex.predicate.FilterCheck
import de.dafuqs.additionalentityattributes.AdditionalEntityAttributes
import eu.pb4.placeholders.api.Placeholders
import net.fabricmc.api.ModInitializer
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback
import net.fabricmc.fabric.api.entity.event.v1.ServerEntityCombatEvents
import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents
import net.fabricmc.fabric.api.loot.v2.LootTableEvents
import net.fabricmc.fabric.api.networking.v1.ServerLoginConnectionEvents
import net.minecraft.core.Registry
import net.minecraft.core.registries.BuiltInRegistries
Expand Down Expand Up @@ -82,18 +84,24 @@ object PlayerEX : ModInitializer {
LivingEntityEvents.ON_DAMAGE.register(EventFactory::onDamage)
LivingEntityEvents.SHOULD_DAMAGE.register(EventFactory::shouldDamage)

LootTableEvents.MODIFY.register(EventFactory::onModifyLootTable)

PlayerEntityEvents.ON_CRITICAL.register(EventFactory::onCritAttack)
PlayerEntityEvents.SHOULD_CRITICAL.register(EventFactory::attackIsCrit)

DamageFactory.forEach(PlayerEXAPI::registerDamageModification)
RefundFactory.forEach(PlayerEXAPI::registerRefundCondition)

ServerEntityCombatEvents.AFTER_KILLED_OTHER_ENTITY.register(EventFactory::entityWasKilled)

PlaceholderFactory.STORE.forEach(Placeholders::register)

Registry.register(BuiltInRegistries.SOUND_EVENT, PlayerEXSoundEvents.LEVEL_UP_SOUND.location, PlayerEXSoundEvents.LEVEL_UP_SOUND)
Registry.register(BuiltInRegistries.SOUND_EVENT, PlayerEXSoundEvents.SPEND_SOUND.location, PlayerEXSoundEvents.SPEND_SOUND)
Registry.register(BuiltInRegistries.SOUND_EVENT, PlayerEXSoundEvents.REFUND_SOUND.location, PlayerEXSoundEvents.REFUND_SOUND)

Registry.register(BuiltInRegistries.LOOT_FUNCTION_TYPE, id("filtered"), FilterCheck.type())

EntityAttributeModifiedEvents.MODIFIED.register { attribute, entity, _, _, _ ->
if (entity?.level() == null) return@register // no entity & no world, skip

Expand Down
8 changes: 7 additions & 1 deletion src/main/kotlin/com/bibireden/playerex/api/PlayerEXTags.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,10 @@ import net.minecraft.world.item.Item
object PlayerEXTags {
@JvmField
val UNBREAKABLE_ITEMS: TagKey<Item> = TagKey.create(Registries.ITEM, PlayerEX.id("unbreakable"))
}

@JvmField
val WEAPONS: TagKey<Item> = TagKey.create(Registries.ITEM, PlayerEX.id("weapons"))

@JvmField
val ARMOR: TagKey<Item> = TagKey.create(Registries.ITEM, PlayerEX.id("armor"))
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,93 @@ class PlayerEXConfigModel {
@JvmField @Nest @Expanded var featureSettings = FeatureSettings()
@JvmField @Nest @Expanded var lifecycleSettings = LifecycleSettings()
@JvmField @Nest @Expanded var advancedSettings = AdvancedSettings()
@JvmField @Nest @Expanded var weaponLevelingSettings = WeaponXpSettings()
@JvmField @Nest @Expanded var armorLevelingSettings = ArmorXpSettings()

@SectionHeader("client_options")

@JvmField @Nest @Expanded var visualSettings = VisualSettings()
@JvmField @Nest @Expanded var soundSettings = SoundSettings()

// SERVER

data class ArmorXpSettings(
@Sync(SyncMode.OVERRIDE_CLIENT)
@JvmField
var enabled: Boolean = true,

@Sync(SyncMode.OVERRIDE_CLIENT)
@JvmField
var armorPerLevel: Double = 0.1,

@Sync(SyncMode.OVERRIDE_CLIENT)
@JvmField
var reductionPerLevel: Double = 0.1,

@Sync(SyncMode.OVERRIDE_CLIENT)
@JvmField
@RangeConstraint(min = 0.0, max = 25.0)
var maxReduction: Double = 25.0,

@Sync(SyncMode.OVERRIDE_CLIENT)
@JvmField
var formula: String = "5x^(1.1)",

@Sync(SyncMode.OVERRIDE_CLIENT)
@JvmField
var xpFromPassive: Int = 10,

@Sync(SyncMode.OVERRIDE_CLIENT)
@JvmField
var xpFromHostile: Int = 20,

@Sync(SyncMode.OVERRIDE_CLIENT)
@JvmField
var xpFromMiniboss: Int = 50,

@Sync(SyncMode.OVERRIDE_CLIENT)
@JvmField
var xpFromBoss: Int = 100,

@Sync(SyncMode.OVERRIDE_CLIENT)
@JvmField
var maxLevel: Int = 500,
)

data class WeaponXpSettings(
@Sync(SyncMode.OVERRIDE_CLIENT)
@JvmField
var enabled: Boolean = true,

@Sync(SyncMode.OVERRIDE_CLIENT)
@JvmField
var damagePerLevel: Double = 0.1,

@Sync(SyncMode.OVERRIDE_CLIENT)
@JvmField
var formula: String = "5x^(1.1)",

@Sync(SyncMode.OVERRIDE_CLIENT)
@JvmField
var xpFromPassive: Int = 10,

@Sync(SyncMode.OVERRIDE_CLIENT)
@JvmField
var xpFromHostile: Int = 20,

@Sync(SyncMode.OVERRIDE_CLIENT)
@JvmField
var xpFromMiniboss: Int = 50,

@Sync(SyncMode.OVERRIDE_CLIENT)
@JvmField
var xpFromBoss: Int = 100,

@Sync(SyncMode.OVERRIDE_CLIENT)
@JvmField
var maxLevel: Int = 500,
)

data class LevelingSettings(
@Sync(SyncMode.OVERRIDE_CLIENT)
@JvmField
Expand Down
11 changes: 11 additions & 0 deletions src/main/kotlin/com/bibireden/playerex/ext/ItemStack.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.bibireden.playerex.ext

import net.minecraft.world.item.ItemStack

var ItemStack.level: Int
get() = this.orCreateTag.getInt("Level")
set(value) = this.orCreateTag.putInt("Level", value)

var ItemStack.xp: Int
get() = this.orCreateTag.getInt("Experience")
set(value) = this.orCreateTag.putInt("Experience", value)
Loading

0 comments on commit b691a03

Please sign in to comment.