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:
@@ -2,26 +2,20 @@
|
||||
|
||||
import { cn } from "@/lib/cn";
|
||||
import { Card, SUIT_IS_RED, SUIT_SYMBOL, rankLabel } from "@/lib/hokm/types";
|
||||
import type { CardBackPattern } from "@/lib/online/types";
|
||||
import { cardBackMotif, cardBackVisual } from "@/lib/cardBack";
|
||||
|
||||
const SIZES = {
|
||||
sm: { w: 44, h: 62, rank: "text-base", pip: "text-lg", center: "text-2xl" },
|
||||
md: { w: 60, h: 84, rank: "text-lg", pip: "text-xl", center: "text-3xl" },
|
||||
lg: { w: 74, h: 104, rank: "text-xl", pip: "text-2xl", center: "text-4xl" },
|
||||
sm: { w: 44, h: 62, rank: "text-[11px]", center: "text-2xl", radius: 7 },
|
||||
md: { w: 62, h: 87, rank: "text-sm", center: "text-3xl", radius: 9 },
|
||||
lg: { w: 78, h: 110, rank: "text-base", center: "text-4xl", radius: 11 },
|
||||
xl: { w: 92, h: 130, rank: "text-lg", center: "text-5xl", radius: 13 },
|
||||
} as const;
|
||||
|
||||
export type CardSize = keyof typeof SIZES;
|
||||
|
||||
interface CardBack {
|
||||
c1: string;
|
||||
c2: string;
|
||||
accent: string;
|
||||
}
|
||||
|
||||
interface CardFront {
|
||||
bg1: string;
|
||||
bg2: string;
|
||||
border: string;
|
||||
}
|
||||
interface CardBack { c1: string; c2: string; accent: string; pattern?: CardBackPattern; motif?: string; }
|
||||
interface CardFront { bg1: string; bg2: string; border: string; }
|
||||
|
||||
interface Props {
|
||||
card?: Card;
|
||||
@@ -44,77 +38,114 @@ export function PlayingCard({
|
||||
}: Props) {
|
||||
const s = SIZES[size];
|
||||
|
||||
/* ── Face-down ─────────────────────────────────────────────────── */
|
||||
if (faceDown || !card) {
|
||||
const styled = back
|
||||
? {
|
||||
width: s.w,
|
||||
height: s.h,
|
||||
borderRadius: 8,
|
||||
background: `repeating-linear-gradient(45deg, ${back.accent}40 0 6px, transparent 6px 12px), linear-gradient(160deg, ${back.c1}, ${back.c2})`,
|
||||
border: `1px solid ${back.accent}80`,
|
||||
boxShadow: "0 6px 14px rgba(0,0,0,0.4)",
|
||||
}
|
||||
: { width: s.w, height: s.h };
|
||||
const visual = back ? cardBackVisual(back.c1, back.c2, back.accent, back.pattern) : null;
|
||||
const styled = back && visual ? {
|
||||
width: s.w, height: s.h, borderRadius: s.radius,
|
||||
background: visual.background,
|
||||
backgroundSize: visual.backgroundSize,
|
||||
border: `1.5px solid ${back.accent}88`,
|
||||
boxShadow: "0 6px 18px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.12)",
|
||||
} : { width: s.w, height: s.h };
|
||||
const motif = back ? cardBackMotif(back.pattern, back.motif) : "✦";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(!back && "card-back", "rounded-lg shrink-0", className)}
|
||||
style={styled}
|
||||
className={cn(!back && "card-back", "shrink-0", className)}
|
||||
style={{ ...styled, borderRadius: s.radius }}
|
||||
aria-hidden
|
||||
>
|
||||
<div className="h-full w-full rounded-lg flex items-center justify-center">
|
||||
<div
|
||||
className={cn("text-lg font-bold", !back && "text-gold-500/70")}
|
||||
style={back ? { color: `${back.accent}cc` } : undefined}
|
||||
>
|
||||
✦
|
||||
</div>
|
||||
<div
|
||||
className="h-full w-full flex items-center justify-center"
|
||||
style={{ borderRadius: s.radius }}
|
||||
>
|
||||
{motif && (
|
||||
<span
|
||||
className={cn("font-bold select-none", !back && "text-gold-500/70")}
|
||||
style={{
|
||||
fontSize: s.w * 0.34,
|
||||
color: back ? `${back.accent}dd` : undefined,
|
||||
textShadow: back ? `0 1px 3px rgba(0,0,0,0.45)` : undefined,
|
||||
}}
|
||||
>
|
||||
{motif}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const red = SUIT_IS_RED[card.suit];
|
||||
const color = red ? "text-rose-600" : "text-slate-900";
|
||||
const symbol = SUIT_SYMBOL[card.suit];
|
||||
/* ── Face-up ────────────────────────────────────────────────────── */
|
||||
const red = SUIT_IS_RED[card.suit];
|
||||
const symbol = SUIT_SYMBOL[card.suit];
|
||||
const label = rankLabel(card.rank);
|
||||
|
||||
// UNO-style: suit-aware background
|
||||
const cardBg = front
|
||||
? `linear-gradient(160deg,${front.bg1},${front.bg2})`
|
||||
: red
|
||||
? "linear-gradient(160deg,#fff8f7,#fdecea)"
|
||||
: "linear-gradient(160deg,#fefefe,#f4f2ec)";
|
||||
|
||||
const borderColor = front?.border ?? (red ? "rgba(200,70,70,0.22)" : "rgba(50,50,80,0.15)");
|
||||
|
||||
// Bold suit colours (UNO-style vivid)
|
||||
const inkColor = red ? "#c0202a" : "#1c1c38";
|
||||
const pipColor = red ? "#e03540" : "#2a2a50";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"card-face rounded-lg shrink-0 relative select-none transition-opacity",
|
||||
dimmed && "opacity-45",
|
||||
className
|
||||
)}
|
||||
className={cn("shrink-0 relative select-none transition-opacity", dimmed && "opacity-40", className)}
|
||||
style={{
|
||||
width: s.w,
|
||||
height: s.h,
|
||||
...(front
|
||||
? { background: `linear-gradient(160deg, ${front.bg1}, ${front.bg2})`, borderColor: front.border }
|
||||
: {}),
|
||||
width: s.w, height: s.h, borderRadius: s.radius,
|
||||
background: cardBg,
|
||||
border: `1.5px solid ${borderColor}`,
|
||||
boxShadow: "0 6px 18px rgba(0,0,0,0.38), inset 0 1px 0 rgba(255,255,255,0.85)",
|
||||
}}
|
||||
>
|
||||
<div className={cn("absolute top-1 left-1.5 leading-none font-bold", color, s.rank)}>
|
||||
<div>{rankLabel(card.rank)}</div>
|
||||
<div className={s.rank}>{symbol}</div>
|
||||
</div>
|
||||
{/* Top-left corner */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-0 flex items-center justify-center font-bold",
|
||||
color,
|
||||
s.center
|
||||
)}
|
||||
className={cn("absolute top-[3px] left-[5px] leading-[1.1] font-black", s.rank)}
|
||||
style={{ color: inkColor }}
|
||||
>
|
||||
<div>{label}</div>
|
||||
<div style={{ color: pipColor, fontSize: "0.82em" }}>{symbol}</div>
|
||||
</div>
|
||||
|
||||
{/* Center symbol — large, bold, slightly shadowed */}
|
||||
<div
|
||||
className={cn("absolute inset-0 flex items-center justify-center font-black", s.center)}
|
||||
style={{
|
||||
color: inkColor,
|
||||
textShadow: red
|
||||
? "0 2px 10px rgba(210,40,40,0.18)"
|
||||
: "0 2px 10px rgba(28,28,56,0.12)",
|
||||
}}
|
||||
>
|
||||
{symbol}
|
||||
</div>
|
||||
|
||||
{/* Bottom-right corner (rotated 180°) */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute bottom-1 right-1.5 leading-none font-bold rotate-180",
|
||||
color,
|
||||
s.rank
|
||||
)}
|
||||
className={cn("absolute bottom-[3px] right-[5px] leading-[1.1] font-black rotate-180", s.rank)}
|
||||
style={{ color: inkColor }}
|
||||
>
|
||||
<div>{rankLabel(card.rank)}</div>
|
||||
<div className={s.rank}>{symbol}</div>
|
||||
<div>{label}</div>
|
||||
<div style={{ color: pipColor, fontSize: "0.82em" }}>{symbol}</div>
|
||||
</div>
|
||||
|
||||
{/* Subtle inner rim for red suits — UNO-style */}
|
||||
{red && (
|
||||
<div
|
||||
className="absolute inset-[3px] pointer-events-none"
|
||||
style={{
|
||||
borderRadius: Math.max(0, s.radius - 4),
|
||||
border: "1px solid rgba(210,40,40,0.14)",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user