PityTracker
Per-player pity/guarantee counters for loot, rewards, or any quality-tier system.
What is it for?
Use PityTracker when your mod has random drops with quality tiers and you want to guarantee a high-tier result after a streak of bad luck:
- Loot caches with Common / Rare / Epic / Legendary tiers.
- Reward chests that should not be frustrating forever.
- Gacha-like systems where the player eventually gets a top-tier item.
It persists via ModDataStore and can import legacy save data.
Core concepts
| Term | Meaning |
|---|---|
PityDefinition | A loot source and its guarantee rules. |
PityTierRule | A quality tier and the maximum opens until it is guaranteed. |
PityCounters | Per-player, per-definition counters. |
guaranteedQuality | The highest tier whose counter has reached its threshold. |
Quick example
using ArcanumLib.Core;
using ArcanumLib.Progression;
var tracker = ArcanumServices.Get<IPityTracker>()!;
// or: var tracker = PityTracker.Current!;
tracker.RegisterDefinition(new PityDefinition
{
definitionId = "mymod:rewards:common",
rules = new List<PityTierRule>
{
new() { qualityTierIndex = 3, opensUntilGuarantee = 30 },
new() { qualityTierIndex = 4, opensUntilGuarantee = 60 }
}
});
int guaranteed = tracker.GetGuaranteedQuality(playerUid, "mymod:rewards:common");
Usage
Register a definition
tracker.RegisterDefinition(new PityDefinition
{
definitionId = "mymod:rewards:common",
rules = new List<PityTierRule>
{
new() { qualityTierIndex = 3, opensUntilGuarantee = 30, displayNameKey = "mymod:rare" },
new() { qualityTierIndex = 4, opensUntilGuarantee = 60, displayNameKey = "mymod:legendary" }
}
});
Record an open
// player opened a "common" reward cache and rolled quality 2 (Uncommon)
tracker.RecordOpen(playerUid, "mymod:rewards:common", 2);
The tracker resets the counter for tiers <= 2 and increments tiers > 2.
Enforce the guarantee
int guaranteed = tracker.GetGuaranteedQuality(playerUid, "mymod:rewards:common");
int finalQuality = Math.Max(guaranteed, rolledQuality);
tracker.RecordOpen(playerUid, "mymod:rewards:common", finalQuality);
Check opens until guarantee
// How many opens until the next guaranteed high-tier drop?
int remaining = tracker.GetOpensUntilGuarantee(playerUid, "mymod:rewards:common");
// Or for a specific tier:
int untilLegendary = tracker.GetOpensUntilGuarantee(playerUid, "mymod:rewards:common", qualityTierIndex: 4);
Returns 0 if a guarantee is already due, or -1 if the definition/player is not found.
Legacy migration
var tracker = ArcanumServices.Get<IPityTracker>()!;
// or: var tracker = PityTracker.Current!;
tracker.AddLegacyFallbackKey("anothermod:oldpity");
If the new arcanumlib:pity store is empty, the tracker tries each registered legacy key once and imports the old counters.
Save
tracker.Save();
ModSystem integration
public class MyPityModSystem : ModSystem
{
private PityTracker? tracker;
public override void StartServerSide(ICoreServerAPI sapi)
{
tracker = ArcanumServices.Get<IPityTracker>()!;
// or: tracker = PityTracker.Current!;
// register definitions
}
public override void OnSaveGameData()
{
tracker?.Save();
}
}
Thread-safety and lifecycle
- All public methods on
PityTrackerare thread-safe (_syncLock). PityTracker.Currentis anIPityTracker?read-only facade backed byArcanumServices. Register the instance withArcanumServices.Register(tracker, ArcanumServiceScope.Server)and it will be returned here.PityTrackerModSystemcreates and saves the global tracker on the server.RecordOpenautomatically marks theModDataStoreas dirty so the nextSavepersists.