AWBW Auto Replay + Play/Pause

Auto-replays with play/pause support, right-click settings, and spacebar toggle

// ==UserScript==
// @name         AWBW Auto Replay + Play/Pause
// @namespace    https://awbw.amarriner.com/
// @version      1.13
// @description  Auto-replays with play/pause support, right-click settings, and spacebar toggle
// @author       twiggy_, updated by ChatGPT
// @match        https://awbw.amarriner.com/*?games_id=*
// @match        https://awbw.amarriner.com/*?replays_id=*
// @icon         https://awbw.amarriner.com/favicon.ico
// @license MIT
// ==/UserScript==

// Tool variables
var replayMenu = document.querySelector('.replay-open')?.parentNode;

var replayImgLink = 'https://i.imgur.com/9b3vzJL.png';

var clicked = false;

var replayLength = 0;
let autoActionPlayer = null;

let isAutoReplaying = false;

let ACTION_DELAY = 1700;
let START_DELAY = 2500;

let ahMoveAR = null;
let ahCaptAR = null;
let ahBuildAR = null;
let ahFireAR = null;
let ahEventAR = null;

let isLastTurn = true;
let isFullGame = false;

let playButton = null;
let pauseButton = null;

function startAutoReplay() {
    if (autoActionPlayer) clearInterval(autoActionPlayer);

    isAutoReplaying = true;
    if (playButton) playButton.disabled = true;
    if (pauseButton) pauseButton.disabled = false;

    let replayFwdBtnAction = document.querySelector(".replay-forward-action");
    if (!replayFwdBtnAction) return;

    autoActionPlayer = setInterval(() => {
        replayFwdBtnAction.click();
    }, ACTION_DELAY);
}

function pauseAutoReplay() {
    if (autoActionPlayer) {
        clearInterval(autoActionPlayer);
        autoActionPlayer = null;
    }
    isAutoReplaying = false;
    if (playButton) playButton.disabled = false;
    if (pauseButton) pauseButton.disabled = true;
}

function togglePlayPause() {
    if (isAutoReplaying) {
        pauseAutoReplay();
    } else {
        startAutoReplay();
    }
}

function addPlayPauseButtons() {
    let replayControls = document.querySelector('.replay-controls');
    if (!replayControls) return;

    playButton = document.createElement('button');
    playButton.textContent = '▶ Play';
    playButton.style.marginLeft = '10px';
    playButton.style.fontSize = '12px';
    playButton.addEventListener('click', startAutoReplay);

    pauseButton = document.createElement('button');
    pauseButton.textContent = '⏸ Pause';
    pauseButton.style.marginLeft = '5px';
    pauseButton.style.fontSize = '12px';
    pauseButton.disabled = true;
    pauseButton.addEventListener('click', pauseAutoReplay);

    replayControls.appendChild(playButton);
    replayControls.appendChild(pauseButton);
}

function addAutoReplayContextMenu() {
    const contextMenu = document.createElement('div');
    contextMenu.id = 'auto-replay-context';
    contextMenu.style.position = 'absolute';
    contextMenu.style.display = 'none';
    contextMenu.style.zIndex = '100';
    contextMenu.style.backgroundColor = '#fff';
    contextMenu.style.border = '1px solid #888';
    contextMenu.style.padding = '8px';

    const delayLabel = document.createElement('label');
    delayLabel.textContent = 'Action Delay (ms):';
    delayLabel.style.display = 'block';
    delayLabel.style.marginBottom = '4px';

    const delayInput = document.createElement('input');
    delayInput.type = 'number';
    delayInput.step = '100';  // <--- make it increment in 100s
    delayInput.value = ACTION_DELAY;
    delayInput.style.width = '60px';

    // Change value by 100 on arrow key or mouse wheel
    delayInput.addEventListener('input', (e) => {
        ACTION_DELAY = parseInt(e.target.value);
    });
    delayInput.addEventListener('wheel', (e) => {
        e.preventDefault();
        let delta = Math.sign(e.deltaY);
        let current = parseInt(delayInput.value) || 0;
        delayInput.value = Math.max(0, current - delta * 100);
        delayInput.dispatchEvent(new Event('input'));
    });

    const modeLabel = document.createElement('div');
    modeLabel.textContent = 'Replay Mode:';
    modeLabel.style.marginTop = '8px';

    const lastTurnRadio = document.createElement('input');
    lastTurnRadio.type = 'radio';
    lastTurnRadio.name = 'replayMode';
    lastTurnRadio.checked = true;
    lastTurnRadio.addEventListener('change', () => {
        isLastTurn = true;
        isFullGame = false;
    });

    const lastTurnLabel = document.createElement('label');
    lastTurnLabel.textContent = 'Last Turn';
    lastTurnLabel.style.marginRight = '10px';

    const fullGameRadio = document.createElement('input');
    fullGameRadio.type = 'radio';
    fullGameRadio.name = 'replayMode';
    fullGameRadio.addEventListener('change', () => {
        isLastTurn = false;
        isFullGame = true;
    });

    const fullGameLabel = document.createElement('label');
    fullGameLabel.textContent = 'Full Game';

    contextMenu.appendChild(delayLabel);
    contextMenu.appendChild(delayInput);
    contextMenu.appendChild(modeLabel);
    contextMenu.appendChild(lastTurnRadio);
    contextMenu.appendChild(lastTurnLabel);
    contextMenu.appendChild(fullGameRadio);
    contextMenu.appendChild(fullGameLabel);

    document.body.appendChild(contextMenu);

    document.getElementById('auto-replay-parent').oncontextmenu = function(e) {
        e.preventDefault();
        contextMenu.style.top = e.pageY + 'px';
        contextMenu.style.left = e.pageX + 'px';
        contextMenu.style.display = 'block';
    };

    document.addEventListener('click', function(e) {
        if (!contextMenu.contains(e.target)) {
            contextMenu.style.display = 'none';
        }
    });
}


window.addEventListener('load', function() {
    addPlayPauseButtons();
    addAutoReplayContextMenu();

    // Add spacebar keybinding
    window.addEventListener('keydown', function(e) {
        if (e.code === 'Space' && !e.target.matches('input, textarea')) {
            e.preventDefault();
            togglePlayPause();
        }
    });
});

// Ensure auto-replay button is always visible
const observer = new MutationObserver(() => {
    const autoReplayDiv = document.getElementById('auto-replay-parent');
    if (autoReplayDiv && autoReplayDiv.style.display === 'none') {
        autoReplayDiv.style.display = 'block';
    }
});
observer.observe(document.body, { childList: true, subtree: true });

// Patch the input so that it increments/decrements in steps of 100
window.addEventListener('DOMContentLoaded', () => {
    const delayInput = document.getElementById('action-delay-input');
    if (delayInput) {
        delayInput.step = '100';
        delayInput.addEventListener('wheel', (e) => {
            e.preventDefault();
            let delta = Math.sign(e.deltaY);
            let current = parseInt(delayInput.value) || 0;
            delayInput.value = Math.max(0, current - delta * 100);
            delayInput.dispatchEvent(new Event('input'));
        });
    }
});


var autoReplayDiv = document.createElement('div');
autoReplayDiv.id = 'auto-replay-parent';
autoReplayDiv.classList.add('game-tools-btn');
autoReplayDiv.style.width = '34px';
autoReplayDiv.style.height = '30px';
autoReplayDiv.style.borderLeft = 'none';

var autoReplayDivHoverSpan = document.createElement('span');
autoReplayDivHoverSpan.classList.add('game-tools-btn-text', 'small_text');
autoReplayDivHoverSpan.innerText = "Auto Replay";

var autoReplayDivBackground = document.createElement('div');
autoReplayDivBackground.classList.add('game-tools-bg');

var autoReplayDivBackgroundSpan = document.createElement('span');
var autoReplayDivBackgroundLink = document.createElement('a');
var autoReplayDivBackgroundImg = document.createElement('img');
autoReplayDivBackgroundImg.src = replayImgLink;
autoReplayDivBackgroundImg.style.verticalAlign = "middle";
autoReplayDivBackgroundImg.style.width = '17px';
autoReplayDivBackgroundImg.style.height = '17px';

autoReplayDiv.appendChild(autoReplayDivBackground);
autoReplayDiv.appendChild(autoReplayDivHoverSpan);
autoReplayDivBackground.appendChild(autoReplayDivBackgroundSpan);
autoReplayDivBackgroundSpan.appendChild(autoReplayDivBackgroundLink);
autoReplayDivBackgroundLink.appendChild(autoReplayDivBackgroundImg);

if (replayMenu) replayMenu.appendChild(autoReplayDiv);
autoReplayDivBackgroundLink.onclick = autoReplay;

var replayCloseBtn = document.querySelector('.replay-close');
if (replayCloseBtn) {
    replayCloseBtn.addEventListener('click', function() {
        clearInterval(autoActionPlayer);
        autoActionPlayer = null;
        isAutoReplaying = false;
        clicked = false;
        if (playButton) playButton.disabled = false;
        if (pauseButton) pauseButton.disabled = true;
    });
}

function autoReplay() {
    if (clicked) return;
    isAutoReplaying = true;
    autoReplayDiv.style.display = 'none';

    if (isLastTurn) {
        replayLength = String((gameDay * Object.keys(playersInfo).length) - (Object.keys(playersInfo).length - (playerKeys.indexOf(currentTurn) + 1)) - 2);
    } else if (isFullGame) {
        replayLength = 0;
    }

    openReplayMode(replayLength);
    setTimeout(startAutoReplay, START_DELAY);
    clicked = true;
}

// Action Handlers
ahMoveAR = actionHandlers.Move;
actionHandlers.Move = function()
{
    ahMoveAR.apply(actionHandlers.Move, arguments);

    if (isAutoReplaying === false) { return; }

    let u = document.querySelectorAll("[data-unit-id='" + String(arguments[0].unit.units_id) + "']");
    u[0].appendChild(unitIdentifierImg);
}

ahCaptAR = actionHandlers.Capt;
actionHandlers.Capt = function()
{
    ahCaptAR.apply(actionHandlers.Capt, arguments);

    if (isAutoReplaying === false) { return; }

    let b = document.querySelectorAll("[data-building-id='" + String(arguments[0].buildingInfo.buildings_id) + "']");
    b[0].appendChild(unitIdentifierImg);
}

ahBuildAR = actionHandlers.Build;
actionHandlers.Build = function()
{
    ahBuildAR.apply(actionHandlers.Build, arguments);

    if (isAutoReplaying === false) { return; }

    let u = document.querySelectorAll("[data-unit-id='" + String(arguments[0].newUnit.units_id) + "']");
    u[0].appendChild(unitIdentifierImg);
}

ahFireAR = actionHandlers.Fire;
actionHandlers.Fire = function()
{
    ahFireAR.apply(actionHandlers.Fire, arguments);

    if (isAutoReplaying === false) { return; }

    let u = document.querySelectorAll("[data-unit-id='" + String(arguments[0].attacker.units_id) + "']");
    u[0].appendChild(unitIdentifierImg);
}

ahEventAR = showEventScreen;
showEventScreen = function()
{
    ahEventAR.apply(showEventScreen, arguments);

    if (isAutoReplaying === false) { return; }

    var eventHeader = document.querySelector('.event-username');

    console.log(eventHeader.innerText);
    console.log(isLastTurn);
    console.log(isFullGame);

    if (eventHeader.innerText.includes("Day") && isLastTurn && !isFullGame)
    {
      unitIdentifierImg.style.display = 'none';
      clearInterval(autoActionPlayer);
      isAutoReplaying = false;
    }
}

// Custom context menu
let contextMenuAutoReplay = document.createElement('div');
contextMenuAutoReplay.id = 'div-context-menu-ar';
contextMenuAutoReplay.classList.add('cls-context-menu-ar');
contextMenuAutoReplay.style.position = 'absolute';
contextMenuAutoReplay.style.height = '76px';
contextMenuAutoReplay.style.paddingTop = '4px';
contextMenuAutoReplay.style.paddingBottom = '4px';
autoReplayDiv.appendChild(contextMenuAutoReplay);

document.getElementById('auto-replay-parent').oncontextmenu = function(e) {
   let elmnt = e.target
   if (elmnt.id.startsWith ("auto-replay")) {
      e.preventDefault();
      let eid = elmnt.id.replace(/link-/,"");
      contextMenuAutoReplay.style.height = '98px';
      contextMenuAutoReplay.style.width = '155px';
      contextMenuAutoReplay.style.top = '37px';
      contextMenuAutoReplay.style.display = 'block';
      let toRepl = "to=" + eid.toString();
   }
}

on(document, "click", function(e)
{
  if (e.target.id == "div-context-menu-ar" ||
      e.target.id == "ar-options-flex-container" ||
      e.target.id == "full-game-radio-btn" ||
      e.target.id == "last-turn-radio-btn" ||
      e.target.id == "action-delay-input") return;
  contextMenuAutoReplay.style.display = 'none';
});

// Title
let autoReplayFlexContainer = document.createElement('div');
autoReplayFlexContainer.id = "ar-options-flex-container";
autoReplayFlexContainer.style.display = 'flex';
autoReplayFlexContainer.style.flexDirection = 'row';
autoReplayFlexContainer.style.marginBottom = '3.5px';
autoReplayFlexContainer.style.alignItems = 'center';

let autoReplaySpanDiv = document.createElement('div');
autoReplaySpanDiv.id = "ar-options--div";
autoReplaySpanDiv.style.display = 'inline-block';
autoReplaySpanDiv.style.width = '100%';
autoReplaySpanDiv.style.textAlign = 'center';

let autoReplaySpan = document.createElement('span');
autoReplaySpan.id = "ar-options-desc";
autoReplaySpan.textContent = "Auto Replay Options";
autoReplaySpan.style.fontSize = "13px";

contextMenuAutoReplay.appendChild(autoReplayFlexContainer);
autoReplayFlexContainer.appendChild(autoReplaySpanDiv);
autoReplaySpanDiv.appendChild(autoReplaySpan);

// Last Turn Radio Button
var lastTurnRadioBtn = document.createElement('input');
lastTurnRadioBtn.id = "last-turn-radio-btn";
lastTurnRadioBtn.classList.add('ar-radio-btn');
lastTurnRadioBtn.type = "radio";

// Last Turn Div
let arOptionsFlexContainer = document.createElement('div');
arOptionsFlexContainer.id = "last-turn-flex-container";
arOptionsFlexContainer.style.display = 'flex';
arOptionsFlexContainer.style.flexDirection = 'column';
arOptionsFlexContainer.style.marginTop = '7px';
arOptionsFlexContainer.style.alignItems = 'center';
arOptionsFlexContainer.style.justifyContent = 'center';

let lastTurnSpanDiv = document.createElement('div');
lastTurnSpanDiv.id = "last-turn-slider-div";
lastTurnSpanDiv.style.display = 'inline-block';
lastTurnSpanDiv.style.width = 'auto';
lastTurnSpanDiv.style.textAlign = 'center';

let lastTurnSpan = document.createElement('span');
lastTurnSpan.id = "last-turn-slider-desc";
lastTurnSpan.textContent = "Last Turn";
lastTurnSpan.style.fontSize = "11px";
lastTurnSpan.style.marginRight = "5px";

contextMenuAutoReplay.appendChild(arOptionsFlexContainer);

// Delay Input
var actionDelayInput = document.createElement('input');
actionDelayInput.id = "action-delay-input";
actionDelayInput.type = "text";
actionDelayInput.style.width = "30px";
actionDelayInput.style.fontSize = "12px";

let actionDelayFlexContainer = document.createElement('div');
actionDelayFlexContainer.id = "action-delay-flex-container";
actionDelayFlexContainer.style.display = 'flex';
actionDelayFlexContainer.style.flexDirection = 'column';
actionDelayFlexContainer.style.marginTop = '7px';
actionDelayFlexContainer.style.alignItems = 'center';
actionDelayFlexContainer.style.justifyContent = 'center';

let actionDelaySpanDiv = document.createElement('div');
actionDelaySpanDiv.id = "action-delay-div";
actionDelaySpanDiv.style.display = 'inline-block';
actionDelaySpanDiv.style.width = 'auto';
actionDelaySpanDiv.style.textAlign = 'center';
actionDelaySpanDiv.style.marginTop = '5px';

let actionDelaySpan = document.createElement('span');
actionDelaySpan.id = "action-delay-desc";
actionDelaySpan.textContent = "Action Delay (ms)";
actionDelaySpan.style.fontSize = "11px";
actionDelaySpan.style.marginLeft = "5px";

arOptionsFlexContainer.appendChild(actionDelaySpanDiv);
actionDelaySpanDiv.appendChild(actionDelayInput);
actionDelaySpanDiv.appendChild(actionDelaySpan);

arOptionsFlexContainer.appendChild(lastTurnSpanDiv);
lastTurnSpanDiv.appendChild(lastTurnRadioBtn);
lastTurnSpanDiv.appendChild(lastTurnSpan);

// Full Game Radio Button
var fullGameRadioBtn = document.createElement('input');
fullGameRadioBtn.id = "full-game-radio-btn";
fullGameRadioBtn.classList.add('ar-radio-btn');
fullGameRadioBtn.type = "radio";

// Full Game Div
let fullGameSpanDiv = document.createElement('div');
fullGameSpanDiv.id = "full-game-slider-div";
fullGameSpanDiv.style.display = 'inline-block';
fullGameSpanDiv.style.width = 'auto';
fullGameSpanDiv.style.textAlign = 'center';
fullGameSpanDiv.style.marginTop = '5px';

let fullGameSpan = document.createElement('span');
fullGameSpan.id = "full-game-slider-desc";
fullGameSpan.textContent = "Full Game";
fullGameSpan.style.fontSize = "11px";

arOptionsFlexContainer.appendChild(fullGameSpanDiv);
fullGameSpanDiv.appendChild(fullGameRadioBtn);
fullGameSpanDiv.appendChild(fullGameSpan);

// Toggle Last Turn Auto Replay Mode
isLastTurn = false;
let toggleLastTurn = function(val)
{
  isLastTurn = true;
  isFullGame = false;

  lastTurnRadioBtn.checked = true;
  fullGameRadioBtn.checked = false;
}
document.getElementById("last-turn-radio-btn").addEventListener ("change", toggleLastTurn, false);

// Toggle Last Turn Auto Replay Mode
isFullGame = false;
let toggleFullGame= function(val)
{
  isLastTurn = false;
  isFullGame = true;

  lastTurnRadioBtn.checked = false;
  fullGameRadioBtn.checked = true;
}
document.getElementById("full-game-radio-btn").addEventListener ("change", toggleFullGame, false);

let updateActionDelay = function(val)
{
  // val = String(val).replace(/[^0-9.]/g, '').replace(/(\..*?)\..*/g, '$1').replace(/^0[^.]/, '0');
  ACTION_DELAY = val.target.value;
  console.log(ACTION_DELAY);
}
document.getElementById("action-delay-input").addEventListener ("input", updateActionDelay, false);

function setDefaultAutoReplayMode()
{
  isLastTurn = true;
  isFullGame = false;

  actionDelayInput.value = String(ACTION_DELAY);

  lastTurnRadioBtn.checked = true;
  fullGameRadioBtn.checked = false;
}

// Stylings
var autoReplayStyles = `
    // Context Menu
    .cls-context-menu-link {
        display:block;
        padding:20px;
        background:#ECECEC;
    }

    .cls-context-menu-ar {
        position: absolute;
        display: none;
        width: 155px;
        height: 98px;
        padding-top: 4px;
        background-color: white;
        z-index: 99;
    }

    .cls-context-menu-ar ul, #context-menu li {
        list-style:none;
        margin:0; padding:0;
        background:white;
    }

    .cls-context-menu-ar { border: 1px solid #888888 !important;}
    .cls-context-menu-ar li { border: 1px solid #888888; }
    .cls-context-menu-ar li:last-child { border:none; }
    .cls-context-menu-ar li a {
        display:block;
        padding:5px 10px;
        text-decoration:none;
        color:blue;
    }
    .cls-context-menu-ar li a:hover {
        background:blue;
        color:#FFF;
    }

    .ar-radio-btn {
      height: 14px;
      width: 14px;
    }

    .ar-radio-btn:hover {
      cursor: pointer;
    }

    nextActionPos = {
        'background': 'rgba(67, 217, 228, 0.4)',
        'box-sizing': 'border-box',
        'border': '1px solid rgb(22, 98, 184)',
        'height': '17px',
        'position': 'absolute',
        'width': '17px'
    }

    .blink {
      animation: blinker 0.35s step-start infinite;
    }

    .rotate {
      animation: rotation 3.5s infinite linear;
    }

    @keyframes blinker {
      50% {
        opacity: 0;
      }
    }

    @keyframes rotation {
      from {
        transform: rotate(0deg);
      }
      to {
        transform: rotate(359deg);
      }
    }
`

var autoReplayStyleSheet = document.createElement("style")
autoReplayStyleSheet.innerText = autoReplayStyles
document.head.appendChild(autoReplayStyleSheet);

setDefaultAutoReplayMode();