make various fixes

This commit is contained in:
2026-07-23 13:37:23 +02:00
parent 402e45e5a9
commit 9eb418afb0
10 changed files with 166 additions and 24 deletions
+3
View File
@@ -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.
+17
View File
@@ -2,6 +2,23 @@
This changelog covers all recorded changes after `1.0.0`, grouped by release version from the git history. 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 ## 1.1.0
- Added Ascend admin commands for inspecting stats, setting attributes, granting XP, and resetting progression. - Added Ascend admin commands for inspecting stats, setting attributes, granting XP, and resetting progression.
+1 -1
View File
@@ -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. # The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
mod_license=All Rights Reserved mod_license=All Rights Reserved
# The mod version. See https://semver.org/ # 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. # 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. # This should match the base package used for the mod sources.
# See https://maven.apache.org/guides/mini/guide-naming-conventions.html # See https://maven.apache.org/guides/mini/guide-naming-conventions.html
@@ -8,8 +8,6 @@ import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.fretux.ascend.player.PlayerStats; import net.fretux.ascend.player.PlayerStats;
import net.fretux.ascend.player.PlayerStatsProvider; import net.fretux.ascend.player.PlayerStatsProvider;
import net.fretux.ascend.player.StatEffects; 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.CommandSourceStack;
import net.minecraft.commands.Commands; import net.minecraft.commands.Commands;
import net.minecraft.commands.arguments.EntityArgument; import net.minecraft.commands.arguments.EntityArgument;
@@ -17,7 +15,6 @@ import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerPlayer;
import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.network.PacketDistributor;
public class AscendCommand { public class AscendCommand {
@SubscribeEvent @SubscribeEvent
@@ -67,7 +64,7 @@ public class AscendCommand {
target.getCapability(PlayerStatsProvider.PLAYER_STATS).ifPresent(stats -> { target.getCapability(PlayerStatsProvider.PLAYER_STATS).ifPresent(stats -> {
stats.setAttributeLevel(attr, value); stats.setAttributeLevel(attr, value);
StatEffects.applyAll(target); StatEffects.applyAll(target);
sync(target, stats); PlayerStatsProvider.sync(target);
ctx.getSource().sendSuccess(() -> ctx.getSource().sendSuccess(() ->
Component.literal("Set " + target.getName().getString() + "'s " + attr + " to " + value), true); 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 -> { target.getCapability(PlayerStatsProvider.PLAYER_STATS).ifPresent(stats -> {
stats.addAscendXP(amount); stats.addAscendXP(amount);
StatEffects.applyAll(target); StatEffects.applyAll(target);
sync(target, stats); PlayerStatsProvider.sync(target);
ctx.getSource().sendSuccess(() -> ctx.getSource().sendSuccess(() ->
Component.literal("Added " + amount + " Ascend XP to " + target.getName().getString()), true); Component.literal("Added " + amount + " Ascend XP to " + target.getName().getString()), true);
}); });
@@ -93,17 +90,11 @@ public class AscendCommand {
PlayerStats fresh = new PlayerStats(); PlayerStats fresh = new PlayerStats();
stats.deserializeNBT(fresh.serializeNBT()); stats.deserializeNBT(fresh.serializeNBT());
StatEffects.applyAll(target); StatEffects.applyAll(target);
sync(target, stats); PlayerStatsProvider.sync(target);
ctx.getSource().sendSuccess(() -> ctx.getSource().sendSuccess(() ->
Component.literal("Reset " + target.getName().getString() + "'s Ascend stats."), true); Component.literal("Reset " + target.getName().getString() + "'s Ascend stats."), true);
}); });
return 1; return 1;
} }
private static void sync(ServerPlayer player, PlayerStats stats) {
PacketHandler.INSTANCE.send(
PacketDistributor.PLAYER.with(() -> player),
new ClientboundSyncStatsPacket(stats.serializeNBT())
);
}
} }
@@ -45,8 +45,13 @@ public class PlayerEvents {
@SubscribeEvent @SubscribeEvent
public static void onPlayerLogin(PlayerEvent.PlayerLoggedInEvent event) { public static void onPlayerLogin(PlayerEvent.PlayerLoggedInEvent event) {
if (!event.getEntity().level().isClientSide) { if (!event.getEntity().level().isClientSide) {
PlayerStatsProvider.sync(event.getEntity()); float healthBeforeFortitude = event.getEntity().getHealth();
StatEffects.applyAll(event.getEntity()); 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());
} }
} }
} }
@@ -1,5 +1,6 @@
package net.fretux.ascend.network; package net.fretux.ascend.network;
import net.fretux.ascend.advancement.AscendAdvancements;
import net.fretux.ascend.compat.AscendMMCompat; import net.fretux.ascend.compat.AscendMMCompat;
import net.fretux.ascend.item.RemembranceEssenceItem; import net.fretux.ascend.item.RemembranceEssenceItem;
import net.fretux.ascend.player.PlayerStatsProvider; import net.fretux.ascend.player.PlayerStatsProvider;
@@ -41,6 +42,7 @@ public class ServerboundShrineChoicePacket {
net.fretux.ascend.player.StatEffects.applyAll(player); net.fretux.ascend.player.StatEffects.applyAll(player);
player.sendSystemMessage(Component.literal("YOU ARE A BLANK SLATE. EMPTY. FORGOTTEN.")); player.sendSystemMessage(Component.literal("YOU ARE A BLANK SLATE. EMPTY. FORGOTTEN."));
RemembranceEssenceItem.consume(player); RemembranceEssenceItem.consume(player);
AscendAdvancements.grant(player, "use_shrine");
} else { } else {
player.sendSystemMessage(Component.literal("YOUR MIND IS TOO EMPTY TO BE WORTH MY TOUCH.")); player.sendSystemMessage(Component.literal("YOUR MIND IS TOO EMPTY TO BE WORTH MY TOUCH."));
player.hurt(player.level().damageSources().magic(), 4.0F); player.hurt(player.level().damageSources().magic(), 4.0F);
@@ -60,6 +62,7 @@ public class ServerboundShrineChoicePacket {
sanity.setInsanity(0); sanity.setInsanity(0);
player.sendSystemMessage(Component.literal("YOUR MIND EXPANDS AS IT BREAKS.")); player.sendSystemMessage(Component.literal("YOUR MIND EXPANDS AS IT BREAKS."));
RemembranceEssenceItem.consume(player); RemembranceEssenceItem.consume(player);
AscendAdvancements.grant(player, "use_shrine");
} else { } else {
player.sendSystemMessage(Component.literal("YOUR MIND IS NOT YET CLEAR ENOUGH.")); 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.")); player.sendSystemMessage(Component.literal("THE SHRINE RESTORES YOUR FRACTURED MIND."));
RemembranceEssenceItem.consume(player); RemembranceEssenceItem.consume(player);
AscendAdvancements.grant(player, "use_shrine");
} else { } else {
player.sendSystemMessage(Component.literal("YOU LACK THE KNOWLEDGE TO REST.")); player.sendSystemMessage(Component.literal("YOU LACK THE KNOWLEDGE TO REST."));
} }
@@ -5,8 +5,6 @@ import net.fretux.ascend.player.StatEffects;
import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerPlayer;
import net.minecraftforge.network.NetworkEvent; import net.minecraftforge.network.NetworkEvent;
import net.minecraftforge.network.PacketDistributor;
import net.minecraft.nbt.CompoundTag;
import java.util.function.Supplier; import java.util.function.Supplier;
@@ -41,11 +39,7 @@ public class ServerboundSpendPointPacket {
player.setHealth(Math.min(player.getHealth() + gainedMaxHealth, player.getMaxHealth())); player.setHealth(Math.min(player.getHealth() + gainedMaxHealth, player.getMaxHealth()));
} }
} }
CompoundTag data = stats.serializeNBT(); PlayerStatsProvider.sync(player);
PacketHandler.INSTANCE.send(
PacketDistributor.PLAYER.with(() -> player),
new ClientboundSyncStatsPacket(data)
);
} }
}); });
}); });
@@ -13,11 +13,14 @@ public class PlayerStats {
"light_scaling", "medium_scaling", "heavy_scaling", "magic_scaling" "light_scaling", "medium_scaling", "heavy_scaling", "magic_scaling"
}; };
private final Map<String, Integer> attributes = new HashMap<>(); private final Map<String, Integer> attributes = new HashMap<>();
private final Map<String, Integer> lastRespecBuild = new HashMap<>();
private int ascendLevel = 1; private int ascendLevel = 1;
private int ascendXP = 0; private int ascendXP = 0;
private int unspentPoints = getPointsPerLevel(); private int unspentPoints = getPointsPerLevel();
private int maxAscendLevel = getConfiguredMaxAscendLevel(); private int maxAscendLevel = getConfiguredMaxAscendLevel();
private int maxAttributePoints = getConfiguredMaxAttributePoints(); private int maxAttributePoints = getConfiguredMaxAttributePoints();
private int respecCount = 0;
private boolean everInvestedWeaponScaling = false;
public PlayerStats() { public PlayerStats() {
for (String attr : ATTRIBUTE_KEYS) { for (String attr : ATTRIBUTE_KEYS) {
attributes.put(attr, 0); attributes.put(attr, 0);
@@ -77,6 +80,9 @@ public class PlayerStats {
} }
unspentPoints -= 1; unspentPoints -= 1;
attributes.put(attribute, level + 1); attributes.put(attribute, level + 1);
if (isWeaponScaling(attribute)) {
everInvestedWeaponScaling = true;
}
return true; return true;
} }
@@ -100,6 +106,9 @@ public class PlayerStats {
if (!attributes.containsKey(attribute)) return; if (!attributes.containsKey(attribute)) return;
int clamped = Math.max(0, Math.min(level, getMaxAttributePoints())); int clamped = Math.max(0, Math.min(level, getMaxAttributePoints()));
attributes.put(attribute, clamped); attributes.put(attribute, clamped);
if (clamped > 0 && isWeaponScaling(attribute)) {
everInvestedWeaponScaling = true;
}
} }
public int getMaxMana() { public int getMaxMana() {
@@ -133,14 +142,47 @@ public class PlayerStats {
public void refundAllPoints() { public void refundAllPoints() {
int totalSpent = 0; int totalSpent = 0;
for (String key : attributes.keySet()) { Map<String, Integer> refundedBuild = new HashMap<>();
int lvl = attributes.get(key); for (String key : ATTRIBUTE_KEYS) {
int lvl = attributes.getOrDefault(key, 0);
totalSpent += lvl; totalSpent += lvl;
refundedBuild.put(key, lvl);
attributes.put(key, 0); attributes.put(key, 0);
} }
if (totalSpent > 0) {
lastRespecBuild.clear();
lastRespecBuild.putAll(refundedBuild);
respecCount++;
}
unspentPoints += totalSpent; 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() { public boolean hasUsedMoonseye() {
return hasUsedMoonseye; return hasUsedMoonseye;
} }
@@ -159,11 +201,18 @@ public class PlayerStats {
tag.putInt("MaxAttributePoints", getMaxAttributePoints()); tag.putInt("MaxAttributePoints", getMaxAttributePoints());
tag.putInt("Knowledge", knowledge); tag.putInt("Knowledge", knowledge);
tag.putBoolean("HasUsedMoonseye", hasUsedMoonseye); tag.putBoolean("HasUsedMoonseye", hasUsedMoonseye);
tag.putInt("RespecCount", respecCount);
tag.putBoolean("EverInvestedWeaponScaling", everInvestedWeaponScaling);
CompoundTag attrTag = new CompoundTag(); CompoundTag attrTag = new CompoundTag();
for (Map.Entry<String, Integer> entry : attributes.entrySet()) { for (Map.Entry<String, Integer> entry : attributes.entrySet()) {
attrTag.putInt(entry.getKey(), entry.getValue()); attrTag.putInt(entry.getKey(), entry.getValue());
} }
tag.put("Attributes", attrTag); tag.put("Attributes", attrTag);
CompoundTag respecBuildTag = new CompoundTag();
for (Map.Entry<String, Integer> entry : lastRespecBuild.entrySet()) {
respecBuildTag.putInt(entry.getKey(), entry.getValue());
}
tag.put("LastRespecBuild", respecBuildTag);
return tag; return tag;
} }
@@ -189,6 +238,19 @@ public class PlayerStats {
attributes.put(key, Math.max(0, Math.min(loaded, getMaxAttributePoints()))); 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")) { if (tag.contains("Knowledge")) {
knowledge = Math.max(0, tag.getInt("Knowledge")); knowledge = Math.max(0, tag.getInt("Knowledge"));
} }
@@ -212,4 +274,10 @@ public class PlayerStats {
private static int getConfiguredMaxAttributePoints() { private static int getConfiguredMaxAttributePoints() {
return AscendConfig.COMMON.maxAttributePoints.get(); return AscendConfig.COMMON.maxAttributePoints.get();
} }
private static boolean isWeaponScaling(String attribute) {
return "light_scaling".equals(attribute)
|| "medium_scaling".equals(attribute)
|| "heavy_scaling".equals(attribute);
}
} }
@@ -1,5 +1,6 @@
package net.fretux.ascend.player; package net.fretux.ascend.player;
import net.fretux.ascend.advancement.AscendAdvancements;
import net.fretux.ascend.network.ClientboundSyncStatsPacket; import net.fretux.ascend.network.ClientboundSyncStatsPacket;
import net.fretux.ascend.network.PacketHandler; import net.fretux.ascend.network.PacketHandler;
import net.minecraft.core.Direction; import net.minecraft.core.Direction;
@@ -43,6 +44,7 @@ public class PlayerStatsProvider implements ICapabilitySerializable<CompoundTag>
return; return;
} }
player.getCapability(PlayerStatsProvider.PLAYER_STATS).ifPresent(stats -> { player.getCapability(PlayerStatsProvider.PLAYER_STATS).ifPresent(stats -> {
AscendAdvancements.checkMilestones(serverPlayer, stats);
CompoundTag data = stats.serializeNBT(); CompoundTag data = stats.serializeNBT();
PacketHandler.INSTANCE.send(PacketDistributor.PLAYER.with(() -> serverPlayer), PacketHandler.INSTANCE.send(PacketDistributor.PLAYER.with(() -> serverPlayer),
new ClientboundSyncStatsPacket(data)); new ClientboundSyncStatsPacket(data));
@@ -34,5 +34,63 @@
"ascend.attribute.display": "%s: %s", "ascend.attribute.display": "%s: %s",
"ascend.stats.knowledge": "Knowledge: %s", "ascend.stats.knowledge": "Knowledge: %s",
"item.ascend.remembrance_essence": "Remembrance Essence", "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"
} }