Server-authoritative economy: wire client to server; entry + rewards on hub
Server: - daily (/api/daily, /api/daily/claim) + shop (/api/shop/buy) + ChargeEntry - GameRoom (via IServiceScopeFactory) deducts ranked entry at match start and applies match rewards at match-over, broadcasting profile + reward over the hub - tested: daily, shop (owned-guard), ranked entry deduction pushed over hub Client: - SignalrService routes profile/coins/plan/daily/shop/match to the server (Bearer); onProfile/onReward hub events; guest/offline fall back to local - session-store syncs profile from hub; game-store serverReward; GameScreen shows live ranked reward from hub (no double submit), submits client-run games - single source of truth in live mode (no economy divergence) Postgres-ready via config (Provider=postgres); EnsureCreated for now. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -96,4 +96,65 @@ public class ProfileService
|
||||
await Ledger(uid, "match", reward.CoinsDelta, s.Ranked ? "ranked" : "casual");
|
||||
return (reward, p);
|
||||
}
|
||||
|
||||
/// <summary>Deduct a ranked entry/stake up front. Returns null if not enough coins.</summary>
|
||||
public async Task<ProfileDto?> ChargeEntry(string uid, int amount)
|
||||
{
|
||||
var p = await GetOrCreate(uid, null);
|
||||
if (amount <= 0) return p;
|
||||
if (p.Coins < amount) return null;
|
||||
p.Coins -= amount;
|
||||
await Save(p);
|
||||
await Ledger(uid, "entry", -amount, "ranked");
|
||||
return p;
|
||||
}
|
||||
|
||||
/* ----------------------------- shop ------------------------------- */
|
||||
|
||||
public async Task<(bool ok, ProfileDto? profile, string error)> ShopBuy(string uid, string kind, string id, int price)
|
||||
{
|
||||
var p = await GetOrCreate(uid, null);
|
||||
var list = kind switch
|
||||
{
|
||||
"avatar" => p.OwnedAvatars,
|
||||
"cardfront" => p.OwnedCardFronts,
|
||||
"cardback" => p.OwnedCardBacks,
|
||||
"reactionpack" => p.OwnedReactionPacks,
|
||||
"stickerpack" => p.OwnedStickerPacks,
|
||||
_ => null,
|
||||
};
|
||||
if (list == null) return (false, null, "bad_kind");
|
||||
if (list.Contains(id)) return (false, p, "owned");
|
||||
if (price < 0 || p.Coins < price) return (false, p, "insufficient");
|
||||
p.Coins -= price;
|
||||
list.Add(id);
|
||||
await Save(p);
|
||||
await Ledger(uid, "shop", -price, $"{kind}:{id}");
|
||||
return (true, p, "");
|
||||
}
|
||||
|
||||
/* ----------------------------- daily ------------------------------ */
|
||||
|
||||
private static readonly int[] DailyRewards = { 100, 150, 200, 300, 400, 500, 1000 };
|
||||
private static string Today => DateTime.UtcNow.ToString("yyyy-MM-dd");
|
||||
|
||||
public async Task<(int day, string? lastClaimed, bool available)> GetDaily(string uid)
|
||||
{
|
||||
var p = await GetOrCreate(uid, null);
|
||||
return (p.DailyDay, p.DailyLastClaimed, p.DailyLastClaimed != Today);
|
||||
}
|
||||
|
||||
public async Task<(int reward, ProfileDto profile, int day)> ClaimDaily(string uid)
|
||||
{
|
||||
var p = await GetOrCreate(uid, null);
|
||||
if (p.DailyLastClaimed == Today) return (0, p, p.DailyDay);
|
||||
int day = p.DailyDay;
|
||||
int reward = DailyRewards[Math.Min(day, DailyRewards.Length) - 1];
|
||||
p.Coins += reward;
|
||||
p.DailyDay = day >= 7 ? 1 : day + 1;
|
||||
p.DailyLastClaimed = Today;
|
||||
await Save(p);
|
||||
await Ledger(uid, "daily", reward, "day" + day);
|
||||
return (reward, p, day);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user