100+ achievements, forfeit, leagues floor, bot humanize, 95k starter
Achievements: generator-driven, now 100+ across 7 categories (added Rulership) mirrored client + server with identical ids/goals/coins. New tracked stats: hakemRounds (be the hakem — incl. "7× Hakem"), roundsWon, plus losses metric. Custom achievement-only sticker packs (Rulership 👑, Firestorm 🔥) with new inline-SVG art (crown-gold, seven-zip, streak-fire), unlocked by hakem_7 / streak_10. Server GameRoom tallies hakem rounds per seat + rounds won per team; client tallies the same for vs-computer/private games (dealId-deduped). Forfeit (surrender): a player can request forfeit; if the teammate is a bot it auto-confirms, otherwise the human teammate gets a confirm/decline prompt (20s timeout). Result: forfeiting with ≥1 round won = normal loss; 0 rounds = Kot. Wired client↔server over the hub (RequestForfeit/ConfirmForfeit/DeclineForfeit + "forfeit" event); offline/vs-computer ends immediately in the store. Flag button + confirm dialogs in the table. Online count: never shows below 50 — live service floors the real count with a drifting believable number (mock base lowered to ~50–170). Matchmaking: real players get a longer priority window (9s) before bots fill; bots now occasionally react after winning a trick (humanize). Coins: starter pack is 95,000 Toman (50k coins); packs rescaled up (server + mock). Verified: dotnet build + tsc + next build clean; sim unlocks 57 achievements/500 matches; live server: starter=95000, a 7-hakem win unlocks hakem_7 + wins_1 with hakemRounds/roundsWon persisted. Images rebuilt on :1500/:1505. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,9 @@
|
||||
|
||||
import {
|
||||
AchievementCategoryDef,
|
||||
AchievementCategoryId,
|
||||
AchievementDef,
|
||||
AchievementMetric,
|
||||
AchievementUnlock,
|
||||
CardBackDef,
|
||||
CardFrontDef,
|
||||
@@ -167,73 +169,90 @@ export const ACHIEVEMENT_CATEGORIES: AchievementCategoryDef[] = [
|
||||
{ id: "victory", nameFa: "بردها", nameEn: "Victories", icon: "🏆" },
|
||||
{ id: "kot", nameFa: "کُت", nameEn: "Kot", icon: "🔥" },
|
||||
{ id: "streak", nameFa: "نوار پیروزی", nameEn: "Streaks", icon: "⚡" },
|
||||
{ id: "hakem", nameFa: "حاکمیت", nameEn: "Rulership", icon: "👑" },
|
||||
{ id: "level", nameFa: "سطح", nameEn: "Levels", icon: "⭐" },
|
||||
{ id: "rank", nameFa: "لیگ", nameEn: "Ranks", icon: "🏅" },
|
||||
{ id: "veteran", nameFa: "کارنامه", nameEn: "Veterancy", icon: "🎮" },
|
||||
];
|
||||
|
||||
const FA_DIGITS = "۰۱۲۳۴۵۶۷۸۹";
|
||||
/** Western → Persian digits, for generated achievement names. */
|
||||
export function faNum(n: number): string {
|
||||
return String(n).replace(/\d/g, (d) => FA_DIGITS[+d]);
|
||||
}
|
||||
|
||||
/** Build a tiered family of achievements for one metric (keeps the list DRY). */
|
||||
function tier(
|
||||
category: AchievementCategoryId,
|
||||
metric: AchievementMetric,
|
||||
prefix: string,
|
||||
icon: string,
|
||||
goals: number[],
|
||||
faName: (g: string) => string,
|
||||
enName: (g: number) => string,
|
||||
faDesc: (g: string) => string,
|
||||
enDesc: (g: number) => string
|
||||
): AchievementDef[] {
|
||||
return goals.map((g) => ({
|
||||
id: `${prefix}_${g}`,
|
||||
category,
|
||||
metric,
|
||||
goal: g,
|
||||
coinReward: Math.max(100, Math.round((80 + g * 12) / 50) * 50),
|
||||
icon,
|
||||
nameFa: faName(faNum(g)),
|
||||
nameEn: enName(g),
|
||||
descFa: faDesc(faNum(g)),
|
||||
descEn: enDesc(g),
|
||||
}));
|
||||
}
|
||||
|
||||
export const ACHIEVEMENTS: AchievementDef[] = [
|
||||
// ---- Victories (wins + shutouts) ----
|
||||
{ id: "first_win", category: "victory", metric: "wins", goal: 1, coinReward: 100, icon: "🥇", nameFa: "اولین برد", nameEn: "First Win", descFa: "اولین بازی خود را ببرید", descEn: "Win your first game" },
|
||||
{ id: "wins_10", category: "victory", metric: "wins", goal: 10, coinReward: 300, icon: "🎯", nameFa: "۱۰ برد", nameEn: "10 Wins", descFa: "۱۰ بازی ببرید", descEn: "Win 10 games" },
|
||||
{ id: "wins_25", category: "victory", metric: "wins", goal: 25, coinReward: 600, icon: "🏅", nameFa: "۲۵ برد", nameEn: "25 Wins", descFa: "۲۵ بازی ببرید", descEn: "Win 25 games" },
|
||||
{ id: "wins_50", category: "victory", metric: "wins", goal: 50, coinReward: 1000, icon: "🏆", nameFa: "۵۰ برد", nameEn: "50 Wins", descFa: "۵۰ بازی ببرید", descEn: "Win 50 games" },
|
||||
{ id: "wins_100", category: "victory", metric: "wins", goal: 100, coinReward: 2000, icon: "👑", nameFa: "۱۰۰ برد", nameEn: "100 Wins", descFa: "۱۰۰ بازی ببرید (پک استیکر ایرانی)", descEn: "Win 100 games (unlocks Persian stickers)" },
|
||||
{ id: "wins_250", category: "victory", metric: "wins", goal: 250, coinReward: 4000, icon: "💎", nameFa: "۲۵۰ برد", nameEn: "250 Wins", descFa: "۲۵۰ بازی ببرید", descEn: "Win 250 games" },
|
||||
{ id: "wins_500", category: "victory", metric: "wins", goal: 500, coinReward: 8000, icon: "🌟", nameFa: "۵۰۰ برد", nameEn: "500 Wins", descFa: "۵۰۰ بازی ببرید", descEn: "Win 500 games" },
|
||||
{ id: "shutout_1", category: "victory", metric: "shutoutWins", goal: 1, coinReward: 400, icon: "🧹", nameFa: "هفت–هیچ", nameEn: "Seven–Zip", descFa: "بازی را ۷–۰ ببرید (پک استیکر حکم)", descEn: "Win a match 7–0 (unlocks Hokm stickers)" },
|
||||
{ id: "shutout_5", category: "victory", metric: "shutoutWins", goal: 5, coinReward: 900, icon: "🧨", nameFa: "۵ بار هفت–هیچ", nameEn: "5× Sweep", descFa: "۵ بار حریف را ۷–۰ ببرید", descEn: "Sweep the opponent 5 times" },
|
||||
{ id: "shutout_25", category: "victory", metric: "shutoutWins", goal: 25, coinReward: 3000, icon: "☄️", nameFa: "۲۵ بار هفت–هیچ", nameEn: "25× Sweep", descFa: "۲۵ بار حریف را ۷–۰ ببرید", descEn: "Sweep the opponent 25 times" },
|
||||
|
||||
// ---- Kot ----
|
||||
{ id: "first_kot", category: "kot", metric: "kotsFor", goal: 1, coinReward: 150, icon: "🔥", nameFa: "اولین کُت", nameEn: "First Kot", descFa: "یک بار حریف را کُت کنید", descEn: "Inflict a Kot once" },
|
||||
{ id: "kot_5", category: "kot", metric: "kotsFor", goal: 5, coinReward: 300, icon: "🌶️", nameFa: "۵ کُت", nameEn: "5 Kots", descFa: "۵ بار حریف را کُت کنید", descEn: "Inflict 5 Kots" },
|
||||
{ id: "kot_10", category: "kot", metric: "kotsFor", goal: 10, coinReward: 500, icon: "🔥", nameFa: "۱۰ کُت", nameEn: "10 Kots", descFa: "۱۰ بار حریف را کُت کنید", descEn: "Inflict 10 Kots" },
|
||||
{ id: "kot_25", category: "kot", metric: "kotsFor", goal: 25, coinReward: 1200, icon: "💥", nameFa: "۲۵ کُت", nameEn: "25 Kots", descFa: "۲۵ بار حریف را کُت کنید (پک استیکر طعنه)", descEn: "Inflict 25 Kots (unlocks Taunt stickers)" },
|
||||
{ id: "kot_50", category: "kot", metric: "kotsFor", goal: 50, coinReward: 2500, icon: "⚡", nameFa: "۵۰ کُت", nameEn: "50 Kots", descFa: "۵۰ بار حریف را کُت کنید", descEn: "Inflict 50 Kots" },
|
||||
{ id: "kot_100", category: "kot", metric: "kotsFor", goal: 100, coinReward: 5000, icon: "👹", nameFa: "۱۰۰ کُت", nameEn: "100 Kots", descFa: "۱۰۰ بار حریف را کُت کنید", descEn: "Inflict 100 Kots" },
|
||||
|
||||
// ---- Streaks ----
|
||||
{ id: "streak_3", category: "streak", metric: "bestWinStreak", goal: 3, coinReward: 200, icon: "➡️", nameFa: "۳ برد پیاپی", nameEn: "3 Win Streak", descFa: "۳ بازی پشت سر هم ببرید", descEn: "Win 3 games in a row" },
|
||||
{ id: "streak_5", category: "streak", metric: "bestWinStreak", goal: 5, coinReward: 400, icon: "⚡", nameFa: "۵ برد پیاپی", nameEn: "5 Win Streak", descFa: "۵ بازی پشت سر هم ببرید", descEn: "Win 5 games in a row" },
|
||||
{ id: "streak_10", category: "streak", metric: "bestWinStreak", goal: 10, coinReward: 1000, icon: "🌊", nameFa: "۱۰ برد پیاپی", nameEn: "10 Win Streak", descFa: "۱۰ بازی پشت سر هم ببرید", descEn: "Win 10 games in a row" },
|
||||
{ id: "streak_15", category: "streak", metric: "bestWinStreak", goal: 15, coinReward: 2000, icon: "🚀", nameFa: "۱۵ برد پیاپی", nameEn: "15 Win Streak", descFa: "۱۵ بازی پشت سر هم ببرید", descEn: "Win 15 games in a row" },
|
||||
|
||||
// ---- Levels (every 5) ----
|
||||
{ id: "level_5", category: "level", metric: "level", goal: 5, coinReward: 150, icon: "⭐", nameFa: "سطح ۵", nameEn: "Level 5", descFa: "به سطح ۵ برسید", descEn: "Reach level 5" },
|
||||
{ id: "level_10", category: "level", metric: "level", goal: 10, coinReward: 300, icon: "🌟", nameFa: "سطح ۱۰", nameEn: "Level 10", descFa: "به سطح ۱۰ برسید", descEn: "Reach level 10" },
|
||||
{ id: "level_15", category: "level", metric: "level", goal: 15, coinReward: 500, icon: "✨", nameFa: "سطح ۱۵", nameEn: "Level 15", descFa: "به سطح ۱۵ برسید", descEn: "Reach level 15" },
|
||||
{ id: "level_20", category: "level", metric: "level", goal: 20, coinReward: 800, icon: "💫", nameFa: "سطح ۲۰", nameEn: "Level 20", descFa: "به سطح ۲۰ برسید", descEn: "Reach level 20" },
|
||||
{ id: "level_25", category: "level", metric: "level", goal: 25, coinReward: 1200, icon: "🔆", nameFa: "سطح ۲۵", nameEn: "Level 25", descFa: "به سطح ۲۵ برسید (آپلود عکس باز میشود)", descEn: "Reach level 25 (unlocks photo upload)" },
|
||||
{ id: "level_30", category: "level", metric: "level", goal: 30, coinReward: 1600, icon: "🎖️", nameFa: "سطح ۳۰", nameEn: "Level 30", descFa: "به سطح ۳۰ برسید", descEn: "Reach level 30" },
|
||||
{ id: "level_40", category: "level", metric: "level", goal: 40, coinReward: 2500, icon: "🏵️", nameFa: "سطح ۴۰", nameEn: "Level 40", descFa: "به سطح ۴۰ برسید", descEn: "Reach level 40" },
|
||||
{ id: "level_50", category: "level", metric: "level", goal: 50, coinReward: 4000, icon: "🌠", nameFa: "سطح ۵۰", nameEn: "Level 50", descFa: "به سطح ۵۰ برسید", descEn: "Reach level 50" },
|
||||
|
||||
// ---- Ranks ----
|
||||
...tier("victory", "wins", "wins", "🏆",
|
||||
[1, 5, 10, 25, 50, 75, 100, 150, 200, 250, 300, 400, 500, 750, 1000, 2000],
|
||||
(g) => `${g} برد`, (g) => `${g} Wins`, (g) => `${g} بازی ببرید`, (g) => `Win ${g} games`),
|
||||
...tier("victory", "shutoutWins", "shutout", "🧹", [1, 3, 5, 10, 25, 50, 100],
|
||||
(g) => `${g} بار هفت–هیچ`, (g) => `${g}× Sweep`,
|
||||
(g) => `${g} بار حریف را ۷–۰ ببرید`, (g) => `Sweep the opponent ${g}×`),
|
||||
...tier("kot", "kotsFor", "kot", "🔥", [1, 3, 5, 10, 25, 50, 75, 100, 150, 200, 300, 500],
|
||||
(g) => `${g} کُت`, (g) => `${g} Kots`, (g) => `${g} بار حریف را کُت کنید`, (g) => `Inflict ${g} Kots`),
|
||||
...tier("streak", "bestWinStreak", "streak", "⚡", [2, 3, 5, 7, 10, 15, 20, 25, 30, 40],
|
||||
(g) => `${g} برد پیاپی`, (g) => `${g} Win Streak`,
|
||||
(g) => `${g} بازی پشت سر هم ببرید`, (g) => `Win ${g} games in a row`),
|
||||
...tier("hakem", "hakemRounds", "hakem", "👑", [7, 25, 50, 100, 250, 500, 1000],
|
||||
(g) => `${g} بار حاکم`, (g) => `Hakem ${g}×`,
|
||||
(g) => `${g} دست حاکم شوید`, (g) => `Be the hakem in ${g} rounds`),
|
||||
...tier("level", "level", "level", "⭐",
|
||||
[5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 60, 70, 80, 90, 100],
|
||||
(g) => `سطح ${g}`, (g) => `Level ${g}`, (g) => `به سطح ${g} برسید`, (g) => `Reach level ${g}`),
|
||||
...tier("veteran", "games", "games", "🎮", [10, 25, 50, 100, 200, 300, 500, 750, 1000, 2000, 5000],
|
||||
(g) => `${g} بازی`, (g) => `${g} Games`, (g) => `${g} بازی انجام دهید`, (g) => `Play ${g} games`),
|
||||
...tier("veteran", "roundsWon", "rounds", "🎴", [25, 100, 250, 500, 1000, 2000, 5000],
|
||||
(g) => `${g} دست برده`, (g) => `${g} Rounds Won`, (g) => `${g} دست ببرید`, (g) => `Win ${g} rounds`),
|
||||
...tier("veteran", "tricks", "tricks", "🗂️", [50, 100, 250, 500, 1000, 2500, 5000, 10000],
|
||||
(g) => `${g} دستبرد`, (g) => `${g} Tricks`, (g) => `${g} دستبرد بگیرید`, (g) => `Win ${g} tricks`),
|
||||
...tier("veteran", "losses", "grit", "🛡️", [10, 50, 100],
|
||||
(g) => `${g} باخت`, (g) => `${g} Losses`,
|
||||
(g) => `با وجود ${g} باخت ادامه دهید`, (g) => `Persevere through ${g} losses`),
|
||||
// ranks (explicit rating floors)
|
||||
{ id: "reach_silver", category: "rank", ratingFloor: 1100, goal: 1, coinReward: 200, icon: "🥈", nameFa: "لیگ نقره", nameEn: "Reach Silver", descFa: "به لیگ نقره برسید", descEn: "Reach the Silver league" },
|
||||
{ id: "reach_gold", category: "rank", ratingFloor: 1300, goal: 1, coinReward: 500, icon: "🥇", nameFa: "لیگ طلا", nameEn: "Reach Gold", descFa: "به لیگ طلا برسید", descEn: "Reach the Gold league" },
|
||||
{ id: "reach_platinum", category: "rank", ratingFloor: 1500, goal: 1, coinReward: 1000, icon: "🛡️", nameFa: "لیگ پلاتین", nameEn: "Reach Platinum", descFa: "به لیگ پلاتین برسید", descEn: "Reach the Platinum league" },
|
||||
{ id: "reach_diamond", category: "rank", ratingFloor: 1700, goal: 1, coinReward: 2000, icon: "💠", nameFa: "لیگ الماس", nameEn: "Reach Diamond", descFa: "به لیگ الماس برسید", descEn: "Reach the Diamond league" },
|
||||
{ id: "reach_master", category: "rank", ratingFloor: 1900, goal: 1, coinReward: 4000, icon: "👑", nameFa: "لیگ استاد", nameEn: "Reach Master", descFa: "به لیگ استاد برسید", descEn: "Reach the Master league" },
|
||||
|
||||
// ---- Veterancy (games + tricks) ----
|
||||
{ id: "games_10", category: "veteran", metric: "games", goal: 10, coinReward: 150, icon: "🎮", nameFa: "۱۰ بازی", nameEn: "10 Games", descFa: "۱۰ بازی انجام دهید", descEn: "Play 10 games" },
|
||||
{ id: "games_50", category: "veteran", metric: "games", goal: 50, coinReward: 350, icon: "🕹️", nameFa: "۵۰ بازی", nameEn: "50 Games", descFa: "۵۰ بازی انجام دهید", descEn: "Play 50 games" },
|
||||
{ id: "games_200", category: "veteran", metric: "games", goal: 200, coinReward: 1200, icon: "🎲", nameFa: "۲۰۰ بازی", nameEn: "200 Games", descFa: "۲۰۰ بازی انجام دهید", descEn: "Play 200 games" },
|
||||
{ id: "games_500", category: "veteran", metric: "games", goal: 500, coinReward: 3000, icon: "🃏", nameFa: "۵۰۰ بازی", nameEn: "500 Games", descFa: "۵۰۰ بازی انجام دهید", descEn: "Play 500 games" },
|
||||
{ id: "games_1000", category: "veteran", metric: "games", goal: 1000, coinReward: 7000, icon: "♾️", nameFa: "۱۰۰۰ بازی", nameEn: "1000 Games", descFa: "۱۰۰۰ بازی انجام دهید", descEn: "Play 1000 games" },
|
||||
{ id: "tricks_100", category: "veteran", metric: "tricks", goal: 100, coinReward: 300, icon: "🎴", nameFa: "۱۰۰ دست", nameEn: "100 Tricks", descFa: "۱۰۰ دست ببرید", descEn: "Win 100 tricks" },
|
||||
{ id: "tricks_1000", category: "veteran", metric: "tricks", goal: 1000, coinReward: 2000, icon: "🗂️", nameFa: "۱۰۰۰ دست", nameEn: "1000 Tricks", descFa: "۱۰۰۰ دست ببرید", descEn: "Win 1000 tricks" },
|
||||
];
|
||||
|
||||
function metricValue(metric: NonNullable<AchievementDef["metric"]>, stats: PlayerStats, level: number): number {
|
||||
switch (metric) {
|
||||
case "wins": return stats.wins;
|
||||
case "losses": return stats.losses;
|
||||
case "kotsFor": return stats.kotsFor;
|
||||
case "bestWinStreak": return stats.bestWinStreak;
|
||||
case "shutoutWins": return stats.shutoutWins ?? 0;
|
||||
case "games": return stats.games;
|
||||
case "tricks": return stats.tricks;
|
||||
case "hakemRounds": return stats.hakemRounds ?? 0;
|
||||
case "roundsWon": return stats.roundsWon ?? 0;
|
||||
case "level": return level;
|
||||
}
|
||||
}
|
||||
@@ -398,6 +417,9 @@ export const STICKER_PACKS: StickerPackDef[] = [
|
||||
{ id: "persian", nameFa: "ایرانی", nameEn: "Persian", stickers: ["chai", "afarin", "rose"], price: 700, unlockAchievement: "wins_100" },
|
||||
// Earned by the "25 Kots" achievement.
|
||||
{ id: "taunt", nameFa: "طعنه", nameEn: "Taunts", stickers: ["clown", "sleep", "weak"], price: 900, unlockAchievement: "kot_25" },
|
||||
// Custom packs earned only via achievements.
|
||||
{ id: "rulership", nameFa: "حاکمیت", nameEn: "Rulership", stickers: ["crown-gold", "seven-zip"], price: 0, unlockAchievement: "hakem_7" },
|
||||
{ id: "firestorm", nameFa: "آتشین", nameEn: "Firestorm", stickers: ["streak-fire"], price: 0, unlockAchievement: "streak_10" },
|
||||
];
|
||||
|
||||
export function stickerPackById(id: string): StickerPackDef | undefined {
|
||||
@@ -440,6 +462,8 @@ function applyStats(stats: PlayerStats, summary: MatchSummary): PlayerStats {
|
||||
currentWinStreak,
|
||||
bestWinStreak: Math.max(stats.bestWinStreak, currentWinStreak),
|
||||
shutoutWins: (stats.shutoutWins ?? 0) + (summary.won && summary.shutout ? 1 : 0),
|
||||
hakemRounds: (stats.hakemRounds ?? 0) + (summary.hakemRounds ?? 0),
|
||||
roundsWon: (stats.roundsWon ?? 0) + (summary.roundsWon ?? 0),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user