Up-in-the-Air – commitdiff

You can use Git to clone the repository via the web URL. Download snapshot (zip)
Add encrypted easter egg feathers
authorJulian Fietkau <git@fietkau.software>
Sun, 29 Sep 2024 02:28:52 +0000 (04:28 +0200)
committerJulian Fietkau <git@fietkau.software>
Sun, 29 Sep 2024 02:28:52 +0000 (04:28 +0200)
main.js

diff --git a/main.js b/main.js
index 98e831289920e67005a152bb8ad38f63c5c09d41..af7ae9ddb288b819212dc6084c8979159fa7f2ba 100644 (file)
--- a/main.js
+++ b/main.js
@@ -129,12 +129,15 @@ function loadAllAssets(game, renderProgressCallback) {
       'textures/house-evening-2.png': 597,
       'textures/house-evening-3.png': 646,
     };
-    for(let unlockable of ['golden', 'ghost']) {
-      if(game.settings['unlocks'].includes(unlockable)) {
-        if(unlockable == 'golden') {
-          todoList['textures/feather-' + unlockable + '.png'] = 1027;
-        } else {
-          todoList['textures/feather-' + unlockable + '.png'] = 1023;
+    for(let unlockable of game.settings['unlocks']) {
+      if(unlockable == 'golden') {
+        todoList['textures/feather-golden.png'] = 1027;
+      } else if(unlockable == 'ghost') {
+        todoList['textures/feather-ghost.png'] = 1023;
+      } else {
+        let unlock = unlockWithKey(game, 'NIbp2kW5' + unlockable + 'e2ZDFl5Y');
+        if(unlock && unlock['type'] == 'feather') {
+          todoList['data:textures/feather-' + unlock['name']] = unlock['url'];
         }
       }
     }
@@ -151,7 +154,7 @@ function loadAllAssets(game, renderProgressCallback) {
       todoList['audio/sound5-' + theme + '.ogg'] = audioThemes[theme][5];
       game.assets.audiothemes.push(theme);
     }
-    let total = Object.keys(todoList).map(k => todoList[k]).reduce((a, b) => a + b, 0);
+    let total = Object.keys(todoList).filter(k => !k.startsWith('data:')).map(k => todoList[k]).reduce((a, b) => a + b, 0);
     let progress = {};
     const loader = {
       'audio': new THREE.AudioLoader(),
@@ -159,15 +162,24 @@ function loadAllAssets(game, renderProgressCallback) {
       'textures': new THREE.TextureLoader(),
     };
     for(let todo in todoList) {
-      progress[todo] = 0;
+      let isDataUri = todo.startsWith('data:');
+      if(isDataUri) {
+        todo = todo.slice(5);
+      } else {
+        progress[todo] = 0;
+      }
       let segments = todo.split('/');
-      if(!game.assets.hasOwnProperty(segments[0])) {
+      if(!(segments[0] in game.assets)) {
         game.assets[segments[0]] = {};
       }
       if(!(segments[0] in loader)) {
         reject('Unsupported resource: ' + todo);
       }
-      loader[segments[0]].load(todo, (result) => {
+      let url = todo;
+      if(isDataUri) {
+        url = todoList['data:' + todo];
+      }
+      loader[segments[0]].load(url, (result) => {
         if(segments[0] == 'textures') {
           result.colorSpace = THREE.SRGBColorSpace;
           result.minFilter = THREE.NearestFilter;
@@ -182,14 +194,18 @@ function loadAllAssets(game, renderProgressCallback) {
           }
         }
         game.assets[segments[0]][segments[1].split('.')[0]] = result;
-        progress[todo] = todoList[todo];
-        if(renderProgressCallback) {
-          renderProgressCallback(Object.keys(progress).map(k => progress[k]).reduce((a, b) => a + b, 0) / total);
+        if(todo in progress) {
+          progress[todo] = todoList[todo];
+          if(renderProgressCallback) {
+            renderProgressCallback(Object.keys(progress).map(k => progress[k]).reduce((a, b) => a + b, 0) / total);
+          }
         }
       }, (xhr) => {
-        progress[todo] = xhr.loaded;
-        if(renderProgressCallback) {
-          renderProgressCallback(Object.keys(progress).map(k => progress[k]).reduce((a, b) => a + b, 0) / total);
+        if(todo in progress) {
+          progress[todo] = xhr.loaded;
+          if(renderProgressCallback) {
+            renderProgressCallback(Object.keys(progress).map(k => progress[k]).reduce((a, b) => a + b, 0) / total);
+          }
         }
       }, (err) => {
         reject('Error while loading ' + todo + ': ' + err);
@@ -211,7 +227,7 @@ function applyForceToFeather(game, vector) {
   game.objects.feather.speed.add(vector);
 }
 
-function init(game, canvas) {
+function initializeGame(game, canvas) {
 
   game.timeProgress = 0;
   game.timeTotal = 258;
@@ -483,7 +499,7 @@ function init(game, canvas) {
   });
   document.body.addEventListener('touchend', e => {
     if(e.target.closest('.ui-container') && game.settings['controls'] != 'mouse' && ['gameplay', 'openingcutscene', 'endingcutscene'].includes(game.ui.currentPage)) {
-      game.ui.moveToPage('pause', true);
+      moveToPage(game, 'pause', true);
       e.preventDefault();
     }
     if(game.settings['controls'] == 'touchpad' || game.settings['controls'] == 'thumbstick') {
@@ -814,7 +830,7 @@ function animate(game, scene) {
         });
         game.view.music.offset = 0;
         game.view.music.play();
-        game.ui.moveToPage('gameplay', true);
+        moveToPage(game, 'gameplay', true);
       }
     } else if(game.ui.currentPage == 'endingcutscene') {
       cameraSwayFactor = cameraSwayFactor * game.timeProgress / 8;
@@ -863,7 +879,7 @@ function animate(game, scene) {
       }
       if(game.timeProgress >= 8) {
         game.ui.root.querySelector('.ui-page.title').classList.add('end');
-        game.ui.moveToPage('outro', true);
+        moveToPage(game, 'outro', true);
       }
     } else if(game.ui.reachedEnd) {
       cameraX = 5;
@@ -883,7 +899,7 @@ function animate(game, scene) {
     game.var.endingEntryRotation.y = game.objects.feather.rotation.y;
     game.var.endingEntryRotation.z = game.objects.feather.rotation.z;
     game.var.endingExitRotation.set(-0.2, 0, -0.2);
-    game.ui.moveToPage('endingcutscene', true);
+    moveToPage(game, 'endingcutscene', true);
   }
 
   if(game.settings['audio']['music'] > 0.0 && game.view.music && !game.view.music.isPlaying) {
@@ -1114,6 +1130,12 @@ function loadSettings(game) {
   if(audioThemeRadio) {
     audioThemeRadio.checked = true;
   }
+  // Custom hash function that ensures our unlockables get stored in the same order,
+  // regardless of the order in which they get unlocked.
+  let miniHash = (input) => {
+    return 4 * input.charCodeAt(0) + 0.1 * input.charCodeAt(1) + 3 * input.charCodeAt(2) + 2 * input.charCodeAt(3);
+  }
+  settings['unlocks'].sort((u1, u2) => miniHash(u1) > miniHash(u2));
   for(let unlockedFeather of settings['unlocks']) {
     if(!game.ui.root.querySelector('.ui-page.options .feather input[value="' + unlockedFeather + '"]')) {
       let radio = document.createElement('input');
@@ -1121,7 +1143,16 @@ function loadSettings(game) {
       radio.name = 'upInTheAirGame-feather';
       radio.value = unlockedFeather;
       let img = document.createElement('img');
-      img.src = 'textures/feather-' + unlockedFeather + '.png';
+      if(unlockedFeather == 'golden' || unlockedFeather == 'ghost') {
+        img.src = 'textures/feather-' + unlockedFeather + '.png';
+      } else {
+        let unlock = unlockWithKey(game, 'NIbp2kW5' + unlockedFeather + 'e2ZDFl5Y');
+        if(unlock && unlock['type'] == 'feather') {
+          img.src = unlock['url'];
+        } else {
+          continue;
+        }
+      }
       img.alt = unlockedFeather[0].toUpperCase() + unlockedFeather.slice(1) + ' feather';
       let label = document.createElement('label');
       label.appendChild(radio);
@@ -1229,7 +1260,7 @@ function applySettings(game) {
       elem.value = value;
       elem.parentNode.nextElementSibling.innerText = value;
     });
-    if(audioCategory == 'music' && game.view) {
+    if(audioCategory == 'music' && game.view && game.view.music) {
       game.view.music.setVolume(game.settings['audio'][audioCategory]);
     }
   }
@@ -1553,7 +1584,7 @@ function unlockFeather(game, feather, url) {
   // Custom hash function that ensures our unlockables get stored in the same order,
   // regardless of the order in which they get unlocked.
   let miniHash = (input) => {
-    return 4 * input.charCodeAt(0) + 3 * input.charCodeAt(2) + 2 * input.charCodeAt(3);
+    return 4 * input.charCodeAt(0) + 0.1 * input.charCodeAt(1) + 3 * input.charCodeAt(2) + 2 * input.charCodeAt(3);
   }
   game.settings['unlocks'].sort((u1, u2) => miniHash(u1) > miniHash(u2));
   let insertAfterFeather = 'purple';
@@ -1564,6 +1595,10 @@ function unlockFeather(game, feather, url) {
   radio.type = 'radio';
   radio.name = 'upInTheAirGame-feather';
   radio.value = feather;
+  radio.addEventListener('change', () => {
+    applySettings(game);
+    createFeather(game);
+  });
   let img = document.createElement('img');
   img.src = url;
   img.alt = feather[0].toUpperCase() + feather.slice(1) + ' feather';
@@ -1580,117 +1615,7 @@ function unlockFeather(game, feather, url) {
   return true;
 }
 
-window['game'] = {
-  state: 'loadingAssets',
-  ui: {
-    root: document.querySelector('.game-upintheair .ui-container'),
-    gamepads: [],
-  },
-  settings: {},
-};
-loadSettings(window['game']);
-applySettings(window['game']);
-
-game.ui.root.querySelectorAll('button.goto').forEach((btn) => {
-  btn.addEventListener('click', (e) => {
-    if(!game.view.music) {
-      game.view.audioListener = new THREE.AudioListener();
-      game.view.camera.add(game.view.audioListener);
-      game.view.music = new THREE.Audio(game.view.audioListener);
-      game.view.music.setBuffer(game.assets['audio']['music-' + game.settings['audio']['theme']]);
-      game.view.music.setVolume(game.settings['audio']['music']);
-      game.view.windSound = new THREE.Audio(game.view.audioListener);
-      game.view.windSound.setBuffer(game.assets['audio']['wind']);
-      game.view.windSound.setVolume(game.settings['audio']['sounds']);
-      game.view.windSound.setLoop(false);
-    }
-    let btn = e.target.closest('button');
-    let target = Array.from(btn.classList).filter(c => c != 'goto')[0];
-    if(target == 'previous') {
-      target = game.ui.previousPage;
-    }
-    game.ui.moveToPage(target);
-  });
-});
-game.ui.root.querySelectorAll('.options .controls input, .options .graphics input, .options .feather input, .options .accessibility input, .options .accessibility select').forEach((elem) => {
-  elem.addEventListener('change', () => {
-    applySettings(game);
-    if(elem.name == 'upInTheAirGame-controls') {
-      game.ui.root.querySelector('.controls .leftside').style.display = (['touchpad', 'thumbstick'].includes(game.settings['controls'])) ? 'block' : 'none';
-      game.ui.root.querySelectorAll('.options .controls p span:not(.' + game.settings['controls'] + ')').forEach(span => span.style.display = 'none');
-      game.ui.root.querySelector('.options .controls span.' + game.settings['controls']).style.display = 'block';
-    } else if(elem.value == 'highcontrast' || elem.name == 'upInTheAirGame-graphics') {
-      createMeshes(game);
-    } else if(elem.name == 'upInTheAirGame-feather') {
-      createFeather(game);
-    }
-  });
-});
-game.ui.root.querySelectorAll('.ui-page .audio input[type=range]').forEach((elem) => {
-  elem.addEventListener('input', (e) => {
-    let audioCategory = Array.from(e.target.classList).filter(v => ['music', 'sounds'].includes(v))[0];
-    game.ui.root.querySelector('.ui-page.options .audio input[type=range].' + audioCategory).value = e.target.value;
-    applySettings(game);
-  });
-});
-game.ui.root.querySelectorAll('.options .audio button').forEach((btn) => {
-  btn.addEventListener('click', (e) => {
-    if(e.target.classList.contains('music')) {
-      if(game.view.music.isPlaying) {
-        game.view.music.stop();
-        if(game.view.music.timeoutID) {
-          clearTimeout(game.view.music.timeoutID);
-          delete game.view.music.timeoutID;
-        }
-      } else {
-        game.view.music.offset = 36;
-        game.view.music.play();
-        game.view.music.timeoutID = setTimeout(() => {
-          game.view.music.stop();
-        }, 6000);
-      }
-    } else if(e.target.classList.contains('sounds')) {
-      playRandomSound(game);
-    }
-  });
-});
-game.ui.root.querySelectorAll('.options .keyboard label button').forEach((btn) => {
-  btn.addEventListener('click', () => {
-    if(game.ui.root.querySelector('.ui-page.keyboard-modal')) {
-      return;
-    }
-    const keyboardModal = document.createElement('div');
-    keyboardModal.classList.add('ui-page', 'keyboard-modal');
-    const instruction = document.createElement('span');
-    const direction = btn.classList[0];
-    keyboardModal.classList.add(direction);
-    instruction.innerText = 'Please press the key for “' + direction[0].toUpperCase() + direction.slice(1) + '”';
-    keyboardModal.appendChild(instruction);
-    game.ui.root.appendChild(keyboardModal);
-  });
-});
-game.ui.root.querySelector('.options .keyboard button[value="reset"]').addEventListener('click', (e) => {
-  const container = e.target.parentNode;
-  container.querySelector('button.up').value = 'ArrowUp|w';
-  container.querySelector('button.right').value = 'ArrowRight|d';
-  container.querySelector('button.down').value = 'ArrowDown|s';
-  container.querySelector('button.left').value = 'ArrowLeft|a';
-  applySettings(game);
-  loadSettings(game);
-});
-game.ui.root.querySelectorAll('.ui-page .areatabs button').forEach((btn) => {
-  btn.addEventListener('click', (e) => {
-    btn.parentNode.querySelectorAll('button').forEach((otherBtn) => {
-      otherBtn.classList.remove('active');
-      let val = otherBtn.classList[0];
-      otherBtn.closest('.ui-page').querySelector('div.' + val).style.display = 'none';
-    });
-    btn.classList.add('active');
-    let val = Array.from(btn.classList).filter(c => c != 'active')[0];
-    btn.closest('.ui-page').querySelector('div.' + val).style.display = 'flex';
-  });
-});
-game.ui.moveToPage = (target, skipFade = false) => {
+function moveToPage(game, target, skipFade = false) {
   let fadeDuration = 250;
   if(skipFade) {
     fadeDuration = 0;
@@ -1710,6 +1635,9 @@ game.ui.moveToPage = (target, skipFade = false) => {
       game.ui.reachedStart = true;
     }, fadeDuration);
   }
+  if(target == 'title') {
+    game.cheatBuffer = '';
+  }
   if(target == 'title' && game.ui.root.querySelector('.ui-page.gameplay p')) {
     game.ui.root.querySelectorAll('.ui-page.gameplay p').forEach(elem => elem.remove());
   }
@@ -1881,7 +1809,207 @@ game.ui.moveToPage = (target, skipFade = false) => {
   if(game.view) {
     game.startTime = game.view.clock.getElapsedTime();
   }
+}
+
+function unlockWithKey(game, input) {
+  input = 'aBYPmb2xCwF2ilfD'+ input + 'PNHFwI2zKZejUv6c';
+  let hash = (input) => {
+    // Adapted with appreciation from bryc:
+    // https://github.com/bryc/code/blob/master/jshash/experimental/cyrb53.js
+    let h1 = 0xdeadbeef, h2 = 0x41c6ce57;
+    for(let i = 0, ch; i < input.length; i++) {
+      ch = input.charCodeAt(i);
+      h1 = Math.imul(h1 ^ ch, 2654435761);
+      h2 = Math.imul(h2 ^ ch, 1597334677);
+    }
+    h1  = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
+    h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
+    h2  = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
+    h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
+    return (h2 >>> 0).toString(16).padStart(8, 0) + (h1 >>> 0).toString(16).padStart(8, 0);
+  }
+  for(let unlockable of game.unlockables) {
+    if(unlockable.accessKey == hash(input)) {
+      let key = hash('UZx2jWen9w5jm0FB' + input + '7DZpEq4OOwv2kiJ1');
+      let seed = parseInt(key.slice(12), 16);
+      let prng = () => {
+        seed |= 0;
+        seed = seed + 0x9e3779b9 | 0;
+        let t = seed ^ seed >>> 16;
+        t = Math.imul(t, 0x21f0aaad);
+        t = t ^ t >>> 15;
+        t = Math.imul(t, 0x735a2d97);
+        return ((t = t ^ t >>> 15) >>> 0);
+      };
+      let data = Uint8Array.from(atob(unlockable.payload), (c) => c.codePointAt(0));
+      let pad;
+      for(let i = 0; i < data.length; i++) {
+        if(i % 4 == 0) {
+          pad = prng();
+          pad = [pad % 256, (pad >> 8) % 256, (pad >> 16) % 256, (pad >> 24) % 256];
+        }
+        data[i] = data[i] ^ pad[i % 4];
+      }
+      data = new TextDecoder().decode(data);
+      let result = JSON.parse(data);
+      if(result['type'] == 'redirect') {
+        return unlockWithKey(game, result['target']);
+      } else {
+        return result;
+      }
+    }
+  }
+  return null;
 };
+
+window['game'] = {
+  state: 'loadingAssets',
+  ui: {
+    root: document.querySelector('.game-upintheair .ui-container'),
+    gamepads: [],
+  },
+  settings: {},
+  // If you're looking at the source code and the following seems scary, don't worry, it's just a few
+  // unlockable feather textures. I liked easter eggs and cheat codes when I was young, and I didn't
+  // want these to be trivially bypassable for people who can read the code.
+  unlockables: [
+    {
+      'accessKey': '5b32eb7ad08488f4',
+      'payload': 'k4JPu3sWfEhcgleieVGghixKSI10qdRRC5tAl39Tzy1U7Rx9EEEYbLx9wCcxAf7wC8r9mJZCOj8bNa7grMbUmTeCeWWPAg==',
+    },
+    {
+      'accessKey': '6273da5894b2dd8b',
+      'payload': 'XAFLhAjcTzsWgGb7DAMCKwHXjoyUg/yxkIUyLcV/7PpktgW3MHtXhh4OkVeANr52RfdbwfVpgO4dxuyYPaFZ4x4JDmI=',
+    },
+    {
+      'accessKey': 'fb8b533451a6dd68',
+      'payload': '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',
+    },
+    {
+      'accessKey': '3c78e25c7b1106b6',
+      'payload': 'dnbO28tXI2i2+o+etuRc0/Cfxd4UpdG1IRgqB0fTwJE1xCoK+TtB/JVGTquaD/stnIkKiA==',
+    },
+    {
+      'accessKey': '5f79141f861a146a',
+      'payload': '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',
+    },
+    {
+      'accessKey': 'c2cfaaebb10f00ab',
+      'payload': 'CAVH/4AwEJzNt950XEAZP3Q92fMNasIje6K5FSBgjqchkxmrFxjlNHjadnrHWdqM+zrB',
+    },
+    {
+      'accessKey': '130b037dadbe1d7a',
+      'payload': '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',
+    },
+    {
+      'accessKey': 'd8e8dd84f4b0c103',
+      'payload': 'EGKsJYSjVVaxCBWPRUGjWuLMl3k7fB/7uKYp8wz28r/5XTaOJF7LnbPMpBwysAR8IR/whArG',
+    },
+  ],
+};
+
+loadSettings(window['game']);
+applySettings(window['game']);
+
+game.ui.root.querySelectorAll('button.goto').forEach((btn) => {
+  btn.addEventListener('click', (e) => {
+    if(!game.view.music) {
+      game.view.audioListener = new THREE.AudioListener();
+      game.view.camera.add(game.view.audioListener);
+      game.view.music = new THREE.Audio(game.view.audioListener);
+      game.view.music.setBuffer(game.assets['audio']['music-' + game.settings['audio']['theme']]);
+      game.view.music.setVolume(game.settings['audio']['music']);
+      game.view.windSound = new THREE.Audio(game.view.audioListener);
+      game.view.windSound.setBuffer(game.assets['audio']['wind']);
+      game.view.windSound.setVolume(game.settings['audio']['sounds']);
+      game.view.windSound.setLoop(false);
+    }
+    let btn = e.target.closest('button');
+    let target = Array.from(btn.classList).filter(c => c != 'goto')[0];
+    if(target == 'previous') {
+      target = game.ui.previousPage;
+    }
+    moveToPage(game, target);
+  });
+});
+game.ui.root.querySelectorAll('.options .controls input, .options .graphics input, .options .feather input, .options .accessibility input, .options .accessibility select').forEach((elem) => {
+  elem.addEventListener('change', () => {
+    applySettings(game);
+    if(elem.name == 'upInTheAirGame-controls') {
+      game.ui.root.querySelector('.controls .leftside').style.display = (['touchpad', 'thumbstick'].includes(game.settings['controls'])) ? 'block' : 'none';
+      game.ui.root.querySelectorAll('.options .controls p span:not(.' + game.settings['controls'] + ')').forEach(span => span.style.display = 'none');
+      game.ui.root.querySelector('.options .controls span.' + game.settings['controls']).style.display = 'block';
+    } else if(elem.value == 'highcontrast' || elem.name == 'upInTheAirGame-graphics') {
+      createMeshes(game);
+    } else if(elem.name == 'upInTheAirGame-feather') {
+      createFeather(game);
+    }
+  });
+});
+game.ui.root.querySelectorAll('.ui-page .audio input[type=range]').forEach((elem) => {
+  elem.addEventListener('input', (e) => {
+    let audioCategory = Array.from(e.target.classList).filter(v => ['music', 'sounds'].includes(v))[0];
+    game.ui.root.querySelector('.ui-page.options .audio input[type=range].' + audioCategory).value = e.target.value;
+    applySettings(game);
+  });
+});
+game.ui.root.querySelectorAll('.options .audio button').forEach((btn) => {
+  btn.addEventListener('click', (e) => {
+    if(e.target.classList.contains('music')) {
+      if(game.view.music.isPlaying) {
+        game.view.music.stop();
+        if(game.view.music.timeoutID) {
+          clearTimeout(game.view.music.timeoutID);
+          delete game.view.music.timeoutID;
+        }
+      } else {
+        game.view.music.offset = 36;
+        game.view.music.play();
+        game.view.music.timeoutID = setTimeout(() => {
+          game.view.music.stop();
+        }, 6000);
+      }
+    } else if(e.target.classList.contains('sounds')) {
+      playRandomSound(game);
+    }
+  });
+});
+game.ui.root.querySelectorAll('.options .keyboard label button').forEach((btn) => {
+  btn.addEventListener('click', () => {
+    if(game.ui.root.querySelector('.ui-page.keyboard-modal')) {
+      return;
+    }
+    const keyboardModal = document.createElement('div');
+    keyboardModal.classList.add('ui-page', 'keyboard-modal');
+    const instruction = document.createElement('span');
+    const direction = btn.classList[0];
+    keyboardModal.classList.add(direction);
+    instruction.innerText = 'Please press the key for “' + direction[0].toUpperCase() + direction.slice(1) + '”';
+    keyboardModal.appendChild(instruction);
+    game.ui.root.appendChild(keyboardModal);
+  });
+});
+game.ui.root.querySelector('.options .keyboard button[value="reset"]').addEventListener('click', (e) => {
+  const container = e.target.parentNode;
+  container.querySelector('button.up').value = 'ArrowUp|w';
+  container.querySelector('button.right').value = 'ArrowRight|d';
+  container.querySelector('button.down').value = 'ArrowDown|s';
+  container.querySelector('button.left').value = 'ArrowLeft|a';
+  applySettings(game);
+  loadSettings(game);
+});
+game.ui.root.querySelectorAll('.ui-page .areatabs button').forEach((btn) => {
+  btn.addEventListener('click', (e) => {
+    btn.parentNode.querySelectorAll('button').forEach((otherBtn) => {
+      otherBtn.classList.remove('active');
+      let val = otherBtn.classList[0];
+      otherBtn.closest('.ui-page').querySelector('div.' + val).style.display = 'none';
+    });
+    btn.classList.add('active');
+    let val = Array.from(btn.classList).filter(c => c != 'active')[0];
+    btn.closest('.ui-page').querySelector('div.' + val).style.display = 'flex';
+  });
+});
 game.ui.root.style.fontSize = (game.ui.root.clientWidth / 50) + 'px';
 window.addEventListener('resize', () => {
   game.ui.root.style.fontSize = (game.ui.root.clientWidth / 50) + 'px';
@@ -1892,12 +2020,12 @@ window.addEventListener('scroll', () => {
   }
   let bbox = game.ui.root.querySelector('canvas').getBoundingClientRect();
   if(bbox.bottom < -100 || bbox.top - bbox.height > 100 || bbox.left + bbox.width < -100 || bbox.left - window.innerWidth > 100) {
-    game.ui.moveToPage('pause', true);
+    moveToPage(game, 'pause', true);
   }
 });
 window.addEventListener('blur', () => {
   if(['gameplay', 'openingcutscene', 'endingcutscene'].includes(game.ui.currentPage)) {
-    game.ui.moveToPage('pause', true);
+    moveToPage(game, 'pause', true);
   }
 });
 document.addEventListener('keydown', (e) => {
@@ -1914,11 +2042,26 @@ document.addEventListener('keydown', (e) => {
     e.stopPropagation();
     return;
   }
+  if(game.ui.currentPage == 'title' && e.key.match(/[a-z]/)) {
+    game.cheatBuffer = (game.cheatBuffer + e.key).slice(-25);
+    for(let len = 10; len <= 25; len++) {
+      if(game.cheatBuffer.length < len) {
+        break;
+      }
+      let unlock = unlockWithKey(game, game.cheatBuffer.slice(-len));
+      if(unlock && unlock['type'] == 'feather' && !game.settings['unlocks'].includes(unlock['name'])) {
+        playRandomSound(game);
+        unlockFeather(game, unlock['name'], unlock['url']);
+        moveToPage(game, 'unlock');
+      }
+    }
+    return;
+  }
   if(e.key == 'Escape') {
     if(['gameplay', 'openingcutscene', 'endingcutscene'].includes(game.ui.currentPage)) {
-      game.ui.moveToPage('pause', true);
+      moveToPage(game, 'pause', true);
     } else if(game.ui.currentPage == 'pause') {
-      game.ui.moveToPage(game.ui.previousPage, true);
+      moveToPage(game, game.ui.previousPage, true);
     }
   }
 });
@@ -1933,7 +2076,8 @@ window.addEventListener('gamepaddisconnected', (e) => {
 game.ui.root.querySelector('.ui-page.pause button.title').addEventListener('click', () => {
   reset(game);
 });
-loadAllAssets(game, (progress) => {
+
+loadAllAssets(window['game'], (progress) => {
   let percentage = Math.floor(100 * progress);
   game.ui.root.querySelector('.ui-page.loading progress').value = percentage;
   game.ui.root.querySelector('.ui-page.loading span').innerText = percentage;
@@ -1961,8 +2105,8 @@ loadAllAssets(game, (progress) => {
     snippet.childNodes[1].textContent = ' ' + audioTheme[0].toUpperCase() + audioTheme.slice(1);
     container.appendChild(snippet);
   }
-  game.ui.moveToPage('title');
-  init(window['game'], game.ui.root.querySelector('canvas'));
+  moveToPage(game, 'title');
+  initializeGame(window['game'], game.ui.root.querySelector('canvas'));
 }, (err) => {
   console.error(err);
 });