Up-in-the-Air – commitdiff

You can use Git to clone the repository via the web URL. Download snapshot (zip)
Big code structure refactor to ease embedding and hotpatching
authorJulian Fietkau <git@fietkau.software>
Mon, 30 Sep 2024 16:17:39 +0000 (18:17 +0200)
committerJulian Fietkau <git@fietkau.software>
Mon, 30 Sep 2024 16:17:39 +0000 (18:17 +0200)
index.html
main.js

index 0e6ac7e4db91f005a181f9d3b8cfa8e61b70a5f8..5ea97690103e340f21932caa88d6a21dbf0e1017 100644 (file)
 </div>
 <script type="module" src="main.js"></script>
 <script>
+function start() {
+  // Wait until game script has finished loading
+  if(window.startUpInTheAirGame) {
+    window['game'] = {};
+    startUpInTheAirGame(window['game']);
+  } else {
+    setTimeout(start, 50);
+  }
+}
+
 if(!['https:', 'http:'].includes(window.location.protocol)) {
   document.querySelectorAll('.upInTheAirGame .ui-page:not(.loading)').forEach(page => page.remove());
   document.querySelector('.upInTheAirGame .loading div').innerText = 'This game cannot run without a web server.';
+} else {
+  start();
 }
 </script>
 </body>
diff --git a/main.js b/main.js
index 694365d212bb53723d1ac62969e23796e2eae08f..8f8dd85f5401497a2d188a0b4073700b0e603cec 100644 (file)
--- a/main.js
+++ b/main.js
@@ -29,7 +29,12 @@ import * as THREE from 'three';
 import { FontLoader } from 'three/addons/loaders/FontLoader.js';
 import { TextGeometry } from 'three/addons/geometries/TextGeometry.js';
 
-function playRandomSound(game) {
+
+window['startUpInTheAirGame'] = (game) => {
+
+game['fn'] = {};
+
+game['fn'].playRandomSound = () => {
   if(!game.view || !game.view.audioListener) {
     return;
   }
@@ -53,15 +58,15 @@ function playRandomSound(game) {
   }
 }
 
-function easeInOut(val) {
+game['fn'].easeInOut = (val) => {
   return -0.5 * Math.cos(val * Math.PI) + 0.5;
 }
 
-function lerp(start, end, progress) {
+game['fn'].lerp = (start, end, progress) => {
   return (1.0 - progress) * start + progress * end;
 }
 
-function loadAllAssets(game, renderProgressCallback) {
+game['fn'].loadAllAssets = (renderProgressCallback) => {
   game.assets = {};
   game.assets.words = {
     'thanks': ['thank you', 'thanks'],
@@ -160,7 +165,7 @@ function loadAllAssets(game, renderProgressCallback) {
       } else if(unlockable == 'ghost') {
         todoList['textures/feather-ghost.png'] = 1023;
       } else {
-        let unlock = unlockWithKey(game, 'NIbp2kW5' + unlockable + 'e2ZDFl5Y');
+        let unlock = game['fn'].unlockWithKey('NIbp2kW5' + unlockable + 'e2ZDFl5Y');
         if(unlock && unlock['type'] == 'feather') {
           todoList['data:textures/feather-' + unlock['name']] = unlock['url'];
         }
@@ -248,12 +253,11 @@ function loadAllAssets(game, renderProgressCallback) {
   });
 }
 
-function applyForceToFeather(game, vector) {
+game['fn'].applyForceToFeather = (vector) => {
   game.objects.feather.speed.add(vector);
 }
 
-function initializeGame(game, canvas) {
-
+game['fn'].initializeGame = (canvas) => {
   game.timeProgress = 0;
   game.timeTotal = 258;
   game.courseRadius = 50;
@@ -340,9 +344,9 @@ function initializeGame(game, canvas) {
   game.view.camera.position.set(-5, -game.courseRadius, game.view.camera.position.z);
   game.view.scene = scene;
 
-  createMeshes(game);
-  createFeather(game);
-  reset(game);
+  game['fn'].createMeshes();
+  game['fn'].createFeather();
+  game['fn'].reset();
 
   function pinwheelPositionUpdate(game, viewportX, viewportY) {
     const vFOV = THREE.MathUtils.degToRad(game.view.camera.fov);
@@ -525,7 +529,7 @@ function initializeGame(game, canvas) {
   });
   document.body.addEventListener('touchend', e => {
     if(e.target.closest('.ui-container') && game.settings['controls'] != 'mouse' && ['gameplay', 'openingcutscene', 'endingcutscene'].includes(game.ui.currentPage)) {
-      moveToPage(game, 'pause', true);
+      game['fn'].moveToPage('pause', true);
       e.preventDefault();
     }
     if(game.settings['controls'] == 'touchpad' || game.settings['controls'] == 'thumbstick') {
@@ -553,10 +557,10 @@ function initializeGame(game, canvas) {
   game.var.endingExitTrajectory = new THREE.Vector3();
   game.var.endingEntryRotation = new THREE.Vector3();
   game.var.endingExitRotation = new THREE.Vector3();
-  game.view.renderer.setAnimationLoop(() => { animate(game, scene); });
+  game.view.renderer.setAnimationLoop(() => { game['fn'].animate(scene); });
 }
 
-function prepareWordMesh(game, word) {
+game['fn'].prepareWordMesh = (word) => {
   while(word.children.length > 0) {
     word.remove(word.children[0]);
   }
@@ -581,7 +585,7 @@ function prepareWordMesh(game, word) {
   }
 }
 
-function reset(game) {
+game['fn'].reset = () => {
   game.controls = {};
   game.controls.positionX = 0;
   game.controls.positionY = 0;
@@ -633,7 +637,7 @@ function reset(game) {
     }
     let wordIndex = Math.floor(Math.random() * wordList.length);
     word.text = wordList.splice(wordIndex, 1)[0];
-    prepareWordMesh(game, word);
+    game['fn'].prepareWordMesh(word);
     word.randomAnimOffset = Math.random();
     const vFOV = THREE.MathUtils.degToRad(game.view.camera.fov);
     let attempts = 0;
@@ -664,7 +668,7 @@ function reset(game) {
   }
 }
 
-function animate(game, scene) {
+game['fn'].animate = ( scene) => {
   if(!('startTime' in game)) {
     game.startTime = game.view.clock.getElapsedTime();
   }
@@ -764,7 +768,7 @@ function animate(game, scene) {
           });
           game.ui.root.querySelector('.ui-page.title .footer').style.opacity = '0';
           game.ui.root.querySelector('.ui-page.title .system-buttons').style.opacity = '0';
-          cameraX += Math.max(0.0, 1 - easeInOut(0.5 + game.timeProgress / 2));
+          cameraX += Math.max(0.0, 1 - game['fn'].easeInOut(0.5 + game.timeProgress / 2));
           cameraY += Math.max(0.0, 10 * Math.pow(0.5 - game.timeProgress / 2, 2));
         } else if(game.timeProgress >= 1.0 && game.timeProgress <= 2.1) {
           game.ui.root.querySelector('.ui-page.title h1').style.opacity = Math.min(1.0, game.timeProgress - 1.0).toFixed(2);
@@ -772,8 +776,8 @@ function animate(game, scene) {
         if(game.timeProgress >= 1.5 && game.timeProgress <= 3.0) {
           game.ui.root.querySelectorAll('.ui-page.title > button').forEach((btn) => {
             let timeOffset = Array.from(btn.parentNode.children).indexOf(btn) - 2;
-            btn.style.left = 10 * easeInOut(Math.max(0.0, Math.min(1.0, 0.3 * timeOffset + 2.5 - game.timeProgress))).toFixed(2) + 'em';
-            let opacity = easeInOut(Math.max(0.0, Math.min(1.0, -0.3 * timeOffset + game.timeProgress - 1.5)));
+            btn.style.left = 10 * game['fn'].easeInOut(Math.max(0.0, Math.min(1.0, 0.3 * timeOffset + 2.5 - game.timeProgress))).toFixed(2) + 'em';
+            let opacity = game['fn'].easeInOut(Math.max(0.0, Math.min(1.0, -0.3 * timeOffset + game.timeProgress - 1.5)));
             btn.style.opacity = opacity.toFixed(2);
             if(opacity == 1.0) {
               btn.disabled = false;
@@ -781,8 +785,8 @@ function animate(game, scene) {
           });
         }
         if(game.timeProgress >= 3.0 && game.timeProgress <= 4.0) {
-          game.ui.root.querySelector('.ui-page.title .footer').style.opacity = easeInOut(Math.max(0.0, Math.min(1.0, game.timeProgress - 3.0))).toFixed(2);
-          game.ui.root.querySelector('.ui-page.title .system-buttons').style.opacity = easeInOut(Math.max(0.0, Math.min(1.0, game.timeProgress - 3.0))).toFixed(2);
+          game.ui.root.querySelector('.ui-page.title .footer').style.opacity = game['fn'].easeInOut(Math.max(0.0, Math.min(1.0, game.timeProgress - 3.0))).toFixed(2);
+          game.ui.root.querySelector('.ui-page.title .system-buttons').style.opacity = game['fn'].easeInOut(Math.max(0.0, Math.min(1.0, game.timeProgress - 3.0))).toFixed(2);
         }
         if(game.timeProgress > 4.0 && !game.ui.reachedStart) {
           game.ui.root.querySelector('.ui-page.title h1').removeAttribute('style');
@@ -794,7 +798,7 @@ function animate(game, scene) {
       }
     } else if(game.ui.currentPage == 'openingcutscene') {
       if(game.ui.reachedEnd) {
-        reset(game);
+        game['fn'].reset();
       }
       if(game.timeProgress < 0.1 && !game.view.windSound.isPlaying) {
         game.view.windSound.stop();
@@ -834,7 +838,7 @@ function animate(game, scene) {
       if(game.timeProgress >= 3.0) {
         let windStrength = Math.max(0.5 * (1 + Math.cos((game.timeProgress - 4.0) * Math.PI)), Math.min(2 * (game.timeProgress - 4.2), 1.2));
         game.objects.feather.position.x = cameraX - 8.45 + 0.15 * windStrength + 13.45 * Math.min(1.0, Math.max(0.0, 1 - Math.pow(Math.max(0.0, 6.0 - game.timeProgress), 2)));
-        game.objects.feather.position.y = -game.courseRadius - 6.4 + 6.4 * easeInOut(Math.min(1,Math.max(0, game.timeProgress - 5.0) / 2));
+        game.objects.feather.position.y = -game.courseRadius - 6.4 + 6.4 * game['fn'].easeInOut(Math.min(1,Math.max(0, game.timeProgress - 5.0) / 2));
         game.objects.feather.position.y += (Math.cos((Math.max(5.0, Math.min(8.0, game.timeProgress)) - 5.0) * 2 * Math.PI / 3) - 1) * 2 * Math.sin(game.timeProgress * 2);
         game.objects.feather.position.z = -9.9 + 9.9 * Math.min(1.0, Math.max(0.0, 1 - Math.pow(Math.max(0.0, 6.0 - game.timeProgress), 2)));
         game.objects.feather.rotation.z = Math.PI / 2.1 - 0.2 * windStrength + 1.45 * Math.max(0.0, game.timeProgress - 5.0);
@@ -853,7 +857,7 @@ function animate(game, scene) {
         game.controls.positionX = 0;
         game.controls.positionY = -3;
       }
-      game.objects.pinwheel.material[4].opacity = easeInOut(Math.max(0, (game.timeProgress - 7)));
+      game.objects.pinwheel.material[4].opacity = game['fn'].easeInOut(Math.max(0, (game.timeProgress - 7)));
       if(game.timeProgress >= 8.0) {
         game.ui.root.querySelectorAll('.ui-page.gameplay p').forEach((elem) => {
           let opacity = Math.max(0.0, Math.min(1.0, 8.0 - game.timeProgress));
@@ -863,23 +867,23 @@ function animate(game, scene) {
         if(!game.view.muted) {
           game.view.music.play();
         }
-        moveToPage(game, 'gameplay', true);
+        game['fn'].moveToPage('gameplay', true);
       }
     } else if(game.ui.currentPage == 'endingcutscene') {
       cameraSwayFactor = cameraSwayFactor * game.timeProgress / 8;
       cameraX = 5 - Math.pow(Math.max(0, 5 - game.timeProgress) / 5, 1.6) * 5;
-      let trajectoryLerpValue = easeInOut(Math.min(6.0, game.timeProgress) / 6);
+      let trajectoryLerpValue = game['fn'].easeInOut(Math.min(6.0, game.timeProgress) / 6);
       game.var.endingEntryTrajectory.addScaledVector(game.objects.feather.speed, delta);
-      game.var.endingExitTrajectory.setX(11.2 * easeInOut(Math.min(1, 1 - Math.max(0, 4 - game.timeProgress) / 4)));
-      game.var.endingExitTrajectory.setY(-game.courseRadius - 7.6 * easeInOut(Math.min(1, 1 - Math.max(0, 4 - game.timeProgress) / 4)));
-      game.var.endingExitTrajectory.setZ(-8.9 * easeInOut(Math.min(1, 1 - Math.max(0, 4 - game.timeProgress) / 4)));
-      game.objects.feather.rotation.x = lerp(game.var.endingEntryRotation.x, game.var.endingExitRotation.x, trajectoryLerpValue);
-      game.objects.feather.rotation.y = lerp(game.var.endingEntryRotation.y, game.var.endingExitRotation.y, trajectoryLerpValue);
-      game.objects.feather.rotation.z = lerp(game.var.endingEntryRotation.z, game.var.endingExitRotation.z, trajectoryLerpValue);
+      game.var.endingExitTrajectory.setX(11.2 * game['fn'].easeInOut(Math.min(1, 1 - Math.max(0, 4 - game.timeProgress) / 4)));
+      game.var.endingExitTrajectory.setY(-game.courseRadius - 7.6 * game['fn'].easeInOut(Math.min(1, 1 - Math.max(0, 4 - game.timeProgress) / 4)));
+      game.var.endingExitTrajectory.setZ(-8.9 * game['fn'].easeInOut(Math.min(1, 1 - Math.max(0, 4 - game.timeProgress) / 4)));
+      game.objects.feather.rotation.x = game['fn'].lerp(game.var.endingEntryRotation.x, game.var.endingExitRotation.x, trajectoryLerpValue);
+      game.objects.feather.rotation.y = game['fn'].lerp(game.var.endingEntryRotation.y, game.var.endingExitRotation.y, trajectoryLerpValue);
+      game.objects.feather.rotation.z = game['fn'].lerp(game.var.endingEntryRotation.z, game.var.endingExitRotation.z, trajectoryLerpValue);
       game.objects.feather.position.lerpVectors(game.var.endingEntryTrajectory, game.var.endingExitTrajectory, trajectoryLerpValue);
-      game.objects.pinwheel.material[4].opacity = easeInOut(Math.max(0, (1 - game.timeProgress)));
+      game.objects.pinwheel.material[4].opacity = game['fn'].easeInOut(Math.max(0, (1 - game.timeProgress)));
       if(!game.settings['highcontrast']) {
-        let letterScale = lerp(0.3, 0.0, easeInOut(Math.max(0, Math.min(1, game.timeProgress - 6))));
+        let letterScale = game['fn'].lerp(0.3, 0.0, game['fn'].easeInOut(Math.max(0, Math.min(1, game.timeProgress - 6))));
         for(let i = 0; i < game.objects.words.length; i++) {
           let word = game.objects.words[i];
           if(!word.collected) {
@@ -912,7 +916,7 @@ function animate(game, scene) {
       }
       if(game.timeProgress >= 8) {
         game.ui.root.querySelector('.ui-page.title').classList.add('end');
-        moveToPage(game, 'outro', true);
+        game['fn'].moveToPage('outro', true);
       }
     } else if(game.ui.reachedEnd) {
       cameraX = 5;
@@ -932,7 +936,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);
-    moveToPage(game, 'endingcutscene', true);
+    game['fn'].moveToPage('endingcutscene', true);
   }
 
   if(game.settings['audio']['music'] > 0.0 && game.view.music && !game.view.music.isPlaying) {
@@ -952,7 +956,7 @@ function animate(game, scene) {
   if(!game.settings['highcontrast']) {
     let sunsetValue = 2.0;
     if(!game.ui.reachedEnd) {
-      sunsetValue = sunsetValue * easeInOut(Math.min(1, Math.max(0, ((game.timeProgress / game.timeTotal) - 0.3) / 0.6)));
+      sunsetValue = sunsetValue * game['fn'].easeInOut(Math.min(1, Math.max(0, ((game.timeProgress / game.timeTotal) - 0.3) / 0.6)));
     }
     if(game.settings['graphics'] <= 2) {
       for(let i = 0; i < 6; i++) {
@@ -988,12 +992,12 @@ function animate(game, scene) {
       game.var.featherBorderForce.setComponent(coord, 3 * Math.sign(game.var.featherLocalPos.getComponent(coord)) - game.var.featherLocalPos.getComponent(coord));
     }
   }
-  applyForceToFeather(game, game.var.featherBorderForce);
+  game['fn'].applyForceToFeather(game.var.featherBorderForce);
   const tiltedGravity = game.gravity.clone();
   game.var.pinwheelDistance.subVectors(game.objects.feather.position, game.objects.pinwheel.position).setZ(0);
 
   const pinwheelForce = 0.5 * Math.max(0, Math.pow(game.var.pinwheelDistance.length(), - 0.5) - 0.5);
-  applyForceToFeather(game, game.var.pinwheelDistance.normalize().multiplyScalar(pinwheelForce));
+  game['fn'].applyForceToFeather(game.var.pinwheelDistance.normalize().multiplyScalar(pinwheelForce));
   if(pinwheelForce < 0.2) {
     if(game.objects.feather.swayDirection > 0 && game.objects.feather.speed.x > 1.5) {
       game.objects.feather.swayDirection *= -1;
@@ -1003,7 +1007,7 @@ function animate(game, scene) {
     tiltedGravity.x += game.objects.feather.swayDirection;
   }
   if(game.objects.feather.speed.y > -1) {
-    applyForceToFeather(game, tiltedGravity);
+    game['fn'].applyForceToFeather(tiltedGravity);
   }
   game.objects.feather.rotation.z = -0.1 * game.objects.feather.speed.x * game.settings['difficulty']['speed'] / 100;
   game.objects.feather.position.addScaledVector(game.objects.feather.speed, delta * game.settings['difficulty']['speed'] / 100);
@@ -1029,7 +1033,7 @@ function animate(game, scene) {
 
   let collectedScale = 0.0;
   if(!game.settings['highcontrast'] && game.settings['graphics'] <= 2) {
-    collectedScale = lerp(0.6, 0.3, 1 - Math.pow(1 - game.objects.words.collectedCount / game.objects.words.length, 2));
+    collectedScale = game['fn'].lerp(0.6, 0.3, 1 - Math.pow(1 - game.objects.words.collectedCount / game.objects.words.length, 2));
   } else if(!game.settings['highcontrast'] && game.settings['graphics'] == 3) {
     collectedScale = 0.3;
   }
@@ -1039,7 +1043,7 @@ function animate(game, scene) {
     if(!word.collected && new THREE.Vector3().subVectors(word.position, game.objects.feather.position).length() < collectingRadius) {
       word.collected = game.view.clock.getElapsedTime();
       game.objects.words.collectedCount += 1;
-      playRandomSound(game);
+      game['fn'].playRandomSound();
     }
     if(word.parent != game.view.scene) {
       // All that happens in here is the positional animation for the word, which
@@ -1083,9 +1087,9 @@ function animate(game, scene) {
         game.var.collectedPos.set(x, y, z);
       }
       if(game.var.notCollectedPos.length() > 0 && game.var.collectedPos.length() > 0) {
-        let collectingProgress = easeInOut(Math.max(0.0, Math.min(1.0, (game.view.clock.getElapsedTime() - word.collected) / collectionAnimationDuration)));
+        let collectingProgress = game['fn'].easeInOut(Math.max(0.0, Math.min(1.0, (game.view.clock.getElapsedTime() - word.collected) / collectionAnimationDuration)));
         letter.position.lerpVectors(game.var.notCollectedPos, game.var.collectedPos, collectingProgress);
-        let scale = lerp(1.0, collectedScale, collectingProgress);
+        let scale = game['fn'].lerp(1.0, collectedScale, collectingProgress);
         letter.scale.set(scale, scale, scale);
       } else if(game.var.notCollectedPos.length() > 0) {
         letter.position.set(game.var.notCollectedPos.x, game.var.notCollectedPos.y, game.var.notCollectedPos.z);
@@ -1107,7 +1111,7 @@ function animate(game, scene) {
   game.view.renderer.render(scene, game.view.camera);
 }
 
-function loadSettings(game) {
+game['fn'].loadSettings = () => {
   let settings = {
     'controls': null, // set during first time launch depending on device
     'virtualinputleft': false,
@@ -1183,7 +1187,7 @@ function loadSettings(game) {
       if(unlockedFeather == 'golden' || unlockedFeather == 'ghost') {
         img.src = 'textures/feather-' + unlockedFeather + '.png';
       } else {
-        let unlock = unlockWithKey(game, 'NIbp2kW5' + unlockedFeather + 'e2ZDFl5Y');
+        let unlock = game['fn'].unlockWithKey('NIbp2kW5' + unlockedFeather + 'e2ZDFl5Y');
         if(unlock && unlock['type'] == 'feather') {
           img.src = unlock['url'];
         } else {
@@ -1228,7 +1232,7 @@ function loadSettings(game) {
   game.settings = settings;
 }
 
-function applySettings(game) {
+game['fn'].applySettings = () => {
   const ui = game.ui.root.querySelector('.ui-page.options');
   if(ui.querySelector('input[name="upInTheAirGame-controls"]:checked')) {
     game.settings['controls'] = ui.querySelector('input[name="upInTheAirGame-controls"]:checked').value;
@@ -1309,7 +1313,7 @@ function applySettings(game) {
   }
 }
 
-function createFeather(game) {
+game['fn'].createFeather = () => {
   let position, rotation;
   if(game.objects.feather) {
     position = game.objects.feather.position;
@@ -1352,7 +1356,7 @@ function createFeather(game) {
   game.objects.feather.twistSpeed = 0.1;
 }
 
-function createMeshes(game) {
+game['fn'].createMeshes = () => {
   if(game.objects.clouds && game.objects.clouds.parent == game.view.scene) {
     game.view.scene.remove(game.objects.clouds);
     if(game.objects.clouds.children.length > 0) {
@@ -1594,13 +1598,13 @@ function createMeshes(game) {
   }
   if(game.objects.words) {
     for(let word of game.objects.words) {
-      prepareWordMesh(game, word);
+      game['fn'].prepareWordMesh(word);
     }
   }
   game.view.scene.add(game.objects.backdrop);
 }
 
-function unlockFeather(game, feather, url) {
+game['fn'].unlockFeather = (feather, url) => {
   if(game.settings['unlocks'].includes(feather)) {
     return false;
   }
@@ -1635,8 +1639,8 @@ function unlockFeather(game, feather, url) {
   radio.name = 'upInTheAirGame-feather';
   radio.value = feather;
   radio.addEventListener('change', () => {
-    applySettings(game);
-    createFeather(game);
+    game['fn'].applySettings();
+    game['fn'].createFeather();
   });
   let img = document.createElement('img');
   img.src = url;
@@ -1645,7 +1649,7 @@ function unlockFeather(game, feather, url) {
   label.appendChild(radio);
   label.appendChild(img);
   game.ui.root.querySelector('.ui-page.options .feather input[value="' + insertAfterFeather + '"]').parentNode.after(label);
-  applySettings(game);
+  game['fn'].applySettings();
   let ui = game.ui.root.querySelector('.ui-page.unlock');
   let img2 = ui.querySelector('img');
   img2.src = img.src;
@@ -1654,7 +1658,7 @@ function unlockFeather(game, feather, url) {
   return true;
 }
 
-function moveToPage(game, target, skipFade = false) {
+game['fn'].moveToPage = (target, skipFade = false) => {
   let fadeDuration = 250;
   if(skipFade) {
     fadeDuration = 0;
@@ -1679,7 +1683,7 @@ function moveToPage(game, target, skipFade = false) {
     game.ui.cheatBuffer = '';
   }
   if(target == 'title' && (!game.ui.currentPage || ['loading', 'controls'].includes(game.ui.currentPage))) {
-    initializeGame(game, game.ui.root.querySelector('canvas'));
+    game['fn'].initializeGame(game.ui.root.querySelector('canvas'));
   }
   if(target == 'title' && game.ui.root.querySelector('.ui-page.gameplay p')) {
     game.ui.root.querySelectorAll('.ui-page.gameplay p').forEach(elem => elem.remove());
@@ -1711,9 +1715,9 @@ function moveToPage(game, target, skipFade = false) {
       finalParagraph.style.display = 'none';
       let neededUnlocking = false;
       if(game.objects.words.collectedCount == 100) {
-        neededUnlocking = unlockFeather(game, 'golden');
+        neededUnlocking = game['fn'].unlockFeather('golden');
       } else {
-        neededUnlocking = unlockFeather(game, 'ghost');
+        neededUnlocking = game['fn'].unlockFeather('ghost');
       }
       if(neededUnlocking) {
         returnButton.innerText = 'Continue';
@@ -1861,7 +1865,7 @@ function moveToPage(game, target, skipFade = false) {
   }
 }
 
-function unlockWithKey(game, input) {
+game['fn'].unlockWithKey = (input) => {
   input = 'aBYPmb2xCwF2ilfD'+ input + 'PNHFwI2zKZejUv6c';
   let hash = (input) => {
     // Adapted with appreciation from bryc:
@@ -1903,7 +1907,7 @@ function unlockWithKey(game, input) {
       data = new TextDecoder().decode(data);
       let result = JSON.parse(data);
       if(result['type'] == 'redirect') {
-        return unlockWithKey(game, result['target']);
+        return game['fn'].unlockWithKey(result['target']);
       } else {
         return result;
       }
@@ -1912,17 +1916,16 @@ function unlockWithKey(game, input) {
   return null;
 };
 
-window['game'] = {
-  state: 'loadingAssets',
-  ui: {
+game['fn'].start = () => {
+  game.ui = {
     root: document.querySelector('.upInTheAirGame .ui-container'),
     gamepads: [],
-  },
-  settings: {},
+  };
+  game.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: [
+  game.unlockables = [
     {
       'accessKey': '5b32eb7ad08488f4',
       'payload': 'k4JPu3sWfEhcgleieVGghixKSI10qdRRC5tAl39Tzy1U7Rx9EEEYbLx9wCcxAf7wC8r9mJZCOj8bNa7grMbUmTeCeWWPAg==',
@@ -1955,250 +1958,287 @@ window['game'] = {
       'accessKey': 'd8e8dd84f4b0c103',
       'payload': 'EGKsJYSjVVaxCBWPRUGjWuLMl3k7fB/7uKYp8wz28r/5XTaOJF7LnbPMpBwysAR8IR/whArG',
     },
-  ],
-};
+  ];
 
-loadSettings(window['game']);
-applySettings(window['game']);
-
-if(game.ui.root.querySelectorAll('.ui-page.credits .area h3').length > 3) {
-  // If the credits have more than three third-level headers, that means we are
-  // in the freeware version and can make the CSS adjustments it needs.
-  let css = document.styleSheets[0];
-  css.insertRule('.upInTheAirGame .ui-page.credits .person { position: relative; height: 4em; padding-left: calc(4em + 1ex); display: flex; flex-direction: column; justify-content: center; }');
-  css.insertRule('.upInTheAirGame .ui-page.credits .person::before { content: " "; position: absolute; left: 0; box-sizing: border-box; width: 4em; height: 4em; background-size: contain; border-radius: .6em; border: .1em solid #d53c59; }');
-  game.ui.root.querySelectorAll('.ui-page.credits .area .person').forEach((person) => {
-    let personName = Array.from(person.classList).filter(c => c != 'person')[0];
-    let imageFormat = (personName == 'nina') ? 'png' : 'jpg';
-    css.insertRule('.upInTheAirGame .ui-page.credits .person.' + personName + '::before { background-image: url("textures/person-' + personName + '.' + imageFormat + '"); }');
+  game['fn'].loadSettings();
+  game['fn'].applySettings();
+
+  if(game.ui.root.querySelectorAll('.ui-page.credits .area h3').length > 3) {
+    // If the credits have more than three third-level headers, that means we are
+    // in the freeware version and can make the CSS adjustments it needs.
+    let css = document.styleSheets[0];
+    css.insertRule('.upInTheAirGame .ui-page.credits .person { position: relative; height: 4em; padding-left: calc(4em + 1ex); display: flex; flex-direction: column; justify-content: center; }');
+    css.insertRule('.upInTheAirGame .ui-page.credits .person::before { content: " "; position: absolute; left: 0; box-sizing: border-box; width: 4em; height: 4em; background-size: contain; border-radius: .6em; border: .1em solid #d53c59; }');
+    game.ui.root.querySelectorAll('.ui-page.credits .area .person').forEach((person) => {
+      let personName = Array.from(person.classList).filter(c => c != 'person')[0];
+      let imageFormat = (personName == 'nina') ? 'png' : 'jpg';
+      css.insertRule('.upInTheAirGame .ui-page.credits .person.' + personName + '::before { background-image: url("textures/person-' + personName + '.' + imageFormat + '"); }');
+    });
+  }
+
+  game.ui.root.querySelectorAll('button.goto').forEach((btn) => {
+    btn.addEventListener('click', (e) => {
+      if(game.view && !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['fn'].moveToPage(target);
+    });
   });
-}
 
-game.ui.root.querySelectorAll('button.goto').forEach((btn) => {
-  btn.addEventListener('click', (e) => {
-    if(game.view && !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.root.querySelectorAll('.options .controls input, .options .graphics input, .options .feather input, .options .accessibility input, .options .accessibility select').forEach((elem) => {
+    elem.addEventListener('change', () => {
+      game['fn'].applySettings();
+      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') {
+        game['fn'].createMeshes();
+      } else if(elem.name == 'upInTheAirGame-feather') {
+        game['fn'].createFeather();
+      }
+    });
+  });
+
+  game.ui.root.querySelector('.ui-page.title .system-buttons input').addEventListener('change', (e) => {
+    game.view.muted = e.target.checked;
+  });
+
+  game.ui.root.querySelector('.ui-page.title .system-buttons button').addEventListener('click', (e) => {
+    if(document.fullscreenElement == game.ui.root.parentNode) {
+      document.exitFullscreen();
+    } else {
+      game.ui.root.parentNode.requestFullscreen();
     }
-    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;
+      game['fn'].applySettings();
+    });
+  });
+
+  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;
+          if(!game.view.muted) {
+            game.view.music.play();
+          }
+          game.view.music.timeoutID = setTimeout(() => {
+            game.view.music.stop();
+          }, 6000);
+        }
+      } else if(e.target.classList.contains('sounds')) {
+        game['fn'].playRandomSound();
+      }
+    });
+  });
+
+  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';
+    game['fn'].applySettings();
+    game['fn'].loadSettings();
+  });
+
+  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 / 48) + 'px';
+  window.addEventListener('resize', () => {
+    game.ui.root.style.fontSize = (game.ui.root.clientWidth / 48) + 'px';
+  });
+
+  window.addEventListener('scroll', () => {
+    if(['gameplay', 'openingcutscene', 'endingcutscene'].includes(game.ui.currentPage)) {
+      return;
+    }
+    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['fn'].moveToPage('pause', true);
     }
   });
-});
-game.ui.root.querySelector('.ui-page.title .system-buttons input').addEventListener('change', (e) => {
-  game.view.muted = e.target.checked;
-});
-game.ui.root.querySelector('.ui-page.title .system-buttons button').addEventListener('click', (e) => {
-  if(document.fullscreenElement == game.ui.root.parentNode) {
-    document.exitFullscreen();
-  } else {
-    game.ui.root.parentNode.requestFullscreen();
-  }
-});
-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);
+  window.addEventListener('blur', () => {
+    if(['gameplay', 'openingcutscene', 'endingcutscene'].includes(game.ui.currentPage)) {
+      game['fn'].moveToPage('pause', true);
+    }
   });
-});
-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;
+
+  document.addEventListener('keydown', (e) => {
+    const keyboardModal = game.ui.root.querySelector('.keyboard-modal');
+    if(keyboardModal) {
+      const direction = [...keyboardModal.classList].filter(c => c != 'ui-page' && c != 'keyboard-modal')[0];
+      if(e.key != 'Escape') {
+        game.ui.root.querySelector('.options .keyboard label button.' + direction).value = e.key;
+        game['fn'].applySettings();
+        game['fn'].loadSettings();
+      }
+      keyboardModal.remove();
+      e.preventDefault();
+      e.stopPropagation();
+      return;
+    }
+    if(game.ui.currentPage == 'title' && e.key.match(/[a-z]/)) {
+      game.ui.cheatBuffer = (game.ui.cheatBuffer + e.key).slice(-25);
+      for(let len = 10; len <= 25; len++) {
+        if(game.ui.cheatBuffer.length < len) {
+          break;
         }
-      } else {
-        game.view.music.offset = 36;
-        if(!game.view.muted) {
-          game.view.music.play();
+        let unlock = game['fn'].unlockWithKey(game.ui.cheatBuffer.slice(-len));
+        if(unlock && unlock['type'] == 'feather' && !game.settings['unlocks'].includes(unlock['name'])) {
+          game['fn'].playRandomSound();
+          game['fn'].unlockFeather(unlock['name'], unlock['url']);
+          game['fn'].moveToPage('unlock');
         }
-        game.view.music.timeoutID = setTimeout(() => {
-          game.view.music.stop();
-        }, 6000);
       }
-    } else if(e.target.classList.contains('sounds')) {
-      playRandomSound(game);
+      return;
+    }
+    if(e.key == 'Escape') {
+      if(['gameplay', 'openingcutscene', 'endingcutscene'].includes(game.ui.currentPage)) {
+        game['fn'].moveToPage('pause', true);
+      } else if(game.ui.currentPage == 'pause') {
+        game['fn'].moveToPage(game.ui.previousPage, true);
+      }
     }
   });
-});
-game.ui.root.querySelectorAll('.options .keyboard label button').forEach((btn) => {
-  btn.addEventListener('click', () => {
-    if(game.ui.root.querySelector('.ui-page.keyboard-modal')) {
-      return;
+
+  window.addEventListener('gamepadconnected', (e) => {
+    game.ui.gamepads.push(e.gamepad);
+  });
+  window.addEventListener('gamepaddisconnected', (e) => {
+    if(game.ui.gamepads.includes(e.gamepad)) {
+      game.ui.gamepads.splice(game.ui.gamepads.indexOf(e.gamepad), 1);
     }
-    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.querySelector('.ui-page.pause button.title').addEventListener('click', () => {
+    game['fn'].reset();
   });
-});
-game.ui.root.style.fontSize = (game.ui.root.clientWidth / 48) + 'px';
-window.addEventListener('resize', () => {
-  game.ui.root.style.fontSize = (game.ui.root.clientWidth / 48) + 'px';
-});
-window.addEventListener('scroll', () => {
-  if(['gameplay', 'openingcutscene', 'endingcutscene'].includes(game.ui.currentPage)) {
-    return;
-  }
-  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) {
-    moveToPage(game, 'pause', true);
-  }
-});
-window.addEventListener('blur', () => {
-  if(['gameplay', 'openingcutscene', 'endingcutscene'].includes(game.ui.currentPage)) {
-    moveToPage(game, 'pause', true);
-  }
-});
-document.addEventListener('keydown', (e) => {
-  const keyboardModal = game.ui.root.querySelector('.keyboard-modal');
-  if(keyboardModal) {
-    const direction = [...keyboardModal.classList].filter(c => c != 'ui-page' && c != 'keyboard-modal')[0];
-    if(e.key != 'Escape') {
-      game.ui.root.querySelector('.options .keyboard label button.' + direction).value = e.key;
-      applySettings(game);
-      loadSettings(game);
-    }
-    keyboardModal.remove();
-    e.preventDefault();
-    e.stopPropagation();
-    return;
-  }
-  if(game.ui.currentPage == 'title' && e.key.match(/[a-z]/)) {
-    game.ui.cheatBuffer = (game.ui.cheatBuffer + e.key).slice(-25);
-    for(let len = 10; len <= 25; len++) {
-      if(game.ui.cheatBuffer.length < len) {
-        break;
+
+  game['fn'].loadAllAssets((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;
+  }).then(() => {
+    if(window.location.hostname == 'fietkau.media' && window.location.pathname == '/up_in_the_air') {
+      game.ui.root.querySelector('.ui-page.title .footer span:last-child').remove();
+    }
+    let controlsInterstitial = false;
+    if(!game.settings['controls']) {
+      controlsInterstitial = true;
+      let control;
+      if(matchMedia('(hover: hover)').matches) {
+        control = 'mouse';
+      } else {
+        control = 'touchpad';
       }
-      let unlock = unlockWithKey(game, game.ui.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');
+      game.ui.root.querySelector('.controls input[value="' + control + '"]').checked = true;
+      game['fn'].applySettings();
+      game['fn'].loadSettings();
+      game.ui.root.querySelectorAll('.ui-page.controls .' + ((control == 'mouse') ? 'touchpad' : 'mouse')).forEach(elem => elem.remove());
+    }
+    if(!game.assets.audiothemes.includes(game.settings['audio']['theme'])) {
+      game.settings['audio']['theme'] = game.assets.audiothemes[0];
+    }
+    if(game.assets.audiothemes.length == 1) {
+      game.ui.root.querySelector('.ui-page.options .audiotheme').style.display = 'none';
+    }
+    let container = game.ui.root.querySelector('.ui-page.options .audiotheme');
+    for(let audioTheme of game.assets.audiothemes) {
+      let snippet = container.children[0].content.cloneNode(true).children[0];
+      snippet.children[0].value = audioTheme;
+      if(audioTheme == game.settings['audio']['theme']) {
+        snippet.children[0].checked = true;
       }
+      snippet.children[0].addEventListener('change', () => {
+        game['fn'].applySettings();
+        game.view.music.setBuffer(game.assets['audio']['music-' + game.settings['audio']['theme']]);
+      });
+      snippet.childNodes[1].textContent = ' ' + audioTheme[0].toUpperCase() + audioTheme.slice(1);
+      container.appendChild(snippet);
     }
-    return;
-  }
-  if(e.key == 'Escape') {
-    if(['gameplay', 'openingcutscene', 'endingcutscene'].includes(game.ui.currentPage)) {
-      moveToPage(game, 'pause', true);
-    } else if(game.ui.currentPage == 'pause') {
-      moveToPage(game, game.ui.previousPage, true);
-    }
-  }
-});
-window.addEventListener('gamepadconnected', (e) => {
-  game.ui.gamepads.push(e.gamepad);
-});
-window.addEventListener('gamepaddisconnected', (e) => {
-  if(game.ui.gamepads.includes(e.gamepad)) {
-    game.ui.gamepads.splice(game.ui.gamepads.indexOf(e.gamepad), 1);
-  }
-});
-game.ui.root.querySelector('.ui-page.pause button.title').addEventListener('click', () => {
-  reset(game);
-});
-
-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;
-}).then(() => {
-  if(window.location.hostname == 'fietkau.media' && window.location.pathname == '/up_in_the_air') {
-    game.ui.root.querySelector('.ui-page.title .footer span:last-child').remove();
-  }
-  let controlsInterstitial = false;
-  if(!game.settings['controls']) {
-    controlsInterstitial = true;
-    let control;
-    if(matchMedia('(hover: hover)').matches) {
-      control = 'mouse';
+    if(controlsInterstitial) {
+      game['fn'].moveToPage('controls');
     } else {
-      control = 'touchpad';
+      game['fn'].moveToPage('title');
     }
-    game.ui.root.querySelector('.controls input[value="' + control + '"]').checked = true;
-    applySettings(game);
-    loadSettings(game);
-    game.ui.root.querySelectorAll('.ui-page.controls .' + ((control == 'mouse') ? 'touchpad' : 'mouse')).forEach(elem => elem.remove());
-  }
-  if(!game.assets.audiothemes.includes(game.settings['audio']['theme'])) {
-    game.settings['audio']['theme'] = game.assets.audiothemes[0];
-  }
-  if(game.assets.audiothemes.length == 1) {
-    game.ui.root.querySelector('.ui-page.options .audiotheme').style.display = 'none';
-  }
-  let container = game.ui.root.querySelector('.ui-page.options .audiotheme');
-  for(let audioTheme of game.assets.audiothemes) {
-    let snippet = container.children[0].content.cloneNode(true).children[0];
-    snippet.children[0].value = audioTheme;
-    if(audioTheme == game.settings['audio']['theme']) {
-      snippet.children[0].checked = true;
-    }
-    snippet.children[0].addEventListener('change', () => {
-      applySettings(game);
-      game.view.music.setBuffer(game.assets['audio']['music-' + game.settings['audio']['theme']]);
-    });
-    snippet.childNodes[1].textContent = ' ' + audioTheme[0].toUpperCase() + audioTheme.slice(1);
-    container.appendChild(snippet);
-  }
-  if(controlsInterstitial) {
-    moveToPage(game, 'controls');
-  } else {
-    moveToPage(game, 'title');
-  }
-}, (err) => {
-  console.error(err);
-});
+  }, (err) => {
+    console.error(err);
+  });
+};
+
+// Set up name mirrors for each function that should survive most minifiers and transpilers
+game['fn']['animate'] = game['fn'].animate;
+game['fn']['applyForceToFeather'] = game['fn'].applyForceToFeather;
+game['fn']['applySettings'] = game['fn'].applySettings;
+game['fn']['createFeather'] = game['fn'].createFeather;
+game['fn']['createMeshes'] = game['fn'].createMeshes;
+game['fn']['easeInOut'] = game['fn'].easeInOut;
+game['fn']['initializeGame'] = game['fn'].initializeGame;
+game['fn']['lerp'] = game['fn'].lerp;
+game['fn']['loadAllAssets'] = game['fn'].loadAllAssets;
+game['fn']['loadSettings'] = game['fn'].loadSettings;
+game['fn']['moveToPage'] = game['fn'].moveToPage;
+game['fn']['playRandomSound'] = game['fn'].playRandomSound;
+game['fn']['prepareWordMesh'] = game['fn'].prepareWordMesh;
+game['fn']['reset'] = game['fn'].reset;
+game['fn']['start'] = game['fn'].start;
+game['fn']['unlockFeather'] = game['fn'].unlockFeather;
+game['fn']['unlockWithKey'] = game['fn'].unlockWithKey;
+
+game['fn'].start();
+
+}
+