validateLevelCurve function
Checks a level curve's structural invariants before it's ever used to grant XP — a broken curve is rejected loudly at construction time (PlayerProgressionService's constructor throws) rather than producing silently-wrong level math later.
Implementation
SdkFailure<void>? validateLevelCurve(List<LevelDefinition> levels) {
if (levels.isEmpty) {
return const SdkFailure(
kind: SdkErrorKind.validation,
message: 'Level curve must not be empty',
);
}
final sorted = [...levels]..sort((a, b) => a.level.compareTo(b.level));
for (var i = 0; i < sorted.length; i++) {
final expected = i + 1;
if (sorted[i].level != expected) {
return SdkFailure(
kind: SdkErrorKind.validation,
message:
'Level curve must be contiguous starting at 1 — expected level '
'$expected, found ${sorted[i].level} (duplicate or missing level)',
);
}
}
var cumulative = 0;
for (var i = 0; i < sorted.length; i++) {
final def = sorted[i];
final isLast = i == sorted.length - 1;
if (isLast) {
if (def.xpToNext != 0) {
return const SdkFailure(
kind: SdkErrorKind.validation,
message:
'Last level in the curve must have xpToNext = 0 (marks max level)',
);
}
continue;
}
if (def.xpToNext <= 0) {
return SdkFailure(
kind: SdkErrorKind.validation,
message: 'Level ${def.level} must have xpToNext > 0',
);
}
if (i > 0 && def.xpToNext < sorted[i - 1].xpToNext) {
return SdkFailure(
kind: SdkErrorKind.validation,
message:
'Level curve must be non-decreasing (level ${def.level} costs less than the previous level)',
);
}
cumulative += def.xpToNext;
if (cumulative < 0 || cumulative > _maxSupportedCumulativeXp) {
return SdkFailure(
kind: SdkErrorKind.validation,
message: 'Level curve cumulative XP overflows the supported range',
);
}
}
return null;
}