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
+16 -1
View File
@@ -13,6 +13,7 @@ import {
} from "./hokm/engine";
import { Card, GameState, RoundResult, Seat, Suit } from "./hokm/types";
import { avatarEmoji } from "./online/types";
import { sound } from "./sound";
const KOT_POINTS = 2;
@@ -111,6 +112,7 @@ export const useGameStore = create<GameStore>((set, get) => {
if (cur.phase !== "playing" || cur.turn !== seat) return;
const card = chooseCardAI(cur, seat);
set({ game: playCard(cur, seat, card) });
sound.play("cardPlay");
scheduleAuto();
}
@@ -123,6 +125,7 @@ export const useGameStore = create<GameStore>((set, get) => {
set({ turnDeadline: null, disconnectedSeat: null, reconnectDeadline: null });
pending = setTimeout(() => {
set({ game: dealForTrump(get().game) });
sound.play("deal");
scheduleAuto();
}, TIMING.hakemDraw);
break;
@@ -145,6 +148,7 @@ export const useGameStore = create<GameStore>((set, get) => {
const cur = get().game;
const suit = chooseTrumpAI(cur.players[cur.hakem!].hand);
set({ game: engineChooseTrump(cur, suit) });
sound.play("trump");
scheduleAuto();
}, TIMING.aiTrump);
}
@@ -160,6 +164,7 @@ export const useGameStore = create<GameStore>((set, get) => {
const cur = get().game;
if (cur.phase !== "playing" || cur.turn !== seat) return;
set({ game: playCard(cur, seat, chooseCardAI(cur, seat)), turnDeadline: null });
sound.play("cardPlay");
scheduleAuto();
}, TURN_MS);
} else {
@@ -190,10 +195,16 @@ export const useGameStore = create<GameStore>((set, get) => {
case "trick-complete":
set({ turnDeadline: null, disconnectedSeat: null, reconnectDeadline: null });
sound.play("trickWin");
pending = setTimeout(() => {
const next = advanceAfterTrick(get().game, KOT_POINTS);
set({ game: next });
if (next.phase === "match-over") recordRound(next.lastRoundResult);
if (next.phase === "match-over") {
recordRound(next.lastRoundResult);
sound.play(next.matchWinner === 0 ? "win" : "lose");
} else if (next.phase === "round-over" && next.lastRoundResult?.kot) {
sound.play("kot");
}
scheduleAuto();
}, TIMING.trickPause);
break;
@@ -226,6 +237,7 @@ export const useGameStore = create<GameStore>((set, get) => {
newMatch: (settings) => {
clearPending();
sound.init();
const initial = createInitialState(settings);
set({
game: selectHakem(initial),
@@ -247,6 +259,7 @@ export const useGameStore = create<GameStore>((set, get) => {
newOnlineMatch: (cfg) => {
clearPending();
sound.init();
const names = cfg.players.map((p) => p.displayName) as GameSettings["names"];
const initial = createInitialState({ names, targetScore: cfg.targetScore });
set({
@@ -271,6 +284,7 @@ export const useGameStore = create<GameStore>((set, get) => {
const g = get().game;
if (g.phase !== "choosing-trump") return;
set({ game: engineChooseTrump(g, suit), turnDeadline: null });
sound.play("trump");
scheduleAuto();
},
@@ -278,6 +292,7 @@ export const useGameStore = create<GameStore>((set, get) => {
const g = get().game;
if (g.phase !== "playing" || g.turn !== 0) return;
set({ game: playCard(g, 0, card), turnDeadline: null });
sound.play("cardPlay");
scheduleAuto();
},