diff --git a/build.gradle b/build.gradle index 5fa3a32..622081e 100644 --- a/build.gradle +++ b/build.gradle @@ -58,22 +58,16 @@ minecraft { // Default run configurations. // These can be tweaked, removed, or duplicated as needed. runs { - // applies to all the run configs below configureEach { workingDirectory project.file('run') - // Recommended logging data for a userdev environment - // The markers can be added/remove as needed separated by commas. - // "SCAN": For mods scan. - // "REGISTRIES": For firing of registry events. - // "REGISTRYDUMP": For getting the contents of all registries. property 'forge.logging.markers', 'REGISTRIES' - - // Recommended logging level for the console - // You can set various levels here. - // Please read: https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels property 'forge.logging.console.level', 'debug' + // IMPORTANT: enable refmap remapping for mixins (Geckolib, Player Animator, etc.) + property 'mixin.env.remapRefMap', 'true' + property 'mixin.env.refMapRemappingFile', "${projectDir}/build/createSrgToMcp/output.srg" + mods { "${mod_id}" { source sourceSets.main @@ -82,7 +76,6 @@ minecraft { } client { - // Comma-separated list of namespaces to load gametests from. Empty = all namespaces. property 'forge.enabledGameTestNamespaces', mod_id } @@ -91,18 +84,12 @@ minecraft { args '--nogui' } - // This run config launches GameTestServer and runs all registered gametests, then exits. - // By default, the server will crash when no gametests are provided. - // The gametest system is also enabled by default for other run configs under the /test command. gameTestServer { property 'forge.enabledGameTestNamespaces', mod_id } data { - // example of overriding the workingDirectory set in configureEach above workingDirectory project.file('run-data') - - // Specify the modid for data generation, where to output the resulting resource, and where to look for existing resources. args '--mod', mod_id, '--all', '--output', file('src/generated/resources/'), '--existing', file('src/main/resources/') } } @@ -120,30 +107,49 @@ repositories { // flatDir { // dir 'libs' // } + maven { + name = "Iron's Maven - Release" + url = "https://code.redspace.io/releases" + content { + includeGroup "io.redspace.ironsspellbooks" + } + } + + maven { + name = "Iron's Maven - Snapshots" + url = "https://code.redspace.io/snapshots" + content { + includeGroup "io.redspace.ironsspellbooks" + } + } + maven { url = "https://maven.enginehub.org/repo/" } + maven { url = "https://dl.cloudsmith.io/public/geckolib3/geckolib/maven/" } + maven { url = "https://maven.theillusivec4.top" } + maven { url = "https://cursemaven.com" } + maven { url = "https://maven.blamejared.com" } + maven { url = "https://maven.kosmx.dev/" } } dependencies { - // Specify the version of Minecraft to use. - // Any artifact can be supplied so long as it has a "userdev" classifier artifact and is a compatible patcher artifact. - // The "userdev" classifier will be requested and setup by ForgeGradle. - // If the group id is "net.minecraft" and the artifact id is one of ["client", "server", "joined"], - // then special handling is done to allow a setup of a vanilla dependency without the use of an external repository. minecraft "net.minecraftforge:forge:${minecraft_version}-${forge_version}" - // Example mod dependency with JEI - using fg.deobf() ensures the dependency is remapped to your development mappings - // The JEI API is declared for compile time use, while the full JEI artifact is used at runtime - // compileOnly fg.deobf("mezz.jei:jei-${mc_version}-common-api:${jei_version}") - // compileOnly fg.deobf("mezz.jei:jei-${mc_version}-forge-api:${jei_version}") - // runtimeOnly fg.deobf("mezz.jei:jei-${mc_version}-forge:${jei_version}") + // Iron's Spellbooks API only + compileOnly fg.deobf("io.redspace.ironsspellbooks:irons_spellbooks:${irons_spells_version}:api") + implementation fg.deobf("io.redspace.ironsspellbooks:irons_spellbooks:${irons_spells_version}") + + // GECKOLIB — deobfuscated, full mod + implementation fg.deobf("software.bernie.geckolib:geckolib-forge-${geckolib_version}") - // Example mod dependency using a mod jar from ./libs with a flat dir repository - // This maps to ./libs/coolmod-${mc_version}-${coolmod_version}.jar - // The group id is ignored when searching -- in this case, it is "blank" - // implementation fg.deobf("blank:coolmod-${mc_version}:${coolmod_version}") + // PLAYER ANIMATOR — deobf + implementation fg.deobf("dev.kosmx.player-anim:player-animation-lib-forge:${player_animator_version}") - // For more info: - // http://www.gradle.org/docs/current/userguide/artifact_dependencies_tutorial.html - // http://www.gradle.org/docs/current/userguide/dependency_management.html + // CAELUS — API only + compileOnly fg.deobf("top.theillusivec4.caelus:caelus-forge:${caelus_version}:api") + implementation fg.deobf("top.theillusivec4.caelus:caelus-forge:${caelus_version}") + + // CURIOS — API only + compileOnly fg.deobf("top.theillusivec4.curios:curios-forge:${curios_version}:api") + implementation fg.deobf("top.theillusivec4.curios:curios-forge:${curios_version}") } // This block of code expands all declared replace properties in the specified resource targets. @@ -152,11 +158,11 @@ dependencies { // See https://docs.gradle.org/current/dsl/org.gradle.language.jvm.tasks.ProcessResources.html tasks.named('processResources', ProcessResources).configure { var replaceProperties = [ - minecraft_version: minecraft_version, minecraft_version_range: minecraft_version_range, - forge_version: forge_version, forge_version_range: forge_version_range, + minecraft_version : minecraft_version, minecraft_version_range: minecraft_version_range, + forge_version : forge_version, forge_version_range: forge_version_range, loader_version_range: loader_version_range, - mod_id: mod_id, mod_name: mod_name, mod_license: mod_license, mod_version: mod_version, - mod_authors: mod_authors, mod_description: mod_description, + mod_id : mod_id, mod_name: mod_name, mod_license: mod_license, mod_version: mod_version, + mod_authors : mod_authors, mod_description: mod_description, ] inputs.properties replaceProperties diff --git a/gradle.properties b/gradle.properties index baf8e8f..b377731 100644 --- a/gradle.properties +++ b/gradle.properties @@ -22,6 +22,11 @@ loader_version_range=[47,) # The default set of supported mapping channels are ["official", "snapshot", "snapshot_nodoc", "stable", "stable_nodoc"]. # Additional mapping channels can be registered through the "channelProviders" extension in a Gradle plugin. # +irons_spells_version=1.20.1-3.4.0.11 +caelus_version=3.1.0+1.20 +curios_version=5.4.7+1.20.1 +geckolib_version=1.20.1:4.7.1.1 +player_animator_version=1.0.2-rc1+1.20 # | Channel | Version | | # |-----------|----------------------|--------------------------------------------------------------------------------| # | official | MCVersion | Official field/method names from Mojang mapping files | diff --git a/src/main/java/net/fretux/ascend/compat/IronsSpellbooksCompat.java b/src/main/java/net/fretux/ascend/compat/IronsSpellbooksCompat.java new file mode 100644 index 0000000..748633c --- /dev/null +++ b/src/main/java/net/fretux/ascend/compat/IronsSpellbooksCompat.java @@ -0,0 +1,203 @@ +package net.fretux.ascend.compat; + +import io.redspace.ironsspellbooks.api.registry.AttributeRegistry; +import net.fretux.ascend.player.PlayerStatsProvider; +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.player.Player; +import net.minecraftforge.fml.ModList; + +import java.util.UUID; + +public class IronsSpellbooksCompat { + private static final UUID INT_MAX_MANA_UUID = + UUID.fromString("11111111-1111-4111-8111-111111111111"); + private static final UUID INT_SPELL_POWER_UUID = + UUID.fromString("22222222-2222-4222-8222-222222222222"); + private static final UUID INT_MANA_REGEN_UUID = + UUID.fromString("33333333-3333-4333-8333-333333333333"); + private static final UUID INT_COOLDOWN_REDUCTION_UUID = + UUID.fromString("44444444-4444-4444-8444-444444444444"); + private static final UUID CHA_MAX_MANA_UUID = + UUID.fromString("55555555-5555-4555-8555-555555555555"); + // reserved for future spell-trade hooks + private static final UUID CHA_SPELL_TRADE_UUID = + UUID.fromString("66666666-6666-4666-8666-666666666666"); + private static final UUID MAG_SPELL_POWER_UUID = + UUID.fromString("77777777-7777-4777-8777-777777777777"); + private static final UUID MAG_MANA_REGEN_UUID = + UUID.fromString("88888888-8888-4888-8888-888888888888"); + private static final UUID MAG_COOLDOWN_REDUCTION_UUID = + UUID.fromString("99999999-9999-4999-8999-999999999999"); + + private IronsSpellbooksCompat() {} + + public static boolean isLoaded() { + return ModList.get().isLoaded("irons_spellbooks"); + } + + public static void applyAll(Player player) { + if (!isLoaded()) return; + player.getCapability(PlayerStatsProvider.PLAYER_STATS).ifPresent(stats -> { + applyIntelligence(player, stats.getAttributeLevel("intelligence")); + applyCharisma(player, stats.getAttributeLevel("charisma")); + applyMagicScaling(player, stats.getAttributeLevel("magic_scaling")); + }); + } + + public static void applyIntelligence(Player player, int intelligence) { + if (intelligence <= 0) { + clearIntelligence(player); + return; + } + applyAddModifier( + player, + (Attribute) AttributeRegistry.MAX_MANA.get(), + INT_MAX_MANA_UUID, + "Ascend INT max mana", + intelligence * 2.0d + ); + double spellPowerPerPoint = 0.004d; + double spellPowerMax = 0.40d; + double spellPowerBonus = Math.min(intelligence * spellPowerPerPoint, spellPowerMax); + applyMultiplyBaseModifier( + player, + (Attribute) AttributeRegistry.SPELL_POWER.get(), + INT_SPELL_POWER_UUID, + "Ascend INT spell power", + spellPowerBonus + ); + double regenPerPoint = 0.002d; + double regenMax = 0.25d; + double regenBonus = Math.min(intelligence * regenPerPoint, regenMax); + applyMultiplyBaseModifier( + player, + (Attribute) AttributeRegistry.MANA_REGEN.get(), + INT_MANA_REGEN_UUID, + "Ascend INT mana regen", + regenBonus + ); + double cdrPerPoint = 0.0015d; + double cdrMax = 0.20d; + double cdrBonus = Math.min(intelligence * cdrPerPoint, cdrMax); + applyMultiplyBaseModifier( + player, + (Attribute) AttributeRegistry.COOLDOWN_REDUCTION.get(), + INT_COOLDOWN_REDUCTION_UUID, + "Ascend INT cooldown reduction", + cdrBonus + ); + } + + private static void clearIntelligence(Player player) { + removeModifier(player, (Attribute) AttributeRegistry.MAX_MANA.get(), INT_MAX_MANA_UUID); + removeModifier(player, (Attribute) AttributeRegistry.SPELL_POWER.get(), INT_SPELL_POWER_UUID); + removeModifier(player, (Attribute) AttributeRegistry.MANA_REGEN.get(), INT_MANA_REGEN_UUID); + removeModifier(player, (Attribute) AttributeRegistry.COOLDOWN_REDUCTION.get(), INT_COOLDOWN_REDUCTION_UUID); + } + + public static void applyCharisma(Player player, int charisma) { + if (charisma <= 0) { + clearCharisma(player); + return; + } + double manaPerPoint = 0.5d; + double bonusMana = charisma * manaPerPoint; + applyAddModifier( + player, + (Attribute) AttributeRegistry.MAX_MANA.get(), + CHA_MAX_MANA_UUID, + "Ascend CHA max mana", + bonusMana + ); + // Trade / reputation hooks can use CHA_SPELL_TRADE_UUID later. + } + + private static void clearCharisma(Player player) { + removeModifier(player, (Attribute) AttributeRegistry.MAX_MANA.get(), CHA_MAX_MANA_UUID); + // Any future CHA ISS modifiers get cleared here. + } + + public static void applyMagicScaling(Player player, int magicScaling) { + if (magicScaling <= 0) { + clearMagicScaling(player); + return; + } + double spPerPoint = 0.006d; + double spMax = 0.60d; + double spBonus = Math.min(magicScaling * spPerPoint, spMax); + applyMultiplyBaseModifier( + player, + (Attribute) AttributeRegistry.SPELL_POWER.get(), + MAG_SPELL_POWER_UUID, + "Ascend Magic Scaling spell power", + spBonus + ); + double regenPerPoint = 0.001d; + double regenMax = 0.15d; + double regenBonus = Math.min(magicScaling * regenPerPoint, regenMax); + applyMultiplyBaseModifier( + player, + (Attribute) AttributeRegistry.MANA_REGEN.get(), + MAG_MANA_REGEN_UUID, + "Ascend Magic Scaling mana regen", + regenBonus + ); + + double cdrPerPoint = 0.001d; + double cdrMax = 0.10d; + double cdrBonus = Math.min(magicScaling * cdrPerPoint, cdrMax); + applyMultiplyBaseModifier( + player, + (Attribute) AttributeRegistry.COOLDOWN_REDUCTION.get(), + MAG_COOLDOWN_REDUCTION_UUID, + "Ascend Magic Scaling cooldown reduction", + cdrBonus + ); + } + + private static void clearMagicScaling(Player player) { + removeModifier(player, (Attribute) AttributeRegistry.SPELL_POWER.get(), MAG_SPELL_POWER_UUID); + removeModifier(player, (Attribute) AttributeRegistry.MANA_REGEN.get(), MAG_MANA_REGEN_UUID); + removeModifier(player, (Attribute) AttributeRegistry.COOLDOWN_REDUCTION.get(), MAG_COOLDOWN_REDUCTION_UUID); + } + + private static void applyAddModifier(Player player, Attribute attribute, UUID id, String name, double value) { + if (attribute == null) return; + AttributeInstance inst = player.getAttribute(attribute); + if (inst == null) return; + inst.removeModifier(id); + if (value != 0.0d) { + inst.addTransientModifier(new AttributeModifier( + id, + name, + value, + AttributeModifier.Operation.ADDITION + )); + } + } + + private static void applyMultiplyBaseModifier(Player player, Attribute attribute, UUID id, String name, double value) { + if (attribute == null) return; + AttributeInstance inst = player.getAttribute(attribute); + if (inst == null) return; + inst.removeModifier(id); + if (value != 0.0d) { + inst.addTransientModifier(new AttributeModifier( + id, + name, + value, + AttributeModifier.Operation.MULTIPLY_BASE + )); + } + } + + private static void removeModifier(Player player, Attribute attribute, UUID id) { + if (attribute == null) return; + AttributeInstance inst = player.getAttribute(attribute); + if (inst != null) { + inst.removeModifier(id); + } + } +} \ No newline at end of file diff --git a/src/main/java/net/fretux/ascend/event/CharismaTradeEvents.java b/src/main/java/net/fretux/ascend/event/CharismaTradeEvents.java new file mode 100644 index 0000000..d018944 --- /dev/null +++ b/src/main/java/net/fretux/ascend/event/CharismaTradeEvents.java @@ -0,0 +1,60 @@ +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.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.MerchantMenu; +import net.minecraft.world.item.trading.MerchantOffer; +import net.minecraftforge.event.entity.player.PlayerContainerEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; + +@Mod.EventBusSubscriber(modid = AscendMod.MODID) +public class CharismaTradeEvents { + + @SubscribeEvent + public static void onOpenMerchant(PlayerContainerEvent.Open event) { + Player player = event.getEntity(); + if (player.level().isClientSide) return; + + AbstractContainerMenu menu = event.getContainer(); + if (!(menu instanceof MerchantMenu merchantMenu)) return; + + player.getCapability(PlayerStatsProvider.PLAYER_STATS).ifPresent(stats -> { + int cha = stats.getAttributeLevel("charisma"); + if (cha <= 0) return; + + double discountFraction = StatEffects.getCharismaTradeDiscount(cha); + if (discountFraction <= 0.0d) return; + + var offers = merchantMenu.getOffers(); + if (offers == null) return; + + for (MerchantOffer offer : offers) { + if (offer == null) continue; + + // Base cost of first input (emeralds, etc.) + int base = offer.getBaseCostA().getCount(); + if (base <= 0) continue; + + // Desired discounted cost + int target = (int) Math.max(1, Math.floor(base * (1.0d - discountFraction))); + + // Current special price diff already includes: + // popularity, hero-of-village, etc. + int currentSpecial = offer.getSpecialPriceDiff(); + + // We want: base + newSpecial = target + int newSpecial = target - base; + + // Adjust relative to current to avoid stacking each open. + int delta = newSpecial - currentSpecial; + if (delta != 0) { + offer.addToSpecialPriceDiff(delta); + } + } + }); + } +} \ No newline at end of file diff --git a/src/main/java/net/fretux/ascend/event/WeaponMagicScalingEvents.java b/src/main/java/net/fretux/ascend/event/WeaponMagicScalingEvents.java new file mode 100644 index 0000000..9207ba4 --- /dev/null +++ b/src/main/java/net/fretux/ascend/event/WeaponMagicScalingEvents.java @@ -0,0 +1,70 @@ +package net.fretux.ascend.event; + +import net.fretux.ascend.AscendMod; +import net.fretux.ascend.player.PlayerStatsProvider; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraftforge.event.entity.living.LivingHurtEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; + +@Mod.EventBusSubscriber(modid = AscendMod.MODID) +public class WeaponMagicScalingEvents { + + @SubscribeEvent + public static void onLivingHurt(LivingHurtEvent event) { + if (!(event.getSource().getEntity() instanceof Player player)) return; + if (event.getEntity() == player) return; + if (player.level().isClientSide) return; + player.getCapability(PlayerStatsProvider.PLAYER_STATS).ifPresent(stats -> { + float base = event.getAmount(); + float modified = base; + if (isMagicDamage(event.getSource())) { + int magicScaling = stats.getAttributeLevel("magic_scaling"); + if (magicScaling > 0) { + double perPoint = 0.005d; + double maxBonus = 0.40d; + double bonusMult = 1.0d + Math.min(magicScaling * perPoint, maxBonus); + modified = (float) (modified * bonusMult); + } + } else { + double attackSpeed = 4.0d; + if (player.getAttribute(Attributes.ATTACK_SPEED) != null) { + attackSpeed = player.getAttribute(Attributes.ATTACK_SPEED).getValue(); + } + + String key = + (attackSpeed >= 1.8d) ? "light_scaling" : + (attackSpeed >= 1.2d) ? "medium_scaling" : + "heavy_scaling"; + + int level = stats.getAttributeLevel(key); + if (level > 0) { + double perPoint = + key.equals("light_scaling") ? 0.003d : + key.equals("medium_scaling") ? 0.004d : + 0.005d; + double maxBonus = 0.40d; + double bonusMult = 1.0d + Math.min(level * perPoint, maxBonus); + modified = (float) (modified * bonusMult); + } + } + + if (modified != base) { + event.setAmount(modified); + } + }); + } + + private static boolean isMagicDamage(DamageSource source) { + String id = source.getMsgId(); + if (id == null) return false; + return id.equals("magic") + || id.equals("indirectMagic") + || id.equals("wither") + || id.equals("dragonBreath") + || id.equals("thrown") + || id.equals("potion"); + } +} diff --git a/src/main/java/net/fretux/ascend/player/StatEffects.java b/src/main/java/net/fretux/ascend/player/StatEffects.java index 8765617..6cd3527 100644 --- a/src/main/java/net/fretux/ascend/player/StatEffects.java +++ b/src/main/java/net/fretux/ascend/player/StatEffects.java @@ -1,5 +1,6 @@ package net.fretux.ascend.player; +import net.fretux.ascend.compat.IronsSpellbooksCompat; import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.ai.attributes.AttributeInstance; import net.minecraft.world.entity.ai.attributes.AttributeModifier; @@ -7,46 +8,51 @@ import net.minecraft.world.entity.ai.attributes.Attributes; import java.util.UUID; +import static net.fretux.ascend.compat.IronsSpellbooksCompat.applyCharisma; +import static net.fretux.ascend.compat.IronsSpellbooksCompat.applyIntelligence; + public final class StatEffects { - // ===== Strength UUIDs ===== private static final UUID STRENGTH_DAMAGE_UUID = UUID.fromString("a7b8b2c3-9b0d-4e1a-8ee3-21f6a7b2c3d4"); private static final UUID STRENGTH_KNOCKBACK_UUID = UUID.fromString("b17f0d74-9cd9-4f90-9ec8-6a1046e2f912"); - // ===== Agility UUIDs ===== private static final UUID AGILITY_SPEED_UUID = - UUID.fromString("c12e9b6f-1d79-4a49-b57f-7f563c10aa11"); - private static final UUID AGILITY_STEALTH_UUID = - UUID.fromString("d4e8a9f2-3b5c-4d0a-9f11-8acb3f0b77e2"); + UUID.fromString("c2f4e9a1-3b8d-4f21-9d44-8176f8791a11"); - // ===== Fortitude UUIDs ===== private static final UUID FORTITUDE_HEALTH_UUID = - UUID.fromString("e2b8ff31-2c3d-4a60-9ce4-12aa4d3f9b77"); + UUID.fromString("d3a4b5c6-7d8e-4f90-8a1b-223344556677"); private static final UUID FORTITUDE_KB_RESIST_UUID = - UUID.fromString("f4c1a9d2-6b8e-4f01-8a21-9e7c5b3d12aa"); + UUID.fromString("e4b5c6d7-8e9f-4012-9abc-334455667788"); private StatEffects() {} - - /** Call on login/clone and after stat changes. */ + public static void applyAll(Player player) { player.getCapability(PlayerStatsProvider.PLAYER_STATS).ifPresent(stats -> { applyStrength(player, stats.getAttributeLevel("strength")); applyAgility(player, stats.getAttributeLevel("agility")); applyFortitude(player, stats.getAttributeLevel("fortitude")); - applyIntelligence(player, stats.getAttributeLevel("intelligence")); + // - Willpower: see getWillpowerSanityMultiplier / getWillpowerTempoMultiplier. + if (IronsSpellbooksCompat.isLoaded()) { + IronsSpellbooksCompat.applyAll(player); + } }); } - // ========== STRENGTH ========== - + /** + * Strength: + * - +0.1 attack damage per point (0.05 hearts) + * - +0.03 knockback per point + * - Armor bypass handled in your combat event using strength level. + */ public static void applyStrength(Player player, int strengthLevel) { + // Damage AttributeInstance atk = player.getAttribute(Attributes.ATTACK_DAMAGE); if (atk != null) { atk.removeModifier(STRENGTH_DAMAGE_UUID); if (strengthLevel > 0) { - double dmgBonus = strengthLevel * 0.1d; // 0.05 hearts per point + double dmgBonus = strengthLevel * 0.1d; atk.addTransientModifier(new AttributeModifier( STRENGTH_DAMAGE_UUID, "Ascend Strength damage bonus", @@ -56,6 +62,7 @@ public final class StatEffects { } } + // Knockback AttributeInstance kb = player.getAttribute(Attributes.ATTACK_KNOCKBACK); if (kb != null) { kb.removeModifier(STRENGTH_KNOCKBACK_UUID); @@ -71,102 +78,149 @@ public final class StatEffects { } } - // ========== AGILITY ========== - + /** + * Agility: + * - Small movement speed bonus per point. + * - Stealth handled externally (e.g. other mods can read agility). + */ public static void applyAgility(Player player, int agilityLevel) { - // Movement speed AttributeInstance move = player.getAttribute(Attributes.MOVEMENT_SPEED); if (move != null) { move.removeModifier(AGILITY_SPEED_UUID); if (agilityLevel > 0) { - // ~+2.5% at 10, +25% at 100 - double speedBonus = agilityLevel * 0.00025d; + // Example: +0.002 per point = +0.2 at 100 (about +20% on base 0.1) + double bonus = agilityLevel * 0.002d; move.addTransientModifier(new AttributeModifier( AGILITY_SPEED_UUID, "Ascend Agility speed bonus", - speedBonus, + bonus, AttributeModifier.Operation.ADDITION )); } } - - // Stealth via follow range reduction - AttributeInstance follow = player.getAttribute(Attributes.FOLLOW_RANGE); - if (follow != null) { - follow.removeModifier(AGILITY_STEALTH_UUID); - if (agilityLevel > 0) { - double perPoint = 0.003d; // 0.3% per point - double maxReduction = 0.5d; // up to -50% - double reduction = Math.min(agilityLevel * perPoint, maxReduction); - - if (reduction > 0.0d) { - double multiplier = 1.0d - reduction; - follow.addTransientModifier(new AttributeModifier( - AGILITY_STEALTH_UUID, - "Ascend Agility stealth bonus", - multiplier - 1.0d, - AttributeModifier.Operation.MULTIPLY_TOTAL - )); - } - } - } } - // ========== FORTITUDE ========== - + /** + * Fortitude: + * - Max HP up per point. + * - Knockback taken reduced via attribute. + * (Poison/hunger/debuff tuning handled via your other event hooks.) + */ public static void applyFortitude(Player player, int fortitudeLevel) { - // Max HP - AttributeInstance maxHp = player.getAttribute(Attributes.MAX_HEALTH); - if (maxHp != null) { - maxHp.removeModifier(FORTITUDE_HEALTH_UUID); + // Max health + AttributeInstance hp = player.getAttribute(Attributes.MAX_HEALTH); + if (hp != null) { + hp.removeModifier(FORTITUDE_HEALTH_UUID); if (fortitudeLevel > 0) { - // +0.2 HP per point (1 heart per 10 points) - double bonusHp = fortitudeLevel * 0.2d; - maxHp.addTransientModifier(new AttributeModifier( + // Example: 0.25 HP (1/8 heart) per point = +12.5 hearts at 100 + double healthBonus = fortitudeLevel * 0.25d; + hp.addTransientModifier(new AttributeModifier( FORTITUDE_HEALTH_UUID, "Ascend Fortitude health bonus", - bonusHp, + healthBonus, AttributeModifier.Operation.ADDITION )); } } - float maxHealthNow = player.getMaxHealth(); - if (player.getHealth() > maxHealthNow) { - player.setHealth(maxHealthNow); - } - // Knockback resistance AttributeInstance kbRes = player.getAttribute(Attributes.KNOCKBACK_RESISTANCE); if (kbRes != null) { kbRes.removeModifier(FORTITUDE_KB_RESIST_UUID); if (fortitudeLevel > 0) { - // 0.3% per point, up to 30% at 100 (with safety cap) - double bonus = fortitudeLevel * 0.003d; - double safeBonus = Math.min(bonus, 0.8d); - if (safeBonus > 0) { - kbRes.addTransientModifier(new AttributeModifier( - FORTITUDE_KB_RESIST_UUID, - "Ascend Fortitude knockback resist", - safeBonus, - AttributeModifier.Operation.ADDITION - )); - } + // Tiny scaling; cap at 60% here + double res = Math.min(0.006d * fortitudeLevel, 0.60d); + kbRes.addTransientModifier(new AttributeModifier( + FORTITUDE_KB_RESIST_UUID, + "Ascend Fortitude knockback resist", + res, + AttributeModifier.Operation.ADDITION + )); } } } - // ========== INTELLIGENCE ========== - /** * Intelligence: - * - Max mana is exposed via PlayerStats (see below). - * - Potions & enchanting are handled via event hooks. - * - * This exists for future attribute-based hooks if needed. + * - Used for mana & potion/enchant effects via other hooks. + * - Here we only expose a mana bonus helper. */ - public static void applyIntelligence(Player player, int intelligenceLevel) { - // No direct vanilla attribute to touch here for mana. - // Potion & enchanting effects are implemented in event handlers. + + // ===== Willpower external API ===== + + /** + * Sanity drain multiplier from Willpower. + * Other mods can call this directly. + * + * - Starts at 1.0 + * - Reduces up to 50% at high Willpower. + */ + public static float getWillpowerSanityMultiplier(int willpowerLevel) { + if (willpowerLevel <= 0) return 1.0f; + double reduction = Math.min(willpowerLevel * 0.005d, 0.5d); // up to -50% + return (float) (1.0d - reduction); + } + + /** + * Tempo gain/multiplier from Willpower. + * Other mods can plug this into stamina/tempo/sprint systems. + * + * - Starts at 1.0 + * - Increases up to +40%. + */ + public static float getWillpowerTempoMultiplier(int willpowerLevel) { + if (willpowerLevel <= 0) return 1.0f; + double bonus = Math.min(willpowerLevel * 0.004d, 0.4d); // up to +40% + return (float) (1.0d + bonus); + } + + /** Convenience: from player capability → sanity multiplier. */ + public static float getPlayerSanityMultiplier(Player player) { + return player.getCapability(PlayerStatsProvider.PLAYER_STATS) + .map(stats -> getWillpowerSanityMultiplier(stats.getAttributeLevel("willpower"))) + .orElse(1.0f); + } + + /** Convenience: from player capability → tempo multiplier. */ + public static float getPlayerTempoMultiplier(Player player) { + return player.getCapability(PlayerStatsProvider.PLAYER_STATS) + .map(stats -> getWillpowerTempoMultiplier(stats.getAttributeLevel("willpower"))) + .orElse(1.0f); + } + + // ===== Intelligence external helper (for your mana system) ===== + + /** + * Example mana bonus: 1 mana per INT, capped by caller if needed. + */ + public static int getIntelligenceManaBonus(int intelligenceLevel) { + return Math.max(0, intelligenceLevel); + } + + // ===== Charisma helper (used in trade discount handler) ===== + + /** + * Charisma → trade discount fraction. + * Returns value in [0, max], e.g. 0.0 = no discount, 0.25 = 25% cheaper. + */ + public static double getCharismaTradeDiscount(int charismaLevel) { + if (charismaLevel <= 0) return 0.0d; + double perPoint = 0.01d; + double max = 0.30d; + return Math.min(charismaLevel * perPoint, max); + } + + public static double getWeaponScalingMultiplier(int scalingLevel) { + if (scalingLevel <= 0) return 1.0d; + double perPoint = 0.005d; + double maxBonus = 0.50d; + return 1.0d + Math.min(scalingLevel * perPoint, maxBonus); + } + + public static double getMagicScalingMultiplier(int scalingLevel) { + if (scalingLevel <= 0) return 1.0d; + double perPoint = 0.0075d; + double maxBonus = 0.75d; + return 1.0d + Math.min(scalingLevel * perPoint, maxBonus); } } \ No newline at end of file