diff --git a/public/views/mp/rushroyale.html b/public/views/mp/rushroyale.html index 86f11969..b903138a 100644 --- a/public/views/mp/rushroyale.html +++ b/public/views/mp/rushroyale.html @@ -310,6 +310,7 @@ const players_re = /^[2345678]$/; const player_width = 228; const players = []; + let sorted_players = []; let num_players = 8; @@ -370,10 +371,10 @@ } } else { if (p2.death_rank == null) { - // player 1 active, player 2 eliminated + // player 1 eliminated, player 2 active return 1; } else { - // both players eliminated - order by rank + // both players eliminated - order by death rank return p1.death_rank - p2.death_rank; } } @@ -382,43 +383,38 @@ } } - function getSortedPlayers() { - const sorted_players = - rank_mode === 'score' - ? [...players].sort(byScoreDescending) - : [...players].sort((p1, p2) => { - if (p1.death_rank == null) { - if (p2.death_rank == null) { - // both players still active - return byScoreDescending(p1, p2); - } else { - // player 1 active, player 2 eliminated - return -1; - } - } else { - if (p2.death_rank == null) { - // player 1 active, player 2 eliminated - return 1; - } else { - // both players eliminated - order by rank - return p1.death_rank - p2.death_rank; - } - } - }); - - const active_players = sorted_players.filter(player => { - if (rank_mode === 'score') { - return !player.hasState('eliminated'); - } else { - return !player.game?.over && !player.hasState('eliminated'); - } - }); + function computeSortedPlayers() { + if (rank_mode === 'score') { + sorted_players = [...players].sort(byScoreDescending); + } else { + sorted_players = [...players].sort((p1, p2) => { + if (p1.death_rank == null) { + if (p2.death_rank == null) { + // both players still active + return byScoreDescending(p1, p2); + } else { + // player 1 active, player 2 eliminated + return -1; + } + } else { + if (p2.death_rank == null) { + // player 1 eliminated, player 2 active + return 1; + } else { + // both players eliminated - order by rank + return p1.death_rank - p2.death_rank; + } + } + }); + } + } - return { sorted_players, active_players }; + function getActivePlayers() { + return sorted_players.filter(player => !player.hasState('eliminated')); // NB: in death mode, eliminated and dead are equivalent } - function updateScore() { - let { sorted_players, active_players } = getSortedPlayers(); + function renderPlayerList() { + const active_players = getActivePlayers(); // reset everything sorted_players.forEach((player, idx) => { @@ -427,46 +423,6 @@ player.dom.rank_node.querySelector('.diff').textContent = ''; }); - if (rank_mode === 'score') { - // in score mode, a score update may end the game: that happens on completing a chase down - const alive_players = active_players.filter( - player => !player.game?.over - ); - - if (alive_players.length === 1) { - if (alive_players[0] === sorted_players[0]) { - // it's a win! (chase down completion) - active_players.forEach(player => { - player.addStateClass('eliminated'); - }); - alive_players[0].addStateClass('first'); - alive_players[0].playWinnerAnimation(); - reset(); - return; - } - } - - // in score rank mode, topped out players who are at the bottom obviously cannot come back, - // so they should be marked eliminated right away - let needs_change = 0; - for (let p_idx = sorted_players.length; p_idx--; ) { - const player = sorted_players[p_idx]; - - if (!player.game?.over) break; - if (!player.hasState('eliminated')) { - player.addStateClass('eliminated'); - needs_change += 1; - } - } - - if (needs_change) { - startCycle(cycle_settings.subsequent_rounds * needs_change); - const resorted = getSortedPlayers(); - sorted_players = resorted.sorted_players; - active_players = resorted.active_players; - } - } - if (active_players.length >= 2) { // grab all the tail players with the same score const lastPlayer = peek(active_players); @@ -524,6 +480,130 @@ } } + function checkWinner() { + const active_players = getActivePlayers(); + + if (active_players.length > 1) return null; + + const winner = sorted_players[0]; + + winner.addStateClass('first'); + winner.addStateClass('first'); + winner.playWinnerAnimation(); + + reset(); + + return winner; + } + + function eliminateBottomDeadPlayers() { + // players who are ranked last and are dead obviously cannot come back, so we need to eliminate them explicitly + // so they should be marked eliminated right away + const eliminated = []; + + for (let p_idx = sorted_players.length; p_idx--; ) { + const player = sorted_players[p_idx]; + + if (!player.game?.over) break; + if (!player.hasState('eliminated')) { + player.addStateClass('eliminated'); + eliminated.push(player); + } + } + + return eliminated; + } + + function handlePlayerScoreUpdate(player) { + computeSortedPlayers(); + + if (rank_mode === 'score') { + const num_kicked = eliminateBottomDeadPlayers().length; + + if (num_kicked) { + if (checkWinner()) { + reset(); + } else { + startCycle(cycle_settings.subsequent_rounds * num_kicked); + } + } + } + + renderPlayerList(); + } + + function handlePlayerTopOut(player) { + player.addStateClass('topout'); + player.dom.rank_node.querySelector('.diff').textContent = 'TOPOUT'; + + let num_kicked = 0; + + if (rank_mode === 'death') { + // in death mode, topout is always a straight elimination + player.addStateClass('eliminated'); + num_kicked = 1; + } else { + // players who topped out might be getting kicked + // but maybe not if he/she is not the lowest active score + // (in which case the topout does eliminate anyone!) + num_kicked = eliminateBottomDeadPlayers().length; + } + + if (num_kicked) { + computeSortedPlayers(); + renderPlayerList(); + + if (checkWinner()) { + reset(); + } else { + startCycle(cycle_settings.subsequent_rounds * num_kicked); + } + } + } + + let endingCycle = false; + + function handleCycleEnd() { + endingCycle = true; + + // figure out who to kick + const kicked_players = kickBottomPlayers(); + + let num_kicked = kicked_players.length; + + if (num_kicked <= 0) { + // Nobody is kicked, so it's a tie win! + getActivePlayers().forEach(player => { + player.playWinnerAnimation(); + }); + reset(); + endingCycle = false; + return; + } + + if (checkWinner()) { + reset(); + renderPlayerList(); + endingCycle = false; + return; + } + + if (rank_mode === 'score') { + // since someone was kicked, it's possible there are now dead players at the bottom, need to eliminate them as well + num_kicked += eliminateBottomDeadPlayers().length; + } + + if (checkWinner()) { + reset(); + } else { + startCycle(cycle_settings.subsequent_rounds * num_kicked); + } + + renderPlayerList(); + + endingCycle = false; + } + let cycle_end_ts; let rafId; let toId; @@ -562,10 +642,10 @@ function startRound() { roundInit(); startCycle(cycle_settings.initial_round); - checkTime(); + updateTime(); } - function checkTime() { + function updateTime() { const remainder = (cycle_end_ts - Date.now()) / 1000; let content = remainder.toFixed(2); @@ -582,7 +662,7 @@ timer.textContent = content; - rafId = window.requestAnimationFrame(checkTime); + rafId = window.requestAnimationFrame(updateTime); } function startCycle(duration) { @@ -590,92 +670,36 @@ toId = clearTimeout(toId); cycle_end_ts = Date.now() + duration * 1000; - toId = setTimeout(endCycle, duration * 1000); + toId = setTimeout(handleCycleEnd, duration * 1000); } - let endingCycle = false; + function kickBottomPlayers() { + const active_players = getActivePlayers(); - function endCycle() { - endingCycle = true; - // figure out who to kick - const num_kicked = kickPlayer(); - - if (num_kicked) { - updateScore(); - startCycle(cycle_settings.subsequent_rounds * num_kicked); - } else { - // round is over - reset(); - } - - endingCycle = false; - } - - function kickPlayer(topOutPlayer) { - const { sorted_players, active_players } = getSortedPlayers(); - - let cut_idx; - - // special handling for top out players to ensure the winner is shown - if (topOutPlayer) { - // having topOutPlayer here means we are in death mode - // which means if there is only one active player, that player IS the lastplayer standing and is the winner - if (active_players.length === 1) { - active_players[0].game?.end(); - active_players[0].playWinnerAnimation(); - reset(); - } else { - startCycle(); - } - - return; - } - - if (active_players.length == 1 && endingCycle) { - // when there's just one active player at cycle end, - // then we're in score mode and this is the end of a failed chase down - // we know it's a failed chased down, because successful chase down are captured in the scoreUpdate() function - const player = active_players[0]; - player.game?.end(); // marks the player eliminated as side effect - sorted_players[0].playWinnerAnimation(); - return; - } + const kicked = []; if (active_players.length >= 2) { // grab all the tail players with the same score const lastPlayer = peek(active_players); const lastPlayerScore = lastPlayer.getScore(); - cut_idx = active_players.length - 2; + let cut_idx = active_players.length - 2; while (active_players[cut_idx].getScore() === lastPlayerScore) cut_idx--; - if (cut_idx === -1) { - // tie victory! - active_players.forEach(player => { - player.game?.end(); - player.playWinnerAnimation(); - }); - } else { + if (cut_idx >= 0) { for (let idx = cut_idx + 1; idx < active_players.length; idx++) { const player = active_players[idx]; player.game?.end(); - - // must set alimited explicitly here (in addition than inside the gameover handler) - // because player could be topped out, and so the gameover handler wouldn't run player.addStateClass('eliminated'); - } - if (cut_idx === 0) { - // we have a winner - sorted_players[0].game?.end(); - sorted_players[0].playWinnerAnimation(); - } else { - return active_players.length - cut_idx - 1; // return number of players kicked + kicked.push(player); } } } + + return kicked; } window.players = players; @@ -828,16 +852,7 @@ if (endingCycle) { this.addStateClass('eliminated'); } else { - this.addStateClass('topout'); - player.dom.rank_node.querySelector('.diff').textContent = - 'TOPOUT'; - - if (rank_mode === 'death') { - this.addStateClass('eliminated'); - - updateScore(); - kickPlayer(this); - } + handlePlayerTopOut(this); } }; @@ -884,7 +899,6 @@ startCountDown: function (seconds) { countDownTimeoutId = clearTimeout(countDownTimeoutId); reset(); - roundInit(); this.__startCountDown(seconds); @@ -896,15 +910,19 @@ players.forEach(player => { player.onScore = () => { + if (endingCycle) return; // discard "fake" score update from death + player.dom.rank_node.querySelector('.score2').textContent = readableScoreFomatter(player.getScore()); - updateScore(); + + handlePlayerScoreUpdate(player); }; }); window.competition = competition; - updateScore(); + computeSortedPlayers(); + renderPlayerList(); }; if (/^\/replay\//.test(location.pathname)) {