feat: UNO-style table, social hub, cosmetics, speed mode, store IAB
Game table & play - UNO-style restyle: suit-aware bolder cards (+xl size), pulsing playable glow, big "YOUR TURN" pill, active-seat ring, trick-win particle burst, round confetti, match coin-rain. - Per-league turn time via turnMsForStake: 15s starter/AI, 10s pro, 7s expert; mirrored server-side in GameRoom.TurnMs. - Speed (Blitz) mode for vs-AI/private: 5s turns, race to 5, ~halved pacing. - Matchmaking waits ~15s (randomized 12-18s) then fills bots; elapsed timer + hint. Rewards / gifts - Richer post-match modal (floating coins, XP bar), celebration overlay reveals the unlocked sticker pack, boosted daily rewards (client+server synced), themed 7-day daily with special day-7. Social - Public profile modal (identity, stats, achievement board) from leaderboard / friends / discover / end-of-game roster; rate-limited add-friend (10/hour). - Social hub: Friends / Discover (player search + suggestions) / Messages inbox. - Profile gender (shown in finder/profile) + social links with public/friends/ hidden visibility, enforced server-side. Cosmetics - Distinct card backs: per-design pattern families (stripes/argyle/grid/dots/ rays/scales/crosshatch/royal/filigree/gem) + luxury motifs (lib/cardBack.ts), consistent on table/shop/profile; +Peacock/Rose-Gold backs. - Purchasable titles (shop Titles section); title shown under the seat on the table and in discover/public profile. - 10 new sticker packs (banter/kol-kol, Persian trends, court cards, moods). - Persistent level+XP bar on Home and every inner screen. Payments - Buy-coins gateway opens in a new tab (no SPA dead-end) + focus refresh. - Store IAB scaffolding: Cafe Bazaar deep-link purchase + redirect-token capture, Myket native-bridge contract, server-side IabService.Verify for both stores, config-driven via Iab__* env. POST /api/coins/iab/verify (JWT). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -161,6 +161,29 @@ export function leagueXpFactor(stake: number): number {
|
||||
/** XP multiplier for premium (pro) players. */
|
||||
export const PREMIUM_XP_MULT = 1.5;
|
||||
|
||||
/* ----------------------------- Turn time ----------------------------- */
|
||||
|
||||
/**
|
||||
* How long a player has to act, by league (derived from the coin stake). Higher
|
||||
* leagues give LESS time, so stronger players must think faster:
|
||||
* Starter / vs-AI / private (stake < 500) → 15s
|
||||
* Pro league (stake ≥ 500) → 10s
|
||||
* Expert league (stake ≥ 1000) → 7s
|
||||
* Both the offline client and the live server use this same mapping so the turn
|
||||
* clock matches in either mode.
|
||||
*/
|
||||
/** Blitz/speed-mode turn time — a flat, fast clock for casual quick games. */
|
||||
export const SPEED_TURN_MS = 5000;
|
||||
/** Speed mode races to fewer points so a match is over fast. */
|
||||
export const SPEED_TARGET_SCORE = 5;
|
||||
|
||||
export function turnMsForStake(stake: number, speed = false): number {
|
||||
if (speed) return SPEED_TURN_MS;
|
||||
if (stake >= 1000) return 7000;
|
||||
if (stake >= 500) return 10000;
|
||||
return 15000;
|
||||
}
|
||||
|
||||
export function matchXp(summary: MatchSummary): number {
|
||||
// Forfeiting (surrendering) earns no XP.
|
||||
if (summary.forfeit && !summary.won) return 0;
|
||||
@@ -362,8 +385,22 @@ export const TITLES: TitleDef[] = [
|
||||
{ id: "immortal", nameFa: "جاودانه", nameEn: "Immortal", hintFa: "سطح ۵۰", hintEn: "Level 50" },
|
||||
{ id: "the_one", nameFa: "یگانه", nameEn: "The One", hintFa: "۵۰۰ برد", hintEn: "500 wins" },
|
||||
{ id: "legend", nameFa: "اسطوره", nameEn: "Legend", hintFa: "لیگ استاد", hintEn: "Master league" },
|
||||
// ✨ Luxury titles — the most prestigious badges in the game
|
||||
{ id: "sultan", nameFa: "سلطان حکم", nameEn: "Hokm Sultan", hintFa: "۱۰۰ کُت", hintEn: "100 kots" },
|
||||
{ id: "emperor", nameFa: "امپراتور", nameEn: "Emperor", hintFa: "سطح ۷۵", hintEn: "Level 75" },
|
||||
{ id: "grandmaster", nameFa: "استاد بزرگ", nameEn: "Grandmaster", hintFa: "امتیاز ۲۱۰۰+", hintEn: "2100+ rating" },
|
||||
// 💰 Purchasable titles — buy with coins (shown in the shop's Titles section)
|
||||
{ id: "vip", nameFa: "ویآیپی", nameEn: "VIP", hintFa: "خرید", hintEn: "Purchase", price: 2500 },
|
||||
{ id: "maestro", nameFa: "اوستا", nameEn: "Maestro", hintFa: "خرید", hintEn: "Purchase", price: 2000 },
|
||||
{ id: "prince", nameFa: "شاهزاده", nameEn: "Prince", hintFa: "خرید", hintEn: "Purchase", price: 3500 },
|
||||
{ id: "mythic", nameFa: "افسانهای", nameEn: "Mythic", hintFa: "خرید", hintEn: "Purchase", price: 6000 },
|
||||
];
|
||||
|
||||
export function titleById(id: string | null | undefined): TitleDef | undefined {
|
||||
if (!id) return undefined;
|
||||
return TITLES.find((t) => t.id === id);
|
||||
}
|
||||
|
||||
export function titleUnlocked(
|
||||
id: string,
|
||||
stats: PlayerStats,
|
||||
@@ -407,6 +444,12 @@ export function titleUnlocked(
|
||||
return stats.wins >= 500;
|
||||
case "legend":
|
||||
return rating >= tierById("master").floor;
|
||||
case "sultan":
|
||||
return stats.kotsFor >= 100;
|
||||
case "emperor":
|
||||
return level >= 75;
|
||||
case "grandmaster":
|
||||
return rating >= 2100;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -416,19 +459,25 @@ export function titleUnlocked(
|
||||
|
||||
// Card BACKS (pattern on the reverse of every card).
|
||||
export const CARD_BACKS: CardBackDef[] = [
|
||||
{ id: "classic", nameFa: "کلاسیک", nameEn: "Classic", c1: "#14274f", c2: "#0a142e", accent: "#d4af37", price: 0, default: true },
|
||||
{ id: "midnight", nameFa: "نیمهشب", nameEn: "Midnight", c1: "#1b2540", c2: "#0a0f1f", accent: "#8aa0c8", price: 1200 },
|
||||
{ id: "sapphire", nameFa: "یاقوت کبود", nameEn: "Sapphire", c1: "#0b3a82", c2: "#06173a", accent: "#6aa6ff", price: 800 },
|
||||
{ id: "emerald", nameFa: "زمرد", nameEn: "Emerald", c1: "#0d6b5e", c2: "#062420", accent: "#2dd4bf", price: 1000 },
|
||||
{ id: "jade", nameFa: "یشم", nameEn: "Jade", c1: "#136f63", c2: "#08221e", accent: "#7fe3c0", price: 2000 },
|
||||
{ id: "onyx", nameFa: "اونیکس", nameEn: "Onyx", c1: "#26262b", c2: "#0c0c10", accent: "#b0b0c0", price: 1500 },
|
||||
{ id: "classic", nameFa: "کلاسیک", nameEn: "Classic", c1: "#14274f", c2: "#0a142e", accent: "#d4af37", price: 0, default: true, pattern: "stripes" },
|
||||
{ id: "midnight", nameFa: "نیمهشب", nameEn: "Midnight", c1: "#1b2540", c2: "#0a0f1f", accent: "#8aa0c8", price: 1200, pattern: "grid" },
|
||||
{ id: "sapphire", nameFa: "یاقوت کبود", nameEn: "Sapphire", c1: "#0b3a82", c2: "#06173a", accent: "#6aa6ff", price: 800, pattern: "dots" },
|
||||
{ id: "emerald", nameFa: "زمرد", nameEn: "Emerald", c1: "#0d6b5e", c2: "#062420", accent: "#2dd4bf", price: 1000, pattern: "argyle" },
|
||||
{ id: "jade", nameFa: "یشم", nameEn: "Jade", c1: "#136f63", c2: "#08221e", accent: "#7fe3c0", price: 2000, pattern: "scales" },
|
||||
{ id: "onyx", nameFa: "اونیکس", nameEn: "Onyx", c1: "#26262b", c2: "#0c0c10", accent: "#b0b0c0", price: 1500, pattern: "crosshatch" },
|
||||
// earned by rank / wins — the higher the rank, the rarer the back
|
||||
{ id: "crimson", nameFa: "ارغوانی", nameEn: "Crimson", c1: "#7a1322", c2: "#2a0710", accent: "#ff8a9c", price: 0, unlockWins: 25 },
|
||||
{ id: "ruby", nameFa: "یاقوت", nameEn: "Ruby", c1: "#7f1d2e", c2: "#2b0a12", accent: "#ff7a90", price: 0, unlockRating: 1300 },
|
||||
{ id: "royal", nameFa: "سلطنتی", nameEn: "Royal", c1: "#4a1d7f", c2: "#1a0a2e", accent: "#c77dff", price: 0, unlockWins: 50 },
|
||||
{ id: "aurora", nameFa: "شفق", nameEn: "Aurora", c1: "#1d4e6e", c2: "#0a2230", accent: "#5be0c8", price: 0, unlockRating: 1500 },
|
||||
{ id: "obsidian", nameFa: "ابسیدین", nameEn: "Obsidian", c1: "#101018", c2: "#000005", accent: "#7c5cff", price: 0, unlockRating: 1700 },
|
||||
{ id: "imperial", nameFa: "شاهنشاهی", nameEn: "Imperial", c1: "#5a3c0a", c2: "#241704", accent: "#ffd76a", price: 0, unlockRating: 1900 },
|
||||
{ id: "crimson", nameFa: "ارغوانی", nameEn: "Crimson", c1: "#7a1322", c2: "#2a0710", accent: "#ff8a9c", price: 0, unlockWins: 25, pattern: "rays" },
|
||||
{ id: "ruby", nameFa: "یاقوت", nameEn: "Ruby", c1: "#7f1d2e", c2: "#2b0a12", accent: "#ff7a90", price: 0, unlockRating: 1300, pattern: "argyle", motif: "♦" },
|
||||
{ id: "royal", nameFa: "سلطنتی", nameEn: "Royal", c1: "#4a1d7f", c2: "#1a0a2e", accent: "#c77dff", price: 0, unlockWins: 50, pattern: "royal", motif: "♛" },
|
||||
{ id: "aurora", nameFa: "شفق", nameEn: "Aurora", c1: "#1d4e6e", c2: "#0a2230", accent: "#5be0c8", price: 0, unlockRating: 1500, pattern: "rays" },
|
||||
{ id: "obsidian", nameFa: "ابسیدین", nameEn: "Obsidian", c1: "#101018", c2: "#000005", accent: "#7c5cff", price: 0, unlockRating: 1700, pattern: "crosshatch", motif: "✦" },
|
||||
{ id: "imperial", nameFa: "شاهنشاهی", nameEn: "Imperial", c1: "#5a3c0a", c2: "#241704", accent: "#ffd76a", price: 0, unlockRating: 1900, pattern: "royal", motif: "♔" },
|
||||
// ✨ Luxury card backs — premium purchasable, each a distinct fancy motif
|
||||
{ id: "diamond", nameFa: "الماس", nameEn: "Diamond", c1: "#1a3a55", c2: "#0a1a2e", accent: "#9fe6ff", price: 2800, pattern: "gem", motif: "◆" },
|
||||
{ id: "blackgold", nameFa: "طلای سیاه", nameEn: "Black Gold", c1: "#1a1407", c2: "#000000", accent: "#ffd76a", price: 3500, pattern: "filigree", motif: "♠" },
|
||||
{ id: "platinum-back", nameFa: "پلاتین", nameEn: "Platinum", c1: "#3a3f4a", c2: "#15171c", accent: "#e6ebf2", price: 4200, pattern: "royal", motif: "✦" },
|
||||
{ id: "peacock-back", nameFa: "طاووس", nameEn: "Peacock", c1: "#0a3a52", c2: "#06202e", accent: "#16d3c0", price: 3000, pattern: "scales", motif: "❖" },
|
||||
{ id: "rosegold-back", nameFa: "رزگلد", nameEn: "Rose Gold", c1: "#5a2438", c2: "#2a0e1c", accent: "#ffb0c4", price: 3200, pattern: "argyle", motif: "♥" },
|
||||
];
|
||||
|
||||
// Card FRONTS (the face background/border behind the suit + rank).
|
||||
@@ -445,6 +494,9 @@ export const CARD_FRONTS: CardFrontDef[] = [
|
||||
{ id: "goldleaf", nameFa: "زرورق", nameEn: "Gold Leaf", bg1: "#fff7df", bg2: "#f2dd9b", border: "#caa53a", price: 0, unlockRating: 1500 },
|
||||
{ id: "crystal", nameFa: "بلور", nameEn: "Crystal", bg1: "#eefcff", bg2: "#cdeefa", border: "#5fb6d6", price: 0, unlockRating: 1700 },
|
||||
{ id: "imperial-face", nameFa: "شاهانه", nameEn: "Imperial", bg1: "#fff4cf", bg2: "#ecc873", border: "#b8862a", price: 0, unlockWins: 100 },
|
||||
// ✨ Luxury card fronts — premium purchasable
|
||||
{ id: "diamond-face", nameFa: "الماس", nameEn: "Diamond", bg1: "#f4fdff", bg2: "#d7f0fb", border: "#7fc6e6", price: 2500 },
|
||||
{ id: "blackgold-face", nameFa: "طلای سیاه", nameEn: "Black Gold", bg1: "#2a2410", bg2: "#14110a", border: "#caa53a", price: 3200 },
|
||||
];
|
||||
|
||||
export function cardBackById(id: string): CardBackDef {
|
||||
@@ -537,7 +589,22 @@ export const STICKER_PACKS: StickerPackDef[] = [
|
||||
// Custom packs earned only via achievements / rank.
|
||||
{ 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" },
|
||||
{ id: "victory", nameFa: "پیروزی", nameEn: "Victory", stickers: ["bardim", "hokm-text"], price: 0, unlockRating: 1500 },
|
||||
|
||||
/* ---- New themed packs: کلکل (banter), Persian trends, Hokm/game ---- */
|
||||
// کلکل / تیکه — trash-talk you fling at the table
|
||||
{ id: "kolkol", nameFa: "کلکل", nameEn: "Banter", stickers: ["sukhti", "yad-begir", "nobate-man", "naz-nakon"], price: 800 },
|
||||
{ id: "tikeh", nameFa: "تیکهانداز", nameEn: "Taunts", stickers: ["kojai", "hool-nasho", "didi-goftam", "bendaz-dige"], price: 1000 },
|
||||
{ id: "shakkak", nameFa: "شاکی", nameEn: "Salty", stickers: ["nakon-eddea", "shans-avordi", "biya-bebin", "kart-nadari"], price: 1000 },
|
||||
// Persian trend phrases / praise
|
||||
{ id: "trends", nameFa: "ترندها", nameEn: "Trends", stickers: ["eyval", "torkundi", "gol-kashti", "harf-nadari"], price: 900 },
|
||||
{ id: "tashvigh", nameFa: "تشویق", nameEn: "Cheers", stickers: ["damet-garm-2", "nush-jan", "be-be", "ghorbunet"], price: 700 },
|
||||
// Hokm / card-game themed
|
||||
{ id: "khanevadeh", nameFa: "خانواده خال", nameEn: "Court Cards", stickers: ["tak-khal", "as-del", "shah-khesht", "bibi-gesht"], price: 1200 },
|
||||
{ id: "victory", nameFa: "پیروزی", nameEn: "Victory", stickers: ["bardim", "hokm-text", "jam-kon", "kish-mat"], price: 0, unlockRating: 1500 },
|
||||
// Extra emotions
|
||||
{ id: "ehsasat", nameFa: "احساسات", nameEn: "Moods", stickers: ["laugh", "shocked", "cry", "smug"], price: 600 },
|
||||
// Mega banter bundle (earned, not sold) — the spicy stuff for rivals
|
||||
{ id: "raghib", nameFa: "رقیب", nameEn: "Rivalry", stickers: ["khdahafez", "weak", "clown", "sleep"], price: 0, unlockAchievement: "kot_10" },
|
||||
];
|
||||
|
||||
export function stickerPackById(id: string): StickerPackDef | undefined {
|
||||
@@ -686,7 +753,7 @@ export function applyMatchResult(
|
||||
|
||||
/* --------------------------- Daily reward ---------------------------- */
|
||||
|
||||
export const DAILY_REWARDS = [100, 150, 200, 300, 400, 500, 1000];
|
||||
export const DAILY_REWARDS = [300, 500, 750, 1000, 1500, 2500, 7500];
|
||||
|
||||
export function dailyRewardFor(day: number): number {
|
||||
return DAILY_REWARDS[Math.min(day, DAILY_REWARDS.length) - 1] ?? 100;
|
||||
|
||||
Reference in New Issue
Block a user