Forfeit = 2x coin loss + 0 XP (no kot); end-of-game roster + add friend
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 21s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m0s
CI/CD / Deploy - local stack (db + server + web) (push) Failing after 0s

Forfeit penalty reworked (client + server gamification, in sync):
- Surrendering team loses DOUBLE the entry coins; winner takes the stake.
- Forfeiter earns NO XP. No kot is applied or mentioned anymore.
- MatchSummary/Dto carry a `forfeit` flag; GameRoom.FinalizeForfeit →
  ApplyRewardsAsync(team) with Forfeit=true (dropped the kot path).
- Forfeit confirm dialogs now alert the real penalty (double coins, no XP).

End-of-game roster: SeatPlayerDto/ServerSeatPlayer + game-store SeatPlayer gain
userId/isBot. New <MatchPlayersList> lists everyone at the table on the final
screen (PostMatchRewardsModal + AI MatchOverlay) with a tactile "Add" button to
send a friend request to real (non-bot, non-self) players ("Sent" after).

Verified: tsc + sim + dotnet + next build clean; stack rebuilt :1500/:1505.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-05 10:40:14 +03:30
parent 6bbdbac23b
commit b739b503eb
13 changed files with 127 additions and 19 deletions
+4
View File
@@ -111,6 +111,8 @@ export function ratingDelta(
export function coinDelta(summary: MatchSummary): number {
// Free games (vs computer / private friend rooms) never touch coins.
if (!summary.ranked) return 0;
// Forfeit: the surrendering team loses double the stake; the winner takes the stake.
if (summary.forfeit) return summary.won ? summary.stake : -2 * summary.stake;
// Ranked: win the stake (+kot bonus scaled to the league), lose the stake.
// Higher leagues stake more, so wins/losses swing bigger.
const kotBonus = summary.won && summary.kotFor ? Math.round(summary.stake * 0.4) : 0;
@@ -160,6 +162,8 @@ export function leagueXpFactor(stake: number): number {
export const PREMIUM_XP_MULT = 1.5;
export function matchXp(summary: MatchSummary): number {
// Forfeiting (surrendering) earns no XP.
if (summary.forfeit && !summary.won) return 0;
// Every game grants XP; the winner earns double.
const base = 40 + summary.tricksWon * 5 + (summary.kotFor ? 30 : 0);
return Math.round(base * (summary.won ? 2 : 1) * leagueXpFactor(summary.stake));
+2
View File
@@ -318,6 +318,7 @@ export interface MatchSummary {
shutout: boolean; // won with the opponent on 0 rounds (e.g. 70)
hakemRounds: number; // rounds you were hakem this match
roundsWon: number; // rounds (dast) your team won this match
forfeit: boolean; // the match ended by forfeit (loser: 2× coin loss, 0 XP)
}
/** A teammate's request to forfeit (surrender) the match. */
@@ -453,6 +454,7 @@ export interface ServerSeatPlayer {
level: number;
connected: boolean;
isBot: boolean;
userId?: string; // present for real players (for add-friend after the match)
}
export interface ServerRoundResult {
winningTeam: number;