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:
@@ -17,7 +17,8 @@ public sealed class Player
|
||||
/// <summary>In-memory matchmaking + room registry. (EF/Postgres persistence is a TODO.)</summary>
|
||||
public sealed class GameManager
|
||||
{
|
||||
private const int QueueWaitMs = 6000;
|
||||
// Real players get priority: wait this long for humans before bots fill in.
|
||||
private const int QueueWaitMs = 9000;
|
||||
|
||||
private static readonly string[] BotNames =
|
||||
{ "آرش", "کیان", "نیلوفر", "سارا", "رضا", "مهسا", "امیر", "پارسا", "الناز", "بابک" };
|
||||
@@ -140,6 +141,9 @@ public sealed class GameManager
|
||||
public void PlayCard(string userId, string cardId) => RoomOf(userId)?.HumanPlay(userId, cardId);
|
||||
public void ChooseTrump(string userId, string suit) => RoomOf(userId)?.HumanChooseTrump(userId, suit);
|
||||
public void SendReaction(string userId, string reaction) => RoomOf(userId)?.Reaction(userId, reaction);
|
||||
public void RequestForfeit(string userId) => RoomOf(userId)?.RequestForfeit(userId);
|
||||
public void ConfirmForfeit(string userId) => RoomOf(userId)?.ConfirmForfeit(userId);
|
||||
public void DeclineForfeit(string userId) => RoomOf(userId)?.DeclineForfeit(userId);
|
||||
|
||||
private readonly ConcurrentDictionary<string, int> _onlineUsers = new();
|
||||
public int OnlineCount => _onlineUsers.Count;
|
||||
|
||||
@@ -42,8 +42,15 @@ public sealed class GameRoom : IDisposable
|
||||
// match-level tally for server-authoritative rewards
|
||||
private readonly int[] _tallyTricks = new int[2];
|
||||
private readonly bool[] _tallyKot = new bool[2];
|
||||
private readonly int[] _hakemRounds = new int[4]; // per-seat hakem count
|
||||
private int _lastHakemDeal = -1;
|
||||
private bool _rewardsApplied;
|
||||
|
||||
// forfeit (surrender) state
|
||||
private int? _forfeitPendingTeam;
|
||||
private string? _forfeitRequester;
|
||||
private Timer? _forfeitTimer;
|
||||
|
||||
public string Id { get; } = Guid.NewGuid().ToString("N")[..8];
|
||||
public string Code { get; } = Guid.NewGuid().ToString("N")[..6].ToUpperInvariant();
|
||||
public SeatSlot[] Seats { get; }
|
||||
@@ -95,25 +102,29 @@ public sealed class GameRoom : IDisposable
|
||||
if (rr.Kot) _tallyKot[rr.WinningTeam] = true;
|
||||
}
|
||||
|
||||
private async Task ApplyRewardsAsync()
|
||||
private async Task ApplyRewardsAsync(int? forfeitTeam = null, bool forfeitKot = false)
|
||||
{
|
||||
if (_rewardsApplied) return;
|
||||
_rewardsApplied = true;
|
||||
int winner = State.MatchWinner ?? 0;
|
||||
int winner = forfeitTeam.HasValue ? 1 - forfeitTeam.Value : (State.MatchWinner ?? 0);
|
||||
int rounds = State.MatchScore[0] + State.MatchScore[1];
|
||||
foreach (var slot in Seats.Where(s => !s.IsBot && s.UserId != null))
|
||||
{
|
||||
int team = slot.Seat % 2;
|
||||
bool won = team == winner;
|
||||
var summary = new MatchSummaryDto
|
||||
{
|
||||
Ranked = Ranked,
|
||||
Stake = Stake,
|
||||
Won = team == winner,
|
||||
KotFor = _tallyKot[team],
|
||||
KotAgainst = _tallyKot[1 - team],
|
||||
Won = won,
|
||||
// On a forfeit, the kot penalty is the surrendering team scoring 0 rounds.
|
||||
KotFor = forfeitTeam.HasValue ? (won && forfeitKot) : _tallyKot[team],
|
||||
KotAgainst = forfeitTeam.HasValue ? (!won && forfeitKot) : _tallyKot[1 - team],
|
||||
TricksWon = _tallyTricks[team],
|
||||
Rounds = rounds,
|
||||
Shutout = team == winner && State.MatchScore[1 - winner] == 0,
|
||||
Shutout = won && State.MatchScore[1 - winner] == 0,
|
||||
HakemRounds = _hakemRounds[slot.Seat],
|
||||
RoundsWon = State.MatchScore[team],
|
||||
};
|
||||
using var scope = _scopes.CreateScope();
|
||||
var svc = scope.ServiceProvider.GetRequiredService<ProfileService>();
|
||||
@@ -170,6 +181,84 @@ public sealed class GameRoom : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------------------------- forfeit ----------------------------- */
|
||||
|
||||
public void RequestForfeit(string userId)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_finished || _forfeitPendingTeam != null) return;
|
||||
var seat = SeatOf(userId);
|
||||
if (seat is null) return;
|
||||
int team = seat.Value % 2;
|
||||
var mate = Seats.FirstOrDefault(s => s.Seat % 2 == team && s.Seat != seat.Value);
|
||||
// No human teammate to ask → forfeit immediately.
|
||||
if (mate is null || mate.IsBot || mate.UserId is null || !mate.Connected)
|
||||
{
|
||||
FinalizeForfeit(team);
|
||||
return;
|
||||
}
|
||||
_forfeitPendingTeam = team;
|
||||
_forfeitRequester = userId;
|
||||
var requester = Seats.First(s => s.UserId == userId);
|
||||
_ = _hub.Clients.User(mate.UserId!).SendAsync("forfeit", new { bySeat = requester.Seat, byName = requester.Name });
|
||||
_forfeitTimer?.Dispose();
|
||||
_forfeitTimer = new Timer(_ =>
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_forfeitPendingTeam == team)
|
||||
{
|
||||
_forfeitPendingTeam = null;
|
||||
_forfeitRequester = null;
|
||||
_ = _hub.Clients.User(mate.UserId!).SendAsync("forfeit", (object?)null);
|
||||
}
|
||||
}
|
||||
}, null, 20000, Timeout.Infinite);
|
||||
}
|
||||
}
|
||||
|
||||
public void ConfirmForfeit(string userId)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_forfeitPendingTeam is null) return;
|
||||
var seat = SeatOf(userId);
|
||||
if (seat is null || seat.Value % 2 != _forfeitPendingTeam.Value || userId == _forfeitRequester) return;
|
||||
_forfeitTimer?.Dispose();
|
||||
FinalizeForfeit(_forfeitPendingTeam.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public void DeclineForfeit(string userId)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_forfeitPendingTeam is null) return;
|
||||
var seat = SeatOf(userId);
|
||||
if (seat is null || seat.Value % 2 != _forfeitPendingTeam.Value) return;
|
||||
var requester = _forfeitRequester;
|
||||
_forfeitPendingTeam = null;
|
||||
_forfeitRequester = null;
|
||||
_forfeitTimer?.Dispose();
|
||||
if (requester != null) _ = _hub.Clients.User(requester).SendAsync("forfeit", (object?)null);
|
||||
}
|
||||
}
|
||||
|
||||
private void FinalizeForfeit(int team)
|
||||
{
|
||||
_forfeitPendingTeam = null;
|
||||
_forfeitRequester = null;
|
||||
_forfeitTimer?.Dispose();
|
||||
bool kot = State.MatchScore[team] == 0;
|
||||
State.MatchWinner = 1 - team;
|
||||
State.Phase = Phase.MatchOver;
|
||||
_timer?.Dispose();
|
||||
_ = ApplyRewardsAsync(team, kot);
|
||||
BroadcastState();
|
||||
if (!_finished) { _finished = true; OnFinished?.Invoke(this); }
|
||||
}
|
||||
|
||||
/* ----------------------- scheduling / driver ----------------------- */
|
||||
|
||||
private void ScheduleAndBroadcast()
|
||||
@@ -187,6 +276,12 @@ public sealed class GameRoom : IDisposable
|
||||
case Phase.ChoosingTrump:
|
||||
{
|
||||
var hakem = State.Hakem!.Value;
|
||||
// Tally hakem rounds once per deal (for the hakem achievements).
|
||||
if (State.DealId != _lastHakemDeal)
|
||||
{
|
||||
_lastHakemDeal = State.DealId;
|
||||
_hakemRounds[hakem]++;
|
||||
}
|
||||
if (Seats[hakem].IsBot)
|
||||
SetTimer(AiTrumpMs, () =>
|
||||
{
|
||||
@@ -220,6 +315,7 @@ public sealed class GameRoom : IDisposable
|
||||
}
|
||||
|
||||
case Phase.TrickComplete:
|
||||
MaybeBotReact();
|
||||
SetTimer(TrickPauseMs, () =>
|
||||
{
|
||||
Rules.AdvanceAfterTrick(State, 2);
|
||||
@@ -242,6 +338,16 @@ public sealed class GameRoom : IDisposable
|
||||
BroadcastState();
|
||||
}
|
||||
|
||||
private static readonly string[] BotEmotes = { "👍", "😎", "🔥", "😂", "👏", "🎯", "🙌" };
|
||||
/// <summary>Occasionally a bot reacts after winning a trick — makes them feel human.</summary>
|
||||
private void MaybeBotReact()
|
||||
{
|
||||
var w = State.LastTrickWinner;
|
||||
if (w is null || !Seats[w.Value].IsBot) return;
|
||||
if (_rng.Next(100) < 18)
|
||||
Broadcast("reaction", new ReactionDto(w.Value, BotEmotes[_rng.Next(BotEmotes.Length)]));
|
||||
}
|
||||
|
||||
private void AutoPlay(int seat)
|
||||
{
|
||||
lock (_lock)
|
||||
|
||||
@@ -40,4 +40,7 @@ public sealed class GameHub : Hub
|
||||
public void PlayCard(string cardId) => _manager.PlayCard(Uid, cardId);
|
||||
public void ChooseTrump(string suit) => _manager.ChooseTrump(Uid, suit);
|
||||
public void SendReaction(string reaction) => _manager.SendReaction(Uid, reaction);
|
||||
public void RequestForfeit() => _manager.RequestForfeit(Uid);
|
||||
public void ConfirmForfeit() => _manager.ConfirmForfeit(Uid);
|
||||
public void DeclineForfeit() => _manager.DeclineForfeit(Uid);
|
||||
}
|
||||
|
||||
@@ -44,64 +44,47 @@ public static class Gamification
|
||||
|
||||
// metric: wins|kotsFor|bestWinStreak|shutoutWins|games|tricks|level ; ratingFloor>0 = rank ach.
|
||||
private record AchDef(string Id, string? Metric, int RatingFloor, int Goal, int Coin, string NameFa, string NameEn, string Icon);
|
||||
private static readonly AchDef[] Achs =
|
||||
// Mirrors src/lib/online/gamification.ts (same ids/goals/coins/metrics).
|
||||
private static int Coin(int g) => Math.Max(100, (int)Math.Floor((80.0 + g * 12) / 50.0 + 0.5) * 50);
|
||||
private static string Fa(int n) =>
|
||||
new string(n.ToString().Select(c => c is >= '0' and <= '9' ? "۰۱۲۳۴۵۶۷۸۹"[c - '0'] : c).ToArray());
|
||||
|
||||
private static AchDef[] Tier(string metric, string prefix, string icon, int[] goals, Func<int, string> faName, Func<int, string> enName)
|
||||
=> goals.Select(g => new AchDef($"{prefix}_{g}", metric, 0, g, Coin(g), faName(g), enName(g), icon)).ToArray();
|
||||
|
||||
private static readonly AchDef[] Achs = BuildAchs();
|
||||
private static AchDef[] BuildAchs()
|
||||
{
|
||||
// victories
|
||||
new("first_win", "wins", 0, 1, 100, "اولین برد", "First Win", "🥇"),
|
||||
new("wins_10", "wins", 0, 10, 300, "۱۰ برد", "10 Wins", "🎯"),
|
||||
new("wins_25", "wins", 0, 25, 600, "۲۵ برد", "25 Wins", "🏅"),
|
||||
new("wins_50", "wins", 0, 50, 1000, "۵۰ برد", "50 Wins", "🏆"),
|
||||
new("wins_100", "wins", 0, 100, 2000, "۱۰۰ برد", "100 Wins", "👑"),
|
||||
new("wins_250", "wins", 0, 250, 4000, "۲۵۰ برد", "250 Wins", "💎"),
|
||||
new("wins_500", "wins", 0, 500, 8000, "۵۰۰ برد", "500 Wins", "🌟"),
|
||||
new("shutout_1", "shutoutWins", 0, 1, 400, "هفت–هیچ", "Seven–Zip", "🧹"),
|
||||
new("shutout_5", "shutoutWins", 0, 5, 900, "۵ بار هفت–هیچ", "5× Sweep", "🧨"),
|
||||
new("shutout_25", "shutoutWins", 0, 25, 3000, "۲۵ بار هفت–هیچ", "25× Sweep", "☄️"),
|
||||
// kot
|
||||
new("first_kot", "kotsFor", 0, 1, 150, "اولین کُت", "First Kot", "🔥"),
|
||||
new("kot_5", "kotsFor", 0, 5, 300, "۵ کُت", "5 Kots", "🌶️"),
|
||||
new("kot_10", "kotsFor", 0, 10, 500, "۱۰ کُت", "10 Kots", "🔥"),
|
||||
new("kot_25", "kotsFor", 0, 25, 1200, "۲۵ کُت", "25 Kots", "💥"),
|
||||
new("kot_50", "kotsFor", 0, 50, 2500, "۵۰ کُت", "50 Kots", "⚡"),
|
||||
new("kot_100", "kotsFor", 0, 100, 5000, "۱۰۰ کُت", "100 Kots", "👹"),
|
||||
// streaks
|
||||
new("streak_3", "bestWinStreak", 0, 3, 200, "۳ برد پیاپی", "3 Win Streak", "➡️"),
|
||||
new("streak_5", "bestWinStreak", 0, 5, 400, "۵ برد پیاپی", "5 Win Streak", "⚡"),
|
||||
new("streak_10", "bestWinStreak", 0, 10, 1000, "۱۰ برد پیاپی", "10 Win Streak", "🌊"),
|
||||
new("streak_15", "bestWinStreak", 0, 15, 2000, "۱۵ برد پیاپی", "15 Win Streak", "🚀"),
|
||||
// levels
|
||||
new("level_5", "level", 0, 5, 150, "سطح ۵", "Level 5", "⭐"),
|
||||
new("level_10", "level", 0, 10, 300, "سطح ۱۰", "Level 10", "🌟"),
|
||||
new("level_15", "level", 0, 15, 500, "سطح ۱۵", "Level 15", "✨"),
|
||||
new("level_20", "level", 0, 20, 800, "سطح ۲۰", "Level 20", "💫"),
|
||||
new("level_25", "level", 0, 25, 1200, "سطح ۲۵", "Level 25", "🔆"),
|
||||
new("level_30", "level", 0, 30, 1600, "سطح ۳۰", "Level 30", "🎖️"),
|
||||
new("level_40", "level", 0, 40, 2500, "سطح ۴۰", "Level 40", "🏵️"),
|
||||
new("level_50", "level", 0, 50, 4000, "سطح ۵۰", "Level 50", "🌠"),
|
||||
// ranks
|
||||
new("reach_silver", null, 1100, 1, 200, "لیگ نقره", "Reach Silver", "🥈"),
|
||||
new("reach_gold", null, 1300, 1, 500, "لیگ طلا", "Reach Gold", "🥇"),
|
||||
new("reach_platinum", null, 1500, 1, 1000, "لیگ پلاتین", "Reach Platinum", "🛡️"),
|
||||
new("reach_diamond", null, 1700, 1, 2000, "لیگ الماس", "Reach Diamond", "💠"),
|
||||
new("reach_master", null, 1900, 1, 4000, "لیگ استاد", "Reach Master", "👑"),
|
||||
// veterancy
|
||||
new("games_10", "games", 0, 10, 150, "۱۰ بازی", "10 Games", "🎮"),
|
||||
new("games_50", "games", 0, 50, 350, "۵۰ بازی", "50 Games", "🕹️"),
|
||||
new("games_200", "games", 0, 200, 1200, "۲۰۰ بازی", "200 Games", "🎲"),
|
||||
new("games_500", "games", 0, 500, 3000, "۵۰۰ بازی", "500 Games", "🃏"),
|
||||
new("games_1000", "games", 0, 1000, 7000, "۱۰۰۰ بازی", "1000 Games", "♾️"),
|
||||
new("tricks_100", "tricks", 0, 100, 300, "۱۰۰ دست", "100 Tricks", "🎴"),
|
||||
new("tricks_1000", "tricks", 0, 1000, 2000, "۱۰۰۰ دست", "1000 Tricks", "🗂️"),
|
||||
};
|
||||
var l = new List<AchDef>();
|
||||
l.AddRange(Tier("wins", "wins", "🏆", new[] { 1, 5, 10, 25, 50, 75, 100, 150, 200, 250, 300, 400, 500, 750, 1000, 2000 }, g => $"{Fa(g)} برد", g => $"{g} Wins"));
|
||||
l.AddRange(Tier("shutoutWins", "shutout", "🧹", new[] { 1, 3, 5, 10, 25, 50, 100 }, g => $"{Fa(g)} بار هفت–هیچ", g => $"{g}× Sweep"));
|
||||
l.AddRange(Tier("kotsFor", "kot", "🔥", new[] { 1, 3, 5, 10, 25, 50, 75, 100, 150, 200, 300, 500 }, g => $"{Fa(g)} کُت", g => $"{g} Kots"));
|
||||
l.AddRange(Tier("bestWinStreak", "streak", "⚡", new[] { 2, 3, 5, 7, 10, 15, 20, 25, 30, 40 }, g => $"{Fa(g)} برد پیاپی", g => $"{g} Win Streak"));
|
||||
l.AddRange(Tier("hakemRounds", "hakem", "👑", new[] { 7, 25, 50, 100, 250, 500, 1000 }, g => $"{Fa(g)} بار حاکم", g => $"Hakem {g}×"));
|
||||
l.AddRange(Tier("level", "level", "⭐", new[] { 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 60, 70, 80, 90, 100 }, g => $"سطح {Fa(g)}", g => $"Level {g}"));
|
||||
l.AddRange(Tier("games", "games", "🎮", new[] { 10, 25, 50, 100, 200, 300, 500, 750, 1000, 2000, 5000 }, g => $"{Fa(g)} بازی", g => $"{g} Games"));
|
||||
l.AddRange(Tier("roundsWon", "rounds", "🎴", new[] { 25, 100, 250, 500, 1000, 2000, 5000 }, g => $"{Fa(g)} دست برده", g => $"{g} Rounds Won"));
|
||||
l.AddRange(Tier("tricks", "tricks", "🗂️", new[] { 50, 100, 250, 500, 1000, 2500, 5000, 10000 }, g => $"{Fa(g)} دستبرد", g => $"{g} Tricks"));
|
||||
l.AddRange(Tier("losses", "grit", "🛡️", new[] { 10, 50, 100 }, g => $"{Fa(g)} باخت", g => $"{g} Losses"));
|
||||
l.Add(new AchDef("reach_silver", null, 1100, 1, 200, "لیگ نقره", "Reach Silver", "🥈"));
|
||||
l.Add(new AchDef("reach_gold", null, 1300, 1, 500, "لیگ طلا", "Reach Gold", "🥇"));
|
||||
l.Add(new AchDef("reach_platinum", null, 1500, 1, 1000, "لیگ پلاتین", "Reach Platinum", "🛡️"));
|
||||
l.Add(new AchDef("reach_diamond", null, 1700, 1, 2000, "لیگ الماس", "Reach Diamond", "💠"));
|
||||
l.Add(new AchDef("reach_master", null, 1900, 1, 4000, "لیگ استاد", "Reach Master", "👑"));
|
||||
return l.ToArray();
|
||||
}
|
||||
|
||||
private static int Metric(string m, StatsDto st, int level) => m switch
|
||||
{
|
||||
"wins" => st.Wins,
|
||||
"losses" => st.Losses,
|
||||
"kotsFor" => st.KotsFor,
|
||||
"bestWinStreak" => st.BestWinStreak,
|
||||
"shutoutWins" => st.ShutoutWins,
|
||||
"games" => st.Games,
|
||||
"tricks" => st.Tricks,
|
||||
"hakemRounds" => st.HakemRounds,
|
||||
"roundsWon" => st.RoundsWon,
|
||||
"level" => level,
|
||||
_ => 0,
|
||||
};
|
||||
@@ -166,6 +149,8 @@ public static class Gamification
|
||||
st.CurrentWinStreak = cur;
|
||||
st.BestWinStreak = Math.Max(st.BestWinStreak, cur);
|
||||
st.ShutoutWins += s.Won && s.Shutout ? 1 : 0;
|
||||
st.HakemRounds += s.HakemRounds;
|
||||
st.RoundsWon += s.RoundsWon;
|
||||
|
||||
var newAch = new List<AchievementUnlockDto>();
|
||||
int achCoins = 0;
|
||||
|
||||
@@ -14,6 +14,8 @@ public class StatsDto
|
||||
public int BestWinStreak { get; set; }
|
||||
public int CurrentWinStreak { get; set; }
|
||||
public int ShutoutWins { get; set; }
|
||||
public int HakemRounds { get; set; }
|
||||
public int RoundsWon { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Mirrors the client UserProfile (camelCase JSON).</summary>
|
||||
@@ -61,6 +63,8 @@ public class MatchSummaryDto
|
||||
public int TricksWon { get; set; }
|
||||
public int Rounds { get; set; }
|
||||
public bool Shutout { get; set; }
|
||||
public int HakemRounds { get; set; }
|
||||
public int RoundsWon { get; set; }
|
||||
}
|
||||
|
||||
public class AchievementUnlockDto
|
||||
|
||||
@@ -11,10 +11,10 @@ public class ProfileService
|
||||
|
||||
public static readonly CoinPackDto[] Packs =
|
||||
{
|
||||
new() { Id = "p1", Coins = 1000, Bonus = 0, PriceToman = 19000 },
|
||||
new() { Id = "p2", Coins = 5000, Bonus = 500, PriceToman = 89000, Tag = "popular" },
|
||||
new() { Id = "p3", Coins = 12000, Bonus = 2000, PriceToman = 179000, Tag = "best" },
|
||||
new() { Id = "p4", Coins = 30000, Bonus = 7000, PriceToman = 399000 },
|
||||
new() { Id = "p1", Coins = 50000, Bonus = 0, PriceToman = 95000, Tag = "starter" },
|
||||
new() { Id = "p2", Coins = 120000, Bonus = 15000, PriceToman = 189000, Tag = "popular" },
|
||||
new() { Id = "p3", Coins = 300000, Bonus = 50000, PriceToman = 389000, Tag = "best" },
|
||||
new() { Id = "p4", Coins = 700000, Bonus = 150000, PriceToman = 790000 },
|
||||
};
|
||||
|
||||
private static ProfileDto Default(string userId, string? name) => new()
|
||||
|
||||
Reference in New Issue
Block a user