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
15 changes: 15 additions & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# These are supported funding model platforms

github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: aontas
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
5 changes: 4 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,7 @@ jobs:
run: tcli publish --config-path ./XPRising/thunderstore.toml --token ${{ secrets.THUNDERSTORE_KEY }} --file ./dist/XPRising-XPRising-${RELEASE_TAG:1}.zip

- name: Publish XPRising.ClientUI to Thunderstore
run: tcli publish --config-path ./ClientUI/thunderstore.toml --token ${{ secrets.THUNDERSTORE_KEY }} --file ./dist/XPRising-ClientUI-${RELEASE_TAG:1}.zip
run: tcli publish --config-path ./ClientUI/thunderstore.toml --token ${{ secrets.THUNDERSTORE_KEY }} --file ./dist/XPRising-ClientUI-${RELEASE_TAG:1}.zip

- name: Set release as latest
run: gh release edit ${{ env.RELEASE_TAG }} --draft=false --latest
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.6] - 2025-05-24

### Changed

- Changed values used to generate weapon mastery gain. This should be a smoother mastery gain experience now.

### Fixed

- Fixed crash that could occur when feeding on enemies
- Added support for more abilities for mastery gain.
- All CHAR_* damage events should now correctly be caught. These would only occur when damaging the summonable steed of Sir Fabian, but they should all be handled appropriately now.

## [0.4.5] - 2025-05-21

### Added
Expand Down
43 changes: 22 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,25 @@
# XPRising

This is a revitalisation of RPGMods. It has similar core ideas, with some reduced config complexity, looking to upgrade into the future.

## About

This mod is now comprised of 3 components: XPRising (Server), ClientUI (Client) and XPShared (Server & Client).
The client portion of this mod is entirely optional, but is recommended as it provides good feedback to the user.
This mod provides a mechanism for players to have their level set by gaining XP in the world, primarily by killing enemies.

There is an optional (but recommended) UI mod that supports displaying XP bars and notifications for players.

#### XPRising
This mod provides the following features:
- Switching from using gear level scheme to and XP based level scheme
- Support for gaining mastery in weapons and bloodlines
- Faction "wanted" system to spawn ambushers
- Support for sending required data to ClientUI
## Features

#### ClientUI
This mod provides the following framework features:
- Displaying progress bars (such as for XP or mastery levels)
- Displaying "action" buttons to extend user interaction
- Displaying notification messages (instead of relying on the chat log)
### XP system
- Players gain experience by killing enemies
- Admin configurable setting for giving players stat bonuses for each level

Note: these features are powered by a server-side mod. The server mod needs to support sending the appropriate info to the client to display appropriate UI elements.
### Mastery system
- Allows players to accrue "mastery" towards weapons and bloodlines
- Mastery systems allow stat bonuses to be applied to players

#### XPShared
This is a basic plugin mod that contains some shared configuration and logic to support sending the appropriate messages between the server and client.
This will be required on both the server and client.
### Wanted system
- A system that tracks player kills against different factions in the game and causes factions to ambush players with enemies as their "heat" level increases.

### XPRising Requirements
## XPRising Requirements

- [BepInExPack V Rising](https://thunderstore.io/c/v-rising/p/BepInEx/BepInExPack_V_Rising/) (Server/Client)
- [VampireCommandFramework](https://thunderstore.io/c/v-rising/p/deca/VampireCommandFramework/) (Server)
Expand All @@ -38,6 +31,10 @@ This will be required on both the server and client.
- [System documentation](Documentation.md): Each of the systems has some further documentation on this link.
- [Unit stat documentation](UnitStats.md): A list of stats and their effects that can be used for global mastery configuration.

## Alternatives

- [Bloodcraft](https://thunderstore.io/c/v-rising/p/zfolmt/Bloodcraft/): RPG mod that provides a much more player-customisable experience that includes selectable classes, professions, familiars and more.

## Contributors

- [Kaltharos](https://github.com/Kaltharos)
Expand Down Expand Up @@ -65,4 +62,8 @@ This will be required on both the server and client.

## By the community, for the community

> It was crucial for us to keep the code open source to ensure excellent support and provide other mod developers the opportunity to develop plugins for XPRising at any time. This project is free and open to everyone, created by the community for the community, and everyone is a part of the development!
> It was crucial for us to keep the code open source to ensure excellent support and provide other mod developers the opportunity to develop plugins for XPRising at any time. This project is free and open to everyone, created by the community for the community, and everyone is a part of the development!

## Donations

If you would like to make a donation, you can do so through [Kofi](https://ko-fi.com/aontas)
4 changes: 2 additions & 2 deletions XPRising/Configuration/GlobalMasteryConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public static void Initialize()
_configFile = new ConfigFile(configPath, true);

// Currently, we are never updating and saving the config file in game, so just load the values.
var globalVBloodMultiplier = _configFile.Bind("Global Mastery", "VBlood Mastery Multiplier", 10.0, "Multiply Mastery gained from VBlood kill.").Value;
var globalVBloodMultiplier = _configFile.Bind("Global Mastery", "VBlood Mastery Multiplier", 5.0, "Multiply Mastery gained from VBlood kill.").Value;
WeaponMasterySystem.VBloodMultiplier = globalVBloodMultiplier;
BloodlineSystem.VBloodMultiplier = globalVBloodMultiplier;
GlobalMasterySystem.SpellMasteryRequiresUnarmed = _configFile.Bind("Global Mastery", "Spell mastery only applies on unarmed", false, "Toggle whether the spell mastery bonus should be always applied or only applied when unarmed").Value;
Expand All @@ -28,7 +28,7 @@ public static void Initialize()
GlobalMasterySystem.DecayInterval = _configFile.Bind("Global Mastery", "Decay Tick Interval", 60, "Amount of seconds per decay tick.").Value;

// Weapon mastery specific config
WeaponMasterySystem.MasteryGainMultiplier = _configFile.Bind("Mastery - Weapon", "Mastery Gain Multiplier", 0.7, "Multiply the gained mastery value by this amount.").Value;
WeaponMasterySystem.MasteryGainMultiplier = _configFile.Bind("Mastery - Weapon", "Mastery Gain Multiplier", 1.0, "Multiply the gained mastery value by this amount.").Value;

// Blood mastery specific config
BloodlineSystem.MercilessBloodlines = _configFile.Bind("Mastery - Blood", "Merciless Bloodlines", BloodlineSystem.MercilessBloodlines, "Causes blood mastery to only grow when you kill something with a matching blood type of that has a quality higher than the current blood mastery").Value;
Expand Down
1 change: 1 addition & 0 deletions XPRising/Hooks/BuffHook.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ private static void SendPlayerUpdate(EntityManager em, Entity entity, bool killO
// If the owner is not a player character, ignore this entity
if (!em.TryGetComponentData<EntityOwner>(entity, out var entityOwner)) return;
if (!em.TryGetComponentData<PlayerCharacter>(entityOwner.Owner, out var playerCharacter)) return;
if (!target.Target._Entity.Has<UnitLevel>()) return;

PlayerCache.FindPlayer(playerCharacter.Name.ToString(), true, out _, out var userEntity);
// target.BloodConsumeSource can buff/debuff the blood quality
Expand Down
2 changes: 1 addition & 1 deletion XPRising/Hooks/StatChangeSystemHook.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ private static void ApplyStatChangesPostfix(
case StatChangeReason.DealDamageSystem_0:
// If the target entity does not have movement, it isn't a unit that will give XP (likely a tree/ore/wall etc)
if (!statChangeEvent.Entity.Has<Movement>()) break;
WeaponMasterySystem.HandleDamageEvent(statChangeEvent.Source, statChangeEvent.Entity);
WeaponMasterySystem.HandleDamageEvent(statChangeEvent.Source, statChangeEvent.Entity, statChangeEvent.OriginalChange);
break;
case StatChangeReason.Any:
case StatChangeReason.Default:
Expand Down
27 changes: 23 additions & 4 deletions XPRising/README_TS.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
XPRising is a server mod that replaces the gear level system with a more traditional levelling system, where you gain XP for killing mobs.
# XPRising

It also includes some systems for gaining mastery over weapons and bloodlines, as well a "Wanted" system that tracks how negative a faction perceives the player.
## About

It now also supports driving the XPRising ClientUI mod to display XP/Mastery/Wanted bars in the client UI.
This mod provides a mechanism for players to have their level set by gaining XP in the world, primarily by killing enemies.

There is an optional (but recommended) [companion UI](https://thunderstore.io/c/v-rising/p/XPRising/ClientUI/) that supports displaying XP bars and notifications for players.

## Features

### XP system
- Players gain experience by killing enemies
- Admin configurable setting for giving players stat bonuses for each level

### Mastery system
- Allows players to accrue "mastery" towards weapons and bloodlines
- Mastery systems allow stat bonuses to be applied to players

### Wanted system
- A system that tracks player kills against different factions in the game and causes factions to ambush players with enemies as their "heat" level increases.

### Installation

Expand Down Expand Up @@ -43,4 +58,8 @@ Join the [modding community](https://vrisingmods.com/discord) and add a post in

### Changelog

Found [here](https://github.com/aontas/XPRising/blob/main/CHANGELOG.md)
Found [here](https://github.com/aontas/XPRising/blob/main/CHANGELOG.md)

### Donations

If you would like to make a donation, you can do so through [Kofi](https://ko-fi.com/aontas)
2 changes: 1 addition & 1 deletion XPRising/Systems/GlobalMasterySystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -412,10 +412,10 @@ private static double ModMastery(ulong steamID, LazyDictionary<MasteryType, Mast
var mastery = playerMastery[type];
var currentMastery = mastery.Mastery;
mastery.Mastery += mastery.CalculateBaseMasteryGrowth(changeInMastery);
Plugin.Log(Plugin.LogSystem.Mastery, LogLevel.Info, $"Mastery changed: {steamID}: {Enum.GetName(type)}: {mastery.Mastery}");
playerMastery[type] = mastery;

var actualMasteryChange = mastery.Mastery - currentMastery;
Plugin.Log(Plugin.LogSystem.Mastery, LogLevel.Info, $"Mastery changed: {steamID}: {Enum.GetName(type)}: {mastery.Mastery:F4}(+{actualMasteryChange:F4})");

if (actualMasteryChange != 0)
{
Expand Down
75 changes: 24 additions & 51 deletions XPRising/Systems/WeaponMasterySystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,88 +14,61 @@ public static class WeaponMasterySystem
private static EntityManager _em = Plugin.Server.EntityManager;

public static double MasteryGainMultiplier = 0.1;
public static double VBloodMultiplier = 15;
public static double VBloodMultiplier = 5;

/// <summary>
/// Calculates and banks any mastery increases for the damage event
/// </summary>
/// <param name="sourceEntity">The ability that is dealing damage to the target</param>
/// <param name="targetEntity">The target that is receiving the damage</param>
public static void HandleDamageEvent(Entity sourceEntity, Entity targetEntity)
/// <param name="change">The HP change due to this event. For damage events, this is a negative number.</param>
public static void HandleDamageEvent(Entity sourceEntity, Entity targetEntity, float change)
{
var spellFactor = 0f;
var physicalFactor = 0f;
if (sourceEntity.TryGetBuffer<DealDamageOnGameplayEvent>(out var dealDamageBuffer))
{
foreach (var dealDamageEvent in dealDamageBuffer)
{
switch (dealDamageEvent.Parameters.MainType)
{
case MainDamageType.Physical:
physicalFactor += dealDamageEvent.Parameters.MainFactor;
break;
case MainDamageType.Spell:
spellFactor += dealDamageEvent.Parameters.MainFactor;
break;
case MainDamageType.Fire:
case MainDamageType.Holy:
case MainDamageType.Silver:
case MainDamageType.Garlic:
case MainDamageType.RadialHoly:
case MainDamageType.RadialGarlic:
case MainDamageType.WeatherLightning:
case MainDamageType.Corruption:
// This is environmental or item damage
break;
}
}
}

sourceEntity.TryGetComponent<EntityOwner>(out var damageOwner);
if (damageOwner.Owner.TryGetComponent<PlayerCharacter>(out var sourcePlayerCharacter))
if (sourceEntity.TryGetComponent<EntityOwner>(out var damageOwner) &&
damageOwner.Owner.TryGetComponent<PlayerCharacter>(out var sourcePlayerCharacter) &&
damageOwner.Owner.TryGetComponent<UnitStats>(out var stats))
{
var abilityGuid = Helper.GetPrefabGUID(sourceEntity);
LogDamage(damageOwner, targetEntity, abilityGuid, spellFactor, physicalFactor);

var masteryType = MasteryHelper.GetMasteryTypeForEffect(abilityGuid.GuidHash, out var ignore, out var uncertain);

float divisor = masteryType == MasteryType.Spell ? stats.SpellPower : stats.PhysicalPower;

LogDamage(damageOwner, targetEntity, abilityGuid, -change, divisor);
if (ignore)
{
return;
}
if (uncertain)
{
LogDamage(damageOwner, targetEntity, abilityGuid, spellFactor, physicalFactor, "NEEDS SUPPORT: ", true);
if (spellFactor > physicalFactor) masteryType = GlobalMasterySystem.MasteryType.Spell;
LogDamage(damageOwner, targetEntity, abilityGuid, change, divisor, "NEEDS SUPPORT: ", true);
return;
}

sourcePlayerCharacter.UserEntity.TryGetComponent<User>(out var sourceUser);
var hasStats = targetEntity.TryGetComponent<UnitStats>(out var victimStats);
var hasLevel = targetEntity.Has<UnitLevel>();
var hasLevel = targetEntity.TryGetComponent<UnitLevel>(out var targetLevel);
var hasMovement = targetEntity.Has<Movement>();
if (hasStats && hasLevel && hasMovement)
if (hasLevel && hasMovement)
{
var damageFactor = masteryType == MasteryType.Spell ? spellFactor : physicalFactor;
var skillMultiplier = damageFactor > 0 ? damageFactor : 1f;
var masteryValue =
MathF.Max(victimStats.PhysicalPower.Value, victimStats.SpellPower.Value) * skillMultiplier;
WeaponMasterySystem.UpdateMastery(sourceUser.PlatformId, masteryType, masteryValue, targetEntity);
var currentMastery = Math.Max(Database.PlayerMastery[sourceUser.PlatformId][masteryType].Mastery, 0.1);
var levelMultiplier = Math.Clamp(targetLevel.Level / currentMastery, 0.1f, 1.3f);
var masteryValue = -change / divisor;
WeaponMasterySystem.UpdateMastery(sourceUser.PlatformId, masteryType, masteryValue * levelMultiplier, targetEntity);
}
else
{
Plugin.Log(Plugin.LogSystem.Mastery, LogLevel.Info, $"Prefab {DebugTool.GetPrefabName(targetEntity)} has [S: {hasStats}, L: {hasLevel}, M: {hasMovement}]");
Plugin.Log(Plugin.LogSystem.Mastery, LogLevel.Info, $"Prefab {DebugTool.GetPrefabName(targetEntity)} has [L: {hasLevel}, M: {hasMovement}]");
}
}
}

public static void UpdateMastery(ulong steamID, MasteryType masteryType, double victimPower, Entity victimEntity)
public static void UpdateMastery(ulong steamID, MasteryType masteryType, double masteryValue, Entity victimEntity)
{
var isVBlood = Helper.IsVBlood(victimEntity);
double masteryValue = victimPower;

var vBloodMultiplier = isVBlood ? VBloodMultiplier : 1;
var changeInMastery = masteryValue * vBloodMultiplier * MasteryGainMultiplier * 0.001;
var changeInMastery = masteryValue * vBloodMultiplier * MasteryGainMultiplier * 0.02;

Plugin.Log(Plugin.LogSystem.Mastery, LogLevel.Info, $"Banking weapon mastery for {steamID}: {Enum.GetName(masteryType)}: [{masteryValue},{changeInMastery}]");
Plugin.Log(Plugin.LogSystem.Mastery, LogLevel.Info, $"Banking weapon mastery for {steamID}: {Enum.GetName(masteryType)}: [{masteryValue:F4},{changeInMastery:F4}]");
GlobalMasterySystem.BankMastery(steamID, victimEntity, masteryType, changeInMastery);
}

Expand Down Expand Up @@ -156,14 +129,14 @@ public static MasteryType WeaponToMasteryType(WeaponType weapon)
}
}

private static void LogDamage(Entity source, Entity target, PrefabGUID abilityPrefab, float spellFactor, float physicalFactor, string prefix = "", bool forceLog = false)
private static void LogDamage(Entity source, Entity target, PrefabGUID abilityPrefab, float change, float divisor, string prefix = "", bool forceLog = false)
{
Plugin.Log(Plugin.LogSystem.Mastery, LogLevel.Info,
() =>
$"{prefix}{GetName(source, out _)} -> " +
$"({DebugTool.GetPrefabName(abilityPrefab)}) -> " +
$"{GetName(target, out _)}" +
$"[spell: {spellFactor}, phys: {physicalFactor}]", forceLog);
$"[diff: {change}, div: {divisor}, val: {change/divisor}]", forceLog);
}

private static string GetName(Entity entity, out bool isUser)
Expand Down
Loading