Explorar el Código

adding credits UI; adding options UI; adding volume controls; adding more visible market buttons; redoing currency progress bar to be more dynamic

Justin Gilman hace 3 semanas
padre
commit
b4e1ab84d6
Se han modificado 8 ficheros con 441 adiciones y 41 borrados
  1. 183 16
      css/game.css
  2. 37 12
      game.js
  3. 72 3
      index.html
  4. 31 0
      ui/creditsui.js
  5. 2 2
      ui/gamestatusui.js
  6. 24 7
      ui/mainmenuui.js
  7. 51 1
      ui/marketui.js
  8. 41 0
      ui/optionsui.js

+ 183 - 16
css/game.css

@@ -128,6 +128,136 @@ button {
     height: 100%;
 }
 
+/**************
+ * Options UI *
+ **************/
+ #options-container {
+    position: fixed;
+    top: 0;
+    left: 0;
+    display: none;
+    width: 100%;
+    height: 100vh;
+    flex-direction: column;
+    justify-content: center;
+    cursor: default;
+ }
+
+
+ #options-panel {
+    display: flex;
+    width: 50%;
+    min-width: 320px;
+    height: 100vh;
+    min-height: 560px;
+    background-color: rgba(64, 64, 64, 0.6);
+    background: linear-gradient(to right, rgba(64, 64, 64, 0.6), 60%, rgba(64, 64, 64, 0.6), 90%, rgba(0, 0, 0, 0));
+    flex-direction: column;
+    justify-content: space-around;
+    align-items: center;
+    translate: -1024px 0;
+}
+
+#options-back-button {
+    color: white;
+    font-size: 72px;
+    position: absolute;
+    top: 0;
+    left: 0;
+    padding-left: 16px;
+}
+
+#options-back-button:hover {
+    cursor: pointer;
+}
+
+#options-header-container {
+    color: white;
+    width: 100%;
+    text-align: center;
+    padding: 16px;
+}
+
+#options-list-container {
+    color: white;
+    flex-grow: 1;
+}
+
+.option-container {
+    padding: 16px;
+    margin: 8px;
+}
+
+/**************
+ * Credits UI *
+ **************/
+ #credits-container {
+    position: fixed;
+    top: 0;
+    left: 0;
+    display: none;
+    width: 100%;
+    height: 100vh;
+    flex-direction: column;
+    justify-content: center;
+    cursor: default;
+ }
+
+
+ #credits-panel {
+    display: flex;
+    width: 50%;
+    height: 100%;
+    background-color: rgba(64, 64, 64, 0.6);
+    background: linear-gradient(to right, rgba(64, 64, 64, 0.6), 60%, rgba(64, 64, 64, 0.6), 90%, rgba(0, 0, 0, 0));
+    flex-direction: column;
+    justify-content: space-around;
+    align-items: center;
+    translate: -1024px 0;
+    position: relative;
+}
+
+#credits-back-button {
+    color: white;
+    font-size: 72px;
+    position: absolute;
+    top: 0;
+    left: 0;
+    padding-left: 16px;
+}
+
+#credits-back-button:hover {
+    cursor: pointer;
+}
+
+#credits-text {
+    width: 512px;
+    text-align: center;
+    color: white;
+    overflow-y: auto;
+    height: 100%;
+    padding: 32px;
+}
+
+.credits-section {
+    padding: 32px;
+}
+.credits-section h3 {
+    margin: 0;
+}
+.credits-section p {
+    margin: 0;
+}
+
+.credits-section a {
+    color: white;
+    text-decoration: none;
+}
+
+.credits-section a:hover {
+    text-decoration: underline;
+}
+
 /**************
  * Navigation *
  **************/
@@ -194,21 +324,21 @@ button {
 }
 
 #currency-meter-container {
-    width: 100%;
-    height: 46px;
+    width: 256px;
     background-color: var(--tan);
     border-radius: 8px;
     display: flex;
-    gap: 0;
+    justify-content: start;
+    gap: 8px;
+    padding: 12px;
 }
 
-#currency-meter-container span {
-    padding: 12px 0 12px 0;
+#currency-meter-container .meter-start,.meter-end {
     color: #7E674F;
 }
 
 #currency-meter-icon {
-    padding: 12px;
+    /* padding: 12px; */
 }
 
 #currency-progress {
@@ -216,23 +346,30 @@ button {
     height: 10px;
     border-radius: 8px;
     background-color: #dcd4ca;
-    margin: 16px 2px 12px 2px;
+    position: relative;
+    display: inline-block;
+    flex-grow: 1;
+    margin: 6px;
 }
 
-#currency-progress[value]::-webkit-progress-bar {
-    background-color: #dcd4ca;
+#currency-meter-container:hover #currency-amount {
+    opacity: 1;
 }
 
-#currency-progress:after {
-    content: attr(value);
-  }
-
-#currency-progress[value]::-moz-progress-bar {
+#currency-progress-amount {
+    width: 50%;
+    height: 10px;
+    border-radius: 8px;
     background-color: #ebac51;
 }
 
-#currency-progress[value]::-webkit-progress-value {
-    background-color: #ebac51;
+#currency-amount {
+    display: block;
+    opacity: 0;
+    text-align: right;
+    line-height: 8px;
+    margin-right: 8px;
+    transition: 0.3s opacity;
 }
 
 #current-day-container {
@@ -800,6 +937,35 @@ ul#potion-properties-list li {
     color: gray;
 }
 
+.add-ingredient-button {
+    border: none;
+    width: 40px;
+    height: 40px;
+    margin-top: 50px;
+    background-color: transparent;
+    font-size: 32px;
+    font-weight: bold;
+}
+.add-ingredient-button:hover {
+    color: gray;
+    cursor: pointer;
+}
+
+.remove-ingredient-button {
+    border: none;
+    width: 40px;
+    height: 40px;
+    margin-top: 50px;
+    background-color: transparent;
+    font-size: 32px;
+    font-weight: bold;
+}
+
+.remove-ingredient-button:hover {
+    color: gray;
+    cursor: pointer;
+}
+
 .ingredient-in-cart-container {
     display: flex;
     flex-direction: column;
@@ -814,6 +980,7 @@ ul#potion-properties-list li {
     height: 24px;
     padding: 16px;
     font-weight: bold;
+    font-size: 32px;
 }
 
 #total-container {

+ 37 - 12
game.js

@@ -30,6 +30,8 @@ import { buildCanvasText } from './library/canvastext.js'
 import { hideGameStatusUI, updateGameStatusUI } from './ui/gamestatusui.js'
 import { gameOverUI, showGameOverUI } from './ui/gameoverui.js'
 import { closeMarketUI, marketUI, openMarketUI } from './ui/marketui.js'
+import { creditsUI, hideCreditsUI } from './ui/creditsui.js'
+import { hideOptionsUI, optionsUI } from './ui/optionsui.js'
 
 export const GAME_SAVE_KEY = "spookonomics-v1"
 const raycast = new THREE.Raycaster()
@@ -111,7 +113,7 @@ async function beginBrew(game, stageData) {
 
     stageData.cauldronUniforms.uBlendTime.value = 0.0
     setTimeout(() => {
-        stageData.soundEffects['audio/bubbling.mp3'].volume(0.5)
+        
         stageData.soundEffects['audio/bubbling.mp3'].play()
         stageData.bubbleParticles.visible = true
         gsap.to(stageData.cauldronUniforms.uBlendTime, {
@@ -131,7 +133,10 @@ async function beginBrew(game, stageData) {
         stageData.selectedIngredients = []
 
         stageData.bottle1.position.set(0, -1.5, 0)
-        stageData.soundEffects['audio/bubbling.mp3'].fade(1, 0, 300)
+        stageData.soundEffects['audio/bubbling.mp3'].fade(1, 0, 300).on('fade', () => {
+            stageData.soundEffects['audio/bubbling.mp3'].stop()
+            stageData.soundEffects['audio/bubbling.mp3'].volume(0.5)
+        })
         stageData.brewWitch.stir = false
         stageData.brewWitch.bounce = false
         stageData.spoon.visible = false
@@ -294,6 +299,8 @@ export function clearSaveData(stageData) {
 
     stageData.ingredientInventory = ["mushroom", "mushroom", "pumpkin", "tomato", "tomato", "lettuce", "lettuce"]
     localStorage.setItem(`${GAME_SAVE_KEY}-ingredient-inventory`, JSON.stringify(stageData.ingredientInventory))
+
+    updateGameStatusUI(game, stageData)
     
 }
 
@@ -331,7 +338,7 @@ export function loadSaveData(stageData) {
     let ingredientInventoryString = localStorage.getItem(`${GAME_SAVE_KEY}-ingredient-inventory`)
     if (!ingredientInventoryString) {
         localStorage.setItem(`${GAME_SAVE_KEY}-ingredient-inventory`, JSON.stringify([]))
-        stageData.ingredientInventory = []
+        stageData.ingredientInventory = ["mushroom", "mushroom", "pumpkin", "tomato", "tomato", "lettuce", "lettuce"]
     } else {
         stageData.ingredientInventory = JSON.parse(ingredientInventoryString)
         stageData.ingredientInventory.sort()
@@ -412,7 +419,8 @@ export async function init(inGame) {
 
     soundEffects['audio/doorClose_4.ogg'].volume(0.5)
     soundEffects['audio/doorOpen_1.ogg'].volume(0.5)
-    stageData.soundEffects['audio/store-entrance-bell.ogg'].volume(0.5)
+    soundEffects['audio/bubbling.mp3'].volume(0.5)
+    soundEffects['audio/store-entrance-bell.ogg'].volume(0.5)
 
     let levelLoader = new LevelLoader(game)
 
@@ -481,14 +489,17 @@ export async function init(inGame) {
     candleLit.visible = false
     game.scene.add(candleLit)
 
-    stageData.cauldronUniforms = {
-        uTime: { value: 0 },
-        uPixelRatio: { value: Math.min(window.devicePixelRatio, 2) },
-        uBlendTime: { value: 0 },
-        uPotionColor: { value: new THREE.Vector3(0, 0.8, 0.2) },
-        uNextPotionColor: { value: new THREE.Vector3(0, 0.8, 0.2) },
-        uTransparency: { value: 0.8 }
-    }
+
+    stageData.cauldronUniforms = THREE.UniformsUtils.merge([
+        THREE.UniformsLib["common"],
+        THREE.UniformsLib["lights"]
+    ])
+    stageData.cauldronUniforms.uTime = { value: 0 },
+    stageData.cauldronUniforms.uPixelRatio = { value: Math.min(window.devicePixelRatio, 2) },
+    stageData.cauldronUniforms.uBlendTime = { value: 0 },
+    stageData.cauldronUniforms.uPotionColor = { value: new THREE.Vector3(0, 0.8, 0.2) },
+    stageData.cauldronUniforms.uNextPotionColor = { value: new THREE.Vector3(0, 0.8, 0.2) },
+    stageData.cauldronUniforms.uTransparency = { value: 0.8 }
 
     stageData.cauldron = await loadGltf(game, 'models/simple_cauldron.gltf.glb')
     const cauldronBell = stageData.cauldron.getObjectByName("Sphere015")
@@ -501,6 +512,7 @@ export async function init(inGame) {
         transparent: true,
         depthWrite: false,
         side: THREE.DoubleSide,
+        lights: true,
     })
     cauldronTop.material = cauldronTopMaterial
 
@@ -810,6 +822,8 @@ export async function init(inGame) {
 
 
     mainMenuUI(game, stageData)
+    optionsUI(game, stageData)
+    creditsUI(game, stageData)
     navigationUI(game, stageData)
     brewUI(game, stageData)
     shopUI(game, stageData)
@@ -824,6 +838,14 @@ export async function init(inGame) {
     stageData.buyIngredients = () => { buyIngredients(game, stageData)}
 
 
+    stageData.adjustMasterVolume = (volume) => {
+        Howler.volume(volume / 100)
+    }
+
+    stageData.adjustMusicVolume = (volume) => {
+        stageData.musicLoop.volume(volume / 100)
+    }
+
     ///////////
     // DEBUG //
     ///////////
@@ -1508,11 +1530,14 @@ export function onKeyPress(code) {
 }
 
 export function returnToMainMenu(game, stageData) {
+    Howler.stop()
     closeBrewUI(game, stageData)
     closeShopUI(game, stageData)
     hideNavigationUI(game, stageData)
     openMainMenuUI(game, stageData)
     hideGameStatusUI(game, stageData)
+    hideOptionsUI(game, stageData)
+    hideCreditsUI(game, stageData)
     stageData.currentRoom = ROOM_BREW
     game.camera.position.copy(stageData.cameraPositions[3].camera)
     game.lookAtFocus = stageData.cameraPositions[3].focus.clone()

+ 72 - 3
index.html

@@ -49,6 +49,73 @@
             </div>
         </div>
     </div>
+    <div id="options-container">
+        <div id="options-panel">
+            <div id="options-back-button">&laquo;</div>
+            <div id="options-header-container">
+                <h1>Options</h1>
+            </div>
+            <div id="options-list-container">
+                <div class="option-container">
+                    <div><label for="master-volume">Master Volume</label></div>
+                    <div><input type="range" min="0" max="100" id="master-volume" /></div>
+                </div>
+                <div class="option-container">
+                    <div><label for="music-volume">Music Volume</label></div>
+                    <div><input type="range" min="0" max="100" id="music-volume" /></div>
+                </div>
+            </div>
+        </div>
+    </div>
+    <div id="credits-container">
+        <div id="credits-panel">
+            <div id="credits-back-button">&laquo;</div>
+            <div id="credits-text">
+                <div class="credits-section">
+                    <h1>Spookonomics</h1>
+                    <h2>The Witch's Brew</h2>
+                    <p><a href="https://threejs-journey.com/challenges/014-halloween-2" target="_blank">A Threejs Journey Student Challenge</a></p>
+                </div>
+                <div class="credits-section">
+                    <h3>Created By</h3>
+                    <p><a href="https://mastodon.gamedev.place/@eyeofmidas" target="_blank">Justin Gilman</a></p>
+                    <p><a href="https://mastodon.design/@danusha" target="_blank">Danielle Gilman</a></p>
+                </div>
+                <div class="credits-section">
+                    <h3>Libraries</h3>
+                    <p><a href="https://threejs.org/" target="_blank">threejs</a></p>
+                    <p><a href="https://gsap.com/" target="_blank">GSAP</a></p>
+                    <p><a href="https://howlerjs.com/" target="_blank">howlerjs</a></p>
+                    <p><a href="https://vite.dev/" target="_blank">vite</a></p>
+                </div>
+                <div class="credits-section">
+                    <h3>3D Models</h3>
+                    <p><a href="https://kaylousberg.itch.io/" target="_blank">Kay Lousberg</a></p>
+                    <p><a href="https://assetquest.itch.io/" target="_blank">AssetQuest</a></p>
+                </div>
+                <div class="credits-section">
+                    <h3>2D Assets</h3>
+                    <p><a href="https://kenney.itch.io/" target="_blank">Kenney</a></p>
+                    <p><a href="https://polyhaven.com/a/kloppenheim_02_puresky" target="_blank">Greg Zaal & Jarod Guest</a></p>
+                    <p><a href="https://fonts.google.com" target="_blank">Google Fonts</a></p>
+                    
+                </div>
+                <div class="credits-section">
+                    <h3>Audio & Music</h3>
+                    <p><a href="https://kenney.itch.io/" target="_blank">Kenney</a></p>
+                    <p><a href="https://www.gamesfxpacks.com" target="_blank">Cyberwave Orchestra</a></p>
+                    <p><a href="https://pixabay.com/sound-effects/" target="_blank">Pixabay</a></p>
+                </div>
+
+                <div class="credits-section">
+                    <h3>Special Thanks</h3>
+                    <p><a href="https://bruno-simon.com/" target="_blank">Bruno Simon</a></p>
+                    <p><a href="https://www.youtube.com/@simondev758" target="_blank">SimonDev</a></p>
+                    <p><a href="https://www.udemy.com/user/nicholas-lever-3/" target="_blank">Nik Lever</a></p>
+                </div>
+            </div>
+        </div>
+    </div>
     <div id="room-navigation">
         <div id="prev"><svg height="40" width="200"><text xml:space="preserve"
                     style="font-size:32px;text-align:start;text-anchor:start;" x="44.315765" y="27.265625" id="text1"><tspan sodipodi:role="line" id="prev-text"
@@ -66,9 +133,9 @@
         <div id="game-status-panel">
             <div id="currency-meter-container">
                 <div id="currency-meter-icon"><img src="./images/coin.svg" /></div>
-                <span>0</span>
-                <progress id="currency-progress" max="1000" value="500" title="500"></progress>
-                <span>1000</span>
+                <div class="meter-start">0</div>
+                <div id="currency-progress"><div id="currency-progress-amount"><span id="currency-amount">100</span></div></div>
+                <div class="meter-end">1000</div>
             </div>
             <div id="current-day-container">
                 <div id="current-day-icon"><img src="./images/day_icon.svg" /></div>
@@ -233,9 +300,11 @@
                         </div>
                         <div class="call-to-action">Click to add to order</div>
                     </div>
+                    <button class="add-ingredient-button">+</button>
                     <div class="ingredient-in-cart-container">
                         <div class="ingredient-in-cart-amount">24</div>
                     </div>
+                    <button class="remove-ingredient-button">-</button>
                 </div>
             </div>
 

+ 31 - 0
ui/creditsui.js

@@ -0,0 +1,31 @@
+import gsap from "gsap"
+import { closeMainMenuUI, openMainMenuUI } from "./mainmenuui"
+export function creditsUI(game, stageData) {
+    document.getElementById("credits-container").addEventListener('click', () => {
+        hideCreditsUI(game, stageData)
+    })
+
+    document.getElementById("credits-back-button").addEventListener('click', () => {
+        hideCreditsUI(game, stageData)
+    })
+
+    document.getElementById("credits-panel").addEventListener('click', event => {
+        event.stopPropagation()
+    })
+}
+
+export function showCreditsUI(game, stageData) {
+    document.getElementById("credits-container").style.display = "flex"
+    const panel = document.getElementById("credits-panel")
+    gsap.to(panel, {x: 0, duration: 0.8, onComplete: () => {
+    }})
+    closeMainMenuUI(game, stageData)
+}
+
+export function hideCreditsUI(game, stageData) {
+    const panel = document.getElementById("credits-panel")
+    gsap.to(panel, {x: -1024, duration: 0.8, onComplete: () => {
+        document.getElementById("credits-container").style.display = "none"
+   }})
+   openMainMenuUI(game, stageData)
+}

+ 2 - 2
ui/gamestatusui.js

@@ -11,8 +11,8 @@ export function hideGameStatusUI(game, stageData) {
 }
 
 export function updateGameStatusUI(game, stageData) {
-    document.getElementById("currency-progress").value = stageData.currency
-    document.getElementById("currency-progress").title = stageData.currency
+    document.getElementById("currency-progress-amount").style.width = `${Math.min(100, Math.ceil(100* (stageData.currency / 1000)))}%`
+    document.getElementById("currency-amount").innerHTML = stageData.currency
 
     document.getElementById("current-day-text").innerHTML = `day ${stageData.currentDay}`
 }

+ 24 - 7
ui/mainmenuui.js

@@ -2,6 +2,8 @@ import gsap from "gsap"
 import { showNavigationUI } from "./navigationui.js"
 import { clearSaveData, loadSaveData } from "../game.js"
 import { showGameStatusUI } from "./gamestatusui.js"
+import { showCreditsUI } from "./creditsui.js"
+import { showOptionsUI } from "./optionsui.js"
 
 export function brewTutorialPrompt(stageData) {
     if(stageData.brewTutorial1.alreadySeen) {
@@ -29,6 +31,14 @@ export async function mainMenuUI(game, stageData) {
         stageData.musicLoop.play()
     })
 
+    document.getElementById("options-button").addEventListener('click', () => {
+        showOptionsUI(game, stageData)
+    })
+
+    document.getElementById("credits-button").addEventListener('click', () => {
+        showCreditsUI(game, stageData)
+    })
+
     document.getElementById("reset-save-button").addEventListener('click', () => {
         let shouldDelete = confirm("Are you sure you want to delete your Spookonomics save data? This cannot be undone.")
         if(shouldDelete) {
@@ -46,17 +56,24 @@ export async function openMainMenuUI(game, stageData) {
     const container = document.getElementById("main-menu-container")
     container.style.display = "block"
     const display = document.getElementById("main-menu-display")
-    display.style.opacity = 0
-    gsap.to(display, {opacity: 1, duration: 0.8})
+    // display.style.opacity = 0
+    // gsap.to(display, {opacity: 1, duration: 0.8})
+
+    gsap.to(display, {x: 0, duration: 0.8, onComplete: () => {
+    }})
 }
 
 export async function closeMainMenuUI(game, stageData) {
     const container = document.getElementById("main-menu-container")
     const display = document.getElementById("main-menu-display")
-    gsap.to(display, {
-        opacity: 0, duration: 0.8, onComplete: () => {
-            container.style.display = "none"
+    // gsap.to(display, {
+    //     opacity: 0, duration: 0.8, onComplete: () => {
+    //         container.style.display = "none"
 
-        }
-    })
+    //     }
+    // })
+
+    gsap.to(display, {x: -1024, duration: 0.8, onComplete: () => {
+         container.style.display = "none"
+    }})
 }

+ 51 - 1
ui/marketui.js

@@ -64,6 +64,21 @@ export function populateMarketIngredients(game, stageData) {
         callToAction.className = 'call-to-action'
         callToAction.innerHTML = "Click to add to order"
 
+        callToAction.addEventListener('click', event => {
+            const ingredientToAdd = sellContainer.getAttribute('data-ingredient')
+
+            const infoToAdd = stageData.ingredientInfo[ingredientToAdd]
+            if(infoToAdd.cost + cartTotal > stageData.currency) {
+                stageData.soundEffects['audio/witch_cackle1.ogg'].play()
+                return
+            }
+
+            stageData.soundEffects['audio/click1.ogg'].play()
+            stageData.cartItems.push(ingredientToAdd)
+            inCartAmount.innerHTML = stageData.cartItems.filter(item => item == ingredientName).length
+            updateCartTotal(game, stageData)
+        })
+
         descriptionContainer.appendChild(title)
         descriptionContainer.appendChild(ingredientPrice)
         descriptionContainer.appendChild(callToAction)
@@ -77,7 +92,38 @@ export function populateMarketIngredients(game, stageData) {
 
         inCartContainer.appendChild(inCartAmount)
 
-        sellContainer.appendChild(iconContainer)
+        const addIngredient = document.createElement('button')
+        addIngredient.className = "add-ingredient-button"
+        addIngredient.innerHTML = "+"
+
+        addIngredient.addEventListener('click', () => {
+            const ingredientToAdd = sellContainer.getAttribute('data-ingredient')
+
+            const infoToAdd = stageData.ingredientInfo[ingredientToAdd]
+            if(infoToAdd.cost + cartTotal > stageData.currency) {
+                stageData.soundEffects['audio/witch_cackle1.ogg'].play()
+                return
+            }
+
+            stageData.soundEffects['audio/click1.ogg'].play()
+            stageData.cartItems.push(ingredientToAdd)
+            inCartAmount.innerHTML = stageData.cartItems.filter(item => item == ingredientName).length
+            updateCartTotal(game, stageData)
+        })
+
+        const removeIngredient = document.createElement('button')
+        removeIngredient.className = "remove-ingredient-button"
+        removeIngredient.innerHTML = "-"
+
+        removeIngredient.addEventListener('click', () => {
+            const ingredientToRemove = sellContainer.getAttribute('data-ingredient')
+            if(stageData.cartItems.indexOf(ingredientToRemove) != -1) {
+                stageData.soundEffects['audio/click1.ogg'].play()
+                stageData.cartItems.splice(stageData.cartItems.indexOf(ingredientToRemove), 1)
+                inCartAmount.innerHTML = stageData.cartItems.filter(item => item == ingredientName).length
+                updateCartTotal(game, stageData)
+            }
+        })
 
         iconContainer.addEventListener('click', () => {
 
@@ -104,8 +150,12 @@ export function populateMarketIngredients(game, stageData) {
                 updateCartTotal(game, stageData)
             }
         })
+
+        sellContainer.appendChild(iconContainer)
         sellContainer.appendChild(descriptionContainer)
+        sellContainer.appendChild(addIngredient)
         sellContainer.appendChild(inCartContainer)
+        sellContainer.appendChild(removeIngredient)
 
         ingredientList.appendChild(sellContainer)
 

+ 41 - 0
ui/optionsui.js

@@ -0,0 +1,41 @@
+import gsap from "gsap"
+import { closeMainMenuUI, openMainMenuUI } from "./mainmenuui"
+export function optionsUI(game, stageData) {
+    document.getElementById("options-container").addEventListener('click', () => {
+        hideOptionsUI(game, stageData)
+    })
+
+    document.getElementById("options-panel").addEventListener('click', event => {
+        event.stopPropagation()
+    })
+
+    document.getElementById("options-back-button").addEventListener('click', () => {
+        hideOptionsUI(game, stageData)
+    })
+
+    document.getElementById("master-volume").addEventListener('change', (event) => {
+        const value = document.getElementById("master-volume").value
+        stageData.adjustMasterVolume(value)
+    })
+
+    document.getElementById("music-volume").addEventListener('change', (event) => {
+        const value = document.getElementById("music-volume").value
+        stageData.adjustMusicVolume(value)
+    })
+}
+
+export function showOptionsUI(game, stageData) {
+    document.getElementById("options-container").style.display = "flex"
+    const panel = document.getElementById("options-panel")
+    gsap.to(panel, {x: 0, duration: 0.8, onComplete: () => {
+    }})
+    closeMainMenuUI(game, stageData)
+}
+
+export function hideOptionsUI(game, stageData) {
+    const panel = document.getElementById("options-panel")
+    gsap.to(panel, {x: -1024, duration: 0.8, onComplete: () => {
+        document.getElementById("options-container").style.display = "none"
+   }})
+   openMainMenuUI(game, stageData)
+}