« get me outta code hell

guess-the-character - Unnamed repository; edit this file 'description' to name the repository.
summary refs log tree commit diff
path: root/main.js
diff options
context:
space:
mode:
Diffstat (limited to 'main.js')
-rw-r--r--main.js419
1 files changed, 419 insertions, 0 deletions
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
+    ));
+});