Split card design into front+back, add sound effects & background music

Card design:
- Separate cardFront + cardBack (each own/equip independently)
- Fronts: classic (free), ivory/rosegold (buy), parchment/mint (earned)
- Backs: classic (free), sapphire/emerald (buy), ruby/royal (earned)
- PlayingCard `front` prop; table applies front to all faces, back to opponents
- Profile has front + back pickers; shop has both sections

Audio:
- Web Audio synth engine (no asset files): SFX for card/deal/trump/trick,
  win/lose, message, notify, award, levelup, purchase, kot + ambient music
- Toggles in profile (Audio) + mute button in game HUD; prefs persisted
- Wired across game-store, rewards, daily, shop, chat

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-04 11:49:19 +03:30
parent db4eade619
commit ae239f4c51
18 changed files with 579 additions and 72 deletions
+43 -7
View File
@@ -4,7 +4,8 @@
import {
AchievementDef,
AchievementUnlock,
CardStyleDef,
CardBackDef,
CardFrontDef,
LeagueInfo,
MatchSummary,
PlayerStats,
@@ -218,16 +219,51 @@ export function titleUnlocked(
/* ---------------------------- Card styles ---------------------------- */
export const CARD_STYLES: CardStyleDef[] = [
{ id: "classic", nameFa: "کلاسیک", nameEn: "Classic", c1: "#14274f", c2: "#0a142e", accent: "#d4af37", price: 0 },
// 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: "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 },
{ id: "ruby", nameFa: "یاقوت", nameEn: "Ruby", c1: "#7f1d2e", c2: "#2b0a12", accent: "#ff7a90", price: 0, unlockRating: 1300 }, // earned
{ id: "royal", nameFa: "سلطنتی", nameEn: "Royal", c1: "#4a1d7f", c2: "#1a0a2e", accent: "#c77dff", price: 0, unlockWins: 50 }, // earned
];
export function cardStyleById(id: string): CardStyleDef {
return CARD_STYLES.find((c) => c.id === id) ?? CARD_STYLES[0];
// Card FRONTS (the face background/border behind the suit + rank).
export const CARD_FRONTS: CardFrontDef[] = [
{ id: "classic", nameFa: "کلاسیک", nameEn: "Classic", bg1: "#fffdf7", bg2: "#f3ead2", border: "rgba(0,0,0,0.12)", price: 0, default: true },
{ id: "ivory", nameFa: "عاج", nameEn: "Ivory", bg1: "#ffffff", bg2: "#eef2f8", border: "#c9ccd6", price: 600 },
{ id: "rosegold", nameFa: "رزگلد", nameEn: "Rose Gold", bg1: "#fff1ee", bg2: "#f6d9cf", border: "#d98a72", price: 900 },
{ id: "parchment", nameFa: "پوست‌نوشت", nameEn: "Parchment", bg1: "#fbf2d8", bg2: "#efd9a3", border: "#caa84a", price: 0, unlockRating: 1300 }, // earned
{ id: "mint", nameFa: "نعنایی", nameEn: "Mint", bg1: "#f0fff8", bg2: "#d3f3e3", border: "#57c79a", price: 0, unlockWins: 50 }, // earned
];
export function cardBackById(id: string): CardBackDef {
return CARD_BACKS.find((c) => c.id === id) ?? CARD_BACKS[0];
}
export function cardFrontById(id: string): CardFrontDef {
return CARD_FRONTS.find((c) => c.id === id) ?? CARD_FRONTS[0];
}
function ownedCosmeticIds(
defs: { id: string; price: number; default?: boolean; unlockRating?: number; unlockWins?: number }[],
profile: UserProfile,
purchased: string[]
): string[] {
const ids = new Set<string>();
for (const d of defs) {
const earned =
(d.unlockRating != null && profile.rating >= d.unlockRating) ||
(d.unlockWins != null && profile.stats.wins >= d.unlockWins);
if (d.default || earned || purchased.includes(d.id)) ids.add(d.id);
}
return [...ids];
}
export function ownedCardBackIds(profile: UserProfile): string[] {
return ownedCosmeticIds(CARD_BACKS, profile, profile.ownedCardBacks ?? []);
}
export function ownedCardFrontIds(profile: UserProfile): string[] {
return ownedCosmeticIds(CARD_FRONTS, profile, profile.ownedCardFronts ?? []);
}
/* --------------------- Reactions (Sheklak / شکلک) -------------------- */