Up-in-the-Air – commitdiff

You can use Git to clone the repository via the web URL. Download snapshot (zip)
Add proper outro screen with sentences
authorJulian Fietkau <git@fietkau.software>
Sat, 28 Sep 2024 18:48:39 +0000 (20:48 +0200)
committerJulian Fietkau <git@fietkau.software>
Sat, 28 Sep 2024 18:48:39 +0000 (20:48 +0200)
index.html
main.js

index 4b9840ad19a01df120f880963c4dbb69546b6993..423819c604840a7e6c720b2185433ff46d877a24 100644 (file)
     top: -99999px;
     opacity: 0;
   }
-  .ui-page > button {
+  .ui-page > button, .ui-page.outro .area button {
     padding: 0;
-    width: 8em;
+    min-width: 8em;
     color: #000;
     font-family: Cookie;
     font-size: 2.5em;
     border-image: url('textures/button-standard.png') 20 / .75em round;
     border-image-outset: .2em;
   }
-  .ui-page > button:hover {
+  .ui-page > button:hover, .ui-page.outro .area button:hover {
     background: #e9ce8a;
     border-image-source: url('textures/button-hover.png');
   }
-  .ui-page > button:active {
+  .ui-page > button:active, .ui-page.outro .area button:active {
     background: #f9c8d5;
     border-image-source: url('textures/button-pressed.png');
   }
     font-size: 1em;
     text-shadow: -.15em -.15em 0 #fff, .15em -.15em 0 #fff, .15em .15em 0 #fff, -.15em .15em 0 #fff, 0 -.19em 0 #fff, .19em 0 0 #fff, 0 .19em 0 #fff, -.19em 0 0 #fff;
   }
+  .font-atkinson .ui-page.gameplay p, .font-opendyslexic .ui-page.gameplay p {
+    font-size-adjust: 1;
+    line-height: 2.5;
+  }
   .ui-page.credits {
     padding: 1em;
     display: flex;
     align-items: center;
     gap: 1em;
   }
+  .ui-page.outro .area {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    font-size: 2em;
+    text-align: center;
+    gap: 1em;
+  }
+  .ui-page.outro .area div.examples {
+    box-sizing: border-box;
+    width: 100%;
+    border-radius: 1em;
+    padding: 2em;
+    background: #ece3d5;
+    display: flex;
+    flex-direction: column;
+    gap: 1.5em;
+    color: #000;
+    font-family: Cookie;
+    font-size: .3em;
+    line-height: 3em;
+  }
+  .font-atkinson .ui-page.outro .area div.examples,
+  .font-opendyslexic .ui-page.outro .area div.examples {
+    padding: .5em;
+    gap: 0.5em;
+    font-size: 1em;
+    line-height: 1em;
+  }
+  .font-opendyslexic .ui-page.outro .area > p {
+    font-size: 0.9em;
+  }
+  .ui-page.outro .area strong {
+    font-weight: normal;
+    color: #d53c59;
+  }
+  .ui-page.outro .area div.examples strong {
+    font-weight: normal;
+    color: #c50031;
+  }
+  .ui-page.outro .area > p:first-child {
+    display: flex;
+    align-items: center;
+    gap: 1ex;
+  }
+  .ui-page.outro .count {
+    font-size: 2em;
+    color: #d53c59;
+  }
+  .ui-page.outro .area button {
+    width: max-content;
+    min-width: unset;
+    margin: 0;
+    padding: 0 .5em;
+    border-width: .3em;
+    font-size: 1em;
+  }
   .ui-page.pause {
     background: #000d;
     padding: 1em;
 <h4>Engine</h4>
 <p><a href="https://threejs.org/" target="_blank">three.js</a> v169 by mrdoob and contributors</p>
 <h4>Inspiration</h4>
-<p>Game concept inspired by: “<a href="https://www.ferryhalim.com/orisinal/g3/high.htm" target="_blank">High Delivery</a>” created by <a href="https://www.ferryhalim.com/" target="_blank">Ferry Halim</a></p>
+<p>Game concept inspired by: “<a href="https://www.ferryhalim.com/orisinal/g3/high.htm" target="_blank">High Delivery</a>” by <a href="https://www.ferryhalim.com/" target="_blank">Ferry Halim</a></p>
 <p class="seealso">See <a href="README.txt" target="_blank">README.txt</a> for detailed licensing information.</p>
 </div>
 <button class="goto title">Back</button>
 <div class="ui-page openingcutscene"></div>
 <div class="ui-page endingcutscene"></div>
 <div class="ui-page outro">
+<div class="area outro">
+<p>You collected <span class="count">0</span> <span class="optionalPlural">words.</span></p>
+<p class="rating"></p>
+<p class="examples">Here are a few sentences using some of them:</p>
+<div class="examples">
+</div>
+<button class="examples">More</button>
+<p>Can you think of anyone to whom you might need to write something along those lines? When was the last time your feelings were left <strong>up in the air</strong>?</p>
+</div>
 <button class="goto title">Return to Title Screen</button>
 </div>
 <div class="ui-page pause">
diff --git a/main.js b/main.js
index 44e6ab7087b6b532c2d2ddc369c3a495ea1b5af2..2dce5f1a434fd988f7caf81371d5862c25c51088 100644 (file)
--- a/main.js
+++ b/main.js
@@ -4,10 +4,6 @@ import * as THREE from 'three';
 import { FontLoader } from 'three/addons/loaders/FontLoader.js';
 import { TextGeometry } from 'three/addons/geometries/TextGeometry.js';
 
-function getRandomWord(game) {
-  return game.assets.words[Math.floor(Math.random() * game.assets.words.length)];
-}
-
 function playRandomSound(game) {
   if(!game.view || !game.view.audioListener) {
     return;
@@ -42,9 +38,58 @@ function lerp(start, end, progress) {
 
 function loadAllAssets(game, renderProgressCallback) {
   game.assets = {};
-  game.assets.words = [
-    "Alpaca", "Bat", "Butterfly", "Deer", "Duck", "Elk", "Giraffe", "Horse",
-    "Lynx", "Llama", "Owl", "Ocelot", "Pig", "Sheep", "Tapir", "Walrus",
+  game.assets.words = {
+    'thanks': ['thank you', 'thanks'],
+    'sorry': ['sorry', 'apologize'],
+    'emotion': ['blessed', 'fortunate', 'glad', 'happy', 'joyous', 'lucky', 'overjoyed', 'thankful'],
+    'verb_general': ['adore', 'appreciate', 'cherish', 'enjoy', 'like', 'love', 'treasure', 'value'],
+    'verb_person': ['admire', 'honor', 'love', 'respect', 'treasure', 'value'],
+    'trait': ['amazing', 'compassionate', 'delightful', 'genuine', 'generous', 'incredible', 'joyful', 'kind', 'passionate', 'patient', 'principled', 'refreshing', 'sweet'],
+  };
+  game.assets.wordList = [...new Set([].concat.apply([], Object.values(game.assets.words)))]; // no need to be sorted
+  game.assets.sentences = [
+    '{thanks} for always listening.',
+    '{thanks} for being there.',
+    '{thanks} for helping me when I needed it most.',
+    '{thanks} for being with me.',
+    '{thanks} for believing in me.',
+    '{thanks} for not giving up.',
+    '{thanks} for believing in me when I myself couldn’t.',
+    '{thanks} for standing by my side.',
+    '{sorry} for what I said.',
+    '{sorry} for not being there.',
+    '{sorry} for forgetting.',
+    '{sorry} for not telling you.',
+    '{sorry} for what I did.',
+    '{sorry} for back then.',
+    '{sorry} for not being honest.',
+    'Just being around you makes me feel {emotion}.',
+    'I have no words for how {emotion} you make me feel.',
+    'I always feel {emotion} in your presence.',
+    'I’m honestly {emotion}.',
+    'I feel {emotion} just for knowing you.',
+    'Every moment with you makes me feel {emotion}.',
+    'I {verb_person} you.',
+    'I {verb_person} you more than anything.',
+    'I deeply {verb_person} you.',
+    'I honestly {verb_person} you.',
+    'I really do {verb_person} you.',
+    'I {verb_general} every moment with you.',
+    'I {verb_general} the way you see the world.',
+    'I {verb_general} you the way you are.',
+    'I {verb_general} how {trait} you are.',
+    'I {verb_general} how {trait} you are.',
+    'I {verb_general} how {trait} you are.',
+    'I always {verb_general} how {trait} you are.',
+    'I deeply {verb_general} how {trait} you are.',
+    'Thinking about how {trait} you are always improves my mood.',
+    'You are the most {trait} person I know.',
+    'You are the most {trait} person I know.',
+    'Your {trait} personality always makes my day.',
+    'Your {trait} personality is my sunshine.',
+    'Your {trait} personality gives me strength.',
+    'I’m astonished how {trait} you are.',
+    'I hope I can learn to be as {trait} as you.',
   ];
   return new Promise((resolve, reject) => {
     let todoList = {
@@ -527,6 +572,7 @@ function reset(game) {
   game.objects.words.collectedCount = 0;
   const interWordDistance = new THREE.Vector3();
   let placementSuccess;
+  let wordList = [];
   for(let i = 0; i < 100; i++) {
     let angleInCourse;
     let clusteringFunction = (val) => Math.max(0.15 + val / 2, 0.7 - 3 * Math.pow(0.75 - 2 * val, 2), 1 - 3 * Math.pow(1 - val, 2));
@@ -540,7 +586,11 @@ function reset(game) {
     let randomCameraX = game.courseRadius * Math.sin(angleInCourse * 2 * Math.PI);
     let randomCameraY = game.courseRadius * -Math.cos(angleInCourse * 2 * Math.PI);
     let word = new THREE.Group();
-    word.text = getRandomWord(game);
+    if(wordList.length == 0) {
+      wordList.push(...game.assets.wordList);
+    }
+    let wordIndex = Math.floor(Math.random() * wordList.length);
+    word.text = wordList.splice(wordIndex, 1)[0];
     prepareWordMesh(game, word);
     word.randomAnimOffset = Math.random();
     const vFOV = THREE.MathUtils.degToRad(game.view.camera.fov);
@@ -1357,7 +1407,7 @@ function createMeshes(game) {
     });
     if(game.settings['graphics'] <= 2) {
       game.assets.fonts.geometry = {};
-      for(let letter of [...new Set(game.assets.words.join(''))]) {
+      for(let letter of [...new Set(game.assets.wordList.join(''))]) {
         if(game.settings['graphics'] == 1) {
           game.assets.fonts.geometry[letter] = new TextGeometry(letter, {
             font: game.assets.fonts.cookie,
@@ -1423,7 +1473,7 @@ function createMeshes(game) {
       minimalLetterGeometry.dx = 0;
       minimalLetterGeometry.dy = 0;
       game.assets.fonts.geometry = {};
-      for(let letter of [...new Set(game.assets.words.join(''))]) {
+      for(let letter of [...new Set(game.assets.wordList.join(''))]) {
         game.assets.fonts.geometry[letter] = minimalLetterGeometry;
       }
       for(let r = 0; r < game.courseRadius / 3; r++) {
@@ -1461,7 +1511,7 @@ function createMeshes(game) {
     highcontrastLetterGeometry.dx = 0;
     highcontrastLetterGeometry.dy = 0;
     game.assets.fonts.geometry = {};
-    for(let letter of [...new Set(game.assets.words.join(''))]) {
+    for(let letter of [...new Set(game.assets.wordList.join(''))]) {
       game.assets.fonts.geometry[letter] = highcontrastLetterGeometry;
     }
     const highContrastBackdropMaterial = new THREE.MeshBasicMaterial({
@@ -1621,6 +1671,89 @@ game.ui.moveToPage = (target, skipFade = false) => {
       delete game.view.music.timeoutID;
     }
   }
+  if(target == 'outro') {
+    if(game.view.music.isPlaying) {
+      game.view.music.stop();
+    }
+    let collectedWords = game.objects.words.filter(w => w.collected).map(w => w.text);
+    game.ui.root.querySelector('.ui-page.outro .count').innerText = game.objects.words.collectedCount;
+    game.ui.root.querySelector('.ui-page.outro .optionalPlural').innerText = 'word' + ((game.objects.words.collectedCount == 1) ? '' : 's') + '.';
+    let ratingElem = game.ui.root.querySelector('.ui-page.outro .rating');
+    let exampleElems = game.ui.root.querySelectorAll('.ui-page.outro .examples');
+    ratingElem.style.display = 'none';
+    exampleElems.forEach(elem => { elem.style.display = 'none'; });
+    if(game.objects.words.collectedCount > 0) {
+      if(game.objects.words.collectedCount == 100) {
+        ratingElem.style.display = 'block';
+        ratingElem.innerText = 'Wow, you managed to collect all of them. Congratulations!';
+      } else {
+        let generateExampleSentences = (wordList) => {
+          let container = game.ui.root.querySelector('.ui-page.outro div.examples');
+          while(container.children.length > 0) {
+            container.children[0].remove();
+          }
+          let words = {};
+          for(let category of Object.keys(game.assets.words)) {
+            words[category] = [];
+            for(let word of game.assets.words[category]) {
+              if(wordList.includes(word)) {
+                words[category].push(word);
+              }
+            }
+          }
+          let result = [];
+          let failedAttempts = 0;
+          while(result.length < 3 && failedAttempts < 1000) {
+            let sentence = game.assets.sentences[Math.floor(Math.random() * game.assets.sentences.length)];
+            while(sentence.indexOf('{') > -1) {
+              let areWeStuck = true;
+              for(let category of Object.keys(words)) {
+                if(sentence.includes('{' + category + '}')) {
+                  if(words[category].length == 0) {
+                    break;
+                  }
+                  let choice = words[category][Math.floor(Math.random() * words[category].length)];
+                  if(category == 'sorry') {
+                    if(choice == 'sorry') {
+                      sentence = sentence.replace('{sorry}', 'I’m {sorry}');
+                    }
+                    if(choice == 'apologize') {
+                      sentence = sentence.replace('{sorry}', 'I {sorry}');
+                    }
+                  }
+                  if(sentence.indexOf('{' + category + '}') == 0) {
+                    choice = choice[0].toUpperCase() + choice.slice(1);
+                  }
+                  sentence = sentence.replace('{' + category + '}', '<strong>' + choice + '</strong>');
+                  words[category].splice(words[category].indexOf(choice), 1);
+                  areWeStuck = false;
+                }
+              }
+              if(areWeStuck) {
+                break;
+              }
+            }
+            if(sentence.indexOf('{') == -1 && !result.includes(sentence)) {
+              result.push(sentence);
+              failedAttempts = 0;
+            }
+            failedAttempts += 1;
+          }
+          for(let sentence of result) {
+            let elem = document.createElement('p');
+            elem.innerHTML = sentence;
+            container.appendChild(elem);
+          }
+        };
+        generateExampleSentences(collectedWords);
+        game.ui.root.querySelector('.ui-page.outro button.examples').addEventListener('click', () => { generateExampleSentences(collectedWords); });
+        exampleElems.forEach(elem => { elem.style.display = 'flex'; });
+      }
+    } else {
+      ratingElem.style.display = 'block';
+      ratingElem.innerText = 'You completed the course while dodging every word. That’s an achievement on its own. Respect!';
+    }
+  }
   if(target == 'options') {
     game.ui.root.querySelectorAll('.options .areatabs button').forEach((btn) => {
       if(btn.classList.contains('general')) {