Store XP packs (expensive), winner 2x XP, premium perks
CI/CD / CI - API (dotnet build + engine sim) (push) Failing after 1m40s
CI/CD / CI - Web (tsc + next build) (push) Failing after 1m20s
CI/CD / Deploy - local stack (db + server + web) (push) Has been skipped

- XP packs in the store (coin-priced, intentionally expensive): xp1 200/5k,
  xp2 600/12k, xp3 1500/25k. Consumable (grant XP, can level up) — server
  ShopBuy handles kind "xp" via an authoritative XpPacks map + Gamification.GrantXp;
  mock mirrors. New shop section + shop.xp/xpHint i18n.
- Every game grants XP and the WINNER earns 2x: matchXp is now
  base*(won?2:1)*leagueFactor (was a flat +80 win bonus). Mirrored server-side.
- Premium (pro) perks: 1.5x XP multiplier (applied in applyMatchResult /
  ApplyMatch by plan), plus animated shimmering gold chat bubbles for your own
  messages (premium-chat CSS; ChatScreen gates on plan).

Verified: tsc + next + dotnet build clean; sim passes; live server — buying xp2
took L1→L3 and deducted 12k coins under the new curve. Images rebuilt :1500/:1505.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-05 00:08:19 +03:30
parent 4199a82c9d
commit fd33f85e9c
9 changed files with 116 additions and 14 deletions
+15 -7
View File
@@ -130,6 +130,13 @@ export function leagueById(id: string): MatchLeague {
return MATCH_LEAGUES.find((l) => l.id === id) ?? MATCH_LEAGUES[0];
}
/** Coin-priced XP packs (XP is intentionally expensive). Server-authoritative. */
export const XP_PACKS: { id: string; xp: number; price: number }[] = [
{ id: "xp1", xp: 200, price: 5000 },
{ id: "xp2", xp: 600, price: 12000 },
{ id: "xp3", xp: 1500, price: 25000 },
];
/* ------------------------------- XP ---------------------------------- */
/** Hard level cap. */
@@ -149,13 +156,13 @@ export function leagueXpFactor(stake: number): number {
return 1;
}
/** XP multiplier for premium (pro) players. */
export const PREMIUM_XP_MULT = 1.5;
export function matchXp(summary: MatchSummary): number {
const base =
40 +
(summary.won ? 80 : 0) +
summary.tricksWon * 5 +
(summary.kotFor ? 30 : 0);
return Math.round(base * leagueXpFactor(summary.stake));
// Every game grants XP; the winner earns double.
const base = 40 + summary.tricksWon * 5 + (summary.kotFor ? 30 : 0);
return Math.round(base * (summary.won ? 2 : 1) * leagueXpFactor(summary.stake));
}
export interface LevelProgress {
@@ -557,7 +564,8 @@ export function applyMatchResult(
const ratingAfter = Math.max(0, ratingBefore + rDelta);
const cDelta = coinDelta(summary);
const xpGain = matchXp(summary);
// Premium (pro) players earn a multiple of XP.
const xpGain = Math.round(matchXp(summary) * (profile.plan === "pro" ? PREMIUM_XP_MULT : 1));
const lvl = addXp(profile.level, profile.xp, xpGain);
const stats = applyStats(profile.stats, summary);