mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-11-14 19:13:54 +01:00
feat: free-play choice + per-game dual leaderboard (backend)
Free-to-play alongside paid, with scores split per game between a Pleb board
(free) and the Hall of Champions (paid). Keys on the existing per-game
game_mode, so no new game columns.
- Leaderboard API: GET /api/leaderboard?board=pleb|champions|all. champions =
game_mode IN (PAID_SINGLE,PAID_CREDITS), pleb = FREE. board is whitelisted.
Migration 019 adds a supporting partial index.
- FREE_PLAY_ENABLED config: offer free play as an explicit choice even when the
instance also sells credits/entry (always on when payments are off).
- play_free socket event -> handleAutoStart(socket, {free:true}); skips all
payment/credits/payout-address gating and records the game as game_mode=FREE
via processGameStart(..., {forceFree:true}) -> _processGameStartFree (no
payment, no payout, counted).
- getPaymentOptionsForUser exposes a play_free option; /api/game-modes exposes
freePlayEnabled.
Adds freePlay.test.js. Full backend suite: 22 suites / 158 tests.
This commit is contained in:
@@ -381,6 +381,7 @@ class SocketHandlers {
|
||||
}
|
||||
});
|
||||
socket.on('auto_start', () => this.handleAutoStart(socket)); // New handler for start button
|
||||
socket.on('play_free', () => this.handleAutoStart(socket, { free: true })); // Explicit free-play choice
|
||||
socket.on('join_queue', () => this.handleJoinQueue(socket)); // Queue instead of auto-start
|
||||
socket.on('early_entry', () => this.handleEarlyEntry(socket)); // Early entry without waiting for block
|
||||
socket.on('address:prompt', () => this.handleAddressPrompt(socket));
|
||||
@@ -402,15 +403,19 @@ class SocketHandlers {
|
||||
* Applies payment eligibility + payout address gating (for payout-eligible modes)
|
||||
* If eligible, starts game immediately (bypassing block queue) and processes game start (credit deduction / payment linkage)
|
||||
*/
|
||||
async handleAutoStart(socket) {
|
||||
async handleAutoStart(socket, opts = {}) {
|
||||
try {
|
||||
// The player explicitly chose FREE play (Pleb board, no payment/payout). Only
|
||||
// honoured when the instance allows free play; otherwise fall through to paid.
|
||||
const wantsFree = opts.free === true && this.gameModeManager?.freePlayEnabled;
|
||||
|
||||
// Rate limiting for game starts — keyed on stable identity + IP so reconnecting
|
||||
// (new socket.id) can't reset the limit.
|
||||
const rlId = stableId(socket, this.sessionManager);
|
||||
const rlIp = clientIp(socket);
|
||||
const rateLimitResult = await this.rateLimiter.checkLimit(rlId, 'game:start', rlIp);
|
||||
if (!rateLimitResult.allowed) {
|
||||
this.broadcastManager.sendStatusUpdate(socket.id, 'warning',
|
||||
this.broadcastManager.sendStatusUpdate(socket.id, 'warning',
|
||||
`Please wait ${Math.ceil(rateLimitResult.retryAfter / 1000)} seconds before starting another game. (${rateLimitResult.remaining} attempts remaining)`);
|
||||
return;
|
||||
}
|
||||
@@ -428,7 +433,8 @@ class SocketHandlers {
|
||||
}
|
||||
|
||||
let canStart = { allowed: true, reason: 'Free mode' };
|
||||
if (this.gameModeManager) {
|
||||
// Free play skips all payment/credits/payout-address gating below.
|
||||
if (this.gameModeManager && !wantsFree) {
|
||||
// Payout address gating for modes that can payout
|
||||
const payoutEligible = (this.gameModeManager.gameMode === 'PAID_SINGLE') || (this.gameModeManager.gameMode === 'PAID_CREDITS' && this.gameModeManager.creditsPayoutEnabled);
|
||||
if (payoutEligible) {
|
||||
@@ -480,9 +486,9 @@ class SocketHandlers {
|
||||
state.proof = game.getProofCommitment();
|
||||
}
|
||||
|
||||
// Process start (credits deduction / payment link)
|
||||
// Process start (free / credits deduction / payment link)
|
||||
if (this.gameModeManager) {
|
||||
const startRes = await this.gameModeManager.processGameStart(socket.id, game.id);
|
||||
const startRes = await this.gameModeManager.processGameStart(socket.id, game.id, { forceFree: wantsFree });
|
||||
if (!startRes.success) {
|
||||
// Abort game
|
||||
this.activeGames.delete(socket.id);
|
||||
|
||||
Reference in New Issue
Block a user