{"version":3,"file":"vr-agent.bundle.js?hash=1e0bdab0075790131522","mappings":";AAAA;AACA;AACA;;;;ACFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;ACPA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","sources":["webpack://game-client-lerna/webpack/bootstrap","webpack://game-client-lerna/webpack/runtime/global","webpack://game-client-lerna/./build/internal-tools/video-recorder/agent.js"],"sourcesContent":["// The require scope\nvar __webpack_require__ = {};\n\n","__webpack_require__.g = (function() {\n\tif (typeof globalThis === 'object') return globalThis;\n\ttry {\n\t\treturn this || new Function('return this')();\n\t} catch (e) {\n\t\tif (typeof window === 'object') return window;\n\t}\n})();","/**\n * Video Recorder — runtime agent (owned by c:/repos/video-recorder).\n *\n * Added as a webpack entry by build/internal-tools/video-recorder/webpack.js; the hooks\n * injected into Communication.ts by loader.js call into window.__VRB.\n *\n * Idea: the game DISPLAYS override bets (and proportionally scaled wins),\n * while the wire traffic to the backend keeps the ORIGINAL bets — backend\n * validation stays happy, no server-side configuration needed.\n *\n *  - incoming loadGame/settings: settings.bets[] is replaced with override\n *    values (config maps original → override); originals are remembered.\n *  - every incoming message: any `bet: { level }` object at any depth is\n *    translated original → override (restore / echo), and the ACTIVE\n *    coefficient (override / original) is derived from it; then every\n *    win / lastWin / lastWinAmount / totalWin / totalWinAmount field at any\n *    depth is multiplied by the active coefficient.\n *  - every outgoing message: `bet.level` is translated back override →\n *    original, and the active coefficient is updated.\n *\n * Config comes from the video-recorder server (GET /api/bets, sync XHR at\n * startup — the WS handshake happens much later, so this is safe and keeps\n * the hooks free of async races). Server discovery mirrors sound-lab:\n * ?vrServer= param → same origin when served from /game/ → localhost:4700.\n */\n(function () {\n    'use strict';\n    if (window.__VRB) return;\n\n    // winAmount = per-line win (what the win presentation actually shows),\n    // scatterWin / totalBonusWin(Amount) = other wire fields BetResponse reads.\n    // Deliberately NOT 'amount' — too generic (jackpot pools, bets, balance).\n    var WIN_KEYS = {\n        win: 1,\n        lastWin: 1,\n        lastWinAmount: 1,\n        totalWin: 1,\n        totalWinAmount: 1,\n        winAmount: 1,\n        scatterWin: 1,\n        totalBonusWin: 1,\n        totalBonusWinAmount: 1,\n    };\n    var MAX_DEPTH = 32;\n\n    var state = {\n        enabled: false,\n        /** original bet value (number key) → override bet value */\n        overrides: {},\n        originalBets: null,\n        overrideBets: null,\n        /** real ORIGINAL denomination per bet index (flat games: from the\n         *  bets×denominations matrix; single-denom games: the one denom for\n         *  every row; multi-denom with linesPerDenomination: null — bets are\n         *  not tied to a single denomination there) */\n        betDenoms: null,\n        /** bets × denominations flattening (5 bets + 5 denominations games).\n         *  The turtle bet popup cannot render a multi-denomination grid for\n         *  mystery-jackpot games (BetCalculatorViewTurtle bails out), and the\n         *  panel could only override the 5 raw values anyway. So the agent\n         *  presents the game ONE denomination (the smallest) and\n         *  bets×denominations flat bet levels whose money totals are identical\n         *  to the original matrix — the exact shape the \"pre-flattened\" games\n         *  already use (25/35 bets, single denomination). Wire traffic is\n         *  translated back to the original (level, denomination) pair. */\n        flatten: false,\n        rawBets: null,   // the game's real bets[] (per-denomination levels)\n        rawDenoms: null, // the game's real denominations[]\n        baseDenom: 0,    // the single denomination the game is shown\n        /** active win multiplier = overrideBet / originalBet of the current bet */\n        coeff: 1,\n        gameKey: null,\n        /** display math for the panel: shownMoney = bet × factor × denom / units */\n        factor: 1,\n        denomination: 1,\n        units: 100,\n        currency: '',\n        /** presentation balance — a private balance the game shows instead of\n         *  the real one, moving by coeff × (real delta) so the drop equals the\n         *  displayed bet and the rise equals the displayed win */\n        balanceOverride: false,\n        startBalance: 0, // in currency (0 = start from the game's real balance)\n        displayBalanceUnits: 0,\n        realBalancePrev: null,\n        balanceInit: false,\n        /** jackpot override — set target values per level; a fixed delta\n         *  (target − original-at-load) is added to currentValue in EVERY\n         *  message that carries jackpotStats (loadGame, jpstats, …), so the\n         *  jackpot shows the new value and keeps ticking from it */\n        jackpotOverride: false,\n        jackpotTargets: [], // per flat level index, in currency (0 = leave)\n        jackpotBase: null,  // original currentValue per level, captured once\n        jackpotDelta: null,\n        /** currency override — replace balance.currency in every message so the\n         *  game displays a different currency code (marketing variations) */\n        currencyOverride: false,\n        currencyCode: '',\n        /** custom-tool visibility — hide the dev overlays (#fps-meter,\n         *  #media-info, #console, dev-tools toggle) for a clean marketing video */\n        toolsVisible: true,\n    };\n\n    function log(msg) {\n        try { console.log('%c[video-recorder] ' + msg, 'color:#4da3ff'); } catch (e) {}\n    }\n\n    function serverBase() {\n        var m = window.location.search.match(/[?&]vrServer=([^&]+)/);\n        if (m) return decodeURIComponent(m[1]).replace(/\\/+$/, '');\n        if (window.location.pathname.indexOf('/game/') === 0) return window.location.origin;\n        return 'http://localhost:4700';\n    }\n    var SERVER = serverBase();\n\n    /**\n     * Mirrors sound-lab: on a deployed build the default server base is\n     * localhost, and the config XHR makes Chrome ask for local-network access\n     * the moment the game loads. So the server is only contacted on an\n     * explicit signal that the tool is in use: `?useAgents=true`, an explicit\n     * `?vrServer=`, or the game served by the tool itself (under /game/).\n     */\n    function agentsEnabled() {\n        var search = window.location.search;\n        if (/[?&]useAgents=true(&|$)/.test(search) || /[?&]vrServer=/.test(search)) return true;\n        return window.location.pathname.indexOf('/game/') === 0;\n    }\n\n    function loadConfig() {\n        if (!agentsEnabled()) {\n            log('no useAgents=true in the URL — config not fetched, override disabled');\n            return;\n        }\n        try {\n            var xhr = new XMLHttpRequest();\n            xhr.open('GET', SERVER + '/api/bets', false); // sync on purpose, see header comment\n            xhr.send(null);\n            if (xhr.status === 200) {\n                var cfg = JSON.parse(xhr.responseText);\n                state.enabled = !!cfg.enabled;\n                state.overrides = {};\n                var src = cfg.overrides || {};\n                for (var k in src) {\n                    var orig = Number(k);\n                    var over = Number(src[k]);\n                    if (isFinite(orig) && orig > 0 && isFinite(over) && over > 0) {\n                        state.overrides[orig] = over;\n                    }\n                }\n                var bal = cfg.balance || {};\n                state.balanceOverride = !!bal.enabled;\n                var amt = Number(bal.amount);\n                state.startBalance = isFinite(amt) && amt >= 0 ? amt : 0;\n                var jp = cfg.jackpots || {};\n                state.jackpotOverride = !!jp.enabled;\n                state.jackpotTargets = Array.isArray(jp.values) ? jp.values.map(Number) : [];\n                var curcfg = cfg.currency || {};\n                state.currencyOverride = !!curcfg.enabled;\n                state.currencyCode = typeof curcfg.code === 'string' ? curcfg.code.trim() : '';\n                state.toolsVisible = cfg.toolsVisible !== false; // default: visible\n                log('config loaded from ' + SERVER + ' — enabled: ' + state.enabled +\n                    ', overrides: ' + JSON.stringify(state.overrides) +\n                    (state.balanceOverride ? ', balance: ' + state.startBalance : '') +\n                    (state.jackpotOverride ? ', jackpots: ' + JSON.stringify(state.jackpotTargets) : '') +\n                    (state.currencyOverride ? ', currency: ' + state.currencyCode : '') +\n                    (state.toolsVisible ? '' : ', tools hidden'));\n            } else {\n                log('config fetch returned ' + xhr.status + ' — override disabled');\n            }\n        } catch (e) {\n            log('config fetch failed (' + e.message + ') — override disabled');\n        }\n    }\n\n    /** original → override (identity when no mapping) */\n    function overrideFor(v) {\n        var over = state.overrides[v];\n        return over !== undefined && over !== null ? over : v;\n    }\n\n    /** override (displayed) → original (real). Prefers positional mapping. */\n    function originalFor(shown) {\n        if (state.overrideBets && state.originalBets) {\n            var idx = state.overrideBets.indexOf(shown);\n            if (idx !== -1) return state.originalBets[idx];\n        }\n        for (var k in state.overrides) {\n            if (state.overrides[k] === shown) return Number(k);\n        }\n        return shown;\n    }\n\n    function setCoeffFromOriginal(orig) {\n        var over = overrideFor(orig);\n        state.coeff = orig > 0 && over !== orig ? over / orig : 1;\n    }\n\n    /** multiply preserving int-ness where possible (wins in units are ints) */\n    function scaleValue(v) {\n        var x = v * state.coeff;\n        return Math.abs(x - Math.round(x)) < 1e-9 ? Math.round(x) : Math.round(x * 100) / 100;\n    }\n\n    /**\n     * InitialSettings.filterBets() drops every bet where\n     * bet × denom × factor / units > maxJurisdictionalBet (same idea for\n     * clientSettings.maxBetLimits) — multiplied override bets would get\n     * filtered out and the bet buttons would keep showing the old values.\n     * Scale the limits by the largest coefficient so the overridden list\n     * passes exactly the same checks the original list did.\n     */\n    function relaxBetLimits(settings) {\n        var maxCoeff = 1;\n        for (var i = 0; i < state.originalBets.length; i++) {\n            var c = state.overrideBets[i] / state.originalBets[i];\n            if (c > maxCoeff) maxCoeff = c;\n        }\n        if (maxCoeff === 1) return;\n        var jur = settings.jurisdiction;\n        if (jur && Array.isArray(jur.maxJurisdictionalBet)) {\n            for (var j = 0; j < jur.maxJurisdictionalBet.length; j++) {\n                var entry = jur.maxJurisdictionalBet[j];\n                if (entry && typeof entry === 'object') {\n                    for (var cur in entry) {\n                        if (typeof entry[cur] === 'number') entry[cur] *= maxCoeff;\n                    }\n                }\n            }\n        }\n        var cs = settings.clientSettings;\n        if (cs && cs.maxBetLimits && typeof cs.maxBetLimits === 'object') {\n            for (var cur2 in cs.maxBetLimits) {\n                if (typeof cs.maxBetLimits[cur2] === 'number') cs.maxBetLimits[cur2] *= maxCoeff;\n            }\n        }\n        log('bet limits relaxed ×' + maxCoeff);\n    }\n\n    /**\n     * Replace the top-level `balance` with our presentation balance. It moves\n     * by coeff × (real delta): on a spin the real delta is (−realBet + realWin),\n     * both scaling by the same coeff, so the drop equals the DISPLAYED bet and\n     * the rise equals the DISPLAYED win — no mismatch with the shown amounts.\n     * Runs AFTER walkBetLevels so state.coeff is the current bet's coefficient.\n     */\n    function applyBalance(msg) {\n        if (!state.balanceOverride) return;\n        if (!msg.balance || typeof msg.balance.balance !== 'number') return;\n        var units = typeof msg.balance.units === 'number' && msg.balance.units > 0 ? msg.balance.units : state.units;\n        var real = msg.balance.balance;\n        if (!state.balanceInit) {\n            state.displayBalanceUnits = state.startBalance > 0 ? state.startBalance * units : real;\n            state.realBalancePrev = real;\n            state.balanceInit = true;\n        } else {\n            var delta = real - state.realBalancePrev;\n            state.realBalancePrev = real;\n            state.displayBalanceUnits += delta * state.coeff;\n            if (state.displayBalanceUnits < 0) state.displayBalanceUnits = 0;\n        }\n        msg.balance.balance = Math.round(state.displayBalanceUnits);\n    }\n\n    /** replace the currency code the game displays (balance.currency drives\n     *  Core.PLAYER_BALANCE.currency and every money label) */\n    function applyCurrency(msg) {\n        if (!state.currencyOverride || !state.currencyCode) return;\n        if (msg.balance && typeof msg.balance.currency === 'string') {\n            msg.balance.currency = state.currencyCode;\n        }\n    }\n\n    /** hide the dev overlays so they don't show up in a marketing recording */\n    function applyToolVisibility() {\n        if (state.toolsVisible) return;\n        try {\n            var style = document.getElementById('vr-hide-tools');\n            if (!style) {\n                style = document.createElement('style');\n                style.id = 'vr-hide-tools';\n                style.textContent =\n                    '#fps-meter,#media-info,#console,#dev-tools,[id^=\"devtool\"],[class*=\"dev-tools\"],' +\n                    // the custom tool's in-page overlay (CustomAppService.preloadIframe);\n                    // its detached window is outside the canvas and needs nothing\n                    '#custom-app-iframe-overlay' +\n                    '{display:none !important;visibility:hidden !important;}';\n                (document.head || document.documentElement).appendChild(style);\n            }\n        } catch (e) {\n            log('tool hide failed: ' + (e && e.message));\n        }\n    }\n\n    /* ---------------- jackpots ---------------- */\n\n    /** visit every jackpot level across all groups, flat index 0..N-1 */\n    function eachJackpotLevel(msg, cb) {\n        if (!msg || !Array.isArray(msg.jackpotStats)) return;\n        var idx = 0;\n        for (var g = 0; g < msg.jackpotStats.length; g++) {\n            var grp = msg.jackpotStats[g];\n            if (grp && Array.isArray(grp.levelStats)) {\n                for (var l = 0; l < grp.levelStats.length; l++) {\n                    cb(grp.levelStats[l], idx++, grp);\n                }\n            }\n        }\n    }\n\n    /**\n     * Shift jackpot currentValue by a fixed delta = target − original. The\n     * base (original per level) is captured on the FIRST message that carries\n     * jackpotStats (loadGame); the delta is then constant, so the jackpot keeps\n     * ticking naturally from the new value across every later jpstats/message.\n     */\n    function applyJackpots(msg) {\n        if (!msg || !Array.isArray(msg.jackpotStats)) return;\n        if (!state.jackpotBase) {\n            state.jackpotBase = [];\n            eachJackpotLevel(msg, function (level, i) {\n                state.jackpotBase[i] = typeof level.currentValue === 'number' ? level.currentValue : 0;\n            });\n            state.jackpotDelta = [];\n            for (var i = 0; i < state.jackpotBase.length; i++) {\n                var target = Number(state.jackpotTargets[i]);\n                state.jackpotDelta[i] = state.jackpotOverride && isFinite(target) && target > 0\n                    ? target * state.units - state.jackpotBase[i]\n                    : 0;\n            }\n            notifyJackpots();\n            if (state.jackpotOverride) log('jackpots base: ' + state.jackpotBase + ' delta: ' + state.jackpotDelta);\n        }\n        if (!state.jackpotOverride) return;\n        eachJackpotLevel(msg, function (level, i) {\n            var d = state.jackpotDelta[i] || 0;\n            if (d && typeof level.currentValue === 'number') {\n                level.currentValue += d;\n                if (level.currentValue < 0) level.currentValue = 0;\n            }\n        });\n    }\n\n    /**\n     * Pass 1 — find every `bet: { level }` object, translate the level\n     * original → override for display and derive the active coefficient.\n     * Runs BEFORE win scaling so restores pick the right coefficient.\n     * In flatten mode the (level, denomination) pair is collapsed into the\n     * flat level over baseDenom first, then mapped through the overrides.\n     */\n    function walkBetLevels(node, depth) {\n        if (!node || typeof node !== 'object' || depth > MAX_DEPTH) return;\n        if (node.bet && typeof node.bet === 'object' && typeof node.bet.level === 'number') {\n            if (state.flatten) {\n                var d = typeof node.bet.denomination === 'number' ? node.bet.denomination : state.baseDenom;\n                var flat = (node.bet.level * d) / state.baseDenom;\n                setCoeffFromOriginal(flat);\n                node.bet.level = overrideFor(flat);\n                if (typeof node.bet.denomination === 'number') node.bet.denomination = state.baseDenom;\n            } else {\n                var lvl = node.bet.level;\n                setCoeffFromOriginal(lvl);\n                node.bet.level = overrideFor(lvl);\n            }\n        }\n        for (var k in node) {\n            var v = node[k];\n            if (v && typeof v === 'object') walkBetLevels(v, depth + 1);\n        }\n    }\n\n    /** Pass 2 — multiply every win field at any depth by the coefficient. */\n    function walkWins(node, depth) {\n        if (!node || typeof node !== 'object' || depth > MAX_DEPTH) return;\n        for (var k in node) {\n            var v = node[k];\n            if (WIN_KEYS[k] === 1) {\n                if (typeof v === 'number' && isFinite(v)) {\n                    node[k] = scaleValue(v);\n                } else if (typeof v === 'string' && v !== '' && isFinite(Number(v))) {\n                    // some restore payloads carry win amounts as strings\n                    node[k] = String(scaleValue(Number(v)));\n                } else if (v && typeof v === 'object') {\n                    walkWins(v, depth + 1);\n                }\n            } else if (v && typeof v === 'object') {\n                walkWins(v, depth + 1);\n            }\n        }\n    }\n\n    /** temporary diagnostic dump for the 5-bets × 5-denominations investigation:\n     *  logs the exact settings shape before and after the transform */\n    function diagSettings(tag, msg) {\n        try {\n            var s = msg.settings || {};\n            var st = msg.game && msg.game.state && msg.game.state.bet;\n            console.log('[video-recorder] DIAG ' + tag + ' ' + JSON.stringify({\n                event: msg.event,\n                bets: s.bets,\n                bet: s.bet,\n                denominations: s.denominations,\n                denomination: s.denomination,\n                factor: s.factor,\n                factors: s.factors,\n                linesPerDenomination: s.linesPerDenomination,\n                maxJurisdictionalBet: s.jurisdiction && s.jurisdiction.maxJurisdictionalBet,\n                maxBetLimits: s.clientSettings && s.clientSettings.maxBetLimits,\n                stateBet: st,\n                units: msg.balance && msg.balance.units,\n            }));\n        } catch (e) {\n            log('diag failed: ' + (e && e.message));\n        }\n    }\n\n    /** every parsed server message goes through here (hook in Communication.ts) */\n    function incoming(msg) {\n        if (!msg || typeof msg !== 'object') return msg;\n        var hadBets = !!(msg.settings && Array.isArray(msg.settings.bets));\n        if (hadBets) diagSettings('IN(raw)', msg);\n        try {\n            // Always REPORT the game's real bets to the panel — even when the\n            // override is disabled or the config never loaded — so the user\n            // configures against real values instead of guessing.\n            if (msg.settings && Array.isArray(msg.settings.bets)) {\n                var s = msg.settings;\n                // multi-denomination game → flatten to bets×denominations\n                // levels over a single denomination (see state.flatten docs).\n                // Games with linesPerDenomination are excluded: their factor\n                // differs per denomination, so the totals would not survive.\n                state.flatten =\n                    Array.isArray(s.denominations) &&\n                    s.denominations.length > 1 &&\n                    !(Array.isArray(s.linesPerDenomination) && s.linesPerDenomination.length);\n                if (state.flatten) {\n                    state.rawBets = s.bets.slice();\n                    state.rawDenoms = s.denominations.slice();\n                    state.baseDenom = Math.min.apply(null, state.rawDenoms);\n                    state.originalBets = [];\n                    state.betDenoms = [];\n                    for (var di = 0; di < state.rawDenoms.length; di++) {\n                        for (var bi = 0; bi < state.rawBets.length; bi++) {\n                            state.originalBets.push((state.rawBets[bi] * state.rawDenoms[di]) / state.baseDenom);\n                            state.betDenoms.push(state.rawDenoms[di]);\n                        }\n                    }\n                } else {\n                    state.rawBets = null;\n                    state.rawDenoms = null;\n                    state.baseDenom = 0;\n                    state.originalBets = s.bets.slice();\n                    state.betDenoms = null; // set below, once the denomination is known\n                }\n                state.overrideBets = state.originalBets.map(overrideFor);\n                if (msg.gameKey) state.gameKey = msg.gameKey;\n                // capture the values the game uses to render the bet buttons:\n                // shownMoney = bet × factor × denomination / units  (BetButton.ts)\n                if (typeof s.factor === 'number') state.factor = s.factor;\n                if (state.flatten) {\n                    state.denomination = state.baseDenom;\n                } else if (typeof s.denomination === 'number') {\n                    state.denomination = s.denomination;\n                }\n                if (!state.flatten) {\n                    // single-denomination game → every bet belongs to that\n                    // denom; multi-denom with linesPerDenomination → a bet is\n                    // played on whichever denom the user selects, no fixed pair\n                    state.betDenoms =\n                        Array.isArray(s.denominations) && s.denominations.length > 1\n                            ? null\n                            : state.originalBets.map(function () { return state.denomination; });\n                }\n                if (msg.balance && typeof msg.balance.units === 'number') state.units = msg.balance.units;\n                if (msg.balance && msg.balance.currency) state.currency = msg.balance.currency;\n                if (state.currencyOverride && state.currencyCode) state.currency = state.currencyCode;\n                notifyBets();\n                if (state.enabled) {\n                    if (state.flatten) {\n                        // translate the selected bet BEFORE the denomination\n                        // fields are replaced (its flat value needs the real\n                        // selected denomination)\n                        if (typeof s.bet === 'number') {\n                            var selDenom = typeof s.denomination === 'number' ? s.denomination : state.baseDenom;\n                            var selFlat = (s.bet * selDenom) / state.baseDenom;\n                            setCoeffFromOriginal(selFlat);\n                            s.bet = overrideFor(selFlat);\n                        }\n                        s.denominations = [state.baseDenom];\n                        s.denomination = state.baseDenom;\n                    } else if (typeof s.bet === 'number') {\n                        setCoeffFromOriginal(s.bet);\n                        s.bet = overrideFor(s.bet);\n                    }\n                    s.bets = state.overrideBets.slice();\n                    relaxBetLimits(s);\n                    log('bets overridden' + (state.flatten ? ' (flattened ' + state.rawBets.length + '×' +\n                        state.rawDenoms.length + ', denom ' + state.baseDenom + ')' : '') +\n                        ': ' + state.originalBets + ' → ' + state.overrideBets);\n                } else {\n                    log('bets reported (override OFF' + (state.flatten ? ', flatten pending' : '') + '): ' +\n                        state.originalBets);\n                }\n            }\n            if (state.enabled) {\n                walkBetLevels(msg, 0);\n            }\n            // balance override is independent of bet override (coeff is 1 when\n            // bet override is off, so the balance just starts from startBalance)\n            applyBalance(msg);\n            // jackpots come in loadGame, jpstats and other messages; capture the\n            // base once and shift currentValue everywhere (independent of enabled)\n            applyJackpots(msg);\n            applyCurrency(msg);\n            if (state.enabled) {\n                walkWins(msg, 0);\n            }\n        } catch (e) {\n            log('incoming transform failed: ' + (e && e.message) + '\\n' + (e && e.stack));\n        }\n        if (hadBets) diagSettings('IN(transformed)', msg);\n        return msg;\n    }\n\n    /** every serialized request goes through here right before stringify */\n    function outgoing(msg) {\n        if (!state.enabled || !msg || typeof msg !== 'object') return msg;\n        // reconnect resends the same (already translated) object — do not translate twice\n        if (msg.__vrbDone) return msg;\n        try {\n            Object.defineProperty(msg, '__vrbDone', { value: true, enumerable: false });\n        } catch (e) {}\n        try {\n            if (msg.bet && typeof msg.bet.level === 'number') {\n                var shown = msg.bet.level;\n                var orig = originalFor(shown);\n                state.coeff = orig > 0 && orig !== shown ? shown / orig : 1;\n                if (state.flatten) {\n                    // the game plays with flat levels over baseDenom — the\n                    // backend must get the REAL (level, denomination) pair.\n                    // Duplicate flat values map to pairs with the same money\n                    // total, so picking the first index is equivalent.\n                    var idx = state.originalBets ? state.originalBets.indexOf(orig) : -1;\n                    if (idx !== -1 && state.rawBets && state.rawDenoms) {\n                        msg.bet.level = state.rawBets[idx % state.rawBets.length];\n                        msg.bet.denomination = state.rawDenoms[Math.floor(idx / state.rawBets.length)];\n                        log('outgoing ' + msg.event + ': bet ' + shown + ' → level ' + msg.bet.level +\n                            ' × denom ' + msg.bet.denomination + ' (coeff ×' + state.coeff + ')');\n                    } else {\n                        log('outgoing ' + msg.event + ': bet ' + shown +\n                            ' not found in the flat bet list — left untouched');\n                    }\n                } else if (orig !== shown) {\n                    msg.bet.level = orig;\n                    log('outgoing ' + msg.event + ': bet.level ' + shown + ' → ' + orig +\n                        ' (coeff ×' + state.coeff + ')');\n                }\n            }\n        } catch (e) {\n            log('outgoing transform failed: ' + (e && e.message));\n        }\n        return msg;\n    }\n\n    /* ---------------- in-game recording (only the game) ---------------- */\n\n    var recState = { recorder: null, chunks: [], raf: 0, mime: '', retryOpts: null };\n\n    function getGameCanvas() {\n        return (\n            document.getElementById('app-canvas') ||\n            document.querySelector('#app-root canvas') ||\n            document.querySelector('canvas')\n        );\n    }\n\n    /** last pointer position in viewport coords (for the cursor overlay) */\n    var mouse = { x: -1, y: -1 };\n    try {\n        window.addEventListener('mousemove', function (e) { mouse.x = e.clientX; mouse.y = e.clientY; }, true);\n        window.addEventListener('pointermove', function (e) { mouse.x = e.clientX; mouse.y = e.clientY; }, true);\n    } catch (e) {}\n\n    /** classic arrow pointer drawn into the recorded frame */\n    function drawCursor(ctx, x, y, scale) {\n        ctx.save();\n        ctx.translate(x, y);\n        ctx.scale(scale, scale);\n        ctx.beginPath();\n        ctx.moveTo(0, 0);\n        ctx.lineTo(0, 17.5);\n        ctx.lineTo(4.4, 13.4);\n        ctx.lineTo(7.3, 19.8);\n        ctx.lineTo(10.2, 18.4);\n        ctx.lineTo(7.4, 12.2);\n        ctx.lineTo(12.4, 11.8);\n        ctx.closePath();\n        ctx.fillStyle = '#fff';\n        ctx.strokeStyle = '#000';\n        ctx.lineWidth = 1.4;\n        ctx.lineJoin = 'round';\n        ctx.fill();\n        ctx.stroke();\n        ctx.restore();\n    }\n\n    function pickRecMime() {\n        var prefs = ['video/webm;codecs=vp9,opus', 'video/webm;codecs=vp8,opus', 'video/webm;codecs=vp9', 'video/webm'];\n        for (var i = 0; i < prefs.length; i++) {\n            if (window.MediaRecorder && MediaRecorder.isTypeSupported(prefs[i])) return prefs[i];\n        }\n        return '';\n    }\n\n    /**\n     * The letterbox bars are the game's own geometry, so ask the game rather\n     * than guess: `Stage` scales the design size into the canvas (always\n     * 100vw x 100vh of the frame) keeping its aspect, then centres it\n     * (mobile, Align.MIDDLE) or top-aligns it (desktop, Align.TOP_CENTER) and\n     * leaves the rest unpainted — one bar top and bottom, or just the one below.\n     * `Stage.initStage` publishes the PIXI Application on\n     * `globalThis.__PIXI_APP__`, which makes the content rect exact:\n     * stage.position, sized design x stage.scale.\n     *\n     * The design size is `renderer.options.width/height`, which Stage SWAPS when\n     * the view is portrait — so both pairs are tried and the one that still fits\n     * inside the view wins (the other orientation overflows it).\n     *\n     * Returns CSS pixels (the units stage.position and screen are in).\n     */\n    function stageGeometry() {\n        try {\n            var app = globalThis.__PIXI_APP__;\n            if (!app || !app.stage || !app.renderer || !app.renderer.options) return null;\n            var viewW = app.renderer.screen.width, viewH = app.renderer.screen.height;\n            var sx = app.stage.scale.x, sy = app.stage.scale.y;\n            var dw = app.renderer.options.width, dh = app.renderer.options.height;\n            if (!(viewW > 0 && viewH > 0 && sx > 0 && sy > 0 && dw > 0 && dh > 0)) return null;\n            var pairs = [[dw, dh], [dh, dw]];\n            var best = null;\n            for (var i = 0; i < pairs.length; i++) {\n                var w = pairs[i][0] * sx, h = pairs[i][1] * sy;\n                if (w > viewW + 1 || h > viewH + 1) continue; // not the live orientation\n                if (!best || w * h > best.w * best.h) best = { w: w, h: h };\n            }\n            if (!best) return null;\n            return {\n                x: app.stage.position.x,\n                y: app.stage.position.y,\n                w: best.w,\n                h: best.h,\n                viewW: viewW,\n                viewH: viewH,\n            };\n        } catch (e) {\n            return null;\n        }\n    }\n\n    /** the same rect in CANVAS pixels — what the capture crops to */\n    function stageContentRect(canvas) {\n        var g = stageGeometry();\n        if (!g) return null;\n        var kx = canvas.width / g.viewW, ky = canvas.height / g.viewH; // CSS px -> canvas px\n        var rect = {\n            x: Math.max(0, Math.round(g.x * kx)),\n            y: Math.max(0, Math.round(g.y * ky)),\n            w: Math.round(g.w * kx),\n            h: Math.round(g.h * ky),\n        };\n        rect.w = Math.min(rect.w, canvas.width - rect.x);\n        rect.h = Math.min(rect.h, canvas.height - rect.y);\n        return rect.w > 0 && rect.h > 0 ? rect : null;\n    }\n\n    /**\n     * Source rect for the capture. The stage geometry is exact; the pixel scan\n     * is the fallback for a game that publishes no PIXI app (HtmlStage) — and it\n     * is only a fallback because reading back a WebGL canvas outside its render\n     * tick can come out blank, which reads as \"nothing to crop\".\n     */\n    function contentRect(canvas) {\n        var rect = stageContentRect(canvas) || detectContentRect(canvas);\n        if (!rect) return null;\n        // ignore negligible bars (<2% each side) — not worth a copy loop\n        if (rect.x <= canvas.width * 0.02 && rect.y <= canvas.height * 0.02 &&\n            rect.w >= canvas.width * 0.96 && rect.h >= canvas.height * 0.96) return null;\n        return rect;\n    }\n\n    /** find the non-black content rectangle so letterbox bars are cropped out.\n     *  returns null if the sample is unusable (e.g. WebGL buffer not readable) */\n    function detectContentRect(canvas) {\n        try {\n            var w = canvas.width, h = canvas.height;\n            if (!w || !h) return null;\n            var sw = Math.min(w, 320);\n            var sh = Math.max(1, Math.round((sw * h) / w));\n            var tmp = document.createElement('canvas');\n            tmp.width = sw; tmp.height = sh;\n            var ctx = tmp.getContext('2d');\n            ctx.drawImage(canvas, 0, 0, sw, sh);\n            var data = ctx.getImageData(0, 0, sw, sh).data;\n            var minX = sw, minY = sh, maxX = -1, maxY = -1;\n            for (var y = 0; y < sh; y++) {\n                for (var x = 0; x < sw; x++) {\n                    var i = (y * sw + x) * 4;\n                    if (data[i] + data[i + 1] + data[i + 2] > 24) {\n                        if (x < minX) minX = x;\n                        if (x > maxX) maxX = x;\n                        if (y < minY) minY = y;\n                        if (y > maxY) maxY = y;\n                    }\n                }\n            }\n            if (maxX < 0) return null; // all black → unusable / nothing to crop\n            var fx = w / sw, fy = h / sh;\n            var rect = {\n                x: Math.floor(minX * fx),\n                y: Math.floor(minY * fy),\n                w: Math.min(w, Math.ceil((maxX - minX + 1) * fx)),\n                h: Math.min(h, Math.ceil((maxY - minY + 1) * fy)),\n            };\n            return rect;\n        } catch (e) {\n            return null; // cross-origin taint or WebGL read failure → skip crop\n        }\n    }\n\n    /**\n     * Audio capture. Howler/Tone are webpack modules here, so there is no\n     * reliable `window.Howler` to tap. Instead we patch AudioNode.connect once,\n     * at agent start: anything that connects to ctx.destination (i.e. reaches\n     * the speakers) is ALSO connected to a MediaStreamDestination we own. This\n     * is engine-agnostic and catches Howler's masterGain and Tone's destination\n     * alike — but ONLY if the patch is installed before they connect. Both may\n     * wire their master chain during module init of the game bundle, which is\n     * why webpack.js puts vr-agent as the FIRST entry (script tags execute in\n     * document order); with the agent last, recordings come out silent.\n     */\n    var audioTaps = []; // [{ ctx, dest }]\n\n    (function patchAudioRouting() {\n        try {\n            if (typeof AudioNode === 'undefined' || !AudioNode.prototype || !AudioNode.prototype.connect) return;\n            var origConnect = AudioNode.prototype.connect;\n            AudioNode.prototype.connect = function (destination) {\n                try {\n                    var ctx = this.context;\n                    if (ctx && destination && destination === ctx.destination && ctx.createMediaStreamDestination) {\n                        var tap = null;\n                        for (var i = 0; i < audioTaps.length; i++) {\n                            if (audioTaps[i].ctx === ctx) { tap = audioTaps[i]; break; }\n                        }\n                        if (!tap) {\n                            tap = { ctx: ctx, dest: ctx.createMediaStreamDestination() };\n                            audioTaps.push(tap);\n                            log('audio tap installed on AudioContext');\n                        }\n                        // duplicate connections are ignored by Web Audio\n                        origConnect.call(this, tap.dest);\n                    }\n                } catch (e) {}\n                return origConnect.apply(this, arguments);\n            };\n        } catch (e) {}\n    })();\n\n    /** kept alive between recordings — GC'd MediaStreamAudioSourceNodes go mute */\n    var audioMix = { ctx: null, nodes: [] };\n\n    /** a NEW AudioContext in the iframe starts suspended until the iframe has\n     *  user activation, and resume() is denied — a mix through it is pure\n     *  silence. Prefer one of the GAME's contexts (already unlocked by its own\n     *  code); create our own only as a last resort and keep resuming it. */\n    function pickMixContext() {\n        for (var i = 0; i < audioTaps.length; i++) {\n            if (audioTaps[i].ctx.state === 'running') return audioTaps[i].ctx;\n        }\n        if (audioTaps.length) return audioTaps[0].ctx;\n        var Ctx = window.AudioContext || window.webkitAudioContext;\n        if (!audioMix.ctx && Ctx) audioMix.ctx = new Ctx();\n        return audioMix.ctx;\n    }\n\n    /** resume every context we depend on — wired to user gestures below,\n     *  because resume() only sticks once the iframe has activation */\n    function resumeAudioContexts() {\n        try {\n            for (var i = 0; i < audioTaps.length; i++) {\n                if (audioTaps[i].ctx.state === 'suspended') audioTaps[i].ctx.resume();\n            }\n            if (audioMix.ctx && audioMix.ctx.state === 'suspended') audioMix.ctx.resume();\n        } catch (e) {}\n    }\n    try {\n        window.addEventListener('pointerdown', resumeAudioContexts, true);\n        window.addEventListener('keydown', resumeAudioContexts, true);\n    } catch (e) {}\n\n    /**\n     * MediaRecorder records ONLY the first audio track of a stream and\n     * silently ignores the rest. Games create several AudioContexts (each tap\n     * = one track) and the actual sound may live in any of them — so mix all\n     * captured tracks into a single one.\n     */\n    function mixToSingleTrack(tracks) {\n        if (tracks.length <= 1) return tracks;\n        try {\n            var ctx = pickMixContext();\n            if (!ctx) return [tracks[0]];\n            if (ctx.state === 'suspended' && ctx.resume) ctx.resume();\n            var dest = ctx.createMediaStreamDestination();\n            audioMix.nodes = [dest];\n            for (var i = 0; i < tracks.length; i++) {\n                var src = ctx.createMediaStreamSource(new MediaStream([tracks[i]]));\n                src.connect(dest);\n                audioMix.nodes.push(src);\n            }\n            log('audio: mixed ' + tracks.length + ' tracks into one (mix ctx: ' + ctx.state + ')');\n            return dest.stream.getAudioTracks();\n        } catch (e) {\n            log('audio mix failed (' + (e && e.message) + ') — using the first track only');\n            return [tracks[0]];\n        }\n    }\n\n    function tapGameAudio() {\n        var tracks = [];\n        for (var i = 0; i < audioTaps.length; i++) {\n            try {\n                var t = audioTaps[i];\n                if (t.ctx.state === 'suspended' && t.ctx.resume) t.ctx.resume();\n                tracks = tracks.concat(t.dest.stream.getAudioTracks());\n            } catch (e) {}\n        }\n        if (tracks.length) {\n            log('audio: ' + tracks.length + ' track(s) from AudioContext tap');\n            return mixToSingleTrack(tracks);\n        }\n        // fallbacks — howler may end up on window or on webpack's `global`\n        try {\n            var H = window.Howler || (typeof global !== 'undefined' && global.Howler);\n            if (H && H.ctx && H.masterGain && H.ctx.createMediaStreamDestination) {\n                var d = H.ctx.createMediaStreamDestination();\n                H.masterGain.connect(d);\n                log('audio: tapped Howler master');\n                return d.stream.getAudioTracks();\n            }\n        } catch (e) {}\n        try {\n            var T = window.Tone;\n            if (T && T.getContext) {\n                var raw = T.getContext().rawContext || T.getContext();\n                if (raw.createMediaStreamDestination) {\n                    var dd = raw.createMediaStreamDestination();\n                    var master = (T.getDestination && T.getDestination()) || T.Master;\n                    (master.output || master).connect(dd);\n                    log('audio: tapped Tone destination');\n                    return dd.stream.getAudioTracks();\n                }\n            }\n        } catch (e) {}\n        // last resort: media elements (Howler HTML5 mode)\n        try {\n            var media = document.querySelectorAll('audio,video');\n            for (var j = 0; j < media.length; j++) {\n                if (media[j].captureStream) {\n                    tracks = tracks.concat(media[j].captureStream().getAudioTracks());\n                }\n            }\n        } catch (e) {}\n        if (!tracks.length) {\n            log('audio: no source found — recording will be silent');\n            return tracks;\n        }\n        return mixToSingleTrack(tracks);\n    }\n\n    function startGameRecording(opts) {\n        opts = opts || {};\n        if (recState.recorder) return;\n        var canvas = getGameCanvas();\n        if (!canvas) { post({ type: 'VR_REC_ERROR', message: 'game canvas not found' }); return; }\n        var fps = opts.fps || 30;\n        // source rect: cropped content or the whole canvas\n        var crop = opts.cropBars ? contentRect(canvas) : null;\n        var src = crop || { x: 0, y: 0, w: canvas.width, h: canvas.height };\n        // target size: scale down to maxHeight (resolution setting), keep aspect\n        var tw = src.w, th = src.h;\n        var maxH = opts.maxHeight || 0;\n        if (maxH && th > maxH) {\n            tw = Math.max(2, Math.round((src.w * maxH) / src.h));\n            th = maxH;\n        }\n        // H.264 (yuv420) refuses ODD dimensions — the MP4 recorder dies right\n        // after start. Even sizes are harmless for VP8/VP9 too, so always\n        // round down (the diff forces the copy loop when needed).\n        tw &= ~1;\n        th &= ~1;\n        // the cursor overlay is painted by us, so it forces the copy loop\n        var withCursor = !!opts.cursor;\n        var needCopy = !!crop || tw !== src.w || th !== src.h || withCursor;\n        var stream;\n        try {\n            if (needCopy) {\n                var out = document.createElement('canvas');\n                out.width = tw; out.height = th;\n                var octx = out.getContext('2d');\n                var draw = function () {\n                    octx.drawImage(canvas, src.x, src.y, src.w, src.h, 0, 0, tw, th);\n                    if (withCursor && mouse.x >= 0) {\n                        var rect = canvas.getBoundingClientRect();\n                        if (rect.width > 0 && rect.height > 0) {\n                            // viewport px → canvas px → output px\n                            var cx = (mouse.x - rect.left) * (canvas.width / rect.width);\n                            var cy = (mouse.y - rect.top) * (canvas.height / rect.height);\n                            if (cx >= src.x && cx <= src.x + src.w && cy >= src.y && cy <= src.y + src.h) {\n                                drawCursor(\n                                    octx,\n                                    (cx - src.x) * (tw / src.w),\n                                    (cy - src.y) * (th / src.h),\n                                    (canvas.width / rect.width) * (tw / src.w),\n                                );\n                            }\n                        }\n                    }\n                    recState.raf = requestAnimationFrame(draw);\n                };\n                draw();\n                stream = out.captureStream(fps);\n            } else {\n                stream = canvas.captureStream(fps);\n            }\n        } catch (e) {\n            post({ type: 'VR_REC_ERROR', message: 'captureStream failed: ' + (e && e.message) });\n            return;\n        }\n        var audioTracks = 0;\n        if (opts.audio !== false) {\n            var tracks = tapGameAudio();\n            audioTracks = tracks.length;\n            tracks.forEach(function (t) { stream.addTrack(t); });\n        }\n        // format: requested mime if supported, else best available\n        var mime = opts.mimeType && window.MediaRecorder && MediaRecorder.isTypeSupported(opts.mimeType)\n            ? opts.mimeType\n            : pickRecMime();\n        // a codecs list without an audio codec makes the recorder silently\n        // drop the audio track — extend it when we actually captured audio\n        if (audioTracks && mime && /codecs=/.test(mime) && !/opus|vorbis|aac|mp4a/i.test(mime)) {\n            var withAudio = mime + (mime.indexOf('webm') !== -1 ? ',opus' : ',mp4a.40.2');\n            if (MediaRecorder.isTypeSupported(withAudio)) mime = withAudio;\n        }\n        // isTypeSupported can say yes while the actual encoder refuses the\n        // config at start (\"not supported by the encoder\"). Build a fallback\n        // chain once: same container without a codecs list → default WebM.\n        if (!opts.__vrFallbacks) {\n            opts.__vrFallbacks = [];\n            var bare = (mime || '').split(';')[0];\n            if (bare && bare !== mime) opts.__vrFallbacks.push(bare);\n            if (bare !== 'video/webm') opts.__vrFallbacks.push('');\n        }\n        recState.mime = mime;\n        var recOpts = mime ? { mimeType: mime } : {};\n        if (opts.videoBitsPerSecond) recOpts.videoBitsPerSecond = opts.videoBitsPerSecond;\n        var recorder;\n        try {\n            recorder = new MediaRecorder(stream, recOpts);\n        } catch (e) {\n            post({ type: 'VR_REC_ERROR', message: 'MediaRecorder failed: ' + (e && e.message) });\n            return;\n        }\n        recState.recorder = recorder;\n        recState.chunks = [];\n        recorder.onerror = function (ev) {\n            var m = (ev && ev.error && ev.error.message) || 'MediaRecorder error';\n            log('recorder error: ' + m);\n            var next = opts.__vrFallbacks.shift();\n            if (next !== undefined && !recState.chunks.length) {\n                // encoder refused the config before producing anything —\n                // restart transparently with the next, simpler format\n                log('retrying recording with ' + (next || 'the default format') + ' …');\n                recState.retryOpts = Object.assign({}, opts, { mimeType: next });\n            } else {\n                post({ type: 'VR_REC_ERROR', message: m });\n            }\n            stopGameRecording();\n        };\n        recorder.ondataavailable = function (ev) { if (ev.data && ev.data.size) recState.chunks.push(ev.data); };\n        recorder.onstop = function () {\n            if (recState.raf) { cancelAnimationFrame(recState.raf); recState.raf = 0; }\n            stream.getTracks().forEach(function (t) { t.stop(); });\n            var retry = recState.retryOpts;\n            recState.recorder = null;\n            if (retry) {\n                recState.retryOpts = null;\n                recState.chunks = [];\n                startGameRecording(retry);\n                return;\n            }\n            var blob = new Blob(recState.chunks, { type: recState.mime || 'video/webm' });\n            recState.chunks = [];\n            post({ type: 'VR_RECORDING', blob: blob, mime: recState.mime || 'video/webm', gameKey: state.gameKey });\n        };\n        recorder.start();\n        post({\n            type: 'VR_REC_STARTED',\n            cropped: !!crop,\n            width: tw,\n            height: th,\n            audioWanted: opts.audio !== false,\n            audioTracks: audioTracks,\n        });\n        log('recording started ' + tw + 'x' + th + (crop ? ' (cropped)' : '') +\n            ' @' + fps + 'fps' + (mime ? ' ' + mime : '') +\n            (opts.videoBitsPerSecond ? ' ' + Math.round(opts.videoBitsPerSecond / 1000000) + 'Mbps' : ''));\n    }\n\n    function stopGameRecording() {\n        if (recState.recorder && recState.recorder.state !== 'inactive') recState.recorder.stop();\n    }\n\n    /* ---------------- UI bridge (postMessage) ---------------- */\n\n    function post(m) {\n        try {\n            if (window.parent !== window) window.parent.postMessage(m, '*');\n        } catch (e) {}\n    }\n\n    function notifyBets() {\n        post({\n            type: 'VR_BETS',\n            originalBets: state.originalBets,\n            overrideBets: state.overrideBets,\n            betDenoms: state.betDenoms,\n            coeff: state.coeff,\n            enabled: state.enabled,\n            gameKey: state.gameKey,\n            factor: state.factor,\n            denomination: state.denomination,\n            units: state.units,\n            currency: state.currency,\n        });\n    }\n\n    /** report the detected jackpot levels (original base values) to the panel */\n    function notifyJackpots() {\n        if (!state.jackpotBase) return;\n        post({\n            type: 'VR_JACKPOTS',\n            base: state.jackpotBase,\n            enabled: state.jackpotOverride,\n            units: state.units,\n            currency: state.currency,\n        });\n    }\n\n    var lastAspect = 0;\n\n    /**\n     * The aspect the tool should give the game frame. Shaped to it, the game has\n     * nothing left over to letterbox — the bars move out of the canvas (and out\n     * of the recording) onto the tool's own surface. `force` re-states it for a\n     * panel that has just (re)connected.\n     */\n    function notifyStage(force) {\n        var g = stageGeometry();\n        if (!g || !g.h) return;\n        var aspect = g.w / g.h;\n        if (!force && Math.abs(aspect - lastAspect) < 0.001) return;\n        lastAspect = aspect;\n        post({ type: 'VR_STAGE', aspect: aspect });\n    }\n\n    window.addEventListener('message', function (ev) {\n        var d = ev.data;\n        if (!d || typeof d !== 'object') return;\n        if (d.type === 'VR_PING') {\n            post({ type: 'VR_PONG', enabled: state.enabled });\n            if (state.originalBets) notifyBets();\n            if (state.jackpotBase) notifyJackpots();\n            notifyStage(true);\n        } else if (d.type === 'VR_START_RECORDING') {\n            startGameRecording(d.options || {});\n        } else if (d.type === 'VR_STOP_RECORDING') {\n            stopGameRecording();\n        }\n    });\n\n    loadConfig();\n    applyToolVisibility();\n    // dev overlays may mount after the agent; keep hiding them briefly\n    if (!state.toolsVisible) {\n        var tries = 0;\n        var iv = setInterval(function () {\n            applyToolVisibility();\n            if (++tries > 20) clearInterval(iv);\n        }, 500);\n    }\n    // The stage does not exist yet when the agent starts (it is the FIRST entry,\n    // the game boots after), and its aspect flips with the orientation — so watch\n    // for it instead of reporting once. Only a real change is posted: the tool\n    // re-shapes the frame from this, which resizes the game, which lands here again.\n    setInterval(notifyStage, 500);\n    window.__VRB = { incoming: incoming, outgoing: outgoing, state: state };\n    post({ type: 'VR_READY', enabled: state.enabled });\n    log('agent ready (enabled: ' + state.enabled + ')');\n})();\n"],"names":[],"sourceRoot":""}