Store XP packs (expensive), winner 2x XP, premium perks
- 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:
@@ -41,10 +41,12 @@ public static class Gamification
|
||||
public const int MaxLevel = 100;
|
||||
public static int XpForLevel(int level) => 100 * level + 15 * level * level;
|
||||
private static double LeagueXpFactor(int stake) => stake >= 1000 ? 2.0 : stake >= 500 ? 1.5 : 1.0;
|
||||
public const double PremiumXpMult = 1.5;
|
||||
public static int MatchXp(MatchSummaryDto s)
|
||||
{
|
||||
int b = 40 + (s.Won ? 80 : 0) + s.TricksWon * 5 + (s.KotFor ? 30 : 0);
|
||||
return (int)Math.Round(b * LeagueXpFactor(s.Stake));
|
||||
// Every game grants XP; the winner earns double.
|
||||
int b = 40 + s.TricksWon * 5 + (s.KotFor ? 30 : 0);
|
||||
return (int)Math.Round(b * (s.Won ? 2 : 1) * LeagueXpFactor(s.Stake));
|
||||
}
|
||||
|
||||
// metric: wins|kotsFor|bestWinStreak|shutoutWins|games|tricks|level ; ratingFloor>0 = rank ach.
|
||||
@@ -138,6 +140,14 @@ public static class Gamification
|
||||
_ => false,
|
||||
};
|
||||
|
||||
/// <summary>Grant raw XP to a profile (store XP packs); mutates level/xp, capped at 100.</summary>
|
||||
public static void GrantXp(ProfileDto p, int xp)
|
||||
{
|
||||
var r = AddXp(p.Level, p.Xp, xp);
|
||||
p.Level = r.level;
|
||||
p.Xp = r.xp;
|
||||
}
|
||||
|
||||
private static (int level, int xp, bool up) AddXp(int level, int xp, int gain)
|
||||
{
|
||||
bool up = false;
|
||||
@@ -154,7 +164,9 @@ public static class Gamification
|
||||
int rDelta = RatingDelta(s, p.Rating, oppRating);
|
||||
int ratingAfter = Math.Max(0, ratingBefore + rDelta);
|
||||
int cDelta = CoinDelta(s);
|
||||
var lvl = AddXp(p.Level, p.Xp, MatchXp(s));
|
||||
// Premium (pro) players earn a multiple of XP.
|
||||
int xpGain = (int)Math.Round(MatchXp(s) * (p.Plan == "pro" ? PremiumXpMult : 1.0));
|
||||
var lvl = AddXp(p.Level, p.Xp, xpGain);
|
||||
|
||||
var st = p.Stats;
|
||||
int cur = s.Won ? st.CurrentWinStreak + 1 : 0;
|
||||
@@ -209,7 +221,7 @@ public static class Gamification
|
||||
CoinsBefore = coinsBefore,
|
||||
CoinsAfter = coinsAfter,
|
||||
CoinsDelta = coinsAfter - coinsBefore,
|
||||
XpGained = MatchXp(s),
|
||||
XpGained = xpGain,
|
||||
LevelBefore = levelBefore,
|
||||
LevelAfter = lvl.level,
|
||||
LeveledUp = lvl.level > levelBefore,
|
||||
|
||||
@@ -112,9 +112,30 @@ public class ProfileService
|
||||
|
||||
/* ----------------------------- shop ------------------------------- */
|
||||
|
||||
// Coin-priced XP packs (XP is intentionally expensive). Server-authoritative.
|
||||
public static readonly Dictionary<string, (int Price, int Xp)> XpPacks = new()
|
||||
{
|
||||
["xp1"] = (5000, 200),
|
||||
["xp2"] = (12000, 600),
|
||||
["xp3"] = (25000, 1500),
|
||||
};
|
||||
|
||||
public async Task<(bool ok, ProfileDto? profile, string error)> ShopBuy(string uid, string kind, string id, int price)
|
||||
{
|
||||
var p = await GetOrCreate(uid, null);
|
||||
|
||||
// XP packs are consumable (grant XP, may level up) — not added to an owned list.
|
||||
if (kind == "xp")
|
||||
{
|
||||
if (!XpPacks.TryGetValue(id, out var pk)) return (false, p, "bad_kind");
|
||||
if (p.Coins < pk.Price) return (false, p, "insufficient");
|
||||
p.Coins -= pk.Price;
|
||||
Gamification.GrantXp(p, pk.Xp);
|
||||
await Save(p);
|
||||
await Ledger(uid, "xp", -pk.Price, id);
|
||||
return (true, p, "");
|
||||
}
|
||||
|
||||
var list = kind switch
|
||||
{
|
||||
"avatar" => p.OwnedAvatars,
|
||||
|
||||
Reference in New Issue
Block a user