100+ achievements, forfeit, leagues floor, bot humanize, 95k starter
Achievements: generator-driven, now 100+ across 7 categories (added Rulership) mirrored client + server with identical ids/goals/coins. New tracked stats: hakemRounds (be the hakem — incl. "7× Hakem"), roundsWon, plus losses metric. Custom achievement-only sticker packs (Rulership 👑, Firestorm 🔥) with new inline-SVG art (crown-gold, seven-zip, streak-fire), unlocked by hakem_7 / streak_10. Server GameRoom tallies hakem rounds per seat + rounds won per team; client tallies the same for vs-computer/private games (dealId-deduped). Forfeit (surrender): a player can request forfeit; if the teammate is a bot it auto-confirms, otherwise the human teammate gets a confirm/decline prompt (20s timeout). Result: forfeiting with ≥1 round won = normal loss; 0 rounds = Kot. Wired client↔server over the hub (RequestForfeit/ConfirmForfeit/DeclineForfeit + "forfeit" event); offline/vs-computer ends immediately in the store. Flag button + confirm dialogs in the table. Online count: never shows below 50 — live service floors the real count with a drifting believable number (mock base lowered to ~50–170). Matchmaking: real players get a longer priority window (9s) before bots fill; bots now occasionally react after winning a trick (humanize). Coins: starter pack is 95,000 Toman (50k coins); packs rescaled up (server + mock). Verified: dotnet build + tsc + next build clean; sim unlocks 57 achievements/500 matches; live server: starter=95000, a 7-hakem win unlocks hakem_7 + wins_1 with hakemRounds/roundsWon persisted. Images rebuilt on :1500/:1505. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { Crown, LogOut, SmilePlus, Volume2, VolumeX, WifiOff } from "lucide-react";
|
||||
import { Crown, Flag, LogOut, SmilePlus, Volume2, VolumeX, WifiOff } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { TURN_MS, useGameStore } from "@/lib/game-store";
|
||||
import { useSoundStore } from "@/lib/sound-store";
|
||||
@@ -46,11 +46,15 @@ function useCardSkins() {
|
||||
};
|
||||
}
|
||||
|
||||
export function GameTable({ onExit }: { onExit?: () => void } = {}) {
|
||||
export function GameTable({
|
||||
onExit,
|
||||
onForfeit,
|
||||
}: { onExit?: () => void; onForfeit?: () => void } = {}) {
|
||||
const game = useGameStore((s) => s.game);
|
||||
const reset = useGameStore((s) => s.reset);
|
||||
const mode = useGameStore((s) => s.mode);
|
||||
const { t } = useI18n();
|
||||
const [askFf, setAskFf] = useState(false);
|
||||
|
||||
const sfx = useSoundStore((s) => s.sfx);
|
||||
const music = useSoundStore((s) => s.music);
|
||||
@@ -84,6 +88,15 @@ export function GameTable({ onExit }: { onExit?: () => void } = {}) {
|
||||
<Volume2 className="size-4 text-gold-400" />
|
||||
)}
|
||||
</button>
|
||||
{onForfeit && (
|
||||
<button
|
||||
onClick={() => setAskFf(true)}
|
||||
className="glass rounded-full p-2.5 hover:bg-navy-800 transition"
|
||||
title={t("forfeit.title")}
|
||||
>
|
||||
<Flag className="size-4 text-rose-300/90" />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={exit}
|
||||
className="glass rounded-full p-2.5 hover:bg-navy-800 transition"
|
||||
@@ -94,6 +107,39 @@ export function GameTable({ onExit }: { onExit?: () => void } = {}) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* forfeit confirm (requester) */}
|
||||
<AnimatePresence>
|
||||
{askFf && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 z-[70] flex items-center justify-center bg-navy-950/80 backdrop-blur-sm p-5"
|
||||
>
|
||||
<div className="glass rounded-3xl p-6 w-full max-w-xs text-center">
|
||||
<div className="text-4xl mb-2">🏳️</div>
|
||||
<h2 className="gold-text text-xl font-black">{t("forfeit.title")}</h2>
|
||||
<p className="text-cream/70 text-sm mt-2">{t("forfeit.ask")}</p>
|
||||
<p className="text-cream/45 text-xs mt-1">{t("forfeit.rule")}</p>
|
||||
<div className="flex gap-2 mt-5">
|
||||
<button
|
||||
onClick={() => {
|
||||
setAskFf(false);
|
||||
onForfeit?.();
|
||||
}}
|
||||
className="flex-1 rounded-xl py-3 bg-rose-500/80 text-white font-bold"
|
||||
>
|
||||
{t("forfeit.confirm")}
|
||||
</button>
|
||||
<button onClick={() => setAskFf(false)} className="flex-1 btn-gold rounded-xl py-3">
|
||||
{t("forfeit.keepPlaying")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Felt table */}
|
||||
<div className="absolute inset-0 flex items-center justify-center p-4">
|
||||
<div className="felt relative w-[min(94vw,1100px)] h-[min(82vh,720px)] rounded-[42%]">
|
||||
|
||||
Reference in New Issue
Block a user