Add new advancements, weapon scaling tooltips, passive agility API, and attribute scaling logic
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
package net.fretux.ascend.advancement;
|
||||
|
||||
import net.fretux.ascend.AscendMod;
|
||||
import net.fretux.ascend.config.AscendConfig;
|
||||
import net.fretux.ascend.player.PlayerStats;
|
||||
import net.minecraft.advancements.Advancement;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
/** Awards Ascend's state-based advancements from server-authoritative player stats. */
|
||||
public final class AscendAdvancements {
|
||||
private static final String CRITERION = "achieved";
|
||||
|
||||
private AscendAdvancements() {
|
||||
}
|
||||
|
||||
public static void checkMilestones(ServerPlayer player, PlayerStats stats) {
|
||||
int level = stats.getAscendLevel();
|
||||
if (level >= 2) grant(player, "first_steps");
|
||||
if (level >= 5) grant(player, "level_5");
|
||||
if (level >= 10) grant(player, "level_10");
|
||||
if (level >= stats.getMaxAscendLevel()) grant(player, "max_level");
|
||||
if (stats.getKnowledge() >= 100) grant(player, "knowledge_100");
|
||||
if (stats.hasUsedMoonseye()) grant(player, "read_moonseye_tome");
|
||||
|
||||
for (String attribute : PlayerStats.ATTRIBUTE_KEYS) {
|
||||
if (stats.getAttributeLevel(attribute) >= stats.getMaxAttributePoints()) {
|
||||
grant(player, "max_attribute");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int vigor = stats.getAttributeLevel("vigor");
|
||||
int mind = stats.getAttributeLevel("mind");
|
||||
int strength = stats.getAttributeLevel("strength");
|
||||
int agility = stats.getAttributeLevel("agility");
|
||||
int intelligence = stats.getAttributeLevel("intelligence");
|
||||
int willpower = stats.getAttributeLevel("willpower");
|
||||
int charisma = stats.getAttributeLevel("charisma");
|
||||
int arcane = stats.getAttributeLevel("arcane");
|
||||
|
||||
if (strength >= 100) grant(player, "bonk_scholar");
|
||||
if (agility >= 100) grant(player, "cannot_hit_what_you_cannot_see");
|
||||
if (vigor >= 100) grant(player, "health_bar_enthusiast");
|
||||
if (intelligence >= 100) grant(player, "big_brain_small_helmet");
|
||||
if (mind >= 100) grant(player, "mana_without_purpose");
|
||||
if (willpower >= 100) grant(player, "unlicensed_therapist");
|
||||
if (charisma >= 100) grant(player, "talk_no_jutsu");
|
||||
if (vigor > 0 && vigor == mind && vigor == strength && vigor == agility
|
||||
&& vigor == intelligence && vigor == willpower && vigor == charisma && vigor == arcane) {
|
||||
grant(player, "perfectly_balanced");
|
||||
}
|
||||
if (stats.getRespecCount() >= 10) grant(player, "indecisive_chosen_one");
|
||||
if (hasUnrecognizableProgressionConfig()) grant(player, "config_enjoyer");
|
||||
if (hasExtremeXpConfig()) grant(player, "game_journalist_mode");
|
||||
if (stats.hasNearlyRestoredLastBuild()) grant(player, "definition_of_insanity");
|
||||
}
|
||||
|
||||
private static boolean hasUnrecognizableProgressionConfig() {
|
||||
int changed = 0;
|
||||
if (AscendConfig.COMMON.xpPerMobKill.get() != 10) changed++;
|
||||
if (AscendConfig.COMMON.xpPerBlockBreak.get() != 2) changed++;
|
||||
if (AscendConfig.COMMON.xpPerCraft.get() != 1) changed++;
|
||||
if (AscendConfig.COMMON.xpPerSmelt.get() != 1) changed++;
|
||||
if (AscendConfig.COMMON.xpPerTrade.get() != 5) changed++;
|
||||
if (AscendConfig.COMMON.xpPerDamageTaken.get() != 2) changed++;
|
||||
if (AscendConfig.COMMON.xpPerMovement.get() != 1) changed++;
|
||||
if (AscendConfig.COMMON.xpPerPotion.get() != 3) changed++;
|
||||
if (AscendConfig.COMMON.pointsPerLevel.get() != 15) changed++;
|
||||
if (AscendConfig.COMMON.maxAscendLevel.get() != 20) changed++;
|
||||
if (AscendConfig.COMMON.maxAttributePoints.get() != 100) changed++;
|
||||
if (Double.compare(AscendConfig.COMMON.xpMultiplier.get(), 1.0D) != 0) changed++;
|
||||
if (Double.compare(AscendConfig.COMMON.attributeScalingMultiplier.get(), 1.0D) != 0) changed++;
|
||||
return changed >= 6;
|
||||
}
|
||||
|
||||
private static boolean hasExtremeXpConfig() {
|
||||
if (AscendConfig.COMMON.xpMultiplier.get() >= 25.0D) return true;
|
||||
int extremeSources = 0;
|
||||
if (AscendConfig.COMMON.xpPerMobKill.get() >= 250) extremeSources++;
|
||||
if (AscendConfig.COMMON.xpPerBlockBreak.get() >= 250) extremeSources++;
|
||||
if (AscendConfig.COMMON.xpPerCraft.get() >= 250) extremeSources++;
|
||||
if (AscendConfig.COMMON.xpPerSmelt.get() >= 250) extremeSources++;
|
||||
if (AscendConfig.COMMON.xpPerTrade.get() >= 250) extremeSources++;
|
||||
if (AscendConfig.COMMON.xpPerDamageTaken.get() >= 250) extremeSources++;
|
||||
if (AscendConfig.COMMON.xpPerMovement.get() >= 250) extremeSources++;
|
||||
if (AscendConfig.COMMON.xpPerPotion.get() >= 250) extremeSources++;
|
||||
return extremeSources >= 4;
|
||||
}
|
||||
|
||||
public static void grant(ServerPlayer player, String advancementPath) {
|
||||
Advancement advancement = player.server.getAdvancements().getAdvancement(
|
||||
ResourceLocation.fromNamespaceAndPath(AscendMod.MODID, advancementPath)
|
||||
);
|
||||
if (advancement != null) {
|
||||
player.getAdvancements().award(advancement, CRITERION);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package net.fretux.ascend.api;
|
||||
|
||||
import net.fretux.ascend.api.event.PassiveAgilityEvent;
|
||||
import net.fretux.ascend.player.AttributeScaling;
|
||||
import net.fretux.ascend.player.PlayerStatsProvider;
|
||||
import net.fretux.ascend.player.StatEffects;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraftforge.common.MinecraftForge;
|
||||
|
||||
/** Public entry point for Ascend's passive Agility integration. */
|
||||
public final class PassiveAgilityApi {
|
||||
private PassiveAgilityApi() {}
|
||||
|
||||
/** Collects all passive Agility contributions registered for this player. */
|
||||
public static int getPassiveAgility(Player player, int investedAgility) {
|
||||
PassiveAgilityEvent event = new PassiveAgilityEvent(player, investedAgility);
|
||||
MinecraftForge.EVENT_BUS.post(event);
|
||||
return event.getPassiveAgility();
|
||||
}
|
||||
|
||||
/** Returns invested and passive Agility, saturating safely at the integer limit. */
|
||||
public static int getTotalAgility(Player player, int investedAgility) {
|
||||
long total = (long) Math.max(0, investedAgility) + getPassiveAgility(player, investedAgility);
|
||||
return (int) Math.min(Integer.MAX_VALUE, total);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Agility normalized against the normal investment cap. Values may
|
||||
* exceed {@code 1.0} when integrations supply passive points above that cap.
|
||||
*/
|
||||
public static double getNormalizedAgility(Player player, int investedAgility) {
|
||||
return AttributeScaling.getExtendedNormalizedAttributeValue(getTotalAgility(player, investedAgility));
|
||||
}
|
||||
|
||||
/** Recalculates the player's movement-speed modifier after passive Agility changes. */
|
||||
public static void refresh(Player player) {
|
||||
player.getCapability(PlayerStatsProvider.PLAYER_STATS).ifPresent(stats ->
|
||||
StatEffects.applyAgility(player, stats.getAttributeLevel("agility")));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package net.fretux.ascend.api.event;
|
||||
|
||||
import net.minecraft.world.damagesource.DamageSource;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraftforge.eventbus.api.Event;
|
||||
|
||||
/**
|
||||
* Fired before Ascend applies armor penetration to a player attack.
|
||||
* Integrations may add penetration, change the cap, or disable the cap for
|
||||
* this hit. Values use fractions: {@code 0.10} ignores ten percent of armor.
|
||||
*/
|
||||
public final class ArmorPenetrationEvent extends Event {
|
||||
private final Player attacker;
|
||||
private final LivingEntity target;
|
||||
private final DamageSource damageSource;
|
||||
private final ItemStack weapon;
|
||||
private double armorPenetration;
|
||||
private double cap;
|
||||
private boolean capEnabled = true;
|
||||
|
||||
public ArmorPenetrationEvent(Player attacker, LivingEntity target, DamageSource damageSource,
|
||||
ItemStack weapon, double armorPenetration, double cap) {
|
||||
this.attacker = attacker;
|
||||
this.target = target;
|
||||
this.damageSource = damageSource;
|
||||
this.weapon = weapon.copy();
|
||||
this.armorPenetration = armorPenetration;
|
||||
this.cap = cap;
|
||||
}
|
||||
|
||||
public Player getAttacker() { return attacker; }
|
||||
public LivingEntity getTarget() { return target; }
|
||||
public DamageSource getDamageSource() { return damageSource; }
|
||||
public ItemStack getWeapon() { return weapon.copy(); }
|
||||
public double getArmorPenetration() { return armorPenetration; }
|
||||
public double getCap() { return cap; }
|
||||
public boolean isCapEnabled() { return capEnabled; }
|
||||
|
||||
public void setArmorPenetration(double armorPenetration) { this.armorPenetration = finiteOrZero(armorPenetration); }
|
||||
public void addArmorPenetration(double armorPenetration) {
|
||||
setArmorPenetration(this.armorPenetration + finiteOrZero(armorPenetration));
|
||||
}
|
||||
public void setCap(double cap) { this.cap = finiteOrZero(cap); }
|
||||
public void setCapEnabled(boolean capEnabled) { this.capEnabled = capEnabled; }
|
||||
public void disableCap() { this.capEnabled = false; }
|
||||
|
||||
/** Returns the fraction of armor ignored after applying the requested cap. */
|
||||
public double getEffectiveArmorPenetration() {
|
||||
return calculateEffectiveArmorPenetration(armorPenetration, cap, capEnabled);
|
||||
}
|
||||
|
||||
/** Pure helper for integrations that need to preview Ascend's cap behavior. */
|
||||
public static double calculateEffectiveArmorPenetration(double armorPenetration, double cap, boolean capEnabled) {
|
||||
double maximum = capEnabled ? clamp(cap) : 1.0;
|
||||
return Math.min(clamp(armorPenetration), maximum);
|
||||
}
|
||||
|
||||
private static double clamp(double value) { return Math.max(0.0, Math.min(1.0, finiteOrZero(value))); }
|
||||
private static double finiteOrZero(double value) { return Double.isFinite(value) ? value : 0.0; }
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package net.fretux.ascend.api.event;
|
||||
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraftforge.eventbus.api.Event;
|
||||
|
||||
/**
|
||||
* Fired whenever Ascend calculates a player's Agility effects.
|
||||
*
|
||||
* <p>Integrations may add passive points from skill trees, equipment, or other
|
||||
* progression systems. Contributions from multiple listeners are additive.
|
||||
* Passive points are applied after the normal investment cap, but still use
|
||||
* Ascend's configured diminishing-return curve.</p>
|
||||
*/
|
||||
public final class PassiveAgilityEvent extends Event {
|
||||
private final Player player;
|
||||
private final int investedAgility;
|
||||
private int passiveAgility;
|
||||
|
||||
public PassiveAgilityEvent(Player player, int investedAgility) {
|
||||
this.player = player;
|
||||
this.investedAgility = Math.max(0, investedAgility);
|
||||
}
|
||||
|
||||
public Player getPlayer() { return player; }
|
||||
public int getInvestedAgility() { return investedAgility; }
|
||||
public int getPassiveAgility() { return passiveAgility; }
|
||||
|
||||
public void setPassiveAgility(int passiveAgility) {
|
||||
this.passiveAgility = Math.max(0, passiveAgility);
|
||||
}
|
||||
|
||||
public void addPassiveAgility(int passiveAgility) {
|
||||
if (passiveAgility <= 0) return;
|
||||
long combined = (long) this.passiveAgility + passiveAgility;
|
||||
this.passiveAgility = (int) Math.min(Integer.MAX_VALUE, combined);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package net.fretux.ascend.client;
|
||||
|
||||
import net.fretux.ascend.client.screen.ShrineScreen;
|
||||
import net.fretux.ascend.player.PlayerStatsProvider;
|
||||
import net.fretux.ascend.player.StatEffects;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
|
||||
public final class ClientPacketHandlers {
|
||||
private ClientPacketHandlers() {
|
||||
}
|
||||
|
||||
public static void openShrineScreen(boolean essenceMode) {
|
||||
Minecraft.getInstance().setScreen(new ShrineScreen(essenceMode));
|
||||
}
|
||||
|
||||
public static void syncStats(CompoundTag data) {
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
var player = mc.player;
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
player.getCapability(PlayerStatsProvider.PLAYER_STATS).ifPresent(stats -> {
|
||||
stats.deserializeNBT(data);
|
||||
StatEffects.applyAll(player);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package net.fretux.ascend.client;
|
||||
|
||||
import net.fretux.ascend.AscendMod;
|
||||
import net.fretux.ascend.scaling.ScalingGrade;
|
||||
import net.fretux.ascend.scaling.WeaponScaling;
|
||||
import net.fretux.ascend.scaling.WeaponScalingRegistry;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.event.entity.player.ItemTooltipEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Mod.EventBusSubscriber(modid = AscendMod.MODID, value = Dist.CLIENT)
|
||||
public final class WeaponScalingTooltips {
|
||||
@SubscribeEvent
|
||||
public static void addScalingTooltip(ItemTooltipEvent event) {
|
||||
WeaponScaling scaling = WeaponScalingRegistry.get(event.getItemStack().getItem());
|
||||
if (scaling == null) return;
|
||||
List<Component> grades = new ArrayList<>();
|
||||
for (String attribute : WeaponScaling.SUPPORTED_ATTRIBUTES) {
|
||||
ScalingGrade grade = scaling.grade(attribute);
|
||||
if (grade != ScalingGrade.NONE) grades.add(Component.translatable("ascend.weapon_scaling.grade",
|
||||
Component.translatable("ascend.attribute." + attribute), grade.name()));
|
||||
}
|
||||
if (!grades.isEmpty()) {
|
||||
event.getToolTip().add(Component.translatable("ascend.weapon_scaling.title").withStyle(ChatFormatting.DARK_GRAY));
|
||||
grades.forEach(line -> event.getToolTip().add(line.copy().withStyle(ChatFormatting.GRAY)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,27 +15,37 @@ import java.util.Map;
|
||||
|
||||
public class StatsScreen extends Screen {
|
||||
private static final int WIDTH = 480;
|
||||
private static final int HEIGHT = 266;
|
||||
private static final int HEIGHT = 280;
|
||||
private static final int PADDING = 12;
|
||||
private static final int COLUMN_GAP = 8;
|
||||
private static final int CARD_GAP = 4;
|
||||
private static final int CARD_HEIGHT = 40;
|
||||
private static final int CARDS_TOP = 64;
|
||||
private static final int CARDS_TOP = 78;
|
||||
private static final int CARD_WIDTH = (WIDTH - PADDING * 2 - COLUMN_GAP) / 2;
|
||||
private static final int BACKGROUND = 0xFF121316;
|
||||
private static final int PANEL = 0xFF1C1E22;
|
||||
private static final int CARD = 0xFF25272D;
|
||||
private static final int BORDER = 0xFF45484F;
|
||||
private static final int PRIMARY_TEXT = 0xFFE4E4E4;
|
||||
private static final int SECONDARY_TEXT = 0xFF999DA5;
|
||||
private static final int ACCENT = 0xFFAAB6C2;
|
||||
private static final int PROGRESS = 0xFFD1A744;
|
||||
private static final int AVAILABLE = 0xFF6EAE67;
|
||||
|
||||
private static final String[] ATTRIBUTES = {
|
||||
"vigor", "mind", "strength", "agility",
|
||||
"intelligence", "willpower", "charisma", "arcane"
|
||||
};
|
||||
private static final Map<String, Integer> ATTRIBUTE_COLORS = Map.of(
|
||||
"vigor", 0xFFD85C5C,
|
||||
"mind", 0xFF6699E8,
|
||||
"strength", 0xFFD78445,
|
||||
"agility", 0xFF71C988,
|
||||
"intelligence", 0xFF9A7BE8,
|
||||
"willpower", 0xFF79B9C8,
|
||||
"charisma", 0xFFE28AB8,
|
||||
"arcane", 0xFFC878E0
|
||||
// Twelve-pixel glyphs: heart, orb, sword, boot, book, eye, speech, rune.
|
||||
private static final Map<String, int[]> ATTRIBUTE_ICONS = Map.of(
|
||||
"vigor", new int[]{0x000, 0x318, 0x7BC, 0xFFE, 0xFFE, 0xFFE, 0x7FC, 0x3F8, 0x1F0, 0x0E0, 0x040, 0x000},
|
||||
"mind", new int[]{0x000, 0x0F0, 0x30C, 0x402, 0x4C2, 0x482, 0x402, 0x402, 0x30C, 0x0F0, 0x000, 0x000},
|
||||
"strength", new int[]{0x060, 0x0F0, 0x0F0, 0x0F0, 0x0F0, 0x0F0, 0x264, 0x3FC, 0x060, 0x060, 0x0F0, 0x000},
|
||||
"agility", new int[]{0x000, 0x3C0, 0x240, 0x240, 0x3C0, 0x240, 0x27C, 0x202, 0x202, 0x3FE, 0x000, 0x000},
|
||||
"intelligence", new int[]{0x000, 0x7BC, 0xC46, 0x842, 0xB5A, 0x842, 0xB5A, 0x842, 0xC46, 0x7BC, 0x040, 0x000},
|
||||
"willpower", new int[]{0x000, 0x000, 0x0F0, 0x30C, 0x492, 0x891, 0x891, 0x492, 0x30C, 0x0F0, 0x000, 0x000},
|
||||
"charisma", new int[]{0x000, 0x7FE, 0x402, 0x402, 0x552, 0x402, 0x402, 0x7FE, 0x180, 0x100, 0x000, 0x000},
|
||||
"arcane", new int[]{0x040, 0x0E0, 0x150, 0x248, 0x444, 0x248, 0x150, 0x0E0, 0x150, 0x248, 0x040, 0x000}
|
||||
);
|
||||
private static final Map<String, Component> ATTRIBUTE_TOOLTIPS = new HashMap<>();
|
||||
|
||||
@@ -124,14 +134,14 @@ public class StatsScreen extends Screen {
|
||||
|
||||
private void drawWindow(GuiGraphics graphics) {
|
||||
RenderSystem.enableBlend();
|
||||
graphics.fill(leftPos - 6, topPos - 6, leftPos + WIDTH + 6, topPos + HEIGHT + 6, 0xCC07070C);
|
||||
graphics.fill(leftPos - 3, topPos - 3, leftPos + WIDTH + 3, topPos + HEIGHT + 3, 0xFF30253C);
|
||||
graphics.fill(leftPos, topPos, leftPos + WIDTH, topPos + HEIGHT, 0xF016131E);
|
||||
graphics.fill(leftPos, topPos, leftPos + WIDTH, topPos + 20, 0xFF2A2036);
|
||||
graphics.fill(leftPos, topPos + 20, leftPos + WIDTH, topPos + 21, 0xFF655275);
|
||||
graphics.fill(leftPos - 6, topPos - 6, leftPos + WIDTH + 6, topPos + HEIGHT + 6, 0xCC090A0C);
|
||||
graphics.fill(leftPos - 3, topPos - 3, leftPos + WIDTH + 3, topPos + HEIGHT + 3, BORDER);
|
||||
graphics.fill(leftPos, topPos, leftPos + WIDTH, topPos + HEIGHT, BACKGROUND);
|
||||
graphics.fill(leftPos, topPos, leftPos + WIDTH, topPos + 20, PANEL);
|
||||
graphics.fill(leftPos, topPos + 20, leftPos + WIDTH, topPos + 21, BORDER);
|
||||
RenderSystem.disableBlend();
|
||||
|
||||
graphics.drawCenteredString(font, title, leftPos + WIDTH / 2, topPos + 6, 0xFFF4EAFB);
|
||||
graphics.drawCenteredString(font, title, leftPos + WIDTH / 2, topPos + 6, PRIMARY_TEXT);
|
||||
}
|
||||
|
||||
private void drawProgression(GuiGraphics graphics, int level, int maxLevel, int xp, int xpNext,
|
||||
@@ -139,45 +149,54 @@ public class StatsScreen extends Screen {
|
||||
int panelX = leftPos + PADDING;
|
||||
int panelY = topPos + 26;
|
||||
int panelWidth = WIDTH - PADDING * 2;
|
||||
graphics.fill(panelX, panelY, panelX + panelWidth, panelY + 31, 0xAA211C29);
|
||||
graphics.fill(panelX, panelY, panelX + 3, panelY + 31, 0xFFCFB060);
|
||||
graphics.fill(panelX, panelY, panelX + panelWidth, panelY + 45, PANEL);
|
||||
|
||||
graphics.drawString(font, Component.translatable("ascend.stats.level", level, maxLevel),
|
||||
panelX + 9, panelY + 5, 0xFFFFDF78, false);
|
||||
graphics.drawString(font, Component.translatable("ascend.stats.knowledge", knowledge),
|
||||
panelX + 112, panelY + 5, 0xFFA9C7FF, false);
|
||||
panelX + 9, panelY + 5, PROGRESS, false);
|
||||
graphics.drawCenteredString(font, Component.translatable("ascend.stats.knowledge", knowledge),
|
||||
panelX + panelWidth / 2, panelY + 5, SECONDARY_TEXT);
|
||||
|
||||
Component points = Component.translatable("ascend.stats.unspent", unspent);
|
||||
int pointsWidth = font.width(points);
|
||||
int pointsX = panelX + panelWidth - pointsWidth - 9;
|
||||
graphics.fill(pointsX - 5, panelY + 2, panelX + panelWidth - 4, panelY + 16, 0x663D623D);
|
||||
graphics.drawString(font, points, pointsX, panelY + 5, unspent > 0 ? 0xFF87F58D : 0xFF9D9D9D, false);
|
||||
graphics.drawString(font, points, pointsX, panelY + 5, unspent > 0 ? AVAILABLE : SECONDARY_TEXT, false);
|
||||
|
||||
int barX = panelX + 9;
|
||||
int barY = panelY + 20;
|
||||
int barWidth = panelWidth - 18;
|
||||
graphics.fill(barX, barY, barX + barWidth, barY + 5, 0xFF0C0A10);
|
||||
double progress = level >= maxLevel || xpNext <= 0 ? 1.0 : Math.min(1.0, (double) xp / xpNext);
|
||||
graphics.fill(barX + 1, barY + 1, barX + 1 + (int) ((barWidth - 2) * progress), barY + 4, 0xFF8E68C3);
|
||||
graphics.fill(barX, barY, barX + barWidth, barY + 7, BORDER);
|
||||
graphics.fill(barX + 1, barY + 1, barX + barWidth - 1, barY + 6, BACKGROUND);
|
||||
double progress = level >= maxLevel || xpNext <= 0 ? 1.0 : Math.max(0.0, Math.min(1.0, (double) xp / xpNext));
|
||||
graphics.fill(barX + 1, barY + 1, barX + 1 + (int) ((barWidth - 2) * progress), barY + 6, PROGRESS);
|
||||
Component xpLabel = level >= maxLevel ? Component.translatable("ascend.stats.xp_max")
|
||||
: Component.translatable("ascend.stats.xp", xp, xpNext);
|
||||
graphics.drawCenteredString(font, xpLabel, barX + barWidth / 2, barY - 2, 0xFFC9C1D0);
|
||||
graphics.drawCenteredString(font, xpLabel, barX + barWidth / 2, barY + 11, PROGRESS);
|
||||
}
|
||||
|
||||
private void drawAttributeCard(GuiGraphics graphics, String attribute, int value, int maximum,
|
||||
int x, int y, boolean hovered) {
|
||||
int accent = ATTRIBUTE_COLORS.getOrDefault(attribute, 0xFFFFFFFF);
|
||||
graphics.fill(x, y, x + CARD_WIDTH, y + CARD_HEIGHT, hovered ? 0xE0332B3D : 0xC925202F);
|
||||
graphics.fill(x, y, x + 3, y + CARD_HEIGHT, accent);
|
||||
graphics.fill(x + 3, y, x + CARD_WIDTH, y + 1, hovered ? 0x667F6B8C : 0x334C4057);
|
||||
graphics.fill(x, y, x + CARD_WIDTH, y + CARD_HEIGHT, hovered ? ACCENT : BORDER);
|
||||
graphics.fill(x + 1, y + 1, x + CARD_WIDTH - 1, y + CARD_HEIGHT - 1, CARD);
|
||||
drawAttributeIcon(graphics, attribute, x + 8, y + 5);
|
||||
|
||||
graphics.drawString(font, Component.translatable("ascend.attribute." + attribute),
|
||||
x + 9, y + 7, 0xFFF1ECF4, false);
|
||||
x + 25, y + 7, PRIMARY_TEXT, false);
|
||||
Component valueLabel = Component.literal(value + " / " + maximum);
|
||||
int valueX = x + CARD_WIDTH - 38 - font.width(valueLabel);
|
||||
graphics.drawString(font, valueLabel, valueX, y + 7, value >= maximum ? 0xFFFFD66B : 0xFFC9C1D0, false);
|
||||
graphics.drawString(font, valueLabel, valueX, y + 7, value >= maximum ? PROGRESS : PRIMARY_TEXT, false);
|
||||
graphics.drawString(font, Component.translatable("ascend.attribute." + attribute + ".short"),
|
||||
x + 9, y + 26, 0xFF9F98A8, false);
|
||||
x + 9, y + 26, SECONDARY_TEXT, false);
|
||||
}
|
||||
|
||||
private void drawAttributeIcon(GuiGraphics graphics, String attribute, int x, int y) {
|
||||
int[] rows = ATTRIBUTE_ICONS.get(attribute);
|
||||
for (int row = 0; row < rows.length; row++) {
|
||||
for (int column = 0; column < 12; column++) {
|
||||
if ((rows[row] & (1 << (11 - column))) != 0) {
|
||||
graphics.fill(x + column, y + row, x + column + 1, y + row + 1, ACCENT);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package net.fretux.ascend.event;
|
||||
|
||||
import net.fretux.ascend.AscendMod;
|
||||
import net.fretux.ascend.player.PlayerStatsProvider;
|
||||
import net.fretux.ascend.player.StatEffects;
|
||||
import net.minecraft.world.effect.MobEffectCategory;
|
||||
import net.minecraft.world.effect.MobEffectInstance;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.entity.projectile.Projectile;
|
||||
import net.minecraftforge.event.entity.living.MobEffectEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
|
||||
@Mod.EventBusSubscriber(modid = AscendMod.MODID)
|
||||
public final class ArcaneEffectEvents {
|
||||
private static final ThreadLocal<Boolean> MODIFYING = ThreadLocal.withInitial(() -> false);
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onEffectAdded(MobEffectEvent.Added event) {
|
||||
if (MODIFYING.get() || event.getEntity().level().isClientSide || event.getOldEffectInstance() != null) return;
|
||||
MobEffectInstance added = event.getEffectInstance();
|
||||
if (added.isInfiniteDuration() || added.getDuration() <= 0) return;
|
||||
|
||||
Player arcaneOwner;
|
||||
boolean beneficialSelf = event.getEntity() instanceof Player player
|
||||
&& added.getEffect().getCategory() == MobEffectCategory.BENEFICIAL;
|
||||
if (beneficialSelf) arcaneOwner = (Player) event.getEntity();
|
||||
else if (added.getEffect().getCategory() == MobEffectCategory.HARMFUL) {
|
||||
if (event.getEffectSource() instanceof Player source && source != event.getEntity()) arcaneOwner = source;
|
||||
else if (event.getEffectSource() instanceof Projectile projectile
|
||||
&& projectile.getOwner() instanceof Player source && source != event.getEntity()) arcaneOwner = source;
|
||||
else return;
|
||||
} else return;
|
||||
|
||||
arcaneOwner.getCapability(PlayerStatsProvider.PLAYER_STATS).ifPresent(stats -> {
|
||||
int arcane = stats.getAttributeLevel("arcane");
|
||||
if (arcane <= 0) return;
|
||||
double multiplier = beneficialSelf ? StatEffects.getArcaneBeneficialDurationMultiplier(arcane)
|
||||
: StatEffects.getArcaneHarmfulDurationMultiplier(arcane);
|
||||
MobEffectInstance active = event.getEntity().getEffect(added.getEffect());
|
||||
if (active == null || active.isInfiniteDuration()) return;
|
||||
int oldDuration = active.getDuration();
|
||||
int newDuration = (int) Math.min(Integer.MAX_VALUE, Math.round(oldDuration * multiplier));
|
||||
if (newDuration <= oldDuration) return;
|
||||
MODIFYING.set(true);
|
||||
try { active.mapDuration(ignored -> newDuration); }
|
||||
finally { MODIFYING.set(false); }
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package net.fretux.ascend.event;
|
||||
|
||||
import com.google.common.collect.Multimap;
|
||||
import net.fretux.ascend.AscendMod;
|
||||
import net.fretux.ascend.api.event.ArmorPenetrationEvent;
|
||||
import net.fretux.ascend.config.AscendConfig;
|
||||
import net.fretux.ascend.player.PlayerStatsProvider;
|
||||
import net.fretux.ascend.player.StatEffects;
|
||||
import net.fretux.ascend.scaling.ScalingGrade;
|
||||
import net.fretux.ascend.scaling.WeaponScaling;
|
||||
import net.fretux.ascend.scaling.WeaponScalingRegistry;
|
||||
import net.minecraft.world.entity.EquipmentSlot;
|
||||
import net.minecraft.world.entity.ai.attributes.AttributeModifier;
|
||||
import net.minecraft.world.entity.ai.attributes.Attributes;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.entity.projectile.AbstractArrow;
|
||||
import net.minecraft.world.entity.projectile.FireworkRocketEntity;
|
||||
import net.minecraft.world.entity.projectile.ThrownTrident;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.tags.DamageTypeTags;
|
||||
import net.minecraft.world.damagesource.CombatRules;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraftforge.common.MinecraftForge;
|
||||
import net.minecraftforge.event.entity.living.LivingHurtEvent;
|
||||
import net.minecraftforge.event.entity.EntityJoinLevelEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.eventbus.api.EventPriority;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
import net.minecraftforge.registries.ForgeRegistries;
|
||||
|
||||
@Mod.EventBusSubscriber(modid = AscendMod.MODID)
|
||||
public final class WeaponScalingEvents {
|
||||
private static final String PROJECTILE_WEAPON_TAG = "ascend:scaling_weapon";
|
||||
|
||||
@SubscribeEvent
|
||||
public static void rememberProjectileWeapon(EntityJoinLevelEvent event) {
|
||||
if (event.getLevel().isClientSide || !(event.getEntity() instanceof AbstractArrow arrow)
|
||||
|| arrow instanceof ThrownTrident || !(arrow.getOwner() instanceof Player player)) return;
|
||||
ItemStack stack = player.getMainHandItem();
|
||||
if (!stack.is(Items.BOW) && !stack.is(Items.CROSSBOW)) return;
|
||||
ResourceLocation id = ForgeRegistries.ITEMS.getKey(stack.getItem());
|
||||
if (id != null) arrow.getPersistentData().putString(PROJECTILE_WEAPON_TAG, id.toString());
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void addWeaponScaling(LivingHurtEvent event) {
|
||||
if (!(event.getSource().getEntity() instanceof Player player) || player.level().isClientSide || event.getEntity() == player) return;
|
||||
Item item = resolveWeapon(player, event.getSource().getDirectEntity());
|
||||
WeaponScaling scaling = item == null ? null : WeaponScalingRegistry.get(item);
|
||||
if (scaling == null) return;
|
||||
player.getCapability(PlayerStatsProvider.PLAYER_STATS).ifPresent(stats -> {
|
||||
double baseDamage = event.getSource().getDirectEntity() instanceof AbstractArrow
|
||||
? event.getAmount() : getPhysicalBaseDamage(new ItemStack(item), event.getAmount());
|
||||
double bonus = WeaponScalingRegistry.calculateWeaponScalingBonus(baseDamage, scaling, player, stats);
|
||||
if (bonus > 0) event.setAmount((float) (event.getAmount() + bonus));
|
||||
});
|
||||
}
|
||||
|
||||
@SubscribeEvent(priority = EventPriority.LOWEST)
|
||||
public static void applyArmorPenetration(LivingHurtEvent event) {
|
||||
if (!(event.getSource().getEntity() instanceof Player player) || player.level().isClientSide || event.getEntity() == player) return;
|
||||
if (event.getSource().is(DamageTypeTags.BYPASSES_ARMOR) || event.getAmount() <= 0 || event.getEntity().getArmorValue() <= 0) return;
|
||||
Item item = resolveWeapon(player, event.getSource().getDirectEntity());
|
||||
WeaponScaling scaling = item == null ? null : WeaponScalingRegistry.get(item);
|
||||
ItemStack weapon = item == null ? ItemStack.EMPTY : new ItemStack(item);
|
||||
player.getCapability(PlayerStatsProvider.PLAYER_STATS).ifPresent(stats -> {
|
||||
double basePenetration = scaling != null && scaling.grade("strength") != ScalingGrade.NONE
|
||||
? StatEffects.getStrengthArmorPenetration(stats.getAttributeLevel("strength")) : 0.0;
|
||||
ArmorPenetrationEvent penetrationEvent = new ArmorPenetrationEvent(player, event.getEntity(),
|
||||
event.getSource(), weapon, basePenetration, configuredArmorPenetrationCap());
|
||||
MinecraftForge.EVENT_BUS.post(penetrationEvent);
|
||||
double penetration = penetrationEvent.getEffectiveArmorPenetration();
|
||||
if (penetration <= 0) return;
|
||||
event.setAmount(getPreArmorDamageForReducedArmor(event.getEntity(), event.getAmount(), penetration));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the pre-armor damage that produces the same result through vanilla's
|
||||
* normal armor pipeline as applying the original hit against reduced armor.
|
||||
*/
|
||||
static float getPreArmorDamageForReducedArmor(LivingEntity target, float incomingDamage, double penetration) {
|
||||
float armor = Math.max(0.0F, target.getArmorValue());
|
||||
float toughness = (float) Math.max(0.0, target.getAttributeValue(Attributes.ARMOR_TOUGHNESS));
|
||||
return getPreArmorDamageForReducedArmor(incomingDamage, armor, toughness, penetration);
|
||||
}
|
||||
|
||||
static float getPreArmorDamageForReducedArmor(float incomingDamage, float armor, float toughness, double penetration) {
|
||||
float reducedArmor = armor * (1.0F - (float) Math.max(0.0, Math.min(1.0, penetration)));
|
||||
float desiredPostArmor = CombatRules.getDamageAfterAbsorb(incomingDamage, reducedArmor, toughness);
|
||||
if (desiredPostArmor <= CombatRules.getDamageAfterAbsorb(incomingDamage, armor, toughness)) return incomingDamage;
|
||||
|
||||
float low = incomingDamage;
|
||||
float high = Math.max(incomingDamage + 1.0F, incomingDamage * 2.0F);
|
||||
while (CombatRules.getDamageAfterAbsorb(high, armor, toughness) < desiredPostArmor && high < Float.MAX_VALUE / 4.0F) {
|
||||
high *= 2.0F;
|
||||
}
|
||||
for (int i = 0; i < 32; i++) {
|
||||
float middle = low + (high - low) * 0.5F;
|
||||
if (CombatRules.getDamageAfterAbsorb(middle, armor, toughness) < desiredPostArmor) low = middle;
|
||||
else high = middle;
|
||||
}
|
||||
return high;
|
||||
}
|
||||
|
||||
private static double configuredArmorPenetrationCap() {
|
||||
return AscendConfig.COMMON_SPEC.isLoaded()
|
||||
? AscendConfig.COMMON.armorPenetrationCap.get()
|
||||
: AscendConfig.COMMON.armorPenetrationCap.getDefault();
|
||||
}
|
||||
|
||||
private static Item resolveWeapon(Player player, net.minecraft.world.entity.Entity direct) {
|
||||
if (direct instanceof FireworkRocketEntity) return null;
|
||||
if (direct instanceof ThrownTrident) return Items.TRIDENT;
|
||||
ItemStack stack = player.getMainHandItem();
|
||||
if (direct instanceof AbstractArrow) {
|
||||
String storedId = direct.getPersistentData().getString(PROJECTILE_WEAPON_TAG);
|
||||
ResourceLocation id = ResourceLocation.tryParse(storedId);
|
||||
if (id != null && ForgeRegistries.ITEMS.containsKey(id)) return ForgeRegistries.ITEMS.getValue(id);
|
||||
return stack.is(Items.BOW) || stack.is(Items.CROSSBOW) ? stack.getItem() : null;
|
||||
}
|
||||
return direct == player && !stack.isEmpty() ? stack.getItem() : null;
|
||||
}
|
||||
|
||||
private static double getPhysicalBaseDamage(ItemStack stack, float eventDamage) {
|
||||
Multimap<net.minecraft.world.entity.ai.attributes.Attribute, AttributeModifier> modifiers =
|
||||
stack.getAttributeModifiers(EquipmentSlot.MAINHAND);
|
||||
double itemDamage = modifiers.get(Attributes.ATTACK_DAMAGE).stream()
|
||||
.filter(modifier -> modifier.getOperation() == AttributeModifier.Operation.ADDITION)
|
||||
.mapToDouble(AttributeModifier::getAmount).sum();
|
||||
return Math.max(0, Math.min(eventDamage, 1.0 + itemDamage));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package net.fretux.ascend.player;
|
||||
|
||||
import net.fretux.ascend.config.AscendConfig;
|
||||
|
||||
/** Shared piecewise soft-cap curve used by attributes, weapons, and integrations. */
|
||||
public final class AttributeScaling {
|
||||
private AttributeScaling() {}
|
||||
|
||||
public static double getEffectiveAttributeValue(int investment) {
|
||||
int value = Math.max(0, Math.min(investment, intValue(AscendConfig.COMMON.maxAttributePoints)));
|
||||
return calculateEffectiveAttributeValue(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the configured curve without enforcing the investable attribute cap.
|
||||
* Intended for combined values that include passive points supplied by integrations.
|
||||
*/
|
||||
public static double getExtendedEffectiveAttributeValue(int investment) {
|
||||
return calculateEffectiveAttributeValue(Math.max(0, investment));
|
||||
}
|
||||
|
||||
private static double calculateEffectiveAttributeValue(int value) {
|
||||
int c1 = intValue(AscendConfig.COMMON.softCap1);
|
||||
int c2 = Math.max(c1, intValue(AscendConfig.COMMON.softCap2));
|
||||
int c3 = Math.max(c2, intValue(AscendConfig.COMMON.softCap3));
|
||||
int c4 = Math.max(c3, intValue(AscendConfig.COMMON.softCap4));
|
||||
double effective = segment(value, 0, c1, 1.0);
|
||||
effective += segment(value, c1, c2, doubleValue(AscendConfig.COMMON.softCapGain2));
|
||||
effective += segment(value, c2, c3, doubleValue(AscendConfig.COMMON.softCapGain3));
|
||||
effective += segment(value, c3, c4, doubleValue(AscendConfig.COMMON.softCapGain4));
|
||||
effective += Math.max(0, value - c4) * doubleValue(AscendConfig.COMMON.softCapGain5);
|
||||
return effective * doubleValue(AscendConfig.COMMON.attributeScalingMultiplier);
|
||||
}
|
||||
|
||||
public static double getNormalizedAttributeValue(int investment) {
|
||||
double max = getEffectiveAttributeValue(intValue(AscendConfig.COMMON.maxAttributePoints));
|
||||
return max <= 0 ? 0 : Math.min(1.0, getEffectiveAttributeValue(investment) / max);
|
||||
}
|
||||
|
||||
/** Returns a value normalized at the investable cap while permitting passive overflow above {@code 1.0}. */
|
||||
public static double getExtendedNormalizedAttributeValue(int investment) {
|
||||
double max = getEffectiveAttributeValue(intValue(AscendConfig.COMMON.maxAttributePoints));
|
||||
return max <= 0 ? 0 : getExtendedEffectiveAttributeValue(investment) / max;
|
||||
}
|
||||
|
||||
private static double segment(int value, int start, int end, double rate) {
|
||||
return Math.max(0, Math.min(value, end) - start) * rate;
|
||||
}
|
||||
|
||||
private static int intValue(net.minecraftforge.common.ForgeConfigSpec.IntValue value) {
|
||||
return AscendConfig.COMMON_SPEC.isLoaded() ? value.get() : value.getDefault();
|
||||
}
|
||||
private static double doubleValue(net.minecraftforge.common.ForgeConfigSpec.DoubleValue value) {
|
||||
return AscendConfig.COMMON_SPEC.isLoaded() ? value.get() : value.getDefault();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package net.fretux.ascend.scaling;
|
||||
|
||||
import net.fretux.ascend.config.AscendConfig;
|
||||
|
||||
public enum ScalingGrade {
|
||||
S, A, B, C, D, E, NONE;
|
||||
|
||||
public double coefficient() {
|
||||
return switch (this) {
|
||||
case S -> AscendConfig.COMMON.gradeS.get();
|
||||
case A -> AscendConfig.COMMON.gradeA.get();
|
||||
case B -> AscendConfig.COMMON.gradeB.get();
|
||||
case C -> AscendConfig.COMMON.gradeC.get();
|
||||
case D -> AscendConfig.COMMON.gradeD.get();
|
||||
case E -> AscendConfig.COMMON.gradeE.get();
|
||||
case NONE -> 0.0;
|
||||
};
|
||||
}
|
||||
|
||||
public static ScalingGrade parse(String value) {
|
||||
try { return valueOf(value.toUpperCase(java.util.Locale.ROOT)); }
|
||||
catch (RuntimeException ignored) { return NONE; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package net.fretux.ascend.scaling;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public record WeaponScaling(Map<String, ScalingGrade> attributes) {
|
||||
public static final String[] SUPPORTED_ATTRIBUTES = {"strength", "agility", "intelligence", "willpower", "charisma", "arcane"};
|
||||
|
||||
public WeaponScaling {
|
||||
attributes = Collections.unmodifiableMap(new LinkedHashMap<>(attributes));
|
||||
}
|
||||
|
||||
public ScalingGrade grade(String attribute) { return attributes.getOrDefault(attribute, ScalingGrade.NONE); }
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package net.fretux.ascend.scaling;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import net.fretux.ascend.AscendMod;
|
||||
import net.fretux.ascend.api.PassiveAgilityApi;
|
||||
import net.fretux.ascend.player.AttributeScaling;
|
||||
import net.fretux.ascend.player.PlayerStats;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.packs.resources.ResourceManager;
|
||||
import net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener;
|
||||
import net.minecraft.util.profiling.ProfilerFiller;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraftforge.event.AddReloadListenerEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
import net.minecraftforge.registries.ForgeRegistries;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Mod.EventBusSubscriber(modid = AscendMod.MODID)
|
||||
public final class WeaponScalingRegistry extends SimpleJsonResourceReloadListener {
|
||||
private static final Gson GSON = new Gson();
|
||||
private static volatile Map<Item, WeaponScaling> entries = createVanillaDefaults();
|
||||
|
||||
public WeaponScalingRegistry() { super(GSON, "weapon_scaling"); }
|
||||
|
||||
@SubscribeEvent
|
||||
public static void addReloadListener(AddReloadListenerEvent event) { event.addListener(new WeaponScalingRegistry()); }
|
||||
|
||||
public static WeaponScaling get(Item item) { return entries.get(item); }
|
||||
|
||||
/** Public API for integrations; datapack reloads remain authoritative and may replace this entry. */
|
||||
public static synchronized void register(Item item, WeaponScaling scaling) {
|
||||
Map<Item, WeaponScaling> copy = new LinkedHashMap<>(entries);
|
||||
copy.put(item, scaling);
|
||||
entries = Map.copyOf(copy);
|
||||
}
|
||||
|
||||
public static double getScalingCoefficient(ScalingGrade grade) { return grade.coefficient(); }
|
||||
|
||||
public static double calculateWeaponScalingBonus(double baseDamage, WeaponScaling scaling, PlayerStats stats) {
|
||||
return calculateWeaponScalingBonus(baseDamage, scaling, null, stats);
|
||||
}
|
||||
|
||||
/** Calculates weapon scaling, including passive Agility supplied through Ascend's public API. */
|
||||
public static double calculateWeaponScalingBonus(double baseDamage, WeaponScaling scaling, Player player, PlayerStats stats) {
|
||||
if (baseDamage <= 0 || scaling == null) return 0;
|
||||
double coefficient = 0;
|
||||
for (String attribute : WeaponScaling.SUPPORTED_ATTRIBUTES) {
|
||||
ScalingGrade grade = scaling.grade(attribute);
|
||||
double normalized = "agility".equals(attribute) && player != null
|
||||
? PassiveAgilityApi.getNormalizedAgility(player, stats.getAttributeLevel(attribute))
|
||||
: AttributeScaling.getNormalizedAttributeValue(stats.getAttributeLevel(attribute));
|
||||
coefficient += getScalingCoefficient(grade) * normalized;
|
||||
}
|
||||
return baseDamage * coefficient;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void apply(Map<ResourceLocation, JsonElement> resources, ResourceManager manager, ProfilerFiller profiler) {
|
||||
Map<Item, WeaponScaling> loaded = createVanillaDefaults();
|
||||
resources.forEach((file, json) -> {
|
||||
try {
|
||||
JsonObject root = json.getAsJsonObject();
|
||||
if (root.has("entries")) {
|
||||
root.getAsJsonArray("entries").forEach(element -> loadEntry(loaded, element.getAsJsonObject()));
|
||||
} else loadEntry(loaded, root);
|
||||
} catch (RuntimeException ex) {
|
||||
AscendMod.LOGGER.warn("Ignoring invalid weapon scaling data {}: {}", file, ex.getMessage());
|
||||
}
|
||||
});
|
||||
entries = Map.copyOf(loaded);
|
||||
AscendMod.LOGGER.info("Loaded {} Ascend weapon scaling entries", entries.size());
|
||||
}
|
||||
|
||||
private static void loadEntry(Map<Item, WeaponScaling> target, JsonObject json) {
|
||||
ResourceLocation id = ResourceLocation.tryParse(json.get("item").getAsString());
|
||||
if (id == null || !ForgeRegistries.ITEMS.containsKey(id)) return;
|
||||
Map<String, ScalingGrade> grades = new LinkedHashMap<>();
|
||||
JsonObject scaling = json.getAsJsonObject("scaling");
|
||||
for (String key : WeaponScaling.SUPPORTED_ATTRIBUTES) {
|
||||
if (scaling.has(key)) grades.put(key, ScalingGrade.parse(scaling.get(key).getAsString()));
|
||||
}
|
||||
target.put(ForgeRegistries.ITEMS.getValue(id), new WeaponScaling(grades));
|
||||
}
|
||||
|
||||
private static Map<Item, WeaponScaling> createVanillaDefaults() {
|
||||
Map<Item, WeaponScaling> result = new LinkedHashMap<>();
|
||||
String[][] defaults = {
|
||||
{"wooden_sword","D","D"},{"stone_sword","D","C"},{"iron_sword","C","C"},{"golden_sword","E","B"},{"diamond_sword","C","B"},{"netherite_sword","B","B"},
|
||||
{"wooden_axe","C","E"},{"stone_axe","B","E"},{"iron_axe","B","D"},{"golden_axe","D","C"},{"diamond_axe","A","D"},{"netherite_axe","A","C"},
|
||||
{"wooden_pickaxe","D","E"},{"stone_pickaxe","C","E"},{"iron_pickaxe","C","E"},{"golden_pickaxe","E","D"},{"diamond_pickaxe","B","E"},{"netherite_pickaxe","B","E"},
|
||||
{"wooden_shovel","D","E"},{"stone_shovel","D","E"},{"iron_shovel","C","E"},{"golden_shovel","E","D"},{"diamond_shovel","C","E"},{"netherite_shovel","C","E"},
|
||||
{"wooden_hoe","E","D"},{"stone_hoe","E","C"},{"iron_hoe","E","B"},{"golden_hoe","E","B"},{"diamond_hoe","D","B"},{"netherite_hoe","D","A"},
|
||||
{"bow","NONE","A"},{"crossbow","D","B"},{"trident","C","B"},{"mace","A","D"}
|
||||
};
|
||||
for (String[] row : defaults) {
|
||||
ResourceLocation id = ResourceLocation.fromNamespaceAndPath("minecraft", row[0]);
|
||||
if (!ForgeRegistries.ITEMS.containsKey(id)) continue;
|
||||
Map<String, ScalingGrade> grades = new LinkedHashMap<>();
|
||||
grades.put("strength", ScalingGrade.parse(row[1]));
|
||||
grades.put("agility", ScalingGrade.parse(row[2]));
|
||||
result.put(ForgeRegistries.ITEMS.getValue(id), new WeaponScaling(grades));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user