Turn timer + auto-play, disconnect/reconnect, cosmetics, queue & paid plan

- Turn timer (20s) for play/trump; system auto-plays a smart move on timeout
- Disconnect handling (mock): wait-for-return countdown, system covers turns
- Cosmetics: titles, card-back styles, custom profile-image upload, badges;
  pickers in Profile; shop sells card styles; reward modal shows new titles
- Paid plan (pro): free players queue when server busy, pro skips; upgrade flow
- OnlineService extended (upgradePlan, richer profile patch); mock implements
  queue + plans; gamification adds TITLES + CARD_STYLES

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-04 10:49:54 +03:30
parent 5776036d78
commit 13ec0d4300
16 changed files with 682 additions and 61 deletions
+67
View File
@@ -4,12 +4,15 @@
import {
AchievementDef,
AchievementUnlock,
CardStyleDef,
LeagueInfo,
MatchSummary,
PlayerStats,
RankTier,
RankTierId,
RewardResult,
TitleDef,
TitleUnlock,
UserProfile,
} from "./types";
@@ -176,6 +179,55 @@ export function achievementProgress(
}
}
/* ------------------------------ Titles ------------------------------- */
export const TITLES: TitleDef[] = [
{ id: "novice", nameFa: "تازه‌کار", nameEn: "Novice", hintFa: "پیش‌فرض", hintEn: "Default" },
{ id: "winner", nameFa: "برنده", nameEn: "Winner", hintFa: "۱۰ برد", hintEn: "10 wins" },
{ id: "kot_master", nameFa: "استاد کُت", nameEn: "Kot Master", hintFa: "۱۰ کُت", hintEn: "10 kots" },
{ id: "veteran", nameFa: "کهنه‌کار", nameEn: "Veteran", hintFa: "سطح ۲۰", hintEn: "Level 20" },
{ id: "champion", nameFa: "قهرمان", nameEn: "Champion", hintFa: "لیگ طلا", hintEn: "Gold league" },
{ id: "legend", nameFa: "اسطوره", nameEn: "Legend", hintFa: "لیگ استاد", hintEn: "Master league" },
];
export function titleUnlocked(
id: string,
stats: PlayerStats,
rating: number,
level: number
): boolean {
switch (id) {
case "novice":
return true;
case "winner":
return stats.wins >= 10;
case "kot_master":
return stats.kotsFor >= 10;
case "veteran":
return level >= 20;
case "champion":
return rating >= tierById("gold").floor;
case "legend":
return rating >= tierById("master").floor;
default:
return false;
}
}
/* ---------------------------- Card styles ---------------------------- */
export const CARD_STYLES: CardStyleDef[] = [
{ id: "classic", nameFa: "کلاسیک", nameEn: "Classic", c1: "#14274f", c2: "#0a142e", accent: "#d4af37", price: 0 },
{ id: "sapphire", nameFa: "یاقوت کبود", nameEn: "Sapphire", c1: "#0b3a82", c2: "#06173a", accent: "#6aa6ff", price: 800 },
{ id: "ruby", nameFa: "یاقوت", nameEn: "Ruby", c1: "#7f1d2e", c2: "#2b0a12", accent: "#ff7a90", price: 800 },
{ id: "emerald", nameFa: "زمرد", nameEn: "Emerald", c1: "#0d6b5e", c2: "#062420", accent: "#2dd4bf", price: 1000 },
{ id: "royal", nameFa: "سلطنتی", nameEn: "Royal", c1: "#4a1d7f", c2: "#1a0a2e", accent: "#c77dff", price: 1500 },
];
export function cardStyleById(id: string): CardStyleDef {
return CARD_STYLES.find((c) => c.id === id) ?? CARD_STYLES[0];
}
/* ---------------------- Apply a match result ------------------------- */
function applyStats(stats: PlayerStats, summary: MatchSummary): PlayerStats {
@@ -239,6 +291,19 @@ export function applyMatchResult(
const coinsAfter = Math.max(0, coinsBefore + cDelta + achievementCoins);
// Titles unlocked by the new state.
const ownedTitles = [...(profile.ownedTitles ?? [])];
const newTitles: TitleUnlock[] = [];
for (const tdef of TITLES) {
if (
titleUnlocked(tdef.id, stats, ratingAfter, lvl.level) &&
!ownedTitles.includes(tdef.id)
) {
ownedTitles.push(tdef.id);
newTitles.push({ id: tdef.id, nameFa: tdef.nameFa, nameEn: tdef.nameEn });
}
}
const leagueBefore = getLeagueInfo(ratingBefore);
const leagueAfter = getLeagueInfo(ratingAfter);
const tierIndex = (id: RankTierId) => RANK_TIERS.findIndex((t) => t.id === id);
@@ -256,6 +321,7 @@ export function applyMatchResult(
stats,
achievements,
unlocked,
ownedTitles,
};
const reward: RewardResult = {
@@ -270,6 +336,7 @@ export function applyMatchResult(
levelAfter: lvl.level,
leveledUp: lvl.level > levelBefore,
newAchievements,
newTitles,
promoted,
demoted,
};