Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions Documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion UnitStats.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
6 changes: 3 additions & 3 deletions XPRising/Configuration/ExperienceConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
17 changes: 7 additions & 10 deletions XPRising/Systems/ExperienceSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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].
*
Expand All @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
6 changes: 6 additions & 0 deletions XPRising/Utils/Alliance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
31 changes: 17 additions & 14 deletions XPRising/Utils/AutoSaveSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
5 changes: 5 additions & 0 deletions XPRising/Utils/MasteryHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down