mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-19 23:28:32 +02:00
fix: resolve security/correctness review + AAA graphics pass
Backend (Tier 0/1/3): single-payout double-pay -> needs_review; admin-retry tx_hash guard; UNIQUE index on games.payment_id (migration 023) + atomic claim; reconnect no longer resets block-timeout; completeGame keys on user_id+dungeon_seed (not socket_id); token-gated /api/user/:socketId/* endpoints; payout min/max enforcement; orphaned-active-game boot recovery; sessionManager cleanup FK fix; XFF rightmost-hop; global uncaughtException/unhandledRejection guard; server binds 127.0.0.1; provably-fair client-seed (HMAC) + RNG [0,1) bound; difficulty preset ranges reach the digger; chat no longer leaks full socket.id; removed dangerous-keyword SQL theater. Match mode (Tier 2, still MATCH_ENABLED-gated): fixed match_queue/match_move crash paths + scheduler dead-code + leaked ceiling timer; pot collected before start with abort+refund; BigInt/basis-point fee math, capped payout; no-address winner claimable; winnerId==placement[0]; finalize schema + payout-first isolation; reconnect re-key with ownership; refunds on all cancel paths; real countdown; map/timer cleanup. +16 tests. Graphics: new overlay FX layer (torch light, vignette, embers, sparkle/burst/flash/shake, self-suspending rAF); gameplay juice; atmospheric screens; CSS design-token chrome with framed modals; richer Kenney tile renderer; imported Kenney dungeon/interior sheets + fx particles. Verification: 45 suites / 312 tests pass; 40 changed JS files parse; 20 server modules load clean; no regressions.
This commit is contained in:
@@ -530,6 +530,11 @@ class MatchRoom extends Room {
|
||||
if (this.status !== 'finished') return this.playerStates;
|
||||
|
||||
const exit = this.dungeon.exit;
|
||||
// A winner already designated by first-to-escape (_checkResolution) must remain the
|
||||
// winner: the first player out wins the race regardless of treasure/speed tiebreakers
|
||||
// among later finishers. When no one escaped (timeout / all-dead) preWinner is null and
|
||||
// ranking alone decides.
|
||||
const preWinner = this.winnerId;
|
||||
const ranked = Array.from(this.playerStates.entries()).map(([id, state]) => {
|
||||
const occ = this.occupants.get(id);
|
||||
const dist = exit && occ && state.alive ? Math.abs(occ.x - exit[0]) + Math.abs(occ.y - exit[1]) : Infinity;
|
||||
@@ -537,6 +542,12 @@ class MatchRoom extends Room {
|
||||
});
|
||||
|
||||
ranked.sort((a, b) => {
|
||||
// Designated escape-winner always ranks first (placement #1 == crypto payout target).
|
||||
if (preWinner) {
|
||||
const aw = a.id === preWinner ? 1 : 0;
|
||||
const bw = b.id === preWinner ? 1 : 0;
|
||||
if (aw !== bw) return bw - aw;
|
||||
}
|
||||
const finishedA = a.state.finished ? 1 : 0;
|
||||
const finishedB = b.state.finished ? 1 : 0;
|
||||
if (finishedA !== finishedB) return finishedB - finishedA;
|
||||
@@ -557,6 +568,12 @@ class MatchRoom extends Room {
|
||||
r.state.score = this._calculateScore(r.state, i + 1, r.dist);
|
||||
}
|
||||
|
||||
// Winner consistency across ALL end paths (escape / timeout / all-dead): the crypto
|
||||
// payout target (winnerId) and the leaderboard's placement #1 always name the SAME
|
||||
// player. finalize() is the single source of truth — it runs after expire() in every
|
||||
// path, so it reconciles any provisional winner expire() may have set.
|
||||
if (ranked.length > 0) this.winnerId = ranked[0].id;
|
||||
|
||||
return this.playerStates;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user