← PORTAL

Put the Libraries Into Your Game

Use the final versions below. Do not mix them with older copies.

1. Your folder

my-game/
    index.html
    rbs-game-library.js
    rbs-gamepad-library.js
    rbs-audio-library.js
    sounds/
    images/

2. Download the final library files

DOWNLOAD GAME LIBRARY DOWNLOAD GAMEPAD LIBRARY DOWNLOAD AUDIO LIBRARY

3. Add the script tags

<script src="rbs-game-library.js"></script>
<script src="rbs-gamepad-library.js"></script>
<script src="rbs-audio-library.js"></script>

<script>
    // YOUR GAME CODE
</script>
Order matters: game library first, then gamepad/audio, then your own game code.

4. Copy a library instead of downloading

GAME LIBRARY · COPY WHOLE FILE
/*
RBS CODE · MASTER GAME LIBRARY · FINAL DAY
===========================================
One library for the beginner "power blocks" AND the later building blocks.

IMPORTANT:
- Functions accept either an element OR its id as text.
- Old examples such as moveWithKeys("player", 7, "game") keep working.
- Dynamic sprites are removed from BOTH the webpage and their array.
*/

let RBS_GAME_ENDED = false;

function get(item) {
    if (typeof item === "string") {
        return document.getElementById(item);
    }
    return item;
}

function getAll(selector) {
    return Array.from(document.querySelectorAll(selector));
}

function move(item, xChange, yChange) {
    item = get(item);
    if (!item) return;
    item.style.left = (item.offsetLeft + xChange) + "px";
    item.style.top = (item.offsetTop + yChange) + "px";
}

function place(item, x, y) {
    item = get(item);
    if (!item) return;
    item.style.left = x + "px";
    item.style.top = y + "px";
}

function hide(item) {
    item = get(item);
    if (item) item.style.display = "none";
}

function show(item) {
    item = get(item);
    if (item) item.style.display = "block";
}

function setText(item, text) {
    item = get(item);
    if (item) item.innerText = text;
}

function setEmoji(item, emoji) {
    setText(item, emoji);
}

function setImage(item, src) {
    item = get(item);
    if (item) item.src = src;
}

function changePicture(item, src) {
    setImage(item, src);
}

function changeNumber(number, amount, display) {
    number = number + amount;
    display = get(display);
    if (display) display.innerText = number;
    return number;
}

function keepInside(item, gameArea) {
    item = get(item);
    gameArea = get(gameArea);
    if (!item || !gameArea) return;

    let maxX = gameArea.clientWidth - item.offsetWidth;
    let maxY = gameArea.clientHeight - item.offsetHeight;

    if (item.offsetLeft < 0) item.style.left = "0px";
    if (item.offsetTop < 0) item.style.top = "0px";
    if (item.offsetLeft > maxX) item.style.left = maxX + "px";
    if (item.offsetTop > maxY) item.style.top = maxY + "px";
}

function touching(a, b) {
    a = get(a);
    b = get(b);

    if (!a || !b) return false;
    if (a.style.display === "none" || b.style.display === "none") return false;

    let x = a.getBoundingClientRect();
    let y = b.getBoundingClientRect();

    return x.left < y.right &&
           x.right > y.left &&
           x.top < y.bottom &&
           x.bottom > y.top;
}

function atEdge(item, gameArea, side) {
    item = get(item);
    gameArea = get(gameArea);
    if (!item || !gameArea) return false;

    if (side === "left") return item.offsetLeft <= 0;
    if (side === "right") return item.offsetLeft + item.offsetWidth >= gameArea.clientWidth;
    if (side === "top") return item.offsetTop <= 0;
    if (side === "bottom") return item.offsetTop + item.offsetHeight >= gameArea.clientHeight;
    return false;
}

function randomPosition(item, gameArea) {
    item = get(item);
    gameArea = get(gameArea);
    if (!item || !gameArea) return;

    let maxX = Math.max(0, gameArea.clientWidth - item.offsetWidth);
    let maxY = Math.max(0, gameArea.clientHeight - item.offsetHeight);

    place(
        item,
        Math.floor(Math.random() * (maxX + 1)),
        Math.floor(Math.random() * (maxY + 1))
    );
}

function randomX(item, gameArea) {
    item = get(item);
    gameArea = get(gameArea);
    if (!item || !gameArea) return;

    let maxX = Math.max(0, gameArea.clientWidth - item.offsetWidth);
    item.style.left = Math.floor(Math.random() * (maxX + 1)) + "px";
}

function moveToward(enemy, target, speed) {
    enemy = get(enemy);
    target = get(target);
    if (!enemy || !target) return;

    let dx = target.offsetLeft - enemy.offsetLeft;
    let dy = target.offsetTop - enemy.offsetTop;
    let distance = Math.sqrt(dx * dx + dy * dy);
    if (distance === 0) return;

    move(enemy, (dx / distance) * speed, (dy / distance) * speed);
}

/*
THREE WAYS TO USE moveWithKeys:

1. Default arrows + stay inside game:
moveWithKeys("player", 10, "game");

2. Custom keys + stay inside game:
moveWithKeys("blue", 10, "game", "w", "s", "a", "d");

3. Custom keys without a boundary:
moveWithKeys("player", 10, "w", "s", "a", "d");
*/
function moveWithKeys(item, speed, third, fourth, fifth, sixth, seventh) {
    item = get(item);
    if (!item) return;

    let gameArea = null;
    let upKey = "ArrowUp";
    let downKey = "ArrowDown";
    let leftKey = "ArrowLeft";
    let rightKey = "ArrowRight";

    if (arguments.length === 3) {
        gameArea = get(third);
    } else if (arguments.length >= 7) {
        gameArea = get(third);
        upKey = fourth;
        downKey = fifth;
        leftKey = sixth;
        rightKey = seventh;
    } else if (arguments.length >= 6) {
        upKey = third;
        downKey = fourth;
        leftKey = fifth;
        rightKey = sixth;
    }

    document.addEventListener("keydown", function(event) {
        if (RBS_GAME_ENDED) return;

        let moved = false;

        if (event.key === upKey) {
            move(item, 0, -speed);
            moved = true;
        }
        if (event.key === downKey) {
            move(item, 0, speed);
            moved = true;
        }
        if (event.key === leftKey) {
            move(item, -speed, 0);
            moved = true;
        }
        if (event.key === rightKey) {
            move(item, speed, 0);
            moved = true;
        }

        if (moved && gameArea) keepInside(item, gameArea);

        if (moved && event.key.startsWith("Arrow")) {
            event.preventDefault();
        }
    });
}

function after(milliseconds, doThis) {
    return setTimeout(doThis, milliseconds);
}

function every(milliseconds, doThis) {
    return setInterval(doThis, milliseconds);
}

function everyFrame(doThis) {
    function loop() {
        doThis();
        requestAnimationFrame(loop);
    }
    requestAnimationFrame(loop);
}

function restartGame() {
    location.reload();
}

function gameOver(message, text) {
    RBS_GAME_ENDED = true;
    setText(message, text || "GAME OVER");
}

/* =========================================================
   BEGINNER POWER BLOCKS
   ========================================================= */

function collect(player, itemOrInventory, scoreOrName, points, gameArea) {
    /*
    Beginner:
    collect("player", "coin", "scoreText", 1, "game");

    Later array lesson:
    collect(keyElement, inventoryArray, "key");
    */
    if (Array.isArray(itemOrInventory)) {
        let item = get(player);
        itemOrInventory.push(scoreOrName);
        hide(item);
        return;
    }

    player = get(player);
    let item = get(itemOrInventory);
    let scoreText = get(scoreOrName);
    gameArea = get(gameArea);

    if (!player || !item || !scoreText || !gameArea) return;

    everyFrame(function() {
        if (RBS_GAME_ENDED) return;

        if (touching(player, item)) {
            let score = Number(scoreText.innerText) || 0;
            score = score + points;
            scoreText.innerText = score;
            randomPosition(item, gameArea);
        }
    });
}

function winAtScore(scoreText, target, message) {
    scoreText = get(scoreText);
    message = get(message);

    everyFrame(function() {
        if (RBS_GAME_ENDED || !scoreText) return;

        if ((Number(scoreText.innerText) || 0) >= target) {
            RBS_GAME_ENDED = true;
            if (message) message.innerText = "YOU WIN!";
        }
    });
}

function loseOnTouch(player, enemy, message, playerAfter) {
    player = get(player);
    enemy = get(enemy);
    message = get(message);

    everyFrame(function() {
        if (RBS_GAME_ENDED) return;

        if (touching(player, enemy)) {
            RBS_GAME_ENDED = true;
            if (playerAfter) setEmoji(player, playerAfter);
            if (message) message.innerText = "GAME OVER";
        }
    });
}

function winOnTouch(player, goal, message) {
    player = get(player);
    goal = get(goal);
    message = get(message);

    everyFrame(function() {
        if (RBS_GAME_ENDED) return;

        if (goal && goal.dataset.locked === "true") return;

        if (touching(player, goal)) {
            RBS_GAME_ENDED = true;
            if (message) message.innerText = "YOU WIN!";
        }
    });
}

function chase(enemy, player, speed) {
    enemy = get(enemy);
    player = get(player);

    everyFrame(function() {
        if (RBS_GAME_ENDED) return;
        moveToward(enemy, player, speed);
    });
}

function patrol(enemy, xSpeed, ySpeed, gameArea) {
    enemy = get(enemy);
    gameArea = get(gameArea);
    if (!enemy || !gameArea) return;

    let dx = xSpeed;
    let dy = ySpeed;

    everyFrame(function() {
        if (RBS_GAME_ENDED) return;

        move(enemy, dx, dy);

        if (atEdge(enemy, gameArea, "left") || atEdge(enemy, gameArea, "right")) {
            dx = -dx;
        }
        if (atEdge(enemy, gameArea, "top") || atEdge(enemy, gameArea, "bottom")) {
            dy = -dy;
        }

        keepInside(enemy, gameArea);
    });
}

function keyOpensDoor(player, key, door, message) {
    player = get(player);
    key = get(key);
    door = get(door);
    message = get(message);
    if (!player || !key || !door) return;

    door.dataset.locked = "true";
    let hasKey = false;

    everyFrame(function() {
        if (RBS_GAME_ENDED) return;

        if (!hasKey && touching(player, key)) {
            hasKey = true;
            hide(key);
            door.dataset.locked = "false";
            if (message) message.innerText = "KEY FOUND! Reach the door.";
        }

        if (hasKey && touching(player, door)) {
            RBS_GAME_ENDED = true;
            if (message) message.innerText = "YOU ESCAPED!";
        }
    });
}

function countdown(seconds, timeText, message) {
    timeText = get(timeText);
    message = get(message);
    let time = seconds;

    if (timeText) timeText.innerText = time;

    let timer = setInterval(function() {
        if (RBS_GAME_ENDED) {
            clearInterval(timer);
            return;
        }

        time = time - 1;
        if (timeText) timeText.innerText = time;

        if (time <= 0) {
            clearInterval(timer);
            RBS_GAME_ENDED = true;
            if (message) message.innerText = "TIME UP!";
        }
    }, 1000);

    return timer;
}

function startCountdown(seconds, display, whenFinished) {
    display = get(display);
    let time = seconds;
    if (display) display.innerText = time;

    let timer = setInterval(function() {
        time = time - 1;
        if (display) display.innerText = time;

        if (time <= 0) {
            clearInterval(timer);
            if (whenFinished) whenFinished();
        }
    }, 1000);

    return timer;
}

function survive(seconds, timeText, message, player) {
    timeText = get(timeText);
    message = get(message);
    player = get(player);
    let time = seconds;

    if (timeText) timeText.innerText = time;

    let timer = setInterval(function() {
        if (RBS_GAME_ENDED) {
            clearInterval(timer);
            return;
        }

        time = time - 1;
        if (timeText) timeText.innerText = time;

        if (time <= 0) {
            clearInterval(timer);
            RBS_GAME_ENDED = true;
            if (message) message.innerText = "YOU SURVIVED!";
            if (player) player.innerText = "🏆";
        }
    }, 1000);

    return timer;
}

/* =========================================================
   ARRAYS
   ========================================================= */

function hasItem(list, item) {
    return list.includes(item);
}

/*
Overloaded:
removeItem(array, thing) removes from an array.
removeItem(element) removes a DOM element.
*/
function removeItem(listOrElement, item) {
    if (Array.isArray(listOrElement)) {
        let index = listOrElement.indexOf(item);
        if (index !== -1) {
            listOrElement.splice(index, 1);
            return true;
        }
        return false;
    }

    let element = get(listOrElement);
    if (element) element.remove();
    return !!element;
}

function showInventory(list, display) {
    display = get(display);
    if (display) display.innerText = list.join(", ");
}

function randomItem(list) {
    if (list.length === 0) return undefined;
    return list[Math.floor(Math.random() * list.length)];
}

function countItem(list, item) {
    let total = 0;
    for (let i = 0; i < list.length; i++) {
        if (list[i] === item) total = total + 1;
    }
    return total;
}

function removeItems(list, item, amount) {
    if (countItem(list, item) < amount) return false;

    for (let i = 0; i < amount; i++) {
        removeItem(list, item);
    }
    return true;
}

function hasAllItems(list, neededItems) {
    for (let i = 0; i < neededItems.length; i++) {
        if (!list.includes(neededItems[i])) return false;
    }
    return true;
}

function touchingAny(item, items) {
    item = get(item);

    for (let i = 0; i < items.length; i++) {
        if (touching(item, items[i])) return items[i];
    }
    return null;
}

function touchingIndex(item, items) {
    item = get(item);

    for (let i = 0; i < items.length; i++) {
        if (touching(item, items[i])) return i;
    }
    return -1;
}

function moveIfClear(item, xChange, yChange, walls, gameArea) {
    item = get(item);
    gameArea = get(gameArea);

    let oldX = item.offsetLeft;
    let oldY = item.offsetTop;

    move(item, xChange, yChange);
    if (gameArea) keepInside(item, gameArea);

    for (let i = 0; i < walls.length; i++) {
        if (touching(item, walls[i])) {
            place(item, oldX, oldY);
            return false;
        }
    }
    return true;
}

function moveAll(items, xChange, yChange) {
    for (let i = 0; i < items.length; i++) {
        move(items[i], xChange, yChange);
    }
}

/* =========================================================
   SMOOTH CONTROLS
   ========================================================= */

let keysDown = [];
let keyTrackingStarted = false;

function startKeyTracking() {
    if (keyTrackingStarted) return;
    keyTrackingStarted = true;

    document.addEventListener("keydown", function(event) {
        if (!keysDown.includes(event.key)) keysDown.push(event.key);

        if (event.key.startsWith("Arrow") || event.key === " ") {
            event.preventDefault();
        }
    });

    document.addEventListener("keyup", function(event) {
        let position = keysDown.indexOf(event.key);
        if (position !== -1) keysDown.splice(position, 1);
    });

    window.addEventListener("blur", function() {
        keysDown.length = 0;
    });
}

function keyIsDown(key) {
    return keysDown.includes(key);
}

/* =========================================================
   PLATFORM PHYSICS
   ========================================================= */

function applyGravity(item, ySpeed, gravityStrength) {
    item = get(item);
    ySpeed = ySpeed + gravityStrength;
    move(item, 0, ySpeed);
    return ySpeed;
}

function standingOnAny(player, platforms) {
    player = get(player);
    if (!player) return false;

    let playerBox = player.getBoundingClientRect();

    for (let i = 0; i < platforms.length; i++) {
        let platformBox = platforms[i].getBoundingClientRect();

        let feetNearTop = Math.abs(playerBox.bottom - platformBox.top) <= 5;
        let xOverlap =
            playerBox.right > platformBox.left &&
            playerBox.left < platformBox.right;

        if (feetNearTop && xOverlap) return true;
    }

    return false;
}

function landOnPlatforms(player, ySpeed, platforms) {
    player = get(player);
    if (!player || ySpeed < 0) return ySpeed;

    let playerBox = player.getBoundingClientRect();

    for (let i = 0; i < platforms.length; i++) {
        let platform = platforms[i];
        let platformBox = platform.getBoundingClientRect();

        let wasAbove =
            playerBox.bottom - ySpeed <= platformBox.top + 3;

        let reachedPlatform =
            playerBox.bottom >= platformBox.top;

        let xOverlap =
            playerBox.right > platformBox.left &&
            playerBox.left < platformBox.right;

        if (wasAbove && reachedPlatform && xOverlap) {
            player.style.top =
                (platform.offsetTop - player.offsetHeight) + "px";
            return 0;
        }
    }

    return ySpeed;
}

/* =========================================================
   DOM + ARRAYS: CREATE AND DELETE REAL GAME OBJECTS
   ========================================================= */

function createSprite(gameArea, picture, x, y, size) {
    gameArea = get(gameArea);
    size = size || 44;

    let sprite = document.createElement("div");
    sprite.className = "sprite";
    sprite.innerText = picture;
    sprite.style.position = "absolute";
    sprite.style.left = x + "px";
    sprite.style.top = y + "px";
    sprite.style.width = size + "px";
    sprite.style.height = size + "px";
    sprite.style.fontSize = size + "px";
    sprite.style.lineHeight = size + "px";
    sprite.style.textAlign = "center";
    sprite.style.userSelect = "none";

    gameArea.appendChild(sprite);
    return sprite;
}

/* Create a sprite AND remember it in an array. */
function addSprite(list, gameArea, picture, x, y, size) {
    let sprite = createSprite(gameArea, picture, x, y, size);
    list.push(sprite);
    return sprite;
}

/*
Remove a sprite from the PAGE and, if supplied, from its ARRAY too.
This prevents invisible/dead objects building up in memory.
*/
function removeSprite(sprite, list) {
    sprite = get(sprite);
    if (!sprite) return false;

    if (Array.isArray(list)) {
        let index = list.indexOf(sprite);
        if (index !== -1) list.splice(index, 1);
    }

    sprite.remove();
    return true;
}

function removeAt(list, index) {
    if (index < 0 || index >= list.length) return false;

    let sprite = list[index];
    if (sprite && sprite.remove) sprite.remove();
    list.splice(index, 1);
    return true;
}

function clearSprites(list) {
    for (let i = list.length - 1; i >= 0; i--) {
        removeAt(list, i);
    }
}

/*
IMPORTANT CLEANUP:
Returns how many objects were removed.
Uses a backwards loop so splice() cannot skip items.
*/
function removeOffscreen(items, gameArea) {
    gameArea = get(gameArea);
    let removed = 0;

    for (let i = items.length - 1; i >= 0; i--) {
        let item = items[i];

        let outside =
            item.offsetLeft + item.offsetWidth < 0 ||
            item.offsetLeft > gameArea.clientWidth ||
            item.offsetTop + item.offsetHeight < 0 ||
            item.offsetTop > gameArea.clientHeight;

        if (outside) {
            removeAt(items, i);
            removed = removed + 1;
        }
    }

    return removed;
}

function makeBullet(shooter, gameArea, bullets, picture, size) {
    shooter = get(shooter);
    gameArea = get(gameArea);
    size = size || 18;

    let x =
        shooter.offsetLeft +
        shooter.offsetWidth / 2 -
        size / 2;

    let y = shooter.offsetTop - size;

    return addSprite(bullets, gameArea, picture || "•", x, y, size);
}

/*
Remove every A/B collision from BOTH arrays and BOTH DOM elements.
Returns number of hits.
*/
function removeCollisions(listA, listB) {
    let hits = 0;

    for (let a = listA.length - 1; a >= 0; a--) {
        let hitSomething = false;

        for (let b = listB.length - 1; b >= 0; b--) {
            if (touching(listA[a], listB[b])) {
                removeAt(listB, b);
                removeAt(listA, a);
                hits = hits + 1;
                hitSomething = true;
                break;
            }
        }

        if (hitSomething) continue;
    }

    return hits;
}
GAMEPAD LIBRARY · COPY WHOLE FILE
/*
RBS CODE · GAMEPAD LIBRARY · FINAL DAY
======================================
Load AFTER rbs-game-library.js.

padIndex:
0 = first controller
1 = second controller
*/

const PAD = {
    A: 0,
    B: 1,
    X: 2,
    Y: 3,
    LB: 4,
    RB: 5,
    LT: 6,
    RT: 7,
    BACK: 8,
    START: 9,
    L3: 10,
    R3: 11,
    UP: 12,
    DOWN: 13,
    LEFT: 14,
    RIGHT: 15,
    HOME: 16
};

const RBS_PAD = {
    previousButtons: [],
    deadZone: 0.15
};

function getGamepad(index = 0) {
    let pads = navigator.getGamepads ? navigator.getGamepads() : [];
    return pads[index] || null;
}

function gamepadConnected(padIndex = 0) {
    return getGamepad(padIndex) !== null;
}

function padButton(number, padIndex = 0) {
    let pad = getGamepad(padIndex);

    return !!(
        pad &&
        pad.buttons[number] &&
        pad.buttons[number].pressed
    );
}

function padButtonValue(number, padIndex = 0) {
    let pad = getGamepad(padIndex);

    if (!pad || !pad.buttons[number]) {
        return 0;
    }

    return pad.buttons[number].value;
}

function padAxis(
    number,
    deadZone = RBS_PAD.deadZone,
    padIndex = 0
) {
    let pad = getGamepad(padIndex);

    if (!pad || pad.axes[number] === undefined) {
        return 0;
    }

    let value = pad.axes[number];

    if (Math.abs(value) < deadZone) {
        return 0;
    }

    return value;
}

function leftStick(
    padIndex = 0,
    deadZone = RBS_PAD.deadZone
) {
    return {
        x: padAxis(0, deadZone, padIndex),
        y: padAxis(1, deadZone, padIndex)
    };
}

function rightStick(
    padIndex = 0,
    deadZone = RBS_PAD.deadZone
) {
    return {
        x: padAxis(2, deadZone, padIndex),
        y: padAxis(3, deadZone, padIndex)
    };
}

function padJustPressed(number, padIndex = 0) {
    let pad = getGamepad(padIndex);

    if (!pad || !pad.buttons[number]) {
        return false;
    }

    let key = padIndex + ":" + number;

    return (
        pad.buttons[number].pressed &&
        !RBS_PAD.previousButtons[key]
    );
}

function updatePadButtons() {
    let pads =
        navigator.getGamepads ?
        navigator.getGamepads() :
        [];

    for (let p = 0; p < pads.length; p++) {
        let pad = pads[p];

        if (!pad) continue;

        for (let b = 0; b < pad.buttons.length; b++) {
            RBS_PAD.previousButtons[p + ":" + b] =
                pad.buttons[b].pressed;
        }
    }
}

async function rumble(
    strong = 1,
    weak = 0.5,
    duration = 150,
    padIndex = 0
) {
    let pad = getGamepad(padIndex);

    if (!pad) return false;

    let actuator =
        pad.vibrationActuator ||
        (pad.hapticActuators &&
         pad.hapticActuators[0]);

    if (!actuator) return false;

    try {
        if (actuator.playEffect) {
            await actuator.playEffect(
                "dual-rumble",
                {
                    startDelay: 0,
                    duration: duration,
                    strongMagnitude:
                        Math.max(
                            0,
                            Math.min(1, strong)
                        ),
                    weakMagnitude:
                        Math.max(
                            0,
                            Math.min(1, weak)
                        )
                }
            );

            return true;
        }

        if (actuator.pulse) {
            await actuator.pulse(
                Math.max(strong, weak),
                duration
            );

            return true;
        }
    } catch (error) {}

    return false;
}

async function stopRumble(padIndex = 0) {
    let pad = getGamepad(padIndex);

    if (!pad) return;

    let actuator =
        pad.vibrationActuator ||
        (pad.hapticActuators &&
         pad.hapticActuators[0]);

    if (!actuator) return;

    try {
        if (actuator.reset) {
            await actuator.reset();
        }
    } catch (error) {}
}

/* =========================================================
   HIGH-LEVEL POWER BLOCKS
   ========================================================= */

function moveWithGamepad(
    item,
    speed,
    gameArea,
    padIndex = 0
) {
    item = get(item);
    gameArea = get(gameArea);

    if (!item || !gameArea) return;

    function loop() {
        if (
            typeof RBS_GAME_ENDED === "undefined" ||
            !RBS_GAME_ENDED
        ) {
            let stick = leftStick(padIndex);

            move(
                item,
                stick.x * speed,
                stick.y * speed
            );

            keepInside(item, gameArea);
        }

        requestAnimationFrame(loop);
    }

    requestAnimationFrame(loop);
}

function driveWithGamepad(
    vehicle,
    steeringSpeed,
    driveSpeed,
    gameArea,
    padIndex = 0
) {
    vehicle = get(vehicle);
    gameArea = get(gameArea);

    if (!vehicle || !gameArea) return;

    function loop() {
        if (
            typeof RBS_GAME_ENDED === "undefined" ||
            !RBS_GAME_ENDED
        ) {
            let stick = leftStick(padIndex);

            let gas =
                padButtonValue(PAD.RT, padIndex);

            let reverse =
                padButtonValue(PAD.LT, padIndex);

            move(
                vehicle,
                stick.x * steeringSpeed,
                (reverse - gas) * driveSpeed
            );

            keepInside(vehicle, gameArea);
        }

        requestAnimationFrame(loop);
    }

    requestAnimationFrame(loop);
}

function rumbleOnTouch(
    item1,
    item2,
    strong = 1,
    weak = 0.5,
    duration = 150,
    padIndex = 0
) {
    item1 = get(item1);
    item2 = get(item2);

    let wasTouching = false;

    function loop() {
        let isTouching =
            item1 &&
            item2 &&
            touching(item1, item2);

        if (isTouching && !wasTouching) {
            rumble(
                strong,
                weak,
                duration,
                padIndex
            );
        }

        wasTouching = isTouching;

        requestAnimationFrame(loop);
    }

    requestAnimationFrame(loop);
}

function moveTwoPlayersWithGamepads(
    player1,
    player2,
    speed,
    gameArea
) {
    moveWithGamepad(
        player1,
        speed,
        gameArea,
        0
    );

    moveWithGamepad(
        player2,
        speed,
        gameArea,
        1
    );
}
AUDIO LIBRARY · COPY WHOLE FILE
// RBS CODE — tiny audio helper library
// Put this file in the SAME folder as your HTML file.
// Then add: <script src="rbs-audio-library.js"></script>

function makeSound(file, volume = 1, loop = false) {
    let sound = new Audio(file);
    sound.volume = volume;
    sound.loop = loop;
    return sound;
}

function playSound(sound) {
    sound.currentTime = 0;
    sound.play().catch(function() {});
}

function playOverlap(file, volume = 1) {
    let sound = new Audio(file);
    sound.volume = volume;
    sound.play().catch(function() {});
    return sound;
}

function stopSound(sound) {
    sound.pause();
    sound.currentTime = 0;
}

function pauseSound(sound) {
    sound.pause();
}

function setVolume(sound, volume) {
    sound.volume = Math.max(0, Math.min(1, volume));
}

function fadeOut(sound, step = 0.05, every = 50) {
    let timer = setInterval(function() {
        sound.volume = Math.max(0, sound.volume - step);
        if (sound.volume <= 0) {
            clearInterval(timer);
            sound.pause();
        }
    }, every);
}

5. Tiny test

Put this in your page after the libraries:

moveWithKeys(
    "player",
    10,
    "game"
);

Your HTML must contain elements with id="player" and id="game".