diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..60caea0 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,3 @@ +Always update the mod's version number in gradle.properties after a change + +Always update CHANGELOG.md with the new changes after a change with the new version number. \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 295089d..7f21f5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,23 @@ This changelog covers all recorded changes after `1.0.0`, grouped by release version from the git history. +## 2.4.1 + +- Fixed rejoining players with fortitude bonus health being left at the vanilla 20 HP after their increased maximum health was restored. +- Applied server-side stat modifiers before the initial login synchronization so the client immediately receives the correct attributes and health state. + +## 2.4.0 + +- Added 20 hidden advancements covering specialized, hybrid, balanced, magic, weapon-scaling, respec, and configuration-based builds. +- Added persistent per-character respec counts, previous-build snapshots, and weapon-scaling investment history for stateful hidden advancement conditions. +- Renamed several existing advancements, including the root, level 5, maximum-level, maximum-attribute, Knowledge, and shrine advancements. + +## 2.3.0 + +- Added an Ascend advancement tree for reaching levels 2, 5, 10, and the configured maximum level (20 by default). +- Added advancements for maxing an attribute, accumulating 100 Knowledge, reading a Moonseye Tome, and successfully using a Shrine of Remembrance. +- Made advancement checks use server-authoritative progression data so milestones also work after login, respawn, and admin-granted progression. + ## 1.1.0 - Added Ascend admin commands for inspecting stats, setting attributes, granting XP, and resetting progression. diff --git a/gradle.properties b/gradle.properties index 99fa1db..7a33b74 100644 --- a/gradle.properties +++ b/gradle.properties @@ -53,7 +53,7 @@ mod_name=Ascend: SbA # The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. mod_license=All Rights Reserved # The mod version. See https://semver.org/ -mod_version=2.2.4 +mod_version=2.4.1 # The group ID for the mod. It is only important when publishing as an artifact to a Maven repository. # This should match the base package used for the mod sources. # See https://maven.apache.org/guides/mini/guide-naming-conventions.html diff --git a/src/main/java/net/fretux/ascend/command/AscendCommand.java b/src/main/java/net/fretux/ascend/command/AscendCommand.java index 084b32d..85cb890 100644 --- a/src/main/java/net/fretux/ascend/command/AscendCommand.java +++ b/src/main/java/net/fretux/ascend/command/AscendCommand.java @@ -8,8 +8,6 @@ import com.mojang.brigadier.exceptions.CommandSyntaxException; import net.fretux.ascend.player.PlayerStats; import net.fretux.ascend.player.PlayerStatsProvider; import net.fretux.ascend.player.StatEffects; -import net.fretux.ascend.network.PacketHandler; -import net.fretux.ascend.network.ClientboundSyncStatsPacket; import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.Commands; import net.minecraft.commands.arguments.EntityArgument; @@ -17,7 +15,6 @@ import net.minecraft.nbt.CompoundTag; import net.minecraft.network.chat.Component; import net.minecraft.server.level.ServerPlayer; import net.minecraftforge.eventbus.api.SubscribeEvent; -import net.minecraftforge.network.PacketDistributor; public class AscendCommand { @SubscribeEvent @@ -67,7 +64,7 @@ public class AscendCommand { target.getCapability(PlayerStatsProvider.PLAYER_STATS).ifPresent(stats -> { stats.setAttributeLevel(attr, value); StatEffects.applyAll(target); - sync(target, stats); + PlayerStatsProvider.sync(target); ctx.getSource().sendSuccess(() -> Component.literal("Set " + target.getName().getString() + "'s " + attr + " to " + value), true); }); @@ -80,7 +77,7 @@ public class AscendCommand { target.getCapability(PlayerStatsProvider.PLAYER_STATS).ifPresent(stats -> { stats.addAscendXP(amount); StatEffects.applyAll(target); - sync(target, stats); + PlayerStatsProvider.sync(target); ctx.getSource().sendSuccess(() -> Component.literal("Added " + amount + " Ascend XP to " + target.getName().getString()), true); }); @@ -93,17 +90,11 @@ public class AscendCommand { PlayerStats fresh = new PlayerStats(); stats.deserializeNBT(fresh.serializeNBT()); StatEffects.applyAll(target); - sync(target, stats); + PlayerStatsProvider.sync(target); ctx.getSource().sendSuccess(() -> Component.literal("Reset " + target.getName().getString() + "'s Ascend stats."), true); }); return 1; } - private static void sync(ServerPlayer player, PlayerStats stats) { - PacketHandler.INSTANCE.send( - PacketDistributor.PLAYER.with(() -> player), - new ClientboundSyncStatsPacket(stats.serializeNBT()) - ); - } } diff --git a/src/main/java/net/fretux/ascend/event/PlayerEvents.java b/src/main/java/net/fretux/ascend/event/PlayerEvents.java index 0307596..1ea55f9 100644 --- a/src/main/java/net/fretux/ascend/event/PlayerEvents.java +++ b/src/main/java/net/fretux/ascend/event/PlayerEvents.java @@ -45,8 +45,13 @@ public class PlayerEvents { @SubscribeEvent public static void onPlayerLogin(PlayerEvent.PlayerLoggedInEvent event) { if (!event.getEntity().level().isClientSide) { - PlayerStatsProvider.sync(event.getEntity()); + float healthBeforeFortitude = event.getEntity().getHealth(); StatEffects.applyAll(event.getEntity()); + if (Math.abs(healthBeforeFortitude - 20.0F) < 0.001F + && event.getEntity().getMaxHealth() > healthBeforeFortitude) { + event.getEntity().setHealth(event.getEntity().getMaxHealth()); + } + PlayerStatsProvider.sync(event.getEntity()); } } } diff --git a/src/main/java/net/fretux/ascend/network/ServerboundShrineChoicePacket.java b/src/main/java/net/fretux/ascend/network/ServerboundShrineChoicePacket.java index 2dc23bb..d4e1532 100644 --- a/src/main/java/net/fretux/ascend/network/ServerboundShrineChoicePacket.java +++ b/src/main/java/net/fretux/ascend/network/ServerboundShrineChoicePacket.java @@ -1,5 +1,6 @@ package net.fretux.ascend.network; +import net.fretux.ascend.advancement.AscendAdvancements; import net.fretux.ascend.compat.AscendMMCompat; import net.fretux.ascend.item.RemembranceEssenceItem; import net.fretux.ascend.player.PlayerStatsProvider; @@ -41,6 +42,7 @@ public class ServerboundShrineChoicePacket { net.fretux.ascend.player.StatEffects.applyAll(player); player.sendSystemMessage(Component.literal("YOU ARE A BLANK SLATE. EMPTY. FORGOTTEN.")); RemembranceEssenceItem.consume(player); + AscendAdvancements.grant(player, "use_shrine"); } else { player.sendSystemMessage(Component.literal("YOUR MIND IS TOO EMPTY TO BE WORTH MY TOUCH.")); player.hurt(player.level().damageSources().magic(), 4.0F); @@ -60,6 +62,7 @@ public class ServerboundShrineChoicePacket { sanity.setInsanity(0); player.sendSystemMessage(Component.literal("YOUR MIND EXPANDS AS IT BREAKS.")); RemembranceEssenceItem.consume(player); + AscendAdvancements.grant(player, "use_shrine"); } else { player.sendSystemMessage(Component.literal("YOUR MIND IS NOT YET CLEAR ENOUGH.")); } @@ -76,6 +79,7 @@ public class ServerboundShrineChoicePacket { }); player.sendSystemMessage(Component.literal("THE SHRINE RESTORES YOUR FRACTURED MIND.")); RemembranceEssenceItem.consume(player); + AscendAdvancements.grant(player, "use_shrine"); } else { player.sendSystemMessage(Component.literal("YOU LACK THE KNOWLEDGE TO REST.")); } diff --git a/src/main/java/net/fretux/ascend/network/ServerboundSpendPointPacket.java b/src/main/java/net/fretux/ascend/network/ServerboundSpendPointPacket.java index c68d327..4920b53 100644 --- a/src/main/java/net/fretux/ascend/network/ServerboundSpendPointPacket.java +++ b/src/main/java/net/fretux/ascend/network/ServerboundSpendPointPacket.java @@ -5,8 +5,6 @@ import net.fretux.ascend.player.StatEffects; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.server.level.ServerPlayer; import net.minecraftforge.network.NetworkEvent; -import net.minecraftforge.network.PacketDistributor; -import net.minecraft.nbt.CompoundTag; import java.util.function.Supplier; @@ -41,11 +39,7 @@ public class ServerboundSpendPointPacket { player.setHealth(Math.min(player.getHealth() + gainedMaxHealth, player.getMaxHealth())); } } - CompoundTag data = stats.serializeNBT(); - PacketHandler.INSTANCE.send( - PacketDistributor.PLAYER.with(() -> player), - new ClientboundSyncStatsPacket(data) - ); + PlayerStatsProvider.sync(player); } }); }); diff --git a/src/main/java/net/fretux/ascend/player/PlayerStats.java b/src/main/java/net/fretux/ascend/player/PlayerStats.java index 11b81ce..04ac79a 100644 --- a/src/main/java/net/fretux/ascend/player/PlayerStats.java +++ b/src/main/java/net/fretux/ascend/player/PlayerStats.java @@ -13,11 +13,14 @@ public class PlayerStats { "light_scaling", "medium_scaling", "heavy_scaling", "magic_scaling" }; private final Map attributes = new HashMap<>(); + private final Map lastRespecBuild = new HashMap<>(); private int ascendLevel = 1; private int ascendXP = 0; private int unspentPoints = getPointsPerLevel(); private int maxAscendLevel = getConfiguredMaxAscendLevel(); private int maxAttributePoints = getConfiguredMaxAttributePoints(); + private int respecCount = 0; + private boolean everInvestedWeaponScaling = false; public PlayerStats() { for (String attr : ATTRIBUTE_KEYS) { attributes.put(attr, 0); @@ -77,6 +80,9 @@ public class PlayerStats { } unspentPoints -= 1; attributes.put(attribute, level + 1); + if (isWeaponScaling(attribute)) { + everInvestedWeaponScaling = true; + } return true; } @@ -100,6 +106,9 @@ public class PlayerStats { if (!attributes.containsKey(attribute)) return; int clamped = Math.max(0, Math.min(level, getMaxAttributePoints())); attributes.put(attribute, clamped); + if (clamped > 0 && isWeaponScaling(attribute)) { + everInvestedWeaponScaling = true; + } } public int getMaxMana() { @@ -133,14 +142,47 @@ public class PlayerStats { public void refundAllPoints() { int totalSpent = 0; - for (String key : attributes.keySet()) { - int lvl = attributes.get(key); + Map refundedBuild = new HashMap<>(); + for (String key : ATTRIBUTE_KEYS) { + int lvl = attributes.getOrDefault(key, 0); totalSpent += lvl; + refundedBuild.put(key, lvl); attributes.put(key, 0); } + if (totalSpent > 0) { + lastRespecBuild.clear(); + lastRespecBuild.putAll(refundedBuild); + respecCount++; + } unspentPoints += totalSpent; } + public int getRespecCount() { + return respecCount; + } + + public boolean hasEverInvestedWeaponScaling() { + return everInvestedWeaponScaling; + } + + public boolean hasNearlyRestoredLastBuild() { + if (lastRespecBuild.isEmpty()) return false; + int originalTotal = 0; + int currentTotal = 0; + int totalDifference = 0; + for (String key : ATTRIBUTE_KEYS) { + int original = lastRespecBuild.getOrDefault(key, 0); + int current = attributes.getOrDefault(key, 0); + originalTotal += original; + currentTotal += current; + totalDifference += Math.abs(original - current); + } + int allowedDifference = Math.max(2, (int) Math.ceil(originalTotal * 0.05D)); + return originalTotal > 0 && currentTotal > 0 + && currentTotal >= originalTotal - allowedDifference + && totalDifference <= allowedDifference; + } + public boolean hasUsedMoonseye() { return hasUsedMoonseye; } @@ -159,11 +201,18 @@ public class PlayerStats { tag.putInt("MaxAttributePoints", getMaxAttributePoints()); tag.putInt("Knowledge", knowledge); tag.putBoolean("HasUsedMoonseye", hasUsedMoonseye); + tag.putInt("RespecCount", respecCount); + tag.putBoolean("EverInvestedWeaponScaling", everInvestedWeaponScaling); CompoundTag attrTag = new CompoundTag(); for (Map.Entry entry : attributes.entrySet()) { attrTag.putInt(entry.getKey(), entry.getValue()); } tag.put("Attributes", attrTag); + CompoundTag respecBuildTag = new CompoundTag(); + for (Map.Entry entry : lastRespecBuild.entrySet()) { + respecBuildTag.putInt(entry.getKey(), entry.getValue()); + } + tag.put("LastRespecBuild", respecBuildTag); return tag; } @@ -189,6 +238,19 @@ public class PlayerStats { attributes.put(key, Math.max(0, Math.min(loaded, getMaxAttributePoints()))); } } + respecCount = Math.max(0, tag.getInt("RespecCount")); + everInvestedWeaponScaling = tag.contains("EverInvestedWeaponScaling") + ? tag.getBoolean("EverInvestedWeaponScaling") + : getAttributeLevel("light_scaling") > 0 + || getAttributeLevel("medium_scaling") > 0 + || getAttributeLevel("heavy_scaling") > 0; + lastRespecBuild.clear(); + if (tag.contains("LastRespecBuild")) { + CompoundTag respecBuildTag = tag.getCompound("LastRespecBuild"); + for (String key : ATTRIBUTE_KEYS) { + lastRespecBuild.put(key, Math.max(0, respecBuildTag.getInt(key))); + } + } if (tag.contains("Knowledge")) { knowledge = Math.max(0, tag.getInt("Knowledge")); } @@ -212,4 +274,10 @@ public class PlayerStats { private static int getConfiguredMaxAttributePoints() { return AscendConfig.COMMON.maxAttributePoints.get(); } + + private static boolean isWeaponScaling(String attribute) { + return "light_scaling".equals(attribute) + || "medium_scaling".equals(attribute) + || "heavy_scaling".equals(attribute); + } } diff --git a/src/main/java/net/fretux/ascend/player/PlayerStatsProvider.java b/src/main/java/net/fretux/ascend/player/PlayerStatsProvider.java index d48896a..3603417 100644 --- a/src/main/java/net/fretux/ascend/player/PlayerStatsProvider.java +++ b/src/main/java/net/fretux/ascend/player/PlayerStatsProvider.java @@ -1,5 +1,6 @@ package net.fretux.ascend.player; +import net.fretux.ascend.advancement.AscendAdvancements; import net.fretux.ascend.network.ClientboundSyncStatsPacket; import net.fretux.ascend.network.PacketHandler; import net.minecraft.core.Direction; @@ -43,6 +44,7 @@ public class PlayerStatsProvider implements ICapabilitySerializable return; } player.getCapability(PlayerStatsProvider.PLAYER_STATS).ifPresent(stats -> { + AscendAdvancements.checkMilestones(serverPlayer, stats); CompoundTag data = stats.serializeNBT(); PacketHandler.INSTANCE.send(PacketDistributor.PLAYER.with(() -> serverPlayer), new ClientboundSyncStatsPacket(data)); diff --git a/src/main/resources/assets/ascend/lang/en_us.json b/src/main/resources/assets/ascend/lang/en_us.json index 1202c4d..0cd1458 100644 --- a/src/main/resources/assets/ascend/lang/en_us.json +++ b/src/main/resources/assets/ascend/lang/en_us.json @@ -34,5 +34,63 @@ "ascend.attribute.display": "%s: %s", "ascend.stats.knowledge": "Knowledge: %s", "item.ascend.remembrance_essence": "Remembrance Essence", - "block.ascend.shrine_of_remembrance": "Shrine of Remembrance" + "block.ascend.shrine_of_remembrance": "Shrine of Remembrance", + + "advancements.ascend.first_steps.title": "A Flame, Barely Kindled", + "advancements.ascend.first_steps.description": "Reach Ascend level 2", + "advancements.ascend.level_5.title": "The Build Begins", + "advancements.ascend.level_5.description": "Reach Ascend level 5", + "advancements.ascend.level_10.title": "Beyond Mortal Limits", + "advancements.ascend.level_10.description": "Reach Ascend level 10", + "advancements.ascend.max_level.title": "This is Ascend", + "advancements.ascend.max_level.description": "Reach the maximum Ascend level", + "advancements.ascend.max_attribute.title": "Fear Not the One-Trick", + "advancements.ascend.max_attribute.description": "Max out any Ascend attribute", + "advancements.ascend.knowledge_100.title": "Things Man Was Not Meant to Know", + "advancements.ascend.knowledge_100.description": "Accumulate 100 Knowledge", + "advancements.ascend.read_moonseye_tome.title": "The Moon Gazes Back", + "advancements.ascend.read_moonseye_tome.description": "Read a Moonseye Tome", + "advancements.ascend.use_shrine.title": "I Forgor", + "advancements.ascend.use_shrine.description": "Make a successful choice at a Shrine of Remembrance", + + "advancements.ascend.bonk_scholar.title": "Bonk Scholar", + "advancements.ascend.bonk_scholar.description": "Reach 100 Strength", + "advancements.ascend.cannot_hit_what_you_cannot_see.title": "You Cannot Hit What You Cannot See", + "advancements.ascend.cannot_hit_what_you_cannot_see.description": "Reach 100 Agility", + "advancements.ascend.health_bar_enthusiast.title": "Health Bar Enthusiast", + "advancements.ascend.health_bar_enthusiast.description": "Reach 100 Fortitude", + "advancements.ascend.big_brain_small_helmet.title": "Big Brain Time", + "advancements.ascend.big_brain_small_helmet.description": "Reach 100 Intelligence", + "advancements.ascend.mana_without_purpose.title": "Mana Without Purpose", + "advancements.ascend.mana_without_purpose.description": "Reach 50 Intelligence without Iron's Spells installed", + "advancements.ascend.unlicensed_therapist.title": "Unlicensed Therapist", + "advancements.ascend.unlicensed_therapist.description": "Reach 100 Willpower", + "advancements.ascend.talk_no_jutsu.title": "Talk No Jutsu", + "advancements.ascend.talk_no_jutsu.description": "Reach 100 Charisma", + "advancements.ascend.perfectly_balanced.title": "Perfectly Balanced", + "advancements.ascend.perfectly_balanced.description": "Invest exactly the same number of points in all ten attributes", + "advancements.ascend.light_work.title": "Light Work", + "advancements.ascend.light_work.description": "Reach 100 Light Scaling", + "advancements.ascend.medium_rare.title": "Medium Rare", + "advancements.ascend.medium_rare.description": "Reach 100 Medium Scaling", + "advancements.ascend.compensating.title": "Compensating", + "advancements.ascend.compensating.description": "Reach 100 Heavy Scaling", + "advancements.ascend.quality_build.title": "Quality Build", + "advancements.ascend.quality_build.description": "Reach 75 in all three weapon scaling attributes", + "advancements.ascend.oops_all_magic.title": "Oops, All Magic", + "advancements.ascend.oops_all_magic.description": "Reach 100 Magic Scaling before investing in weapon scaling", + "advancements.ascend.glass_cannonical.title": "Glass Cannonical", + "advancements.ascend.glass_cannonical.description": "Reach 90 Intelligence and Magic Scaling while keeping Fortitude below 10", + "advancements.ascend.muscle_wizard.title": "Muscle Wizard", + "advancements.ascend.muscle_wizard.description": "Reach 100 Strength and Magic Scaling", + "advancements.ascend.optimized_casting_environment.title": "Optimized Casting Environment", + "advancements.ascend.optimized_casting_environment.description": "Reach 80 Intelligence, Willpower, and Magic Scaling", + "advancements.ascend.indecisive_chosen_one.title": "Indecisive Chosen One", + "advancements.ascend.indecisive_chosen_one.description": "Respec ten times on the same character", + "advancements.ascend.config_enjoyer.title": "Config Enjoyer", + "advancements.ascend.config_enjoyer.description": "Make the intended progression unrecognizable", + "advancements.ascend.game_journalist_mode.title": "Game Journalist Mode", + "advancements.ascend.game_journalist_mode.description": "Set Ascend XP gain extremely high", + "advancements.ascend.definition_of_insanity.title": "The Definition of Insanity", + "advancements.ascend.definition_of_insanity.description": "Reset your attributes, then restore nearly the same build" }