diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..164567e --- /dev/null +++ b/.github/FUNDING.yml @@ -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'] diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b5a9a8b..6996302 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 \ No newline at end of file + 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 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index acb11dc..c6db058 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.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 diff --git a/README.md b/README.md index 041af64..96be60e 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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) @@ -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! \ No newline at end of file +> 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) \ No newline at end of file diff --git a/XPRising/Configuration/GlobalMasteryConfig.cs b/XPRising/Configuration/GlobalMasteryConfig.cs index ef1d30b..61ac677 100644 --- a/XPRising/Configuration/GlobalMasteryConfig.cs +++ b/XPRising/Configuration/GlobalMasteryConfig.cs @@ -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; @@ -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; diff --git a/XPRising/Hooks/BuffHook.cs b/XPRising/Hooks/BuffHook.cs index 1007417..e5edd46 100644 --- a/XPRising/Hooks/BuffHook.cs +++ b/XPRising/Hooks/BuffHook.cs @@ -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(entity, out var entityOwner)) return; if (!em.TryGetComponentData(entityOwner.Owner, out var playerCharacter)) return; + if (!target.Target._Entity.Has()) return; PlayerCache.FindPlayer(playerCharacter.Name.ToString(), true, out _, out var userEntity); // target.BloodConsumeSource can buff/debuff the blood quality diff --git a/XPRising/Hooks/StatChangeSystemHook.cs b/XPRising/Hooks/StatChangeSystemHook.cs index 3b0b933..74a87e4 100644 --- a/XPRising/Hooks/StatChangeSystemHook.cs +++ b/XPRising/Hooks/StatChangeSystemHook.cs @@ -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()) break; - WeaponMasterySystem.HandleDamageEvent(statChangeEvent.Source, statChangeEvent.Entity); + WeaponMasterySystem.HandleDamageEvent(statChangeEvent.Source, statChangeEvent.Entity, statChangeEvent.OriginalChange); break; case StatChangeReason.Any: case StatChangeReason.Default: diff --git a/XPRising/README_TS.md b/XPRising/README_TS.md index 318549b..99d73b4 100644 --- a/XPRising/README_TS.md +++ b/XPRising/README_TS.md @@ -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 @@ -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) \ No newline at end of file +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) \ No newline at end of file diff --git a/XPRising/Systems/GlobalMasterySystem.cs b/XPRising/Systems/GlobalMasterySystem.cs index cf4c94d..f09463f 100644 --- a/XPRising/Systems/GlobalMasterySystem.cs +++ b/XPRising/Systems/GlobalMasterySystem.cs @@ -412,10 +412,10 @@ private static double ModMastery(ulong steamID, LazyDictionary /// Calculates and banks any mastery increases for the damage event /// /// The ability that is dealing damage to the target /// The target that is receiving the damage - public static void HandleDamageEvent(Entity sourceEntity, Entity targetEntity) + /// The HP change due to this event. For damage events, this is a negative number. + public static void HandleDamageEvent(Entity sourceEntity, Entity targetEntity, float change) { - var spellFactor = 0f; - var physicalFactor = 0f; - if (sourceEntity.TryGetBuffer(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(out var damageOwner); - if (damageOwner.Owner.TryGetComponent(out var sourcePlayerCharacter)) + if (sourceEntity.TryGetComponent(out var damageOwner) && + damageOwner.Owner.TryGetComponent(out var sourcePlayerCharacter) && + damageOwner.Owner.TryGetComponent(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(out var sourceUser); - var hasStats = targetEntity.TryGetComponent(out var victimStats); - var hasLevel = targetEntity.Has(); + var hasLevel = targetEntity.TryGetComponent(out var targetLevel); var hasMovement = targetEntity.Has(); - 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); } @@ -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) diff --git a/XPRising/Utils/MasteryHelper.cs b/XPRising/Utils/MasteryHelper.cs index 7ecdae6..43e40d4 100644 --- a/XPRising/Utils/MasteryHelper.cs +++ b/XPRising/Utils/MasteryHelper.cs @@ -207,6 +207,7 @@ public static GlobalMasterySystem.MasteryType GetMasteryTypeForEffect(int effect case Effects.AB_Vampire_VeilOfIllusion_TriggerBonusEffects: case Effects.AB_Vampire_VeilOfShadow_TriggerBonusEffects: case Effects.AB_Vampire_VeilOfStorm_TriggerBonusEffects: + case Effects.AB_Vampire_VeilOfStorm_SpellMod_SparklingIllusion: // Blood case Effects.AB_Blood_BloodFountain_Ground_Impact: case Effects.AB_Blood_BloodFountain_Spellmod_Recast_Ground_Impact: @@ -231,6 +232,7 @@ public static GlobalMasterySystem.MasteryType GetMasteryTypeForEffect(int effect case Effects.AB_Blood_VampiricCurse_SpellMod_Area: // Chaos case Effects.AB_Chaos_Aftershock_AreaThrow: + case Effects.AB_Chaos_Aftershock_GreatSword_AreaThrow: case Effects.AB_Chaos_Aftershock_GreatSword_Projectile: case Effects.AB_Chaos_Aftershock_Projectile: case Effects.AB_Chaos_Aftershock_SpellMod_KnockbackArea: @@ -268,12 +270,16 @@ public static GlobalMasterySystem.MasteryType GetMasteryTypeForEffect(int effect case Effects.AB_Frost_IceNova_RingArea: case Effects.AB_Frost_IceNova_SpellMod_Recast_Throw: case Effects.AB_Frost_IceNova_Throw: + case Effects.AB_Frost_Passive_FrostNova: + case Effects.AB_Frost_Passive_FrostNova_ChillWeave: case Effects.AB_Frost_Shared_SpellMod_FrostWeapon_Buff: case Effects.AB_FrostBarrier_Pulse: case Effects.AB_FrostBarrier_Recast_Cone: case Effects.AB_FrostCone_Cone: // Illusion case Effects.AB_Illusion_Curse_Debuff: + case Effects.AB_Illusion_Curse_Projectile: + case Effects.AB_Illusion_MistTrance_SpellMod_DamageOnAttackBuff: case Effects.AB_Illusion_Mosquito_Area_Explosion: case Effects.AB_Illusion_Mosquito_Summon: case Effects.AB_Illusion_PhantomAegis_SpellMod_Explode: @@ -363,11 +369,17 @@ public static GlobalMasterySystem.MasteryType GetMasteryTypeForEffect(int effect case Effects.AB_Lucie_PlayerAbility_WondrousHealingPotion_Throw_Throw: // Throwing potion back to boss // ignore weapon coatings case Effects.AB_Vampire_Coating_Blood_Area: + case Effects.AB_Vampire_Coating_Blood_Buff: case Effects.AB_Vampire_Coating_Chaos_Area: + case Effects.AB_Vampire_Coating_Chaos_Buff: case Effects.AB_Vampire_Coating_Frost_Area: + case Effects.AB_Vampire_Coating_Frost_Buff: case Effects.AB_Vampire_Coating_Frost_Stagger_Buff: + case Effects.AB_Vampire_Coating_Illusion_Area: case Effects.AB_Vampire_Coating_Illusion_Buff: case Effects.AB_Vampire_Coating_Storm_Buff: + case Effects.AB_Vampire_Coating_Unholy_Area: + case Effects.AB_Vampire_Coating_Unholy_BoneSpirit: case Effects.AB_Vampire_Coating_Unholy_BoneSpirit_HitBuff: case Effects.AB_Vampire_Coating_Unholy_Buff: ignore = true; @@ -431,16 +443,20 @@ 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) - // Not sure why units are appearing in this list. They always seem to be from user -> CHAR_Militia_Fabian_VBlood? - case (int)Units.CHAR_Gloomrot_SpiderTank_Zapper: - case (int)Units.CHAR_Unholy_SkeletonWarrior_Summon: - case (int)Units.CHAR_Gloomrot_TractorBeamer: case (int)Effects.AB_Vampire_Horse_Severance_Buff: Plugin.Log(Plugin.LogSystem.Mastery, LogLevel.Info, $"{effect} has been through mastery helper as being ignored - check this"); ignore = true; return GlobalMasterySystem.MasteryType.None; } + // CHAR_Militia_Fabian_VBlood summons a steed that if a player minion hits, it provides the entity of the minion as the source. + // Ignore all minion attacks. + if (Enum.IsDefined(typeof(Units), effect)) + { + ignore = true; + return GlobalMasterySystem.MasteryType.None; + } + uncertain = true; return GlobalMasterySystem.MasteryType.None; }