balance(achievements): strictly-escalating milestone coin rewards
The old reward formula rounded (40+6g)/50 on the raw goal, so adjacent milestones could pay the same (e.g. "1 win" and "5 wins" both 50) and the curve was lumpy — not a standard escalating-reward ladder. Reward now scales by tier index: 50, 150, 250 … capped at 1500, strictly increasing per milestone. Mirrored client (gamification.ts) + server (Gamification.cs) so live grants match. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -56,12 +56,13 @@ public static class Gamification
|
|||||||
// metric: wins|kotsFor|bestWinStreak|shutoutWins|games|tricks|level ; ratingFloor>0 = rank ach.
|
// metric: wins|kotsFor|bestWinStreak|shutoutWins|games|tricks|level ; ratingFloor>0 = rank ach.
|
||||||
private record AchDef(string Id, string? Metric, int RatingFloor, int Goal, int Coin, string NameFa, string NameEn, string Icon);
|
private record AchDef(string Id, string? Metric, int RatingFloor, int Goal, int Coin, string NameFa, string NameEn, string Icon);
|
||||||
// Mirrors src/lib/online/gamification.ts (same ids/goals/coins/metrics).
|
// Mirrors src/lib/online/gamification.ts (same ids/goals/coins/metrics).
|
||||||
private static int Coin(int g) => Math.Min(1500, Math.Max(50, (int)Math.Floor((40.0 + g * 6) / 50.0 + 0.5) * 50));
|
// Reward escalates strictly per milestone tier (50, 150, 250 … capped 1500).
|
||||||
|
private static int CoinAt(int i) => Math.Min(1500, 50 + i * 100);
|
||||||
private static string Fa(int n) =>
|
private static string Fa(int n) =>
|
||||||
new string(n.ToString().Select(c => c is >= '0' and <= '9' ? "۰۱۲۳۴۵۶۷۸۹"[c - '0'] : c).ToArray());
|
new string(n.ToString().Select(c => c is >= '0' and <= '9' ? "۰۱۲۳۴۵۶۷۸۹"[c - '0'] : c).ToArray());
|
||||||
|
|
||||||
private static AchDef[] Tier(string metric, string prefix, string icon, int[] goals, Func<int, string> faName, Func<int, string> enName)
|
private static AchDef[] Tier(string metric, string prefix, string icon, int[] goals, Func<int, string> faName, Func<int, string> enName)
|
||||||
=> goals.Select(g => new AchDef($"{prefix}_{g}", metric, 0, g, Coin(g), faName(g), enName(g), icon)).ToArray();
|
=> goals.Select((g, i) => new AchDef($"{prefix}_{g}", metric, 0, g, CoinAt(i), faName(g), enName(g), icon)).ToArray();
|
||||||
|
|
||||||
private static readonly AchDef[] Achs = BuildAchs();
|
private static readonly AchDef[] Achs = BuildAchs();
|
||||||
private static AchDef[] BuildAchs()
|
private static AchDef[] BuildAchs()
|
||||||
|
|||||||
@@ -253,12 +253,13 @@ function tier(
|
|||||||
faDesc: (g: string) => string,
|
faDesc: (g: string) => string,
|
||||||
enDesc: (g: number) => string
|
enDesc: (g: number) => string
|
||||||
): AchievementDef[] {
|
): AchievementDef[] {
|
||||||
return goals.map((g) => ({
|
return goals.map((g, i) => ({
|
||||||
id: `${prefix}_${g}`,
|
id: `${prefix}_${g}`,
|
||||||
category,
|
category,
|
||||||
metric,
|
metric,
|
||||||
goal: g,
|
goal: g,
|
||||||
coinReward: Math.min(1500, Math.max(50, Math.round((40 + g * 6) / 50) * 50)),
|
// Reward escalates strictly per milestone tier (50, 150, 250 … capped 1500).
|
||||||
|
coinReward: Math.min(1500, 50 + i * 100),
|
||||||
icon,
|
icon,
|
||||||
nameFa: faName(faNum(g)),
|
nameFa: faName(faNum(g)),
|
||||||
nameEn: enName(g),
|
nameEn: enName(g),
|
||||||
|
|||||||
Reference in New Issue
Block a user