grantXp method
Grants amount XP under transactionId (idempotent — calling this
again with the same id, even after a restart, is a no-op that returns
the current snapshot). If this crosses one or more level thresholds,
each newly-reached level's LevelDefinition.unlockRewardLines (if any)
is granted exactly once through pipeline (when supplied), and
onLevelUp fires once per level, in order — the level is capped at the
curve's max level regardless of how much XP is granted past it.
Implementation
Future<SdkResult<PlayerProgressionSnapshot>> grantXp({
required int amount,
required String transactionId,
}) => _guard.runExclusive('progression', () async {
if (amount <= 0 || transactionId.isEmpty) {
return const SdkFailure(
kind: SdkErrorKind.validation,
message: 'Invalid XP grant',
);
}
if (_transactions.contains(transactionId)) {
return SdkSuccess(snapshot.value);
}
final nextTotal = _totalXpEarned + amount;
if (nextTotal < _totalXpEarned || nextTotal > _maxSupportedCumulativeXp) {
return const SdkFailure(
kind: SdkErrorKind.validation,
message: 'XP grant would overflow the supported range',
);
}
final newLevel = levelForTotalXp(_curve, nextTotal);
final leveledUpTo = [
for (var lvl = _highestUnlockedLevel + 1; lvl <= newLevel; lvl++) lvl,
];
// Snapshot for rollback: if persisting fails the grant must not be
// remembered as done, or a retry with the same id returns success while
// nothing was ever saved.
final previousTotal = _totalXpEarned;
final previousHighest = _highestUnlockedLevel;
final previousTransactions = List<String>.of(_transactions);
_totalXpEarned = nextTotal;
_appendTransaction(transactionId);
if (leveledUpTo.isNotEmpty) _highestUnlockedLevel = newLevel;
try {
await _persist();
} catch (_) {
_totalXpEarned = previousTotal;
_highestUnlockedLevel = previousHighest;
_transactions
..clear()
..addAll(previousTransactions);
rethrow;
}
_recompute();
for (final lvl in leveledUpTo) {
final def = _curve[lvl - 1];
if (def.unlockRewardLines.isNotEmpty && pipeline != null) {
await pipeline!.grant(
source: RewardSource.other,
transactionId: 'level_up:$lvl',
lines: def.unlockRewardLines,
);
}
onLevelUp.value = LevelUpEvent(
level: lvl,
rewardLines: def.unlockRewardLines,
);
}
return SdkSuccess(snapshot.value);
});