100 gated gifts (level/rating-locked) + requirement system
CI/CD / CI - API (dotnet build + engine sim) (push) Has been cancelled
CI/CD / CI - Web (tsc + next build) (push) Has been cancelled
CI/CD / Deploy - local stack (db + server + web) (push) Has been cancelled

Adds ~100 new purchasable gifts that are LOCKED until a level/rating gate is met,
then buyable with coins — value scales with the gate:
- 45 gift avatars (types.ts), 35 gift titles + 20 gift card backs (gamification.ts),
  all reusing existing renderers. Tier (1-5) encoded in the id (-t<n>-).
- Gate model: GIFT_TIERS (shared) → reqLevel/reqRating on AvatarDef/TitleDef/
  CardBackDef + ShopItem. Tiers: t1 free, t2 Lv10, t3 Lv20, t4 Lv35, t5 Rating1700.
- Shop UI: locked cards dim + show the requirement (Lock + "Level 20"), buy
  disabled until met; mock buyItem enforces it offline.
- Server enforces generically — ProfileService parses the tier from the id and
  checks the player's level/rating (no 100-entry mirror). Mirrors GIFT_TIERS.
- i18n shop.reqLevel/reqRating (fa+en).

Verified: tsc + sim + next build + dotnet build all pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-07 00:02:28 +03:30
parent e49df07c0f
commit 38ac8b06d1
6 changed files with 166 additions and 6 deletions
@@ -125,10 +125,28 @@ public class ProfileService
["xp3"] = (8000, 1500),
};
// Gated gifts encode their tier in the id (`-t<n>-`); the gate is derived from
// the tier so the server enforces it without a 100-entry catalog mirror.
// Mirrors GIFT_TIERS in src/lib/online/types.ts.
private static readonly (int Level, int Rating)[] GiftGate =
{ (0, 0), (0, 0), (10, 0), (20, 0), (35, 0), (0, 1700) }; // index = tier (1..5)
private static (int Level, int Rating) GiftGateFor(string id)
{
var m = System.Text.RegularExpressions.Regex.Match(id, @"-t(\d)-");
if (m.Success && int.TryParse(m.Groups[1].Value, out var tier) && tier >= 1 && tier <= 5)
return GiftGate[tier];
return (0, 0);
}
public async Task<(bool ok, ProfileDto? profile, string error)> ShopBuy(string uid, string kind, string id, int price)
{
var p = await GetOrCreate(uid, null);
// Gated gift: locked until the player meets the tier's level/rating gate.
var gate = GiftGateFor(id);
if (p.Level < gate.Level || p.Rating < gate.Rating) return (false, p, "locked");
// XP packs are consumable (grant XP, may level up) — not added to an owned list.
if (kind == "xp")
{