« get me outta code hell

Initial commit - guess-the-character - Unnamed repository; edit this file 'description' to name the repository.
summary refs log tree commit diff
diff options
context:
space:
mode:
authorFlorrie <towerofnix@gmail.com>2019-01-16 14:37:45 -0400
committerFlorrie <towerofnix@gmail.com>2019-01-16 14:37:45 -0400
commitb3a6398b03839d9a9c5f2a1a9bac144867e283a6 (patch)
tree012b8673f319efa1b5789bbc0fcaa4b986614799
Initial commit
-rw-r--r--.editorconfig9
-rw-r--r--data.js153
-rw-r--r--faces/common/al.pngbin0 -> 487 bytes
-rw-r--r--faces/common/asg.pngbin0 -> 737 bytes
-rw-r--r--faces/common/pap.pngbin0 -> 336 bytes
-rw-r--r--faces/common/sans.pngbin0 -> 377 bytes
-rw-r--r--faces/common/tor.pngbin0 -> 397 bytes
-rw-r--r--faces/common/und.pngbin0 -> 453 bytes
-rw-r--r--faces/deltarune/ber.pngbin0 -> 499 bytes
-rw-r--r--faces/deltarune/jok.pngbin0 -> 618 bytes
-rw-r--r--faces/deltarune/lan.pngbin0 -> 714 bytes
-rw-r--r--faces/deltarune/noe.pngbin0 -> 886 bytes
-rw-r--r--faces/deltarune/ral.pngbin0 -> 886 bytes
-rw-r--r--faces/deltarune/rud.pngbin0 -> 760 bytes
-rw-r--r--faces/deltarune/sus.pngbin0 -> 622 bytes
-rw-r--r--fonts/DTM-Mono.otfbin0 -> 24412 bytes
-rw-r--r--index.html46
-rw-r--r--main.js419
-rw-r--r--sounds/common/al.wavbin0 -> 4878 bytes
-rw-r--r--sounds/common/asg.wavbin0 -> 16580 bytes
-rw-r--r--sounds/common/pap.wavbin0 -> 17368 bytes
-rw-r--r--sounds/common/sans.wavbin0 -> 24948 bytes
-rw-r--r--sounds/common/tor.wavbin0 -> 14220 bytes
-rw-r--r--sounds/common/und.wavbin0 -> 11564 bytes
-rw-r--r--sounds/deltarune/ber.wavbin0 -> 16188 bytes
-rw-r--r--sounds/deltarune/echo.wavbin0 -> 88638 bytes
-rw-r--r--sounds/deltarune/jok.wavbin0 -> 4258 bytes
-rw-r--r--sounds/deltarune/lan.wavbin0 -> 4360 bytes
-rw-r--r--sounds/deltarune/noe.wavbin0 -> 3862 bytes
-rw-r--r--sounds/deltarune/ral.wavbin0 -> 12250 bytes
-rw-r--r--sounds/deltarune/rud.wavbin0 -> 3310 bytes
-rw-r--r--sounds/deltarune/sus.wavbin0 -> 11464 bytes
-rw-r--r--style.css223
33 files changed, 850 insertions, 0 deletions
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..e73c4f9
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,9 @@
+root = true
+
+[*]
+end_of_line = lf
+insert_final_newline = true
+charset = utf-8
+indent_size = 4
+indent_style = space
+trim_trailing_whitespace = true
diff --git a/data.js b/data.js
new file mode 100644
index 0000000..e6f4c77
--- /dev/null
+++ b/data.js
@@ -0,0 +1,153 @@
+/**
+ *
+ * hi this is the data for the game
+ *
+ */
+
+'use strict';
+
+window.gameData = {
+    characters: [
+        {
+            name: 'Alphys',
+            games: ['undertale', 'deltarune'],
+            file: 'al',
+            volume: 1.0, speed: 50, // TODO: I don't think Alphys is physically capable of speaking up
+            messages: [
+                `Watching someone on\na screen really makes\nyou root for them.`,
+                `S-so, ahhh, now I\nwant to help you!`,
+                `Using my knowledge,\nI can easily guide\nyou through Hotland!`
+            ]
+        },
+        {
+            name: 'Asgore',
+            games: ['undertale', 'deltarune'],
+            file: 'asg',
+            volume: 0.5, speed: 80,
+            messages: [
+                `Oh?\nIs someone there?`,
+                `Would you like\na cup of tea?`
+            ]
+        },
+        {
+            name: 'Berdly',
+            games: ['deltarune'],
+            file: 'ber',
+            volume: 0.5, speed: 50,
+            messages: [
+                `Sorry, I'm already\npartners with the 2nd\nsmartest student.`,
+                `Your unique skillset\nmight help a LOT on this\nassignment!`,
+                `We're FINE being alone!`
+            ]
+        },
+        {
+            name: 'Jevil',
+            games: ['deltarune'],
+            file: 'jok',
+            volume: 0.5, speed: 50,
+            messages: [
+                `UEE HEE HEE, THE KEY, THE KEY.`,
+                `A MARVELOUS FUN IS ABOUT TO\nBREAK FREE.`,
+                `WHEN YOUR HP DROPS TO 0, YOU LOSE!`
+            ]
+        },
+        {
+            name: 'Lancer',
+            games: ['deltarune'],
+            file: 'lan',
+            volume: 0.5, speed: 50,
+            messages: [
+                `You're going to be\nthrashed!`,
+                `AHHHHH HA HA HA HA HA HA\nHA HA!!!!`,
+                `Merry Christmas!`
+            ]
+        },
+        {
+            name: 'Noelle',
+            games: ['deltarune'],
+            file: 'noe',
+            volume: 0.5, speed: 40,
+            messages: [
+                `I could ask Ms. Alphys\nif we could make\na group of 3!`,
+                `B-but if I die, you're\npaying for my funeral, Kris!`,
+                `(Though, honestly, if\nshe's nice I might die, too...)`
+            ]
+        },
+        {
+            name: 'Papyrus',
+            games: ['undertale', 'deltarune'],
+            file: 'pap',
+            volume: 0.4, speed: 50,
+            messages: [
+                `I GUESS\nI CAN MAKE AN\nALLOWANCE FOR YOU!`,
+                `WOW!!!\nI HAVE FRIENDS!!!`,
+                `YOU TAUGHT ME A\nLOT, HUMAN.`
+            ]
+        },
+        {
+            name: 'Ralsei',
+            games: ['deltarune'],
+            file: 'ral',
+            volume: 0.5, speed: 50,
+            messages: [
+                `I hope we can be good\nfriends, Kris.`,
+                `It'll fall asleep, and we'll win peacefully!`,
+                `As heroes, we have the\npower to make a peaceful\nfuture!`
+            ]
+        },
+        {
+            name: 'Rudy',
+            games: ['deltarune'],
+            file: 'rud',
+            volume: 0.5, speed: 50,
+            messages: [
+                `Well jingle my goshdarn\nbells! Looks like\nKrismas came early!`,
+                `Don't worry, there's no\nway you can bore me\nright now!`,
+                `Yikes, man!`
+            ]
+        },
+        {
+            name: 'Sans',
+            games: ['undertale', 'deltarune'],
+            file: 'sans',
+            volume: 0.4, speed: 50,
+            messages: [
+                `anyways,\nyou're a human, right?`,
+                `i dont really care about\ncapturing anybody.`,
+                `staring at this lamp.\nits really cool.`
+            ]
+        },
+        {
+            name: 'Susie',
+            games: ['deltarune'],
+            file: 'sus',
+            volume: 0.5, speed: 50,
+            messages: [
+                `God, can you walk any\nslower, or what?`,
+                `Oh, you're not dead.\nSweet.`,
+                `Why don't we just climb\nover this spiked fence?`
+            ]
+        },
+        {
+            name: 'Toriel',
+            games: ['undertale', 'deltarune'],
+            file: 'tor',
+            volume: 0.5, speed: 50,
+            messages: [
+                `You have done\nexcellently thus\nfar, my child.`,
+                `Thank you for trusting\nme.`,
+                `If you have a need for\nanything, just call.`
+            ]
+        },
+        {
+            name: 'Undyne',
+            games: ['undertale', 'deltarune'],
+            file: 'und',
+            messages: [
+                `Homemade noodles\nare the best!`,
+                `BUT I JUST BUY\nSTORE-BRAND!`,
+                `NGAHHHHHHHHH\nHHHHHHHHHH!!!`
+            ]
+        }
+    ]
+};
diff --git a/faces/common/al.png b/faces/common/al.png
new file mode 100644
index 0000000..9c751ba
--- /dev/null
+++ b/faces/common/al.png
Binary files differdiff --git a/faces/common/asg.png b/faces/common/asg.png
new file mode 100644
index 0000000..1fb230d
--- /dev/null
+++ b/faces/common/asg.png
Binary files differdiff --git a/faces/common/pap.png b/faces/common/pap.png
new file mode 100644
index 0000000..98779a4
--- /dev/null
+++ b/faces/common/pap.png
Binary files differdiff --git a/faces/common/sans.png b/faces/common/sans.png
new file mode 100644
index 0000000..c29854e
--- /dev/null
+++ b/faces/common/sans.png
Binary files differdiff --git a/faces/common/tor.png b/faces/common/tor.png
new file mode 100644
index 0000000..3408f37
--- /dev/null
+++ b/faces/common/tor.png
Binary files differdiff --git a/faces/common/und.png b/faces/common/und.png
new file mode 100644
index 0000000..0b33165
--- /dev/null
+++ b/faces/common/und.png
Binary files differdiff --git a/faces/deltarune/ber.png b/faces/deltarune/ber.png
new file mode 100644
index 0000000..b4692bf
--- /dev/null
+++ b/faces/deltarune/ber.png
Binary files differdiff --git a/faces/deltarune/jok.png b/faces/deltarune/jok.png
new file mode 100644
index 0000000..948d353
--- /dev/null
+++ b/faces/deltarune/jok.png
Binary files differdiff --git a/faces/deltarune/lan.png b/faces/deltarune/lan.png
new file mode 100644
index 0000000..dbcbb2c
--- /dev/null
+++ b/faces/deltarune/lan.png
Binary files differdiff --git a/faces/deltarune/noe.png b/faces/deltarune/noe.png
new file mode 100644
index 0000000..a775b55
--- /dev/null
+++ b/faces/deltarune/noe.png
Binary files differdiff --git a/faces/deltarune/ral.png b/faces/deltarune/ral.png
new file mode 100644
index 0000000..98cd9a8
--- /dev/null
+++ b/faces/deltarune/ral.png
Binary files differdiff --git a/faces/deltarune/rud.png b/faces/deltarune/rud.png
new file mode 100644
index 0000000..654c612
--- /dev/null
+++ b/faces/deltarune/rud.png
Binary files differdiff --git a/faces/deltarune/sus.png b/faces/deltarune/sus.png
new file mode 100644
index 0000000..5c42624
--- /dev/null
+++ b/faces/deltarune/sus.png
Binary files differdiff --git a/fonts/DTM-Mono.otf b/fonts/DTM-Mono.otf
new file mode 100644
index 0000000..4135289
--- /dev/null
+++ b/fonts/DTM-Mono.otf
Binary files differdiff --git a/index.html b/index.html
new file mode 100644
index 0000000..bd0759c
--- /dev/null
+++ b/index.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <meta charset="utf-8">
+        <title>guess the character</title>
+        <link rel="stylesheet" href="style.css">
+    </head>
+    <body>
+        <canvas id="canvas"></canvas>
+        <div id="everything-lol">
+            <noscript>
+                <p>* Like, enable JavaScript.</p>
+            </noscript>
+            <div id="js-not-working">
+                <p>Uh, this is awkward. The JavaScript code isn't working.</p>
+                <p>Maybe it isn't loaded in yet? Just give it a minute, if your browser says the page is still loading...</p>
+                <p>Otherwise, your browser might be kinda old. Try a recent version of Firefox or Chrome or something like that.</p>
+                <p>Or like, who knows. Maybe I broke the code and it just doesn't work anymore.</p>
+            </div>
+            <div id="crashed">
+                <p>Oh heck this is the worst thing possible. <span class="red">The game crashed.</span></p>
+                <p>Sorry!!! Here is the error. Please share this with the game author on, like, The Internet.</p>
+                <pre id="crash-trace"></pre>
+            </div>
+            <div id="container">
+                <h1>GUESS THE CHARACTER</h1>
+                <div id="intro">
+                    <p>is a game in which you GUESS THE CHARACTER based on the sound of their speech.</p>
+                    <p>Loading (<span id="load-progress">0</span>%)...</p>
+                    <p><a href="#" id="play-link">Play</a></p>
+                </div>
+            </div>
+            <div id="game">
+                <div id="choice-container"></div>
+                <div id="results" class="hide">
+                    <p id="correct"><span>Correct!</span></p>
+                    <p id="incorrect"><span>Incorrect.</span><br>The correct choice was <span id="correct-character">Noelle</span>.</p>
+                    <p><a href="#" id="next-link">Next</a></p>
+                </div>
+                <div id="scorekeeper"></div>
+            </div>
+        </div>
+        <script src="data.js"></script>
+        <script src="main.js"></script>
+    </body>
+</html>
diff --git a/main.js b/main.js
new file mode 100644
index 0000000..97966f5
--- /dev/null
+++ b/main.js
@@ -0,0 +1,419 @@
+/**
+ *
+ * IDEAS LOL
+ *
+ * - make the game, dummy
+ *
+ * - hide the image, or the name -- i.e. force the user to guess based on one
+ *   or the other, not both
+ *
+ * - pick __ HARD __ selections of choices, not just totally random ones
+ *
+ */
+
+// initializy stuff --------------------------------------------------------ooo
+
+'use strict';
+
+// sorry this is cursed
+const { gameData } = window;
+
+document.body.classList.add('js-enabled');
+
+// resource loading --------------------------------------------------------o.o
+
+const resources = {
+    imageMap: new Map(),
+    soundMap: new Map()
+};
+
+let loadTotal = 0;
+let loadSoFar = 0;
+
+function loadResources() {
+    // 2 assets for each character: image, sound.
+    loadTotal = gameData.characters.length * 2;
+
+    return Promise.all([
+        loadImages(),
+        loadSounds()
+    ]);
+}
+
+async function loadImages() {
+    for (const character of gameData.characters) {
+        const image = new Image();
+        resources.imageMap.set(character, image);
+
+        const path = `faces/${getCharacterPath(character)}.png`;
+        image.src = path;
+
+        await new Promise((resolve, reject) => {
+            image.addEventListener('load', () => resolve());
+
+            image.addEventListener('error', () => {
+                reject(new Error('Failed to load image from path ' + path));
+            });
+        });
+
+        loadSoFar++;
+        updateLoadProgress();
+    }
+}
+
+async function loadSounds() {
+    for (const character of gameData.characters) {
+        const audio = new Audio();
+        resources.soundMap.set(character, audio);
+
+        const path = `sounds/${getCharacterPath(character)}.wav`;
+        audio.src = path;
+
+        await new Promise((resolve, reject) => {
+            audio.addEventListener('canplaythrough', () => resolve());
+
+            audio.addEventListener('error', () => {
+                reject(new Error('Failed to load audio from path ' + path));
+            });
+        });
+
+        loadSoFar++;
+        updateLoadProgress();
+    }
+}
+
+function updateLoadProgress() {
+    const el = document.getElementById('load-progress');
+    if (loadSoFar < loadTotal) {
+        el.innerHTML = Math.floor(loadSoFar / loadTotal * 100);
+    } else {
+        el.parentElement.innerHTML = 'Loaded!';
+    }
+}
+
+function getCharacterPath(character) {
+    let path = '';
+    if (character.games.length >= 2) {
+        path += 'common/';
+    } else if (character.games.length === 1) {
+        path += character.games[0] + '/';
+    } else {
+        // uhhhh what
+        throw new error(character.name + " isn't actually part of any games??");
+    }
+    path += character.file;
+    return path;
+}
+
+// talky talky -------------------------------------------------------------oWo
+
+function wrapMessage(msg, length = 23) {
+    // so h*ck, apparently word wrapping is Not Automatic in undertale
+    // best we just specify word-wraps in the unmodified message text.
+    // keeping this function around just in case though.
+    const words = msg.split(' ');
+    const lines = [[]];
+    let col = 0;
+    for (let i = 0; i < words.length; i++) {
+        const word = words[i];
+        col += word.length + 1;
+        if (col > length) {
+            lines.push([word]);
+            col = 0;
+        } else {
+            lines[lines.length - 1].push(word);
+        }
+    }
+    return lines.map(l => l.join(' ')).join('\n');
+}
+
+const audioCtx = new AudioContext();
+const analyser = audioCtx.createAnalyser();
+analyser.fftSize = 256;
+analyser.connect(audioCtx.destination);
+
+async function doSpeech(character, msg) {
+    const audio = resources.soundMap.get(character);
+    if (!audio) {
+        return;
+    }
+
+    for (let i = 0; i < msg.length; i++) {
+        const char = msg[i];
+        // Yeah all these timings are wrong but they're decent estimations.
+        if (char === ',' || char === '.') {
+            await delay(300);
+            while (msg[i + 1] === ',') i++; // Deal with ellipsis
+        } else if (char === '\n') {
+            await delay(80);
+        } else if (char === ' ') {
+            await delay(30);
+        } else {
+            const a = audio.cloneNode();
+            a.volume = character.volume || 0.5;
+
+            const source = audioCtx.createMediaElementSource(a);
+            source.connect(analyser);
+
+            a.play();
+            await delay(character.speed || 50);
+        }
+    }
+}
+
+const canvas = document.getElementById('canvas');
+const streamData = new Uint8Array(128);
+
+function sampleAudioStream() {
+    analyser.getByteFrequencyData(streamData);
+
+    const rect = document.body.getBoundingClientRect();
+    canvas.width = rect.width;
+    canvas.height = rect.height;
+    const ctx = canvas.getContext('2d');
+    ctx.clearRect(0, 0, canvas.width, canvas.height);
+
+    let lastTop = null;
+    for (let i = 0; i < 60; i++) { // i < N is arbitrary, this value seems pretty good for most voices
+        const w = canvas.width / 60;
+        const x = w * i;
+        const h = (0.9 * canvas.height / 255) * streamData[i];
+        ctx.fillStyle = 'white';
+
+        const top = h / 2;
+        if (lastTop) ctx.fillRect(x, canvas.height / 2 + top, 1, lastTop - top);
+        ctx.fillRect(x, canvas.height / 2 + top, w, 2);
+        if (lastTop) ctx.fillRect(x, canvas.height / 2 - top, 1, top - lastTop);
+        ctx.fillRect(x, canvas.height / 2 - top, w, 2);
+        lastTop = top;
+    }
+
+    requestAnimationFrame(sampleAudioStream);
+}
+
+requestAnimationFrame(sampleAudioStream);
+
+// display choices ---------------------------------------------------------www
+
+function displayChoices(characters, confirmCharacterCb) {
+    return new Promise(async resolve => {
+        const container = document.getElementById('choice-container');
+        while (container.firstChild) {
+            container.removeChild(container.firstChild);
+        }
+        container.classList.remove('hide');
+
+        let selected = false;
+        for (const character of characters) {
+            const el = document.createElement('a');
+            el.href = '#';
+            el.classList.add('character-choice');
+            el.appendChild(resources.imageMap.get(character));
+            const span = document.createElement('span');
+            span.appendChild(document.createTextNode(character.name));
+            el.appendChild(span);
+            container.appendChild(el);
+
+            el.addEventListener('click', () => {
+                if (!selected) {
+                    selected = true;
+                    resolve(character);
+                }
+            });
+        }
+
+        container.children[0].focus();
+        for (const el of container.children) {
+            el.classList.add('visible');
+            await delay(800 / container.children.length);
+        }
+    });
+}
+
+// utilitylol --------------------------------------------------------------uwu
+
+function characterWithName(name) {
+    return gameData.characters.find(chr => chr.name === name);
+}
+
+function pickRandom(array) {
+    return array[Math.floor(Math.random() * array.length)];
+}
+
+function spliceRandom(array) {
+    const val = pickRandom(array);
+    array.splice(array.indexOf(val), 1);
+    return val;
+}
+
+function delay(ms) {
+    return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+function waitForEvent(el, eventName) {
+    return new Promise(resolve => {
+        el.addEventListener(eventName, function cb(evt) {
+            el.removeEventListener(eventName, cb);
+            resolve(evt);
+        }); // if only {once: true} were "more supported"
+        //     as   if   that' s   something  I  care  about  in this game
+    });
+}
+
+// scorekeeper -------------------------------------------------------------***
+
+function setupScorekeeper(numCharacters) {
+    const container = document.getElementById('scorekeeper');
+    for (let i = 0; i < numCharacters; i++) {
+        const el = document.createElement('div')
+        el.classList.add('marker');
+        el.classList.add('unset');
+        container.appendChild(el);
+    }
+}
+
+function markScorekeeper(cls, character) {
+    const el = scorekeeper.querySelector('.unset');
+    el.classList.remove('unset');
+    el.classList.add(cls);
+    el.appendChild(resources.imageMap.get(character).cloneNode());
+    el.title = character.name
+}
+
+// uh start it now ---------------------------------------------------------aA!
+
+async function mainLol() {
+    const testChar = 'Noellef';
+
+    await loadResources();
+
+    document.body.addEventListener('keydown', evt => {
+        const choiceContainer = document.getElementById('choice-container');
+        if (!choiceContainer.classList.contains('hide')) {
+            if (/^[0-9]$/.test(evt.key)) {
+                const el = choiceContainer.children[evt.key - 1];
+                if (document.activeElement === el) {
+                    el.click();
+                } else if (el) {
+                    el.focus();
+                }
+            } else if (event.key === 'ArrowRight') {
+                const el = document.activeElement;
+                if (el.parentElement === choiceContainer && el.nextSibling) {
+                    el.nextSibling.focus();
+                }
+            } else if (event.key === 'ArrowLeft') {
+                const el = document.activeElement;
+                if (el.parentElement === choiceContainer && el.previousSibling) {
+                    el.previousSibling.focus();
+                }
+            }
+        }
+
+        if (event.which === 90) { // Z
+            if (document.activeElement.click) {
+                document.activeElement.click();
+            }
+        }
+    });
+
+    const gameEl = document.getElementById('game');
+
+    const playLink = document.getElementById('play-link');
+    playLink.focus();
+    await waitForEvent(playLink, 'click');
+
+    document.body.classList.add('game');
+
+    const deckGames = ['deltarune'];
+    const hardMode = false;
+
+    const characterDeck = gameData.characters.filter(chr => chr.games.some(g => deckGames.includes(g)));
+
+    setupScorekeeper(characterDeck.length);
+
+    while (characterDeck.length) {
+        gameEl.classList.remove('correct');
+        gameEl.classList.remove('incorrect');
+
+        const correctCharacter = characterWithName(testChar) || spliceRandom(characterDeck);
+
+        const numChoices = 6;
+        const potentialOptions = characterDeck.slice().filter(chr => chr !== correctCharacter);
+        const correctIndex = Math.min(Math.floor(Math.random() * numChoices), potentialOptions.length);
+        const allCharacters = [];
+        for (let i = 0; i < numChoices; i++) {
+            if (i === correctIndex) {
+                allCharacters.push(correctCharacter);
+            } else if (potentialOptions.length) {
+                let choice;
+                allCharacters.push(spliceRandom(potentialOptions));
+            } else {
+                break
+            }
+        }
+
+        let msg;
+        if (hardMode) {
+            msg = pickRandom(['Oh?', 'Hello!']);
+        } else {
+            msg = pickRandom(correctCharacter.messages);
+        }
+
+        await doSpeech(correctCharacter, msg);
+        await delay(500);
+
+        const choice = await displayChoices(allCharacters);
+
+        const container = document.getElementById('choice-container');
+        for (let i = 0; i < allCharacters.length; i++) {
+            const el = container.children[i];
+            if (i === correctIndex) {
+                el.classList.add('correct');
+            } else {
+                el.classList.add('incorrect');
+            }
+            if (i === allCharacters.indexOf(choice)) {
+                el.classList.add('chosen');
+            } else {
+                el.classList.add('not-chosen');
+            }
+        }
+
+        if (choice === correctCharacter) {
+            markScorekeeper('correct', correctCharacter);
+        } else {
+            markScorekeeper('incorrect', correctCharacter);
+        }
+
+        await delay(200);
+
+        if (choice === correctCharacter) {
+            gameEl.classList.add('correct');
+        } else {
+            gameEl.classList.add('incorrect');
+            const el = document.getElementById('correct-character');
+            el.innerHTML = '';
+            el.appendChild(document.createTextNode(correctCharacter.name));
+        }
+
+        document.getElementById('results').classList.remove('hide');
+
+        const nextLink = document.getElementById('next-link');
+        nextLink.focus();
+        await waitForEvent(nextLink, 'click');
+
+        document.getElementById('choice-container').classList.add('hide');
+        document.getElementById('results').classList.add('hide');
+
+        await delay(400);
+    }
+}
+
+mainLol().catch(error => {
+    document.body.classList.add('crashed');
+    document.getElementById('crash-trace').appendChild(document.createTextNode(
+        error.message + '\n' +
+        error.stack
+    ));
+});
diff --git a/sounds/common/al.wav b/sounds/common/al.wav
new file mode 100644
index 0000000..e99a61f
--- /dev/null
+++ b/sounds/common/al.wav
Binary files differdiff --git a/sounds/common/asg.wav b/sounds/common/asg.wav
new file mode 100644
index 0000000..eba4a26
--- /dev/null
+++ b/sounds/common/asg.wav
Binary files differdiff --git a/sounds/common/pap.wav b/sounds/common/pap.wav
new file mode 100644
index 0000000..d015d82
--- /dev/null
+++ b/sounds/common/pap.wav
Binary files differdiff --git a/sounds/common/sans.wav b/sounds/common/sans.wav
new file mode 100644
index 0000000..06cd4d7
--- /dev/null
+++ b/sounds/common/sans.wav
Binary files differdiff --git a/sounds/common/tor.wav b/sounds/common/tor.wav
new file mode 100644
index 0000000..65a735c
--- /dev/null
+++ b/sounds/common/tor.wav
Binary files differdiff --git a/sounds/common/und.wav b/sounds/common/und.wav
new file mode 100644
index 0000000..f4eb7f7
--- /dev/null
+++ b/sounds/common/und.wav
Binary files differdiff --git a/sounds/deltarune/ber.wav b/sounds/deltarune/ber.wav
new file mode 100644
index 0000000..a0e7049
--- /dev/null
+++ b/sounds/deltarune/ber.wav
Binary files differdiff --git a/sounds/deltarune/echo.wav b/sounds/deltarune/echo.wav
new file mode 100644
index 0000000..e751310
--- /dev/null
+++ b/sounds/deltarune/echo.wav
Binary files differdiff --git a/sounds/deltarune/jok.wav b/sounds/deltarune/jok.wav
new file mode 100644
index 0000000..5049c16
--- /dev/null
+++ b/sounds/deltarune/jok.wav
Binary files differdiff --git a/sounds/deltarune/lan.wav b/sounds/deltarune/lan.wav
new file mode 100644
index 0000000..0fbc700
--- /dev/null
+++ b/sounds/deltarune/lan.wav
Binary files differdiff --git a/sounds/deltarune/noe.wav b/sounds/deltarune/noe.wav
new file mode 100644
index 0000000..aa9350f
--- /dev/null
+++ b/sounds/deltarune/noe.wav
Binary files differdiff --git a/sounds/deltarune/ral.wav b/sounds/deltarune/ral.wav
new file mode 100644
index 0000000..39b76a1
--- /dev/null
+++ b/sounds/deltarune/ral.wav
Binary files differdiff --git a/sounds/deltarune/rud.wav b/sounds/deltarune/rud.wav
new file mode 100644
index 0000000..1daab6f
--- /dev/null
+++ b/sounds/deltarune/rud.wav
Binary files differdiff --git a/sounds/deltarune/sus.wav b/sounds/deltarune/sus.wav
new file mode 100644
index 0000000..8e106f0
--- /dev/null
+++ b/sounds/deltarune/sus.wav
Binary files differdiff --git a/style.css b/style.css
new file mode 100644
index 0000000..1c2a35d
--- /dev/null
+++ b/style.css
@@ -0,0 +1,223 @@
+@font-face {
+    font-family: DTM-Mono;
+    src: url("fonts/DTM-Mono.otf") format("opentype");
+}
+
+canvas {
+    position: fixed;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    pointer-events: none;
+    z-index: -1;
+}
+
+#everything-lol {
+    height: 400px;
+    border: 1px dotted #222;
+    padding: 20px;
+    background: rgba(0, 0, 0, 0.8);
+    box-shadow: 0 0 32px black;
+    position: relative;
+}
+
+body:not(.js-enabled) #container {
+    display: none;
+}
+
+#js-not-working {
+    text-align: start;
+}
+
+body.js-enabled #js-not-working {
+    display: none !important;
+}
+
+body:not(.crashed) #crashed {
+    display: none;
+}
+
+body.game #intro {
+    display: none;
+}
+
+#crash-trace {
+    text-align: start;
+    border: 1pt dotted white;
+    padding: 4pt;
+}
+
+#choice-container {
+    display: flex;
+    flex-direction: row;
+    justify-content: center;
+    align-items: center;
+    position: relative;
+    top: 0;
+}
+
+#choice-container.hide, #results.hide {
+    top: 10px;
+    opacity: 0;
+    transition: top 0.5s, opacity 0.5s;
+}
+
+#results {
+    transition: opacity 0.5s;
+}
+
+.character-choice {
+    color: white;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    padding: 15px;
+    margin: 5px;
+    position: relative;
+    cursor: pointer;
+    border: 2px solid transparent;
+}
+
+.character-choice img {
+    margin-bottom: 5px;
+    background-color: black;
+}
+
+.character-choice:focus {
+    outline: 1px dotted yellow;
+}
+
+.character-choice:not(.visible) {
+    opacity: 0;
+    bottom: 10px;
+}
+
+.character-choice.visible {
+    opacity: 1;
+    bottom: 0;
+    transition: opacity 0.5s, bottom 0.5s, color 0.2s;
+}
+
+.character-choice {
+    --my-color: white;
+    color: var(--my-color);
+}
+
+.character-choice.correct {
+    --my-color: green;
+}
+
+.character-choice.incorrect {
+    --my-color: red;
+}
+
+.character-choice.chosen {
+    border-color: var(--my-color);
+    bottom: 5px;
+}
+
+.character-choice.not-chosen {
+    bottom: -5px;
+    opacity: 0.6;
+}
+
+.character-choice.correct.not-chosen {
+    border: 2px dashed var(--my-color);
+}
+
+#game:not(.correct) #correct {
+    display: none;
+}
+
+#game:not(.incorrect) #incorrect {
+    display: none;
+}
+
+#correct span {
+    color: green;
+}
+
+#incorrect span:first-child {
+    color: red;
+}
+
+#incorrect span:last-child {
+    color: green;
+}
+
+#scorekeeper {
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    width: 100%;
+    height: 30px;
+    display: flex;
+}
+
+#scorekeeper .marker {
+    flex-basis: 0;
+    flex-grow: 1;
+    height: 100%;
+    opacity: 0.5;
+    border-top: 1px solid rgba(255, 255, 255, 0.5);
+    transition: background-color 0.5s;
+}
+
+#scorekeeper .marker:not(:last-child) {
+    border-right: 1px solid rgba(255, 255, 255, 0.5);
+}
+
+#scorekeeper .marker img {
+    object-fit: contain;
+    height: calc(100% - 4px);
+    margin-top: 2px;
+    background: black;
+}
+
+#scorekeeper .marker.unset {
+    background-color: #111;
+}
+
+#scorekeeper .marker.correct {
+    background-color: green;
+}
+
+#scorekeeper .marker.incorrect {
+    background-color: red;
+}
+
+body, html {
+    width: 100%;
+    height: 100%;
+    padding: 0;
+    margin: 0;
+}
+
+body {
+    background-color: #080808;
+    color: white;
+    font-family: DTM-Mono;
+    text-align: center;
+
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    padding: 10px;
+    box-sizing: border-box;
+}
+
+h1 {
+    letter-spacing: 1pt;
+}
+
+p {
+    width: 40em;
+}
+
+a {
+    color: yellow;
+    text-decoration: none;
+    border-bottom: 1px dotted yellow;
+}