Economy: free vs paid games + buy-coins page; friends remove confirmation

- Coins only matter for ranked: free games (vs computer / private friend rooms)
  cost nothing; random ranked requires an entry (stake), gated by balance →
  routes to buy-coins when short
- Buy Coins page (CoinPack/getCoinPacks/buyCoins; mock credits now, real
  Zarinpal/IDPay TODO); TopBar coins → buy; lobby create-room is Free
- Friends: removed instant red ✕ delete; UserMinus → inline confirm before remove

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-04 16:28:59 +03:30
parent fe136f7ee4
commit cdb8d522dd
12 changed files with 257 additions and 38 deletions
+4 -3
View File
@@ -104,10 +104,11 @@ export function ratingDelta(
/* ------------------------------- Coins ------------------------------- */
export function coinDelta(summary: MatchSummary): number {
const base = summary.won ? (summary.ranked ? 50 : 25) : 10;
const stakeNet = summary.won ? summary.stake : -summary.stake;
// Free games (vs computer / private friend rooms) never touch coins.
if (!summary.ranked) return 0;
// Ranked: win the stake (+kot bonus), lose the stake.
const kotBonus = summary.won && summary.kotFor ? 40 : 0;
return base + stakeNet + kotBonus;
return (summary.won ? summary.stake : -summary.stake) + kotBonus;
}
/* ------------------------------- XP ---------------------------------- */
+21
View File
@@ -21,6 +21,7 @@ import {
AppNotification,
AuthSession,
ChatMessage,
CoinPack,
Conversation,
DailyRewardState,
Friend,
@@ -772,6 +773,26 @@ export class MockOnlineService implements OnlineService {
/* --------------------- leaderboard / shop / daily ------------------ */
async getCoinPacks(): Promise<CoinPack[]> {
return [
{ id: "p1", coins: 1000, bonus: 0, priceToman: 19000 },
{ id: "p2", coins: 5000, bonus: 500, priceToman: 89000, tag: "popular" },
{ id: "p3", coins: 12000, bonus: 2000, priceToman: 179000, tag: "best" },
{ id: "p4", coins: 30000, bonus: 7000, priceToman: 399000 },
];
}
async buyCoins(packId: string) {
const p = await this.getProfile();
const pack = (await this.getCoinPacks()).find((x) => x.id === packId);
if (!pack) return { ok: false, coins: 0 };
// NOTE: real payment (Zarinpal/IDPay) goes here. For now we credit instantly.
const added = pack.coins + pack.bonus;
this.profile = { ...p, coins: p.coins + added };
this.saveProfile();
return { ok: true, profile: this.profile, coins: added };
}
private onlineCount = 600 + Math.floor(Math.random() * 900);
async getOnlineCount(): Promise<number> {
// gentle random walk so the badge feels alive
+5
View File
@@ -7,6 +7,7 @@ import {
AppNotification,
AuthSession,
ChatMessage,
CoinPack,
Conversation,
DailyRewardState,
Friend,
@@ -111,6 +112,10 @@ export interface OnlineService {
buyItem(id: string): Promise<{ ok: boolean; profile?: UserProfile; messageFa: string; messageEn: string }>;
getDailyState(): Promise<DailyRewardState>;
claimDaily(): Promise<{ reward: number; profile: UserProfile; day: number }>;
/* ----- coin purchases (real payment gateway: TODO Zarinpal/IDPay) ----- */
getCoinPacks(): Promise<CoinPack[]>;
buyCoins(packId: string): Promise<{ ok: boolean; profile?: UserProfile; coins: number }>;
}
import { MockOnlineService } from "./mock-service";
+2
View File
@@ -293,4 +293,6 @@ export class SignalrService implements OnlineService {
buyItem(id: string) { return this.mock.buyItem(id); }
getDailyState(): Promise<DailyRewardState> { return this.mock.getDailyState(); }
claimDaily(): Promise<{ reward: number; profile: UserProfile; day: number }> { return this.mock.claimDaily(); }
getCoinPacks() { return this.mock.getCoinPacks(); }
buyCoins(id: string) { return this.mock.buyCoins(id); }
}
+10
View File
@@ -327,6 +327,16 @@ export interface ShopItem {
preview: string; // emoji/avatar id/color
}
/* ------------------------------ Coin packs --------------------------- */
export interface CoinPack {
id: string;
coins: number;
bonus: number; // extra coins
priceToman: number;
tag?: "popular" | "best";
}
/* --------------------------- Daily reward ---------------------------- */
export interface DailyRewardState {