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:
@@ -7,6 +7,7 @@ import { Sticker } from "@/components/online/Sticker";
|
||||
import { useSessionStore } from "@/lib/session-store";
|
||||
import { useI18n } from "@/lib/i18n";
|
||||
import { getService } from "@/lib/online/service";
|
||||
import { sound } from "@/lib/sound";
|
||||
import { ShopItem } from "@/lib/online/types";
|
||||
import { cn } from "@/lib/cn";
|
||||
|
||||
@@ -23,19 +24,26 @@ export function ShopScreen() {
|
||||
|
||||
if (!profile) return null;
|
||||
|
||||
const owns = (item: ShopItem) =>
|
||||
item.kind === "avatar"
|
||||
? profile.ownedAvatars.includes(item.id)
|
||||
: item.kind === "cardstyle"
|
||||
? profile.ownedCardStyles.includes(item.id)
|
||||
: item.kind === "reactionpack"
|
||||
? profile.ownedReactionPacks.includes(item.id)
|
||||
: profile.ownedStickerPacks.includes(item.id);
|
||||
const owns = (item: ShopItem) => {
|
||||
switch (item.kind) {
|
||||
case "avatar":
|
||||
return profile.ownedAvatars.includes(item.id);
|
||||
case "cardfront":
|
||||
return profile.ownedCardFronts.includes(item.id);
|
||||
case "cardback":
|
||||
return profile.ownedCardBacks.includes(item.id);
|
||||
case "reactionpack":
|
||||
return profile.ownedReactionPacks.includes(item.id);
|
||||
default:
|
||||
return profile.ownedStickerPacks.includes(item.id);
|
||||
}
|
||||
};
|
||||
|
||||
const buy = async (item: ShopItem) => {
|
||||
const res = await getService().buyItem(item.id);
|
||||
if (res.ok && res.profile) {
|
||||
setProfile(res.profile);
|
||||
sound.play("purchase");
|
||||
} else {
|
||||
setMsg(locale === "fa" ? res.messageFa : res.messageEn);
|
||||
setTimeout(() => setMsg(""), 1800);
|
||||
@@ -43,7 +51,8 @@ export function ShopScreen() {
|
||||
};
|
||||
|
||||
const avatars = items.filter((i) => i.kind === "avatar");
|
||||
const cardstyles = items.filter((i) => i.kind === "cardstyle");
|
||||
const cardfronts = items.filter((i) => i.kind === "cardfront");
|
||||
const cardbacks = items.filter((i) => i.kind === "cardback");
|
||||
const reactions = items.filter((i) => i.kind === "reactionpack");
|
||||
const stickers = items.filter((i) => i.kind === "stickerpack");
|
||||
|
||||
@@ -71,9 +80,30 @@ export function ShopScreen() {
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section title={t("shop.cardstyles")}>
|
||||
<Section title={t("shop.cardfronts")}>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{cardstyles.map((item) => (
|
||||
{cardfronts.map((item) => (
|
||||
<ItemCard
|
||||
key={item.id}
|
||||
item={item}
|
||||
owned={owns(item)}
|
||||
onBuy={() => buy(item)}
|
||||
preview={
|
||||
<span
|
||||
className="w-8 h-11 rounded-md border flex items-center justify-center text-slate-900"
|
||||
style={{ background: `linear-gradient(160deg, #ffffff, ${item.preview})`, borderColor: "rgba(0,0,0,0.18)" }}
|
||||
>
|
||||
♠
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section title={t("shop.cardbacks")}>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{cardbacks.map((item) => (
|
||||
<ItemCard
|
||||
key={item.id}
|
||||
item={item}
|
||||
|
||||
Reference in New Issue
Block a user