// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import { assert } from 'chrome://resources/js/assert.js';
import { loadTimeData } from 'chrome://resources/js/load_time_data.js';
import { HIDDEN_CLASS } from '../constants.js';
import { DEFAULT_DIMENSIONS, FPS, IS_HIDPI, IS_IOS, IS_MOBILE, IS_RTL } from './constants.js';
import { DistanceMeter } from './distance_meter.js';
import { GameOverPanel } from './game_over_panel.js';
import { GeneratedSoundFx } from './generated_sound_fx.js';
import { Horizon } from './horizon.js';
import { CollisionBox, GAME_TYPE, spriteDefinitionByType } from './offline_sprite_definitions.js';
import { Status as TrexStatus, Trex } from './trex.js';
import { getTimeStamp } from './utils.js';
var A11yStrings;
(function (A11yStrings) {
    A11yStrings["ARIA_LABEL"] = "dinoGameA11yAriaLabel";
    A11yStrings["DESCRIPTION"] = "dinoGameA11yDescription";
    A11yStrings["GAME_OVER"] = "dinoGameA11yGameOver";
    A11yStrings["HIGH_SCORE"] = "dinoGameA11yHighScore";
    A11yStrings["JUMP"] = "dinoGameA11yJump";
    A11yStrings["STARTED"] = "dinoGameA11yStartGame";
    A11yStrings["SPEED_LABEL"] = "dinoGameA11ySpeedToggle";
})(A11yStrings || (A11yStrings = {}));
const defaultBaseConfig = {
    audiocueProximityThreshold: 190,
    audiocueProximityThresholdMobileA11y: 250,
    bgCloudSpeed: 0.2,
    bottomPad: 10,
    // Scroll Y threshold at which the game can be activated.
    canvasInViewOffset: -10,
    clearTime: 3000,
    cloudFrequency: 0.5,
    fadeDuration: 1,
    flashDuration: 1000,
    gameoverClearTime: 1200,
    initialJumpVelocity: 12,
    invertFadeDuration: 12000,
    maxBlinkCount: 3,
    maxClouds: 6,
    maxObstacleLength: 3,
    maxObstacleDuplication: 2,
    resourceTemplateId: 'audio-resources',
    speed: 6,
    speedDropCoefficient: 3,
    arcadeModeInitialTopPosition: 35,
    arcadeModeTopPositionPercent: 0.1,
};
const normalModeConfig = {
    acceleration: 0.001,
    audiocueProximityThreshold: 190,
    audiocueProximityThresholdMobileA11y: 250,
    gapCoefficient: 0.6,
    invertDistance: 700,
    maxSpeed: 13,
    mobileSpeedCoefficient: 1.2,
    speed: 6,
};
const slowModeConfig = {
    acceleration: 0.0005,
    audiocueProximityThreshold: 170,
    audiocueProximityThresholdMobileA11y: 220,
    gapCoefficient: 0.3,
    invertDistance: 350,
    maxSpeed: 9,
    mobileSpeedCoefficient: 1.5,
    speed: 4.2,
};
/**
 * CSS class names.
 */
var RunnerClasses;
(function (RunnerClasses) {
    RunnerClasses["ARCADE_MODE"] = "arcade-mode";
    RunnerClasses["CANVAS"] = "runner-canvas";
    RunnerClasses["CONTAINER"] = "runner-container";
    RunnerClasses["CRASHED"] = "crashed";
    RunnerClasses["ICON"] = "icon-offline";
    RunnerClasses["ICON_DISABLED"] = "icon-disabled";
    RunnerClasses["INVERTED"] = "inverted";
    RunnerClasses["SNACKBAR"] = "snackbar";
    RunnerClasses["SNACKBAR_SHOW"] = "snackbar-show";
    RunnerClasses["TOUCH_CONTROLLER"] = "controller";
})(RunnerClasses || (RunnerClasses = {}));
/**
 * Sound FX. Reference to the ID of the audio tag on interstitial page.
 */
var RunnerSounds;
(function (RunnerSounds) {
    RunnerSounds["BUTTON_PRESS"] = "offline-sound-press";
    RunnerSounds["HIT"] = "offline-sound-hit";
    RunnerSounds["SCORE"] = "offline-sound-reached";
})(RunnerSounds || (RunnerSounds = {}));
/**
 * Key code mapping.
 */
const runnerKeycodes = {
    jump: [38, 32], // Up, spacebar
    duck: [40], // Down
    restart: [13], // Enter
};
var RunnerEvents;
(function (RunnerEvents) {
    RunnerEvents["ANIM_END"] = "webkitAnimationEnd";
    RunnerEvents["CLICK"] = "click";
    RunnerEvents["KEYDOWN"] = "keydown";
    RunnerEvents["KEYUP"] = "keyup";
    RunnerEvents["POINTERDOWN"] = "pointerdown";
    RunnerEvents["POINTERUP"] = "pointerup";
    RunnerEvents["RESIZE"] = "resize";
    RunnerEvents["TOUCHEND"] = "touchend";
    RunnerEvents["TOUCHSTART"] = "touchstart";
    RunnerEvents["VISIBILITY"] = "visibilitychange";
    RunnerEvents["BLUR"] = "blur";
    RunnerEvents["FOCUS"] = "focus";
    RunnerEvents["LOAD"] = "load";
    RunnerEvents["GAMEPADCONNECTED"] = "gamepadconnected";
})(RunnerEvents || (RunnerEvents = {}));
let runnerInstance = null;
const ARCADE_MODE_URL = 'chrome://dino/';
const RESOURCE_POSTFIX = 'offline-resources-';
/**
 * T-Rex runner.
 */
export class Runner {
    outerContainerEl;
    containerEl = null;
    // A div to intercept touch events. Only set while (playing && useTouch).
    touchController = null;
    canvas = null;
    canvasCtx = null;
    a11yStatusEl = null;
    slowSpeedCheckboxLabel = null;
    slowSpeedCheckbox = null;
    slowSpeedToggleEl = null;
    origImageSprite = null;
    altCommonImageSprite = null;
    altGameImageSprite = null;
    imageSprite = null;
    config;
    // Logical dimensions of the container.
    dimensions = DEFAULT_DIMENSIONS;
    gameType = null;
    spriteDefinition = spriteDefinitionByType.original;
    spriteDef = null;
    // Alt game mode state.
    altGameModeActive = false;
    altGameModeFlashTimer = null;
    altGameAssetsFailedToLoad = false;
    fadeInTimer = 0;
    // UI components.
    tRex = null;
    distanceMeter = null;
    gameOverPanel = null;
    horizon = null;
    msPerFrame = 1000 / FPS;
    time = 0;
    distanceRan = 0;
    runningTime = 0;
    currentSpeed;
    resizeTimerId;
    raqId = 0;
    playCount = 0;
    // Whether the easter egg has been disabled. CrOS enterprise enrolled devices.
    isDisabled = loadTimeData.valueExists('disabledEasterEgg');
    // Whether the easter egg has been activated.
    activated = false;
    // Whether the game is currently in play state.
    playing = false;
    playingIntro = false;
    crashed = false;
    paused = false;
    inverted = false;
    isDarkMode = false;
    updatePending = false;
    hasSlowdownInternal = false;
    hasAudioCuesInternal = false;
    highestScore = 0;
    syncHighestScore = false;
    invertTimer = 0;
    invertTrigger = false;
    soundFx = {};
    audioContext = null;
    generatedSoundFx = null;
    // Gamepad state.
    pollingGamepads = false;
    gamepadIndex;
    previousGamepad = null;
    // Initialize the singleton instance of Runner. Should only be called once.
    static initializeInstance(outerContainerId, config) {
        assert(runnerInstance === null);
        runnerInstance = new Runner(outerContainerId, config);
        if (!runnerInstance.isDisabled) {
            runnerInstance.loadImages();
        }
        return runnerInstance;
    }
    static getInstance() {
        assert(runnerInstance);
        return runnerInstance;
    }
    constructor(outerContainerId, configParam) {
        const outerContainerElement = document.querySelector(outerContainerId);
        assert(outerContainerElement);
        this.outerContainerEl = outerContainerElement;
        this.config =
            configParam || Object.assign({}, defaultBaseConfig, normalModeConfig);
        this.currentSpeed = this.config.speed;
        if (this.isDisabled) {
            this.setupDisabledRunner();
            return;
        }
        if (this.isAltGameModeEnabled()) {
            this.initAltGameType();
        }
        window.initializeEasterEggHighScore = this.initializeHighScore.bind(this);
    }
    // GameStateProvider implementation.
    get hasSlowdown() {
        return this.hasSlowdownInternal;
    }
    // GameStateProvider implementation.
    get hasAudioCues() {
        return this.hasAudioCuesInternal;
    }
    /**
     * Whether an alternative game mode is enabled, returns true if the load time
     * data specifies it and its assets loaded successfully. Returns false
     * otherwise.
     * GameStateProvider implementation.
     */
    isAltGameModeEnabled() {
        if (this.altGameAssetsFailedToLoad) {
            return false;
        }
        return loadTimeData.valueExists('enableAltGameMode');
    }
    // GeneratedSoundFxProvider implementation.
    getGeneratedSoundFx() {
        assert(this.generatedSoundFx);
        return this.generatedSoundFx;
    }
    // ImageSpriteProvider implementation.
    getSpriteDefinition() {
        return this.spriteDefinition;
    }
    // ImageSpriteProvider implementation.
    getOrigImageSprite() {
        assert(this.origImageSprite);
        return this.origImageSprite;
    }
    // ImageSpriteProvider implementation.
    getRunnerImageSprite() {
        assert(this.imageSprite);
        return this.imageSprite;
    }
    // ImageSpriteProvider implementation.
    getRunnerAltGameImageSprite() {
        return this.altGameImageSprite;
    }
    // ImageSpriteProvider implementation.
    getAltCommonImageSprite() {
        return this.altCommonImageSprite;
    }
    // ConfigProvider implementation.
    getConfig() {
        return this.config;
    }
    /**
     * Initialize alternative game type.
     */
    initAltGameType() {
        assert(loadTimeData.valueExists('altGameType'));
        if (GAME_TYPE.length > 0) {
            const parsedValue = Number.parseInt(loadTimeData.getValue('altGameType'), 10);
            const type = GAME_TYPE[parsedValue - 1];
            this.gameType = type || null;
        }
    }
    /**
     * For disabled instances, set up a snackbar with the disabled message.
     */
    setupDisabledRunner() {
        this.containerEl = document.createElement('div');
        this.containerEl.className = RunnerClasses.SNACKBAR;
        this.containerEl.textContent = loadTimeData.getValue('disabledEasterEgg');
        this.outerContainerEl.appendChild(this.containerEl);
        // Show notification when the activation key is pressed.
        document.addEventListener(RunnerEvents.KEYDOWN, e => {
            if (runnerKeycodes.jump.includes(e.keyCode)) {
                assert(this.containerEl);
                this.containerEl.classList.add(RunnerClasses.SNACKBAR_SHOW);
                const iconElement = document.querySelector('.icon');
                assert(iconElement);
                iconElement.classList.add(RunnerClasses.ICON_DISABLED);
            }
        });
    }
    /**
     * Sets individual settings for debugging.
     */
    updateConfigSetting(setting, value) {
        this.config[setting] = value;
    }
    /**
     * Sets individual settings for debugging.
     */
    updateTrexConfigSetting(setting, value) {
        assert(this.tRex);
        switch (setting) {
            case 'gravity':
            case 'minJumpHeight':
            case 'speedDropCoefficient':
                this.tRex.config[setting] = value;
                break;
            case 'initialJumpVelocity':
                this.tRex.setJumpVelocity(value);
                break;
            case 'speed':
                this.setSpeed(value);
                break;
            default:
                break;
        }
    }
    /**
     * Creates an on page image element from the base 64 encoded string source.
     * @param resourceName Name in data object,
     * @return The created element.
     */
    createImageElement(resourceName) {
        const imgSrc = loadTimeData.valueExists(resourceName) ?
            loadTimeData.getString(resourceName) :
            null;
        if (imgSrc) {
            const el = document.createElement('img');
            el.id = resourceName;
            el.src = imgSrc;
            const resourcesElement = document.getElementById('offline-resources');
            assert(resourcesElement);
            resourcesElement.appendChild(el);
            return el;
        }
        return null;
    }
    /**
     * Cache the appropriate image sprite from the page and get the sprite sheet
     * definition.
     */
    loadImages() {
        let scale = '1x';
        this.spriteDef = this.getSpriteDefinition().ldpi;
        if (IS_HIDPI) {
            scale = '2x';
            this.spriteDef = this.getSpriteDefinition().hdpi;
        }
        const imageSpriteElement = document.querySelector(`#${RESOURCE_POSTFIX + scale}`);
        assert(imageSpriteElement);
        this.imageSprite = imageSpriteElement;
        if (this.gameType) {
            this.altGameImageSprite =
                (this.createImageElement('altGameSpecificImage' + scale));
            this.altCommonImageSprite =
                (this.createImageElement('altGameCommonImage' + scale));
        }
        this.origImageSprite = this.getRunnerImageSprite();
        // Disable the alt game mode if the sprites can't be loaded.
        if (!this.getRunnerAltGameImageSprite() === null ||
            this.getAltCommonImageSprite() === null) {
            this.altGameAssetsFailedToLoad = true;
            this.altGameModeActive = false;
        }
        if (this.getRunnerImageSprite().complete) {
            this.init();
        }
        else {
            // If the images are not yet loaded, add a listener.
            this.getRunnerImageSprite().addEventListener(RunnerEvents.LOAD, this.init.bind(this));
        }
    }
    /**
     * Load and decode base 64 encoded sounds.
     */
    loadSounds() {
        if (IS_IOS) {
            return;
        }
        this.audioContext = new AudioContext();
        const resourceTemplateElement = document.querySelector(`#${this.config.resourceTemplateId}`);
        assert(resourceTemplateElement);
        const resourceTemplate = resourceTemplateElement.content;
        for (const sound in RunnerSounds) {
            const audioElement = resourceTemplate.querySelector(`#${RunnerSounds[sound]}`);
            assert(audioElement);
            let soundSrc = audioElement.src;
            soundSrc = soundSrc.substr(soundSrc.indexOf(',') + 1);
            const buffer = decodeBase64ToArrayBuffer(soundSrc);
            // Async, so no guarantee of order in array.
            this.audioContext.decodeAudioData(buffer, audioBuffer => {
                this.soundFx = {
                    ...this.soundFx,
                    [sound]: audioBuffer,
                };
            });
        }
    }
    /**
     * Sets the game speed. Adjust the speed accordingly if on a smaller screen.
     */
    setSpeed(newSpeed) {
        const speed = newSpeed || this.currentSpeed;
        // Reduce the speed on smaller mobile screens.
        if (this.dimensions.width < DEFAULT_DIMENSIONS.width) {
            const mobileSpeed = this.hasSlowdown ? speed :
                speed * this.dimensions.width /
                    DEFAULT_DIMENSIONS.width * this.config.mobileSpeedCoefficient;
            this.currentSpeed = mobileSpeed > speed ? speed : mobileSpeed;
        }
        else if (newSpeed) {
            this.currentSpeed = newSpeed;
        }
    }
    /**
     * Game initialiser.
     */
    init() {
        assert(this.spriteDef);
        const iconElement = document.querySelector('.' + RunnerClasses.ICON);
        assert(iconElement);
        // Hide the static icon.
        iconElement.style.visibility = 'hidden';
        if (this.isArcadeMode()) {
            document.title =
                document.title + ' - ' + getA11yString(A11yStrings.ARIA_LABEL);
        }
        this.adjustDimensions();
        this.setSpeed();
        const ariaLabel = getA11yString(A11yStrings.ARIA_LABEL);
        this.containerEl = document.createElement('div');
        this.containerEl.setAttribute('role', IS_MOBILE ? 'button' : 'application');
        this.containerEl.setAttribute('tabindex', '0');
        this.containerEl.setAttribute('title', getA11yString(A11yStrings.DESCRIPTION));
        this.containerEl.setAttribute('aria-label', ariaLabel);
        this.containerEl.className = RunnerClasses.CONTAINER;
        // Player canvas container.
        this.canvas = createCanvas(this.containerEl, this.dimensions.width, this.dimensions.height);
        // Live region for game status updates.
        this.a11yStatusEl = document.createElement('span');
        this.a11yStatusEl.className = 'offline-runner-live-region';
        this.a11yStatusEl.setAttribute('aria-live', 'assertive');
        this.a11yStatusEl.textContent = '';
        // Add checkbox to slow down the game.
        this.slowSpeedCheckboxLabel = document.createElement('label');
        this.slowSpeedCheckboxLabel.className = 'slow-speed-option hidden';
        this.slowSpeedCheckboxLabel.textContent =
            getA11yString(A11yStrings.SPEED_LABEL);
        this.slowSpeedCheckbox = document.createElement('input');
        this.slowSpeedCheckbox.setAttribute('type', 'checkbox');
        this.slowSpeedCheckbox.setAttribute('title', getA11yString(A11yStrings.SPEED_LABEL));
        this.slowSpeedCheckbox.setAttribute('tabindex', '0');
        this.slowSpeedCheckbox.setAttribute('checked', 'checked');
        this.slowSpeedToggleEl = document.createElement('span');
        this.slowSpeedToggleEl.className = 'slow-speed-toggle';
        this.slowSpeedCheckboxLabel.appendChild(this.slowSpeedCheckbox);
        this.slowSpeedCheckboxLabel.appendChild(this.slowSpeedToggleEl);
        if (IS_IOS) {
            this.outerContainerEl.appendChild(this.a11yStatusEl);
        }
        else {
            this.containerEl.appendChild(this.a11yStatusEl);
        }
        const canvasContext = this.canvas.getContext('2d');
        assert(canvasContext);
        this.canvasCtx = canvasContext;
        this.canvasCtx.fillStyle = '#f7f7f7';
        this.canvasCtx.fill();
        updateCanvasScaling(this.canvas);
        // Horizon contains clouds, obstacles and the ground.
        this.horizon = new Horizon(this.canvas, this.spriteDef, this.dimensions, this.config.gapCoefficient, /* resourceProvider= */ this);
        // Distance meter
        this.distanceMeter = new DistanceMeter(this.canvas, this.spriteDef.textSprite, this.dimensions.width, 
        /* imageSpriteProvider= */ this);
        // Draw t-rex
        this.tRex = new Trex(this.canvas, this.spriteDef.tRex, /* resourceProvider= */ this);
        this.outerContainerEl.appendChild(this.containerEl);
        this.outerContainerEl.appendChild(this.slowSpeedCheckboxLabel);
        this.startListening();
        this.update();
        window.addEventListener(RunnerEvents.RESIZE, this.debounceResize.bind(this));
        // Handle dark mode
        const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
        this.isDarkMode = darkModeMediaQuery && darkModeMediaQuery.matches;
        darkModeMediaQuery.addListener((e) => {
            this.isDarkMode = e.matches;
        });
    }
    /**
     * Create the touch controller. A div that covers whole screen.
     */
    createTouchController() {
        this.touchController = document.createElement('div');
        this.touchController.className = RunnerClasses.TOUCH_CONTROLLER;
        this.touchController.addEventListener(RunnerEvents.TOUCHSTART, this);
        this.touchController.addEventListener(RunnerEvents.TOUCHEND, this);
        this.outerContainerEl.appendChild(this.touchController);
    }
    /**
     * Debounce the resize event.
     */
    debounceResize() {
        if (this.resizeTimerId === undefined) {
            this.resizeTimerId = setInterval(this.adjustDimensions.bind(this), 250);
        }
    }
    /**
     * Adjust game space dimensions on resize.
     */
    adjustDimensions() {
        clearInterval(this.resizeTimerId);
        this.resizeTimerId = undefined;
        const boxStyles = window.getComputedStyle(this.outerContainerEl);
        const padding = Number(boxStyles.paddingLeft.substr(0, boxStyles.paddingLeft.length - 2));
        this.dimensions.width = this.outerContainerEl.offsetWidth - padding * 2;
        if (this.isArcadeMode()) {
            this.dimensions.width =
                Math.min(DEFAULT_DIMENSIONS.width, this.dimensions.width);
            if (this.activated) {
                this.setArcadeModeContainerScale();
            }
        }
        // Redraw the elements back onto the canvas.
        if (this.canvas) {
            assert(this.distanceMeter);
            assert(this.horizon);
            assert(this.tRex);
            assert(this.containerEl);
            this.canvas.width = this.dimensions.width;
            this.canvas.height = this.dimensions.height;
            updateCanvasScaling(this.canvas);
            this.distanceMeter.calcXpos(this.dimensions.width);
            this.clearCanvas();
            this.horizon.update(0, 0, true, /*showNightMode = */ false);
            this.tRex.update(0);
            // Outer container and distance meter.
            if (this.playing || this.crashed || this.paused) {
                this.containerEl.style.width = this.dimensions.width + 'px';
                this.containerEl.style.height = this.dimensions.height + 'px';
                this.distanceMeter.update(0, Math.ceil(this.distanceRan));
                this.stop();
            }
            else {
                this.tRex.draw(0, 0);
            }
            // Game over panel.
            if (this.crashed && this.gameOverPanel) {
                this.gameOverPanel.updateDimensions(this.dimensions.width);
                this.gameOverPanel.draw(this.altGameModeActive, this.tRex);
            }
        }
    }
    /**
     * Play the game intro.
     * Canvas container width expands out to the full width.
     */
    playIntro() {
        if (!this.activated && !this.crashed) {
            assert(this.tRex);
            assert(this.containerEl);
            this.playingIntro = true;
            this.tRex.playingIntro = true;
            // CSS animation definition.
            const keyframes = '@-webkit-keyframes intro { ' +
                'from { width:' + this.tRex.config.width + 'px }' +
                'to { width: ' + this.dimensions.width + 'px }' +
                '}';
            const styleSheet = document.styleSheets[0];
            assert(styleSheet);
            styleSheet.insertRule(keyframes, 0);
            this.containerEl.addEventListener(RunnerEvents.ANIM_END, this.startGame.bind(this));
            this.containerEl.style.webkitAnimation = 'intro .4s ease-out 1 both';
            this.containerEl.style.width = this.dimensions.width + 'px';
            this.setPlayStatus(true);
            this.activated = true;
        }
        else if (this.crashed) {
            this.restart();
        }
    }
    /**
     * Update the game status to started.
     */
    startGame() {
        assert(this.containerEl);
        assert(this.tRex);
        if (this.isArcadeMode()) {
            this.setArcadeMode();
        }
        this.toggleSpeed();
        this.runningTime = 0;
        this.playingIntro = false;
        this.tRex.playingIntro = false;
        this.containerEl.style.webkitAnimation = '';
        this.playCount++;
        if (this.hasAudioCuesInternal) {
            this.getGeneratedSoundFx().background();
            this.containerEl.setAttribute('title', getA11yString(A11yStrings.JUMP));
        }
        // Handle tabbing off the page. Pause the current game.
        document.addEventListener(RunnerEvents.VISIBILITY, this.onVisibilityChange.bind(this));
        window.addEventListener(RunnerEvents.BLUR, this.onVisibilityChange.bind(this));
        window.addEventListener(RunnerEvents.FOCUS, this.onVisibilityChange.bind(this));
    }
    clearCanvas() {
        assert(this.canvasCtx);
        this.canvasCtx.clearRect(0, 0, this.dimensions.width, this.dimensions.height);
    }
    /**
     * Checks whether the canvas area is in the viewport of the browser
     * through the current scroll position.
     */
    isCanvasInView() {
        assert(this.containerEl);
        return this.containerEl.getBoundingClientRect().top >
            this.config.canvasInViewOffset;
    }
    /**
     * Enable the alt game mode. Switching out the sprites.
     */
    enableAltGameMode() {
        this.imageSprite = this.getRunnerAltGameImageSprite();
        assert(this.gameType);
        assert(this.tRex);
        assert(this.horizon);
        this.spriteDefinition = spriteDefinitionByType[this.gameType];
        if (IS_HIDPI) {
            this.spriteDef = this.getSpriteDefinition().hdpi;
        }
        else {
            this.spriteDef = this.getSpriteDefinition().ldpi;
        }
        this.altGameModeActive = true;
        this.tRex.enableAltGameMode(this.spriteDef.tRex);
        this.horizon.enableAltGameMode(this.spriteDef);
        if (this.hasAudioCuesInternal) {
            this.getGeneratedSoundFx()?.background();
        }
    }
    /**
     * Update the game frame and schedules the next one.
     */
    update() {
        assert(this.tRex);
        this.updatePending = false;
        const now = getTimeStamp();
        let deltaTime = now - (this.time || now);
        // Flashing when switching game modes.
        if (this.altGameModeFlashTimer !== null) {
            if (this.altGameModeFlashTimer <= 0) {
                this.altGameModeFlashTimer = null;
                this.tRex.setFlashing(false);
                this.enableAltGameMode();
            }
            else if (this.altGameModeFlashTimer > 0) {
                this.altGameModeFlashTimer -= deltaTime;
                this.tRex.update(deltaTime);
                deltaTime = 0;
            }
        }
        this.time = now;
        if (this.playing) {
            assert(this.distanceMeter);
            assert(this.horizon);
            assert(this.canvasCtx);
            this.clearCanvas();
            // Additional fade in - Prevents jump when switching sprites
            if (this.altGameModeActive &&
                this.fadeInTimer <= this.config.fadeDuration) {
                this.fadeInTimer += deltaTime / 1000;
                this.canvasCtx.globalAlpha = this.fadeInTimer;
            }
            else {
                this.canvasCtx.globalAlpha = 1;
            }
            if (this.tRex.jumping) {
                this.tRex.updateJump(deltaTime);
            }
            this.runningTime += deltaTime;
            const hasObstacles = this.runningTime > this.config.clearTime;
            // First jump triggers the intro.
            if (this.tRex.jumpCount === 1 && !this.playingIntro) {
                this.playIntro();
            }
            // The horizon doesn't move until the intro is over.
            if (this.playingIntro) {
                this.horizon.update(0, this.currentSpeed, hasObstacles, /* showNightMode = */ false);
            }
            else if (!this.crashed) {
                const showNightMode = this.isDarkMode !== this.inverted;
                deltaTime = !this.activated ? 0 : deltaTime;
                this.horizon.update(deltaTime, this.currentSpeed, hasObstacles, showNightMode);
            }
            const firstObstacle = this.horizon.obstacles[0];
            // Check for collisions.
            let collision = hasObstacles && firstObstacle &&
                this.checkForCollision(firstObstacle, this.tRex);
            // For a11y, audio cues.
            if (this.hasAudioCuesInternal && hasObstacles) {
                assert(firstObstacle);
                const jumpObstacle = firstObstacle.typeConfig.type !== 'collectable';
                if (!firstObstacle.jumpAlerted) {
                    const threshold = this.config.audiocueProximityThreshold;
                    const adjProximityThreshold = threshold +
                        (threshold * Math.log10(this.currentSpeed / this.config.speed));
                    if (firstObstacle.xPos < adjProximityThreshold) {
                        if (jumpObstacle) {
                            this.getGeneratedSoundFx().jump();
                        }
                        firstObstacle.jumpAlerted = true;
                    }
                }
            }
            // Activated alt game mode.
            if (this.isAltGameModeEnabled() && collision && firstObstacle &&
                firstObstacle.typeConfig.type === 'collectable') {
                this.horizon.removeFirstObstacle();
                this.tRex.setFlashing(true);
                collision = false;
                this.altGameModeFlashTimer = this.config.flashDuration;
                this.runningTime = 0;
                if (this.hasAudioCuesInternal) {
                    this.getGeneratedSoundFx().collect();
                }
            }
            if (!collision) {
                this.distanceRan += this.currentSpeed * deltaTime / this.msPerFrame;
                if (this.currentSpeed < this.config.maxSpeed) {
                    this.currentSpeed += this.config.acceleration;
                }
            }
            else {
                this.gameOver();
            }
            const playAchievementSound = this.distanceMeter.update(deltaTime, Math.ceil(this.distanceRan));
            if (!this.hasAudioCuesInternal && playAchievementSound) {
                this.playSound(this.soundFx.SCORE);
            }
            // Night mode.
            if (!this.isAltGameModeEnabled()) {
                if (this.invertTimer > this.config.invertFadeDuration) {
                    this.invertTimer = 0;
                    this.invertTrigger = false;
                    this.invert(false);
                }
                else if (this.invertTimer) {
                    this.invertTimer += deltaTime;
                }
                else {
                    const actualDistance = this.distanceMeter.getActualDistance(Math.ceil(this.distanceRan));
                    if (actualDistance > 0) {
                        this.invertTrigger = !(actualDistance % this.config.invertDistance);
                        if (this.invertTrigger && this.invertTimer === 0) {
                            this.invertTimer += deltaTime;
                            this.invert(false);
                        }
                    }
                }
            }
        }
        if (this.playing ||
            (!this.activated && this.tRex.blinkCount < this.config.maxBlinkCount)) {
            this.tRex.update(deltaTime);
            this.scheduleNextUpdate();
        }
    }
    handleEvent(e) {
        switch (e.type) {
            case RunnerEvents.KEYDOWN:
            case RunnerEvents.TOUCHSTART:
            case RunnerEvents.POINTERDOWN:
                this.onKeyDown(e);
                break;
            case RunnerEvents.KEYUP:
            case RunnerEvents.TOUCHEND:
            case RunnerEvents.POINTERUP:
                this.onKeyUp(e);
                break;
            case RunnerEvents.GAMEPADCONNECTED:
                this.onGamepadConnected();
                break;
            default:
        }
    }
    /**
     * Initialize audio cues if activated by focus on the canvas element.
     */
    handleCanvasKeyPress(e) {
        if (!this.activated && !this.hasAudioCuesInternal) {
            this.toggleSpeed();
            this.hasAudioCuesInternal = true;
            this.generatedSoundFx = new GeneratedSoundFx();
            this.config.clearTime *= 1.2;
        }
        else if (e instanceof KeyboardEvent && runnerKeycodes.jump.includes(e.keyCode)) {
            this.onKeyDown(e);
        }
    }
    /**
     * Prevent space key press from scrolling.
     */
    preventScrolling(e) {
        if (e.keyCode === 32) {
            e.preventDefault();
        }
    }
    /**
     * Toggle speed setting if toggle is shown.
     */
    toggleSpeed() {
        if (this.hasAudioCuesInternal) {
            assert(this.slowSpeedCheckbox);
            const speedChange = this.hasSlowdown !== this.slowSpeedCheckbox.checked;
            if (speedChange) {
                assert(this.horizon);
                assert(this.tRex);
                this.hasSlowdownInternal = this.slowSpeedCheckbox.checked;
                const updatedConfig = this.hasSlowdown ? slowModeConfig : normalModeConfig;
                this.config = Object.assign(defaultBaseConfig, updatedConfig);
                this.currentSpeed = updatedConfig.speed;
                this.tRex.enableSlowConfig();
                this.horizon.adjustObstacleSpeed();
            }
            if (this.playing) {
                this.disableSpeedToggle(true);
            }
        }
    }
    /**
     * Show the speed toggle.
     * From focus event or when audio cues are activated.
     */
    showSpeedToggle(e) {
        const isFocusEvent = e && e.type === 'focus';
        if (this.hasAudioCuesInternal || isFocusEvent) {
            assert(this.slowSpeedCheckboxLabel);
            this.slowSpeedCheckboxLabel.classList.toggle(HIDDEN_CLASS, isFocusEvent ? false : !this.crashed);
        }
    }
    /**
     * Disable the speed toggle.
     */
    disableSpeedToggle(disable) {
        assert(this.slowSpeedCheckbox);
        if (disable) {
            this.slowSpeedCheckbox.setAttribute('disabled', 'disabled');
        }
        else {
            this.slowSpeedCheckbox.removeAttribute('disabled');
        }
    }
    /**
     * Bind relevant key / mouse / touch listeners.
     */
    startListening() {
        assert(this.containerEl);
        assert(this.canvas);
        // A11y keyboard / screen reader activation.
        this.containerEl.addEventListener(RunnerEvents.KEYDOWN, this.handleCanvasKeyPress.bind(this));
        if (!IS_MOBILE) {
            this.containerEl.addEventListener(RunnerEvents.FOCUS, this.showSpeedToggle.bind(this));
        }
        this.canvas.addEventListener(RunnerEvents.KEYDOWN, this.preventScrolling.bind(this));
        this.canvas.addEventListener(RunnerEvents.KEYUP, this.preventScrolling.bind(this));
        // Keys.
        document.addEventListener(RunnerEvents.KEYDOWN, this);
        document.addEventListener(RunnerEvents.KEYUP, this);
        // Touch / pointer.
        this.containerEl.addEventListener(RunnerEvents.TOUCHSTART, this);
        document.addEventListener(RunnerEvents.POINTERDOWN, this);
        document.addEventListener(RunnerEvents.POINTERUP, this);
        if (this.isArcadeMode()) {
            // Gamepad
            window.addEventListener(RunnerEvents.GAMEPADCONNECTED, this);
        }
    }
    /**
     * Process keydown.
     */
    onKeyDown(e) {
        // Prevent native page scrolling whilst tapping on mobile.
        if (IS_MOBILE && this.playing) {
            e.preventDefault();
        }
        if (this.isCanvasInView()) {
            // Allow toggling of speed toggle.
            if (e instanceof KeyboardEvent &&
                runnerKeycodes.jump.includes(e.keyCode) &&
                e.target === this.slowSpeedCheckbox) {
                return;
            }
            if (!this.crashed && !this.paused) {
                // For a11y, screen reader activation.
                const isMobileMouseInput = IS_MOBILE && e instanceof PointerEvent &&
                    e.type === RunnerEvents.POINTERDOWN && e.pointerType === 'mouse' &&
                    (e.target === this.containerEl ||
                        (IS_IOS &&
                            (e.target === this.touchController || e.target === this.canvas)));
                assert(this.tRex);
                if ((e instanceof KeyboardEvent &&
                    runnerKeycodes.jump.includes(e.keyCode)) ||
                    e.type === RunnerEvents.TOUCHSTART || isMobileMouseInput) {
                    e.preventDefault();
                    // Starting the game for the first time.
                    if (!this.playing) {
                        // Started by touch so create a touch controller.
                        if (!this.touchController && e.type === RunnerEvents.TOUCHSTART) {
                            this.createTouchController();
                        }
                        if (isMobileMouseInput) {
                            this.handleCanvasKeyPress(e);
                        }
                        this.loadSounds();
                        this.setPlayStatus(true);
                        this.update();
                        if (window.errorPageController) {
                            window.errorPageController.trackEasterEgg();
                        }
                    }
                    // Start jump.
                    if (!this.tRex.jumping && !this.tRex.ducking) {
                        if (this.hasAudioCuesInternal) {
                            this.getGeneratedSoundFx().cancelFootSteps();
                        }
                        else {
                            this.playSound(this.soundFx.BUTTON_PRESS);
                        }
                        this.tRex.startJump(this.currentSpeed);
                    }
                }
                else if (this.playing && e instanceof KeyboardEvent &&
                    runnerKeycodes.duck.includes(e.keyCode)) {
                    e.preventDefault();
                    if (this.tRex.jumping) {
                        // Speed drop, activated only when jump key is not pressed.
                        this.tRex.setSpeedDrop();
                    }
                    else if (!this.tRex.jumping && !this.tRex.ducking) {
                        // Duck.
                        this.tRex.setDuck(true);
                    }
                }
            }
        }
    }
    /**
     * Process key up.
     */
    onKeyUp(e) {
        assert(this.tRex);
        const keyCode = ('keyCode' in e) ? e.keyCode : 0;
        const isjumpKey = runnerKeycodes.jump.includes(keyCode) ||
            e.type === RunnerEvents.TOUCHEND || e.type === RunnerEvents.POINTERUP;
        if (this.isRunning() && isjumpKey) {
            this.tRex.endJump();
        }
        else if (runnerKeycodes.duck.includes(keyCode)) {
            this.tRex.speedDrop = false;
            this.tRex.setDuck(false);
        }
        else if (this.crashed) {
            // Check that enough time has elapsed before allowing jump key to restart.
            const deltaTime = getTimeStamp() - this.time;
            if (this.isCanvasInView() &&
                (runnerKeycodes.restart.includes(keyCode) ||
                    this.isLeftClickOnCanvas(e) ||
                    (deltaTime >= this.config.gameoverClearTime &&
                        runnerKeycodes.jump.includes(keyCode)))) {
                this.handleGameOverClicks(e);
            }
        }
        else if (this.paused && isjumpKey) {
            // Reset the jump state
            this.tRex.reset();
            this.play();
        }
    }
    /**
     * Process gamepad connected event.
     */
    onGamepadConnected() {
        if (!this.pollingGamepads) {
            this.pollGamepadState();
        }
    }
    /**
     * rAF loop for gamepad polling.
     */
    pollGamepadState() {
        const gamepads = navigator.getGamepads();
        this.pollActiveGamepad(gamepads);
        this.pollingGamepads = true;
        requestAnimationFrame(this.pollGamepadState.bind(this));
    }
    /**
     * Polls for a gamepad with the jump button pressed. If one is found this
     * becomes the "active" gamepad and all others are ignored.
     */
    pollForActiveGamepad(gamepads) {
        for (const [i, gamepad] of gamepads.entries()) {
            if (gamepad && gamepad.buttons.length > 0 &&
                gamepad.buttons[0].pressed) {
                this.gamepadIndex = i;
                this.pollActiveGamepad(gamepads);
                return;
            }
        }
    }
    /**
     * Polls the chosen gamepad for button presses and generates KeyboardEvents
     * to integrate with the rest of the game logic.
     */
    pollActiveGamepad(gamepads) {
        if (this.gamepadIndex === undefined) {
            this.pollForActiveGamepad(gamepads);
            return;
        }
        const gamepad = gamepads[this.gamepadIndex];
        if (!gamepad) {
            this.gamepadIndex = undefined;
            this.pollForActiveGamepad(gamepads);
            return;
        }
        // The gamepad specification defines the typical mapping of physical buttons
        // to button indices: https://w3c.github.io/gamepad/#remapping
        this.pollGamepadButton(gamepad, 0, 38); // Jump
        if (gamepad.buttons.length >= 2) {
            this.pollGamepadButton(gamepad, 1, 40); // Duck
        }
        if (gamepad.buttons.length >= 10) {
            this.pollGamepadButton(gamepad, 9, 13); // Restart
        }
        this.previousGamepad = gamepad;
    }
    /**
     * Generates a key event based on a gamepad button.
     */
    pollGamepadButton(gamepad, buttonIndex, keyCode) {
        const state = gamepad.buttons[buttonIndex]?.pressed || false;
        let previousState = false;
        if (this.previousGamepad) {
            previousState =
                this.previousGamepad.buttons[buttonIndex]?.pressed || false;
        }
        // Generate key events on the rising and falling edge of a button press.
        if (state !== previousState) {
            const e = new KeyboardEvent(state ? RunnerEvents.KEYDOWN : RunnerEvents.KEYUP, { keyCode: keyCode });
            document.dispatchEvent(e);
        }
    }
    /**
     * Handle interactions on the game over screen state.
     * A user is able to tap the high score twice to reset it.
     */
    handleGameOverClicks(e) {
        if (e.target !== this.slowSpeedCheckbox) {
            assert(this.distanceMeter);
            e.preventDefault();
            if (this.distanceMeter.hasClickedOnHighScore(e) && this.highestScore) {
                if (this.distanceMeter.isHighScoreFlashing()) {
                    // Subsequent click, reset the high score.
                    this.saveHighScore(0, true);
                    this.distanceMeter.resetHighScore();
                }
                else {
                    // First click, flash the high score.
                    this.distanceMeter.startHighScoreFlashing();
                }
            }
            else {
                this.distanceMeter.cancelHighScoreFlashing();
                this.restart();
            }
        }
    }
    /**
     * Returns whether the event was a left click on canvas.
     * On Windows right click is registered as a click.
     */
    isLeftClickOnCanvas(e) {
        if (!(e instanceof MouseEvent)) {
            return false;
        }
        return e.button != null && e.button < 2 &&
            e.type === RunnerEvents.POINTERUP &&
            (e.target === this.canvas ||
                (IS_MOBILE && this.hasAudioCuesInternal &&
                    e.target === this.containerEl));
    }
    /**
     * RequestAnimationFrame wrapper.
     */
    scheduleNextUpdate() {
        if (!this.updatePending) {
            this.updatePending = true;
            this.raqId = requestAnimationFrame(this.update.bind(this));
        }
    }
    /**
     * Whether the game is running.
     */
    isRunning() {
        return !!this.raqId;
    }
    /**
     * Set the initial high score as stored in the user's profile.
     */
    initializeHighScore(highScore) {
        assert(this.distanceMeter);
        this.syncHighestScore = true;
        highScore = Math.ceil(highScore);
        if (highScore < this.highestScore) {
            if (window.errorPageController) {
                window.errorPageController.updateEasterEggHighScore(this.highestScore);
            }
            return;
        }
        this.highestScore = highScore;
        this.distanceMeter.setHighScore(this.highestScore);
    }
    /**
     * Sets the current high score and saves to the profile if available.
     * @param distanceRan Total distance ran.
     * @param  resetScore Whether to reset the score.
     */
    saveHighScore(distanceRan, resetScore) {
        assert(this.distanceMeter);
        this.highestScore = Math.ceil(distanceRan);
        this.distanceMeter.setHighScore(this.highestScore);
        // Store the new high score in the profile.
        if (this.syncHighestScore && window.errorPageController) {
            if (resetScore) {
                window.errorPageController.resetEasterEggHighScore();
            }
            else {
                window.errorPageController.updateEasterEggHighScore(this.highestScore);
            }
        }
    }
    /**
     * Game over state.
     */
    gameOver() {
        assert(this.distanceMeter);
        assert(this.tRex);
        assert(this.containerEl);
        this.playSound(this.soundFx.HIT);
        vibrate(200);
        this.stop();
        this.crashed = true;
        this.distanceMeter.achievement = false;
        this.tRex.update(100, TrexStatus.CRASHED);
        // Game over panel.
        if (!this.gameOverPanel) {
            const origSpriteDef = IS_HIDPI ? spriteDefinitionByType.original.hdpi :
                spriteDefinitionByType.original.ldpi;
            if (this.canvas) {
                if (this.isAltGameModeEnabled()) {
                    this.gameOverPanel = new GameOverPanel(this.canvas, origSpriteDef.textSprite, origSpriteDef.restart, this.dimensions, /* imageSpriteProvider= */ this, origSpriteDef.altGameEnd, this.altGameModeActive);
                }
                else {
                    this.gameOverPanel = new GameOverPanel(this.canvas, origSpriteDef.textSprite, origSpriteDef.restart, this.dimensions, /* imageSpriteProvider= */ this);
                }
            }
        }
        assert(this.gameOverPanel);
        this.gameOverPanel.draw(this.altGameModeActive, this.tRex);
        // Update the high score.
        if (this.distanceRan > this.highestScore) {
            this.saveHighScore(this.distanceRan);
        }
        // Reset the time clock.
        this.time = getTimeStamp();
        if (this.hasAudioCuesInternal) {
            this.getGeneratedSoundFx().stopAll();
            assert(this.containerEl);
            this.announcePhrase(getA11yString(A11yStrings.GAME_OVER)
                .replace('$1', this.distanceMeter.getActualDistance(this.distanceRan)
                .toString()) +
                ' ' +
                getA11yString(A11yStrings.HIGH_SCORE)
                    .replace('$1', this.distanceMeter.getActualDistance(this.highestScore)
                    .toString()));
            this.containerEl.setAttribute('title', getA11yString(A11yStrings.ARIA_LABEL));
        }
        this.showSpeedToggle();
        this.disableSpeedToggle(false);
    }
    stop() {
        this.setPlayStatus(false);
        this.paused = true;
        cancelAnimationFrame(this.raqId);
        this.raqId = 0;
        if (this.hasAudioCuesInternal) {
            this.getGeneratedSoundFx().stopAll();
        }
    }
    play() {
        if (!this.crashed) {
            assert(this.tRex);
            this.setPlayStatus(true);
            this.paused = false;
            this.tRex.update(0, TrexStatus.RUNNING);
            this.time = getTimeStamp();
            this.update();
            if (this.hasAudioCuesInternal) {
                this.getGeneratedSoundFx().background();
            }
        }
    }
    restart() {
        if (!this.raqId) {
            assert(this.containerEl);
            assert(this.gameOverPanel);
            assert(this.tRex);
            assert(this.horizon);
            assert(this.distanceMeter);
            this.playCount++;
            this.runningTime = 0;
            this.setPlayStatus(true);
            this.toggleSpeed();
            this.paused = false;
            this.crashed = false;
            this.distanceRan = 0;
            this.setSpeed(this.config.speed);
            this.time = getTimeStamp();
            this.containerEl.classList.remove(RunnerClasses.CRASHED);
            this.clearCanvas();
            this.distanceMeter.reset();
            this.horizon.reset();
            this.tRex.reset();
            this.playSound(this.soundFx.BUTTON_PRESS);
            this.invert(true);
            this.update();
            this.gameOverPanel.reset();
            if (this.hasAudioCuesInternal) {
                this.getGeneratedSoundFx().background();
            }
            this.containerEl.setAttribute('title', getA11yString(A11yStrings.JUMP));
            this.announcePhrase(getA11yString(A11yStrings.STARTED));
        }
    }
    setPlayStatus(isPlaying) {
        if (this.touchController) {
            this.touchController.classList.toggle(HIDDEN_CLASS, !isPlaying);
        }
        this.playing = isPlaying;
    }
    /**
     * Whether the game should go into arcade mode.
     */
    isArcadeMode() {
        // In RTL languages the title is wrapped with the left to right mark
        // control characters &#x202A; and &#x202C but are invisible.
        return document.title.startsWith(ARCADE_MODE_URL, IS_RTL ? 1 : 0);
    }
    /**
     * Hides offline messaging for a fullscreen game only experience.
     */
    setArcadeMode() {
        document.body.classList.add(RunnerClasses.ARCADE_MODE);
        this.setArcadeModeContainerScale();
    }
    /**
     * Sets the scaling for arcade mode.
     */
    setArcadeModeContainerScale() {
        assert(this.containerEl);
        const windowHeight = window.innerHeight;
        const scaleHeight = windowHeight / this.dimensions.height;
        const scaleWidth = window.innerWidth / this.dimensions.width;
        const scale = Math.max(1, Math.min(scaleHeight, scaleWidth));
        const scaledCanvasHeight = this.dimensions.height * scale;
        // Positions the game container at 10% of the available vertical window
        // height minus the game container height.
        const translateY = Math.ceil(Math.max(0, (windowHeight - scaledCanvasHeight -
            this.config.arcadeModeInitialTopPosition) *
            this.config.arcadeModeTopPositionPercent)) *
            window.devicePixelRatio;
        const cssScale = IS_RTL ? -scale + ',' + scale : scale;
        this.containerEl.style.transform =
            'scale(' + cssScale + ') translateY(' + translateY + 'px)';
    }
    /**
     * Pause the game if the tab is not in focus.
     */
    onVisibilityChange(e) {
        if (document.hidden || e.type === 'blur' ||
            document.visibilityState !== 'visible') {
            this.stop();
        }
        else if (!this.crashed) {
            assert(this.tRex);
            this.tRex.reset();
            this.play();
        }
    }
    /**
     * Play a sound.
     */
    playSound(soundBuffer) {
        if (soundBuffer) {
            assert(this.audioContext);
            const sourceNode = this.audioContext.createBufferSource();
            sourceNode.buffer = soundBuffer;
            sourceNode.connect(this.audioContext.destination);
            sourceNode.start(0);
        }
    }
    /**
     * Inverts the current page / canvas colors.
     * @param reset Whether to reset colors.
     */
    invert(reset) {
        const htmlEl = document.firstElementChild;
        assert(htmlEl);
        if (reset) {
            htmlEl.classList.toggle(RunnerClasses.INVERTED, false);
            this.invertTimer = 0;
            this.inverted = false;
        }
        else {
            this.inverted =
                htmlEl.classList.toggle(RunnerClasses.INVERTED, this.invertTrigger);
        }
    }
    /**
     * For screen readers make an announcement to the live region.
     * @param phrase Sentence to speak.
     */
    announcePhrase(phrase) {
        if (this.a11yStatusEl) {
            this.a11yStatusEl.textContent = '';
            this.a11yStatusEl.textContent = phrase;
        }
    }
    /**
     * Check for a collision.
     * @param obstacle Obstacle object.
     * @param tRex T-rex object.
     * @param canvasCtx Optional canvas context for drawing collision boxes.
     */
    checkForCollision(obstacle, tRex, canvasCtx) {
        // Adjustments are made to the bounding box as there is a 1 pixel white
        // border around the t-rex and obstacles.
        const tRexBox = new CollisionBox(tRex.xPos + 1, tRex.yPos + 1, tRex.config.width - 2, tRex.config.height - 2);
        const obstacleBox = new CollisionBox(obstacle.xPos + 1, obstacle.yPos + 1, obstacle.typeConfig.width * obstacle.size - 2, obstacle.typeConfig.height - 2);
        // Debug outer box
        if (canvasCtx) {
            drawCollisionBoxes(canvasCtx, tRexBox, obstacleBox);
        }
        // Simple outer bounds check.
        if (boxCompare(tRexBox, obstacleBox)) {
            const collisionBoxes = obstacle.collisionBoxes;
            let tRexCollisionBoxes = [];
            if (this.isAltGameModeEnabled()) {
                const runnerSpriteDefinition = this.getSpriteDefinition();
                assert(runnerSpriteDefinition);
                assert(runnerSpriteDefinition.tRex);
                tRexCollisionBoxes = runnerSpriteDefinition.tRex.collisionBoxes;
            }
            else {
                tRexCollisionBoxes = tRex.getCollisionBoxes();
            }
            // Detailed axis aligned box check.
            for (const tRexCollisionBox of tRexCollisionBoxes) {
                for (const obstacleCollixionBox of collisionBoxes) {
                    // Adjust the box to actual positions.
                    const adjTrexBox = createAdjustedCollisionBox(tRexCollisionBox, tRexBox);
                    const adjObstacleBox = createAdjustedCollisionBox(obstacleCollixionBox, obstacleBox);
                    const crashed = boxCompare(adjTrexBox, adjObstacleBox);
                    // Draw boxes for debug.
                    if (canvasCtx) {
                        drawCollisionBoxes(canvasCtx, adjTrexBox, adjObstacleBox);
                    }
                    if (crashed) {
                        return [adjTrexBox, adjObstacleBox];
                    }
                }
            }
        }
        return null;
    }
}
/**
 * Updates the canvas size taking into
 * account the backing store pixel ratio and
 * the device pixel ratio.
 *
 * See article by Paul Lewis:
 * http://www.html5rocks.com/en/tutorials/canvas/hidpi/
 *
 * @return Whether the canvas was scaled.
 */
function updateCanvasScaling(canvas, width, height) {
    const context = canvas.getContext('2d');
    assert(context);
    // Query the various pixel ratios
    const devicePixelRatio = Math.floor(window.devicePixelRatio) || 1;
    /** @suppress {missingProperties} */
    const backingStoreRatio = ('webkitBackingStorePixelRatio' in context) ?
        Math.floor(context.webkitBackingStorePixelRatio) :
        1;
    const ratio = devicePixelRatio / backingStoreRatio;
    // Upscale the canvas if the two ratios don't match
    if (devicePixelRatio !== backingStoreRatio) {
        const oldWidth = width || canvas.width;
        const oldHeight = height || canvas.height;
        canvas.width = oldWidth * ratio;
        canvas.height = oldHeight * ratio;
        canvas.style.width = oldWidth + 'px';
        canvas.style.height = oldHeight + 'px';
        // Scale the context to counter the fact that we've manually scaled
        // our canvas element.
        context.scale(ratio, ratio);
        return true;
    }
    else if (devicePixelRatio === 1) {
        // Reset the canvas width / height. Fixes scaling bug when the page is
        // zoomed and the devicePixelRatio changes accordingly.
        canvas.style.width = canvas.width + 'px';
        canvas.style.height = canvas.height + 'px';
    }
    return false;
}
/**
 * Returns a string from loadTimeData data object.
 */
function getA11yString(stringName) {
    return loadTimeData.valueExists(stringName) ?
        loadTimeData.getString(stringName) :
        '';
}
/**
 * Vibrate on mobile devices.
 * @param duration Duration of the vibration in milliseconds.
 */
function vibrate(duration) {
    if (IS_MOBILE && window.navigator.vibrate) {
        window.navigator.vibrate(duration);
    }
}
/**
 * Create canvas element.
 * @param container Element to append canvas to.
 */
function createCanvas(container, width, height, classname) {
    const canvas = document.createElement('canvas');
    canvas.className =
        classname ? RunnerClasses.CANVAS + ' ' + classname : RunnerClasses.CANVAS;
    canvas.width = width;
    canvas.height = height;
    container.appendChild(canvas);
    return canvas;
}
/**
 * Decodes the base 64 audio to ArrayBuffer used by Web Audio.
 */
function decodeBase64ToArrayBuffer(base64String) {
    const len = (base64String.length / 4) * 3;
    const str = atob(base64String);
    const arrayBuffer = new ArrayBuffer(len);
    const bytes = new Uint8Array(arrayBuffer);
    for (let i = 0; i < len; i++) {
        bytes[i] = str.charCodeAt(i);
    }
    return bytes.buffer;
}
//******************************************************************************
/**
 * Adjust the collision box.
 * @param box The original box.
 * @param adjustment Adjustment box.
 * @return The adjusted collision box object.
 */
function createAdjustedCollisionBox(box, adjustment) {
    return new CollisionBox(box.x + adjustment.x, box.y + adjustment.y, box.width, box.height);
}
/**
 * Draw the collision boxes for debug.
 */
function drawCollisionBoxes(canvasCtx, tRexBox, obstacleBox) {
    canvasCtx.save();
    canvasCtx.strokeStyle = '#f00';
    canvasCtx.strokeRect(tRexBox.x, tRexBox.y, tRexBox.width, tRexBox.height);
    canvasCtx.strokeStyle = '#0f0';
    canvasCtx.strokeRect(obstacleBox.x, obstacleBox.y, obstacleBox.width, obstacleBox.height);
    canvasCtx.restore();
}
/**
 * Compare two collision boxes for a collision.
 * @return Whether the boxes intersected.
 */
function boxCompare(tRexBox, obstacleBox) {
    const tRexBoxX = tRexBox.x;
    const tRexBoxY = tRexBox.y;
    const obstacleBoxX = obstacleBox.x;
    const obstacleBoxY = obstacleBox.y;
    // Axis-Aligned Bounding Box method.
    if (tRexBoxX < obstacleBoxX + obstacleBox.width &&
        tRexBoxX + tRexBox.width > obstacleBoxX &&
        tRexBoxY < obstacleBoxY + obstacleBox.height &&
        tRexBox.height + tRexBoxY > obstacleBoxY) {
        return true;
    }
    return false;
}
