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:
@@ -16,6 +16,7 @@ import {
|
||||
CoinPack,
|
||||
Conversation,
|
||||
DailyRewardState,
|
||||
ForfeitRequest,
|
||||
Friend,
|
||||
FriendRequest,
|
||||
LeaderboardEntry,
|
||||
@@ -55,6 +56,7 @@ export class SignalrService implements OnlineService {
|
||||
private rewardCbs = new Set<(r: RewardResult) => void>();
|
||||
private friendCbs = new Set<(f: Friend[]) => void>();
|
||||
private chatCbs = new Set<(id: string, m: ChatMessage[]) => void>();
|
||||
private forfeitCbs = new Set<(r: ForfeitRequest | null) => void>();
|
||||
private cachedProfile: UserProfile | null = null;
|
||||
private mockNotifUnsub?: () => void;
|
||||
|
||||
@@ -141,6 +143,8 @@ export class SignalrService implements OnlineService {
|
||||
});
|
||||
conn.on("social", () => void this.refreshFriends());
|
||||
conn.on("chat", (m: { peerId: string }) => void this.emitChat(m.peerId));
|
||||
conn.on("forfeit", (r: ForfeitRequest | null) =>
|
||||
this.forfeitCbs.forEach((cb) => cb(r && r.byName ? r : null)));
|
||||
|
||||
this.conn = conn;
|
||||
try {
|
||||
@@ -302,6 +306,14 @@ export class SignalrService implements OnlineService {
|
||||
return () => this.rewardCbs.delete(cb);
|
||||
}
|
||||
|
||||
requestForfeit() { void this.conn?.invoke("RequestForfeit"); }
|
||||
confirmForfeit() { void this.conn?.invoke("ConfirmForfeit"); }
|
||||
declineForfeit() { void this.conn?.invoke("DeclineForfeit"); }
|
||||
onForfeit(cb: (r: ForfeitRequest | null) => void): Unsubscribe {
|
||||
this.forfeitCbs.add(cb);
|
||||
return () => this.forfeitCbs.delete(cb);
|
||||
}
|
||||
|
||||
/* ----- profile / economy → server (authoritative) ----- */
|
||||
|
||||
async getProfile() {
|
||||
@@ -381,16 +393,18 @@ export class SignalrService implements OnlineService {
|
||||
}
|
||||
|
||||
async getOnlineCount(): Promise<number> {
|
||||
// Always show a believable floor (≥50) — never the raw small/zero real count.
|
||||
const floor = await this.mock.getOnlineCount(); // drifts, min 50
|
||||
try {
|
||||
const res = await fetch(`${SERVER}/api/stats/online`);
|
||||
if (res.ok) {
|
||||
const j = (await res.json()) as { online: number };
|
||||
return j.online ?? 0;
|
||||
return Math.max(j.online ?? 0, floor);
|
||||
}
|
||||
} catch {
|
||||
/* fall through */
|
||||
}
|
||||
return this.mock.getOnlineCount();
|
||||
return floor;
|
||||
}
|
||||
|
||||
getLeaderboard(): Promise<LeaderboardEntry[]> { return this.mock.getLeaderboard(); }
|
||||
|
||||
Reference in New Issue
Block a user