diff --git a/CHANGELOG.md b/CHANGELOG.md index c6db058..3655a96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `Fixed` for any bug fixes. - `Security` in case of vulnerabilities. +## [0.4.7] - 2025-05-27 + +### Changed + +- Change XP scaling outside a valid "level range". Default level range is 30, which gives plenty of scope for normal play but effectively restricts players from cheesing high level mobs when low level to massively boost their level. + +### Fixed + +- Fixed support for keeping level bonuses configured in `globalMasteryConfig.json` when mastery config preset is set to something other than `custom` +- Boss feeding event explosions no longer trigger any mastery changes +- Added handling for potential edge case when accessing allied player lookup + ## [0.4.6] - 2025-05-24 ### Changed diff --git a/Documentation.md b/Documentation.md index 6400114..ce93e4c 100644 --- a/Documentation.md +++ b/Documentation.md @@ -9,6 +9,10 @@ To configure the player level bonus: - Edit the `xpBuffConfig` section in the generated config in `BepInEx\config\XPRising_XXXXX\Data\globalMasteryConfig.json` - Note that this config is only generated after running the server once - See [UnitStats](UnitStats.md) for more configuration documentation. +- The maximum level difference a mob can be compared to the player so that they can receive appropriate XP is governed by `LevelRange`. Players will receive less XP from mobs that are lower level, dropping to a minimum value at the specified range. Players will receive more XP from mobs that are higher level, until a peak is reached, which will then drop back down to the standard value before dropping further. + - At 0 level difference, this is the standard XP + - At +range level difference, the player receives minimal XP + - At -range level difference, the player receives the same XP as at 0 level difference. Between 0 difference and -range difference, XP gain increases then decreases in a single sawtooth pattern. This is intended as a protection against abusing unbound level difference XP gains. ## Mastery System The mastery system allows players to get extra buffs as they master weapons/bloodlines/spells. diff --git a/UnitStats.md b/UnitStats.md index 05a6c91..b454f70 100644 --- a/UnitStats.md +++ b/UnitStats.md @@ -117,7 +117,7 @@ Note that different weapons will have different damage coefficients. All the dam | BonusMountMovementSpeed | other | | | | BonusMovementSpeed | other | | | | BonusPhysicalPower | offensive | 1 | 1 physical power | -| BonusShapeshiftMovementSpeed | other | | | +| BonusShapeshiftMovementSpeed | other | 0.2 | baseSpeed * (1 + value) => 6.5*1.2 => 7.8 | | BonusSpellPower | offensive | 1 | 1 spell power | | CCReduction | defensive | 50 | half the CC amount (2 sec stun -> 1 sec) | | CooldownRecoveryRate | offensive | 0.15 | minus 1 sec | diff --git a/XPRising/Configuration/ExperienceConfig.cs b/XPRising/Configuration/ExperienceConfig.cs index 92e41b4..d4eb954 100644 --- a/XPRising/Configuration/ExperienceConfig.cs +++ b/XPRising/Configuration/ExperienceConfig.cs @@ -24,10 +24,10 @@ public static void Initialize() "Formula: EXPGained * VBloodMultiplier * EXPMultiplier").Value; ExperienceSystem.GroupMaxDistance = _configFile.Bind("Experience", "Group Range", 40f, "Set the maximum distance an ally (player) has to be from the player for them to share EXP with the player. Set this to 0 to disable groups.").Value; ExperienceSystem.GroupXpBuffGrowth = _configFile.Bind("Experience", "Group XP buff", 0.3f, "Set the amount of additional XP that a player will get for each additional player in their group.\n" + - "Example with buff of 0.3: 2 players = 1.3 XP multiplyer; 3 players = 1.3 x 1.3 = 1.69 XP multiplier").Value; + "Example with buff of 0.3: 2 players = 1.3 XP multiplier; 3 players = 1.3 x 1.3 = 1.69 XP multiplier").Value; ExperienceSystem.MaxGroupXpBuff = _configFile.Bind("Experience", "Max group XP buff", 2f, "Set the maximum increase in XP that a player can gain when playing in a group.").Value; - ExperienceSystem.MaxXpGainPercentage = _configFile.Bind("Experience", "Max XP Gain Percent", 50f, "Set the maximum XP a player can gain, based on the percentage of XP required for the current level.\n" + - "For example, if the player's level takes 300 XP, a value of 50% will result in the max XP gain for a single kill to be 150 XP. Set to 0 to disable.").Value; + ExperienceSystem.LevelRange = _configFile.Bind("Experience", "Level range", 30f, "Sets a level range over which player XP gain is maximised.\n" + + "Check documentation for a longer description.").Value; ExperienceSystem.PvpXpLossPercent = _configFile.Bind("Rates, Experience", "PvP XP Loss Percent", 0f, "Sets the percentage of XP to the next level lost on a PvP death").Value; ExperienceSystem.PveXpLossPercent = _configFile.Bind("Rates, Experience", "PvE XP Loss Percent", 10f, "Sets the percentage of XP to the next level lost on a PvE death").Value; diff --git a/XPRising/Systems/ExperienceSystem.cs b/XPRising/Systems/ExperienceSystem.cs index 141bab5..0a1091f 100644 --- a/XPRising/Systems/ExperienceSystem.cs +++ b/XPRising/Systems/ExperienceSystem.cs @@ -20,7 +20,7 @@ public class ExperienceSystem public static float VBloodMultiplier = 15; public static int MaxLevel = 100; public static float GroupMaxDistance = 50; - public static float MaxXpGainPercentage = 50f; + public static float LevelRange = 20; public static float PvpXpLossPercent = 0; public static float PveXpLossPercent = 10; @@ -43,9 +43,9 @@ public class ExperienceSystem * * mob level=> | same | +5 | -5 | +5 => -5 | same (VBlood only) | * _______________|________|______|______|__________|____________________| - * Total kills | 3930 | 2745 | 7220 | 4221 | 262 | + * Total kills | 3864 | 2845 | 6309 | 4002 | 257 | * lvl 0 kills | 10 | 2 | 10 | 2 | 1 | - * Last lvl kills | 108 | 108 | 108 | 108 | 8 | + * Last lvl kills | 85 | 85 | 160 | 85 | 6 | * * +5/-5 offset to levels in the above table as still clamped to the range [1, 100]. * @@ -59,8 +59,6 @@ public class ExperienceSystem */ private const float ExpConstant = 0.3f; private const float ExpPower = 2.2f; - private const float ExpLevelDiffMultiplier = 0.07f; - private const float MinAllowedLevelDiff = -12; // This is updated on server start-up to match server settings start level public static int StartingExp = 0; @@ -153,14 +151,13 @@ private static void AssignExp(Alliance.ClosePlayer player, int calculatedPlayerL private static int CalculateXp(int playerLevel, int mobLevel, double multiplier) { // Using a min level difference here to ensure that the user can get a basic level of XP - var levelDiff = Math.Max(mobLevel - playerLevel, MinAllowedLevelDiff); + var levelDiff = mobLevel - playerLevel; - var baseXpGain = (int)(Math.Max(1, mobLevel * multiplier * (1 + levelDiff * ExpLevelDiffMultiplier))*ExpMultiplier); - var maxGain = MaxXpGainPercentage > 0 ? (int)Math.Ceiling((ConvertLevelToXp(playerLevel + 1) - ConvertLevelToXp(playerLevel)) * (MaxXpGainPercentage * 0.01f)) : int.MaxValue; + var baseXpGain = (int)(Math.Max(1, mobLevel * multiplier * (1 + Math.Min(mobLevel - (mobLevel/LevelRange)*levelDiff, levelDiff)*(1/LevelRange)))*ExpMultiplier); - Plugin.Log(LogSystem.Xp, LogLevel.Info, $"--- Max(1, {mobLevel} * {multiplier:F3} * (1 + {levelDiff} * {ExpLevelDiffMultiplier}))*{ExpMultiplier} => {baseXpGain} => Clamped between [1,{maxGain}]"); + Plugin.Log(LogSystem.Xp, LogLevel.Info, $"--- Max(1, {mobLevel} * {multiplier:F3} * (1 + {levelDiff}))*{ExpMultiplier} => {baseXpGain} => Clamped between [1,inf]"); // Clamp the XP gain to be within 1 XP and "maxGain" XP. - return Math.Clamp(baseXpGain, 1, maxGain); + return Math.Max(baseXpGain, 1); } public static void DeathXpLoss(Entity playerEntity, Entity killerEntity) { diff --git a/XPRising/Utils/Alliance.cs b/XPRising/Utils/Alliance.cs index a696a81..0ed4a72 100644 --- a/XPRising/Utils/Alliance.cs +++ b/XPRising/Utils/Alliance.cs @@ -236,6 +236,12 @@ public static void GetPlayerClanAllies(Entity playerCharacter, Plugin.LogSystem Plugin.Log(system, LogLevel.Info, "No Associated User!"); } } + + // If somehow the query failed to get any online players, just add ourselves to the ally list. + if (playerGroup.Allies.Count == 0) + { + playerGroup.Allies.Add(playerCharacter); + } Cache.AllianceAutoPlayerAllies[playerCharacter] = playerGroup; } diff --git a/XPRising/Utils/AutoSaveSystem.cs b/XPRising/Utils/AutoSaveSystem.cs index 51223f9..f41c95a 100644 --- a/XPRising/Utils/AutoSaveSystem.cs +++ b/XPRising/Utils/AutoSaveSystem.cs @@ -195,23 +195,26 @@ private static bool InternalLoadDatabase(bool useInitialiser, LoadMethod loadMet // Load the global mastery file if (Plugin.WeaponMasterySystemActive || Plugin.BloodlineSystemActive || Plugin.ExperienceSystemActive) { - // Write it out to file if it does not exist. - // This is to ensure that the file gets written out, as there is no corresponding SaveDB call. This is due to the loaded MasteryConfig being the - // evaluated form of the configuration (the config supports using templates). - if (GlobalMasterySystem.MasteryConfigPreset == GlobalMasterySystem.CustomPreset) - { - Plugin.Log(Plugin.LogSystem.Mastery, LogLevel.Info, $"Confirming custom preset file exists"); - ConfirmFile(SavesPath, GlobalMasteryConfigJson, () => JsonSerializer.Serialize(GlobalMasterySystem.DefaultMasteryConfig(), PrettyJsonOptions)); - } - else + // Check that the mastery file exists. This will be used for both the mastery systems and the XP level buff system. + Plugin.Log(Plugin.LogSystem.Mastery, LogLevel.Info, $"Confirming custom preset file exists"); + ConfirmFile(SavesPath, GlobalMasteryConfigJson, () => JsonSerializer.Serialize(GlobalMasterySystem.DefaultMasteryConfig(), PrettyJsonOptions)); + + // Load the config from file. This is required as we will need to at least load the XP config, regardless of the mastery config preset. + var config = new GlobalMasteryConfig(); + anyErrors |= LoadDB(GlobalMasteryConfigJson, loadMethod, useInitialiser, ref config, GlobalMasterySystem.DefaultMasteryConfig); + + // If we are not using the custom preset, overwrite any existing configuration while keeping the xpBuffConfig section. + // There is no corresponding SaveDB call, so we want to save this now. + if (GlobalMasterySystem.MasteryConfigPreset != GlobalMasterySystem.CustomPreset) { - // If this is not the custom preset, forcibly overwrite any changes. Plugin.Log(Plugin.LogSystem.Mastery, LogLevel.Info, $"Ensuring '{GlobalMasterySystem.MasteryConfigPreset}' preset file is being written."); - EnsureFile(SavesPath, GlobalMasteryConfigJson, () => JsonSerializer.Serialize(GlobalMasterySystem.DefaultMasteryConfig(), PrettyJsonOptions)); + var preset = GlobalMasterySystem.DefaultMasteryConfig(); + preset.XpBuffConfig = config.XpBuffConfig; + EnsureFile(SavesPath, GlobalMasteryConfigJson, () => JsonSerializer.Serialize(preset, PrettyJsonOptions)); + + // Set the config to the preset + config = preset; } - - var config = new GlobalMasteryConfig(); - anyErrors |= LoadDB(GlobalMasteryConfigJson, loadMethod, useInitialiser, ref config, GlobalMasterySystem.DefaultMasteryConfig); // Load the config (or the default config) into the system. GlobalMasterySystem.SetMasteryConfig(config); diff --git a/XPRising/Utils/MasteryHelper.cs b/XPRising/Utils/MasteryHelper.cs index 43e40d4..8dee657 100644 --- a/XPRising/Utils/MasteryHelper.cs +++ b/XPRising/Utils/MasteryHelper.cs @@ -354,6 +354,10 @@ public static GlobalMasterySystem.MasteryType GetMasteryTypeForEffect(int effect return GlobalMasterySystem.MasteryType.WeaponClaws; // Effects that shouldn't do anything to mastery. case Effects.AB_FeedBoss_03_Complete_AreaDamage: // Boss death explosion + case Effects.AB_FeedBoss_FeedOnDracula_03_Complete_AreaDamage: // Boss death explosion + case Effects.AB_FeedDraculaBloodSoul_03_Complete_AreaDamage: // Boss death explosion + case Effects.AB_FeedDraculaOrb_03_Complete_AreaDamage: // Boss death explosion + case Effects.AB_FeedGateBoss_03_Complete_AreaDamage: // Boss death explosion case Effects.AB_ChurchOfLight_Priest_HealBomb_Buff: // Used as the lvl up animation case Effects.AB_Charm_Projectile: // Charming a unit case Effects.AB_Charm_Channeling_Target_Debuff: // Charming a unit @@ -444,6 +448,7 @@ public static GlobalMasterySystem.MasteryType GetMasteryTypeForEffect(int effect // Should this spell just contribute to spell damage? case 123399875: // Spell_Corruption_Tier3_Snare_Throw (TODO: put this in a file) case (int)Effects.AB_Vampire_Horse_Severance_Buff: + case (int)Effects.AB_Horse_Vampire_Thrust_TriggerArea: Plugin.Log(Plugin.LogSystem.Mastery, LogLevel.Info, $"{effect} has been through mastery helper as being ignored - check this"); ignore = true; return GlobalMasterySystem.MasteryType.None;