Преглед изворни кода

adding ability to buy ingredients in the market; adding ingredient inventory; adding ingredient numbers to UI; adding brew restrictions on ingredient inventory; adding more ui sound effects; adding music loop

Justin Gilman пре 3 недеља
родитељ
комит
09dfc0e50b
10 измењених фајлова са 617 додато и 110 уклоњено
  1. BIN
      assets/audio/Vampires LOOP (All Instruments).ogg
  2. 215 1
      css/game.css
  3. 8 4
      data/ingredients.json
  4. 97 27
      game.js
  5. 69 47
      index.html
  6. 13 0
      library/arrayhelpers.js
  7. 45 22
      ui/gameui.js
  8. 2 0
      ui/mainmenuui.js
  9. 164 0
      ui/marketui.js
  10. 4 9
      ui/shopui.js

BIN
assets/audio/Vampires LOOP (All Instruments).ogg


+ 215 - 1
css/game.css

@@ -223,6 +223,10 @@ button {
     background-color: #dcd4ca;
 }
 
+#currency-progress:after {
+    content: attr(value);
+  }
+
 #currency-progress[value]::-moz-progress-bar {
     background-color: #ebac51;
 }
@@ -312,6 +316,7 @@ button {
     flex-grow: 1;
     border-radius: 8px;
     padding: 4px;
+    position: relative;
 }
 
 .ingredient-container img {
@@ -319,7 +324,19 @@ button {
     height: 100%;
 }
 
-
+.ingredient-container .ingredient-inventory-brew-count {
+    position: absolute;
+    border-radius: 50%;
+    color: white;
+    text-align: right;
+    line-height: 24px;
+    right: 0;
+    bottom: 0;
+    width: 24px;
+    height: 24px;
+    font-size: 24px;
+    padding: 8px 12px;
+}
 
 .ingredient-container:hover {
     border: 4px dashed var(--light-brown);
@@ -662,6 +679,203 @@ ul#potion-properties-list li {
 }
 
 
+/*************
+ * Market UI *
+ *************/
+ #market-container {
+    position: fixed;
+    top: 0;
+    right: 0;
+    display: none;
+    width: 100%;
+    height: 100vh;
+}
+
+#market-panel {
+    margin-top: auto;
+    margin-bottom: auto;
+    display: flex;
+    flex-direction: column;
+    flex-wrap: nowrap;
+    position: fixed;
+    right: 0;
+    width: 512px;
+    translate: 1024px 0;
+    background-color: var(--tan);
+    margin: 30px;
+    border-radius: 8px;
+    border: 8px solid var(--light-brown);
+    box-shadow: 0 8px var(--dark-brown);
+}
+
+#market-header-container {
+    background-color: var(--light-brown);
+    box-shadow: 0 8px var(--dark-brown);
+}
+
+#market-header-container h1 {
+    color: white;
+    text-align: center;
+    margin: 16px;
+}
+
+.ingredient-sell-container {
+    display: flex;
+    flex-direction: row;
+    height: 126px;
+}
+
+.ingredient-icon-container {
+    border: 4px dashed #dec8ad;
+    border-radius: 50px;
+    margin: 15px;
+    width: 80px;
+    height: 80px;
+    padding: 4px;
+    position: relative;
+}
+
+.ingredient-icon-container {
+    position: relative;
+    border: 4px dashed #dec8ad;
+    border-radius: 16px;
+    padding: 4px;
+}
+
+.ingredient-icon-container:hover {
+    border: 4px dashed var(--light-brown);
+    cursor: pointer;
+}
+
+.ingredient-icon-container img {
+    width: 100%;
+    height: 100%;
+    border-radius: 8px;
+}
+
+.ingredient-icon-container .ingredient-inventory-count {
+    position: absolute;
+    border-radius: 50%;
+    color: white;
+    text-align: right;
+    line-height: 24px;
+    right: 0;
+    bottom: 0;
+    width: 24px;
+    height: 24px;
+    font-size: 24px;
+    padding: 8px 12px;
+}
+
+.ingredient-description {
+    display: flex;
+    flex-grow: 2;
+    flex-direction: column;
+    align-items: start;
+    padding: 8px;
+    justify-content: center;
+}
+
+.ingredient-description h2 {
+    margin: 0;
+    padding: 0;
+    font-size: 18px;
+}
+
+.ingredient-price {
+    padding: 4px;
+}
+
+.ingredient-price .ingredient-price-value {
+    margin-top: -24px;
+    margin-left: 24px;
+}
+
+.ingredient-price img {
+    padding-right: 8px;
+}
+
+.call-to-action {
+    font-size: 12px;
+    color: gray;
+}
+
+.ingredient-in-cart-container {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+}
+
+.ingredient-in-cart-container:hover {
+    cursor: pointer;
+}
+
+.ingredient-in-cart-amount {
+    height: 24px;
+    padding: 16px;
+    font-weight: bold;
+}
+
+#total-container {
+    border-top: 1px dotted gray;
+    text-align: right;
+    padding: 16px;
+    margin-bottom: 20px;
+    display: flex;
+    flex-direction: column;
+}
+#total-icon-container {
+    margin-left: auto;
+    margin-top: 8px;
+    padding: 8px;
+}
+
+#total-title {
+    font-size: 18px;
+}
+
+#total-amount {
+    display: flex;
+    flex-direction: row;
+}
+
+#total-amount-value {
+    font-size: 42px;
+    text-align: right;
+}
+
+
+
+#market-button-container {
+    height: 0;
+}
+
+#market-button-inner-container {
+    position: relative;
+    padding: 2px;
+    margin-top: -25px;
+    margin-left: 300px;
+    border-radius: 32px;
+    width: 180px;
+    background-color: var(--light-purple);
+}
+
+#market-button-container button {
+    font-size: 24px;
+    border: 4px solid var(--dark-purple);
+    color: white;
+    padding: 8px;
+    border-radius: 32px;
+    width: 180px;
+    background-color: var(--light-purple);
+}
+
+#market-button-container button:hover {
+    cursor: pointer;
+    border-color: white;
+}
+
+
 
 /****************
  * Game Over UI *

+ 8 - 4
data/ingredients.json

@@ -10,7 +10,8 @@
             "amplitude": 0.125,
             "frequency": 4,
             "rotation": 0.01
-        }
+        },
+        "cost": 10
     },
     "tomato": {
         "name": "tomato",
@@ -23,7 +24,8 @@
             "amplitude": 0.125,
             "frequency": 3.7,
             "rotation": 0.01
-        }
+        },
+        "cost": 10
     },
     "lettuce": {
         "name": "lettuce",
@@ -36,7 +38,8 @@
             "amplitude": 0.0625,
             "frequency": 2.7,
             "rotation": 0.005
-        }
+        },
+        "cost": 10
     },
     "mushroom": {
         "name": "mushroom",
@@ -49,6 +52,7 @@
             "amplitude": 0.125,
             "frequency": 3.6,
             "rotation": 0.01
-        }
+        },
+        "cost": 10
     }
 }

+ 97 - 27
game.js

@@ -17,7 +17,7 @@ import cauldronFragment from './shaders/cauldron/fragment.glsl'
 
 // UI
 import { brewTutorialPrompt, closeMainMenuUI, mainMenuUI, openMainMenuUI } from './ui/mainmenuui.js'
-import { brewUI, openBrewUI, closeBrewUI } from './ui/gameui.js'
+import { brewUI, openBrewUI, closeBrewUI, updateIngredients } from './ui/gameui.js'
 import { closeShopUI, openShopUI, shopTutorialPrompt, shopUI } from './ui/shopui.js'
 import { navigationUI, hideNavigationUI, nextRoom, previousRoom } from './ui/navigationui.js'
 
@@ -29,6 +29,7 @@ import Shopper from './shopper.js'
 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'
 
 export const GAME_SAVE_KEY = "spookonomics-v1"
 const raycast = new THREE.Raycaster()
@@ -78,6 +79,14 @@ async function beginBrew(game, stageData) {
         stageData.soundEffects['audio/witch_cackle1.ogg'].play()
         return
     }
+    const newInventory = [...stageData.ingredientInventory]
+    newInventory.remove(stageData.selectedIngredients[0])
+    newInventory.remove(stageData.selectedIngredients[1])
+    
+    if(stageData.ingredientInventory.length - newInventory.length < 2) {
+        stageData.soundEffects['audio/witch_cackle1.ogg'].play()
+        return
+    }
 
     stageData.cauldron.isBrewing = true
 
@@ -95,8 +104,6 @@ async function beginBrew(game, stageData) {
     stageData.bottle1.position.y = -1.5
     game.scene.add(stageData.bottle1)
 
-    stageData.selectedIngredients = []
-
     stageData.soundEffects['audio/click1.ogg'].play()
 
     const nextColorData = stageData.potionToBrew.color
@@ -118,6 +125,8 @@ async function beginBrew(game, stageData) {
 
     setTimeout(() => {
         addValueToSave(stageData.potionInventory, 'potion-inventory', stageData.potionToBrew.name)
+        removeValueFromSave(stageData.ingredientInventory, 'ingredient-inventory', stageData.selectedIngredients[0])
+        removeValueFromSave(stageData.ingredientInventory, 'ingredient-inventory', stageData.selectedIngredients[1])
         stageData.potionToBrew = null
         stageData.selectedIngredients = []
 
@@ -128,6 +137,8 @@ async function beginBrew(game, stageData) {
         stageData.spoon.visible = false
         stageData.bubbleParticles.visible = false
 
+        updateIngredients(game, stageData)
+
         gsap.to(stageData.bottle1.position, {
             duration: 3, y: 1.5, ease: "elastic", onComplete: () => {
                 setTimeout(() => {
@@ -268,16 +279,22 @@ export function removeValueFromSave(container, key, value) {
 export function clearSaveData(stageData) {
     localStorage.removeItem(`${GAME_SAVE_KEY}-potion-inventory`)
     localStorage.removeItem(`${GAME_SAVE_KEY}-potion-stocked`)
-    localStorage.setItem(`${GAME_SAVE_KEY}-potion-inventory`, JSON.stringify([]))
     stageData.potionInventory = []
-    localStorage.setItem(`${GAME_SAVE_KEY}-potion-stocked`, JSON.stringify([]))
+    localStorage.setItem(`${GAME_SAVE_KEY}-potion-inventory`, JSON.stringify(stageData.potionInventory))
     stageData.potionStocked = []
-
+    localStorage.setItem(`${GAME_SAVE_KEY}-potion-stocked`, JSON.stringify(stageData.potionStocked))
+    
     stageData.currency = 100
     localStorage.setItem(`${GAME_SAVE_KEY}-currency`, stageData.currency)
 
     stageData.currentDay = 1
     localStorage.setItem(`${GAME_SAVE_KEY}-currentday`, stageData.currentDay)
+
+    stageData.cartItems = []
+
+    stageData.ingredientInventory = ["mushroom", "mushroom", "pumpkin", "tomato", "tomato", "lettuce", "lettuce"]
+    localStorage.setItem(`${GAME_SAVE_KEY}-ingredient-inventory`, JSON.stringify(stageData.ingredientInventory))
+    
 }
 
 export function loadSaveData(stageData) {
@@ -310,6 +327,32 @@ export function loadSaveData(stageData) {
         localStorage.setItem(`${GAME_SAVE_KEY}-currentday`, 1)
         stageData.currentDay = 1
     }
+
+    let ingredientInventoryString = localStorage.getItem(`${GAME_SAVE_KEY}-ingredient-inventory`)
+    if (!ingredientInventoryString) {
+        localStorage.setItem(`${GAME_SAVE_KEY}-ingredient-inventory`, JSON.stringify([]))
+        stageData.ingredientInventory = []
+    } else {
+        stageData.ingredientInventory = JSON.parse(ingredientInventoryString)
+        stageData.ingredientInventory.sort()
+    }
+}
+
+function buyIngredients(game, stageData) {
+    let amountToDeduct = 0
+    stageData.cartItems.forEach(item => {
+        amountToDeduct += stageData.ingredientInfo[item].cost
+        addValueToSave(stageData.ingredientInventory, "ingredient-inventory", item)
+    })
+    
+    stageData.currency = addAmountToSave("currency", -amountToDeduct)
+
+    if(amountToDeduct > 0) {
+        stageData.soundEffects['audio/cash-register.ogg'].play()
+    }
+
+    updateGameStatusUI(game, stageData)
+    closeMarketUI(game, stageData)
 }
 
 export async function init(inGame) {
@@ -321,6 +364,7 @@ export async function init(inGame) {
     stageData.soundEffects = soundEffects
 
     stageData.selectedIngredients = []
+    stageData.cartItems = []
 
     stageData.cameraPositions = [
         { "camera": new THREE.Vector3(-15, 5, 7), "focus": new THREE.Vector3(-13, 0.1, 0) },
@@ -359,8 +403,16 @@ export async function init(inGame) {
         })
     })
 
+    stageData.musicLoop = new Howl({
+        src: ['audio/Vampires LOOP (All Instruments).ogg'],
+        preload: true,
+        loop: true,
+        volume: 0.5
+    })
+
     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)
 
     let levelLoader = new LevelLoader(game)
 
@@ -645,26 +697,26 @@ export async function init(inGame) {
     game.entities.push(stageData.sign)
     game.scene.add(stageData.sign)
 
-    const mushroomCrate = await loadGltf(game, 'models/crate_mushrooms.gltf')
-    mushroomCrate.scale.set(0.5, 0.5, 0.5)
-    mushroomCrate.position.x = 19.5
-    mushroomCrate.position.z = 0.5
-    mushroomCrate.rotateOnAxis(THREE.Object3D.DEFAULT_UP, Math.PI / 4)
-    game.entities.push(mushroomCrate)
-    game.scene.add(mushroomCrate)
-
-    const tomatoCrate = await loadGltf(game, 'models/crate_tomatoes.gltf')
-    tomatoCrate.scale.set(0.5, 0.5, 0.5)
-    tomatoCrate.position.x = 21
-    game.entities.push(tomatoCrate)
-    game.scene.add(tomatoCrate)
-
-    const lettuceCrate = await loadGltf(game, 'models/crate_lettuce.gltf')
-    lettuceCrate.scale.set(0.5, 0.5, 0.5)
-    lettuceCrate.position.x = 18.5
-    lettuceCrate.position.z = -0.5
-    game.entities.push(lettuceCrate)
-    game.scene.add(lettuceCrate)
+    stageData.mushroomCrate = await loadGltf(game, 'models/crate_mushrooms.gltf')
+    stageData.mushroomCrate.scale.set(0.5, 0.5, 0.5)
+    stageData.mushroomCrate.position.x = 19.5
+    stageData.mushroomCrate.position.z = 0.5
+    stageData.mushroomCrate.rotateOnAxis(THREE.Object3D.DEFAULT_UP, Math.PI / 4)
+    game.entities.push(stageData.mushroomCrate)
+    game.scene.add(stageData.mushroomCrate)
+
+    stageData.tomatoCrate = await loadGltf(game, 'models/crate_tomatoes.gltf')
+    stageData.tomatoCrate.scale.set(0.5, 0.5, 0.5)
+    stageData.tomatoCrate.position.x = 21
+    game.entities.push(stageData.tomatoCrate)
+    game.scene.add(stageData.tomatoCrate)
+
+    stageData.lettuceCrate = await loadGltf(game, 'models/crate_lettuce.gltf')
+    stageData.lettuceCrate.scale.set(0.5, 0.5, 0.5)
+    stageData.lettuceCrate.position.x = 18.5
+    stageData.lettuceCrate.position.z = -0.5
+    game.entities.push(stageData.lettuceCrate)
+    game.scene.add(stageData.lettuceCrate)
 
     const coffin = await loadGltf(game, 'models/coffin_decorated.gltf')
     coffin.position.x = 23
@@ -761,6 +813,7 @@ export async function init(inGame) {
     navigationUI(game, stageData)
     brewUI(game, stageData)
     shopUI(game, stageData)
+    marketUI(game, stageData)
     gameOverUI(game, stageData)
 
     updateGameStatusUI(game, stageData)
@@ -768,6 +821,7 @@ export async function init(inGame) {
 
     stageData.beginBrew = () => { beginBrew(game, stageData) }
     stageData.beginSell = () => { beginSell(game, stageData) }
+    stageData.buyIngredients = () => { buyIngredients(game, stageData)}
 
 
     ///////////
@@ -1155,6 +1209,22 @@ export function onClick() {
 
                     if (isAChildOf(stageData.sign, intersect.object)) {
                         previousRoom(game, stageData)
+                        return false
+                    }
+
+                    if(isAChildOf(stageData.mushroomCrate, intersect.object) ||
+                    isAChildOf(stageData.lettuceCrate, intersect.object) ||
+                    isAChildOf(stageData.tomatoCrate, intersect.object) ||
+                    isAChildOf(stageData.sellingSkeleton, intersect.object) ||
+                    isAChildOf(stageData.marketWitch, intersect.object)
+                ) {
+
+                        if (stageData.sellingSkeleton.marketMenuOpen) {
+                            closeMarketUI(game, stageData)
+                        } else {
+                            openMarketUI(game, stageData)
+                        }
+                        return false
                     }
                     break;
 
@@ -1193,7 +1263,7 @@ function nextCustomer() {
     const customerMotionPath = [{ x: -11, y: 0.1, z: 3 }, { x: -8, y: 0.1, z: 3 }, { x: -4, y: 0.1, z: 10 }]
     setTimeout(() => {
         stageData.soundEffects['audio/store-entrance-bell.ogg'].play()
-    }, 4000)
+    }, 3000)
     gsap.to(firstCustomer.object3d.position, {
         duration: 8, motionPath: customerMotionPath, onComplete: () => {
             game.scene.remove(firstCustomer.object3d)

+ 69 - 47
index.html

@@ -11,17 +11,20 @@
     <link rel="icon" type="image/icon" href="./favicon.ico">
     <title>Spookonomics: The Witch's Brew</title>
     <meta property="og:title" content="Spookonomics: The Witch's Brew - Halloween 2024" />
-    <meta property="og:description" content="A browser-based game of mysterious potions and spooky capitalism. A humble witch gathers ingredients and brews speciality potions for her supernatural clientele." />
+    <meta property="og:description"
+        content="A browser-based game of mysterious potions and spooky capitalism. A humble witch gathers ingredients and brews speciality potions for her supernatural clientele." />
     <meta property="og:image" content="https://eyeofmidas.com/spookonomics/og_image.png" />
     <meta property="og:url" content="https://eyeofmidas.com/spookonomics" />
 
     <link rel="preconnect" href="https://fonts.googleapis.com">
-<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
-<link href="https://fonts.googleapis.com/css2?family=Alice&family=Quicksand&display=swap" rel="stylesheet">
+    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+    <link href="https://fonts.googleapis.com/css2?family=Alice&family=Quicksand&display=swap" rel="stylesheet">
 </head>
 
 <body>
-    <div id="loading"><div id="loading-inner"><img src="./loading_ghost.gif" />loading...</div></div>
+    <div id="loading">
+        <div id="loading-inner"><img src="./loading_ghost.gif" />loading...</div>
+    </div>
     <canvas class="webgl"></canvas>
     <script type="module" src="./index.js"></script>
     <div id="main-menu-container">
@@ -47,50 +50,26 @@
         </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"
-              style="font-size:32px;text-align:start;text-anchor:start;"
-              x="44.315765"
-              y="27.265625">Shop</tspan></text>
-         <path
-            d="M 0,16 41.72141,31.318914 19.128231,16 41.72141,0.681086 Z"
-            id="path1"
-            sodipodi:nodetypes="ccccc" />
-            </svg>
+        <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"
+                        style="font-size:32px;text-align:start;text-anchor:start;" x="44.315765" y="27.265625">Shop</tspan></text><path d="M 0,16 41.72141,31.318914 19.128231,16 41.72141,0.681086 Z" id="path1"
+                    sodipodi:nodetypes="ccccc" /></svg>
         </div>
 
-        <div id="next"><svg height="40" width="200"><text
-            xml:space="preserve"
-            style="font-size:32px;text-align:end;text-anchor:end;"
-            x="158.06747"
-            y="26.584539"
-            id="text2"><tspan
-              sodipodi:role="line"
-              id="next-text"
-              style="font-size:32px;text-align:end;text-anchor:end;"
-              x="158.06747"
-              y="26.584539">Market</tspan></text>
-         <path
-            d="M 200,16 158.27859,30.637828 180.87177,16 158.27859,0 Z"
-            id="path3"
-            sodipodi:nodetypes="ccccc" />
-            </svg>
+        <div id="next"><svg height="40" width="200"><text xml:space="preserve"
+                    style="font-size:32px;text-align:end;text-anchor:end;" x="158.06747" y="26.584539" id="text2"><tspan sodipodi:role="line" id="next-text" style="font-size:32px;text-align:end;text-anchor:end;"
+                        x="158.06747" y="26.584539">Market</tspan></text><path d="M 200,16 158.27859,30.637828 180.87177,16 158.27859,0 Z" id="path3"
+                    sodipodi:nodetypes="ccccc" /></svg>
         </div>
     </div>
     <div id="game-status-container">
         <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>
+                <span>0</span>
+                <progress id="currency-progress" max="1000" value="500" title="500"></progress>
+                <span>1000</span>
+            </div>
             <div id="current-day-container">
                 <div id="current-day-icon"><img src="./images/day_icon.svg" /></div>
                 <div id="current-day-text">day 1</div>
@@ -235,6 +214,48 @@
         </div>
     </div>
 
+    <div id="market-container">
+        <div id="market-panel">
+            <div id="market-header-container">
+                <h1>Ingredients</h1>
+            </div>
+            <div id="ingredients-list">
+                <div class="ingredient-sell-container" data-ingredient="pumpkin">
+                    <div class="ingredient-icon-container">
+                        <img src="./images/ingredient_pumpkin.png" />
+                        <div class="ingredient-inventory-count">5</div>
+                    </div>
+                    <div class="ingredient-description">
+                        <h2>Pumpkin</h2>
+                        <div class="ingredient-price">
+                            <img src="./images/coin.svg" />
+                            <div class="ingredient-price-value">10</div>
+                        </div>
+                        <div class="call-to-action">Click to add to order</div>
+                    </div>
+                    <div class="ingredient-in-cart-container">
+                        <div class="ingredient-in-cart-amount">24</div>
+                    </div>
+                </div>
+            </div>
+
+            <div id="total-container">
+                <div id="total-title">Total</div>
+                <div id="total-amount">
+                    <div id="total-icon-container">
+                        <img src="./images/coin.svg" />
+                    </div>
+                    <div id="total-amount-value">57</div>
+                </div>
+            </div>
+            <div id="market-button-container">
+                <div id="market-button-inner-container">
+                    <button id="buy-ingredients">Purchase</button>
+                </div>
+            </div>
+        </div>
+    </div>
+
     <div id="game-over-container">
         <div id="game-over-blackout">
             <div id="game-over-panel">
@@ -250,13 +271,14 @@
         </div>
     </div>
     <!-- Google tag (gtag.js) -->
-	<script async src="https://www.googletagmanager.com/gtag/js?id=G-H4BYTFBKJT"></script>
-	<script>
-		window.dataLayer = window.dataLayer || [];
-		function gtag() { dataLayer.push(arguments); }
-		gtag('js', new Date());
+    <script async src="https://www.googletagmanager.com/gtag/js?id=G-H4BYTFBKJT"></script>
+    <script>
+        window.dataLayer = window.dataLayer || [];
+        function gtag() { dataLayer.push(arguments); }
+        gtag('js', new Date());
 
-		gtag('config', 'G-H4BYTFBKJT');
-	</script>
+        gtag('config', 'G-H4BYTFBKJT');
+    </script>
 </body>
+
 </html>

+ 13 - 0
library/arrayhelpers.js

@@ -40,4 +40,17 @@ Array.prototype.shuffle = function() {
         this[j] = swap
     }
     return this
+}
+
+Array.prototype.groupCount = function() {
+    const grouped = {}
+    this.forEach(value => {
+        if (!grouped[value]) {
+            grouped[value] = 1
+        } else {
+            grouped[value]++
+        }
+    })
+
+    return grouped
 }

+ 45 - 22
ui/gameui.js

@@ -41,15 +41,7 @@ function displayBrewPotion(game, stageData) {
     potionIcon.src = potionToBrew.image
     document.getElementById('potion-icon').appendChild(potionIcon)
 
-    const groupedPotionInventory = {}
-    stageData.potionInventory.forEach(potionName => {
-        if (!groupedPotionInventory[potionName]) {
-            groupedPotionInventory[potionName] = 1
-        } else {
-            groupedPotionInventory[potionName]++
-        }
-    })
-
+    const groupedPotionInventory = stageData.potionInventory.groupCount()
     const potionCount = groupedPotionInventory[potionToBrew.name] ?? 0
 
     const potionCountElement = document.getElementById('potion-icon-container').getElementsByClassName("potion-inventory-count")[0]
@@ -70,31 +62,58 @@ function displayBrewPotion(game, stageData) {
     
 }
 
-export async function brewUI(game, stageData) {
-    document.getElementById("brew-container").addEventListener('click', () => {
-        closeBrewUI(game, stageData)
-    })
+export function updateIngredients(game, stageData) {
+    const container = document.getElementById("brew-ingredients-container")
+    container.innerHTML = ''
+    const groupedIngredientInventory = stageData.ingredientInventory.groupCount()
 
-    document.getElementById('brew-right-side').addEventListener('click', (event) => {
-        event.stopPropagation()
-    })
-    
-    const ingredientSelectedButtons = document.getElementById('brew-selected-container').children
-    const ingredientButtons = document.getElementById('brew-ingredients-container').children
-    for(const button of ingredientButtons) {
-         button.addEventListener("click", (event) => {
+    for(let ingredientName in stageData.ingredientInfo) {
+        const info = stageData.ingredientInfo[ingredientName]
+        
+        const ingredientContainer = document.createElement('div')
+        ingredientContainer.className = 'ingredient-container'
+        ingredientContainer.setAttribute('data-ingredient', ingredientName)
+
+        const ingredientImage = document.createElement('img')
+        ingredientImage.src = info.image
+        ingredientImage.title = info.title
+
+
+        const ingredientCount = document.createElement('div')
+        ingredientCount.className = 'ingredient-inventory-brew-count'
+        ingredientCount.innerHTML = groupedIngredientInventory[ingredientName] ?? 0
+
+        ingredientContainer.appendChild(ingredientImage)
+        ingredientContainer.appendChild(ingredientCount)
+        ingredientContainer.addEventListener('click', (event) => {
             event.stopPropagation()
             if(stageData.selectedIngredients.length < 2) {
                 stageData.soundEffects['audio/click1.ogg'].play()
-                const ingredientName = button.getAttribute('data-ingredient')
+                const ingredientName = ingredientContainer.getAttribute('data-ingredient')
+
                 stageData.selectedIngredients.push(ingredientName)
                 displaySelectedIngredients(game, stageData)
             }
 
             displayBrewPotion(game, stageData)
          })
+
+        container.appendChild(ingredientContainer)
     }
+}
 
+export async function brewUI(game, stageData) {
+    document.getElementById("brew-container").addEventListener('click', () => {
+        closeBrewUI(game, stageData)
+    })
+
+    document.getElementById('brew-right-side').addEventListener('click', (event) => {
+        event.stopPropagation()
+    })    
+
+    updateIngredients(game, stageData)   
+    
+    const ingredientSelectedButtons = document.getElementById('brew-selected-container').children
      for(let buttonIndex = 0; buttonIndex < ingredientSelectedButtons.length; buttonIndex++) {
          const button = ingredientSelectedButtons[buttonIndex]
          button.addEventListener("click", (event) => {
@@ -121,6 +140,7 @@ export async function openBrewUI(game,stageData) {
         return
     }
     
+    
     const brewContainer = document.getElementById("brew-container")
     brewContainer.style.display = "block"
     if(game.canvas.clientWidth >= 1024) {
@@ -133,9 +153,11 @@ export async function openBrewUI(game,stageData) {
         gsap.to(game.camera.position, {y: currentPosition.camera.y - 1.5, duration: 0.8})
     }
 
+    updateIngredients(game, stageData)
     displaySelectedIngredients(game, stageData)
     displayBrewPotion(game, stageData)
 
+    stageData.soundEffects['audio/drawKnife2.ogg'].play()
     const brewRightSide = document.getElementById("brew-right-side")
     gsap.to(brewRightSide, {x: 0, duration: 0.8, onComplete: () => {
         stageData.cauldron.brewMenuOpen = true
@@ -160,6 +182,7 @@ export async function closeBrewUI(game, stageData) {
         gsap.to(game.camera.position, {y: currentPosition.camera.y, duration: 0.8})
     }
 
+    stageData.soundEffects['audio/drawKnife2.ogg'].play()
     const brewRightSide = document.getElementById("brew-right-side")
     gsap.to(brewRightSide, {x: 1024, duration: 0.8, onComplete: () => {
         brewContainer.style.display = "none"

+ 2 - 0
ui/mainmenuui.js

@@ -25,6 +25,8 @@ export async function mainMenuUI(game, stageData) {
         const camPosition = stageData.cameraPositions[1]
         gsap.to(game.camera.position, {duration: 2.5, x: camPosition.camera.x, y:camPosition.camera.y, z:camPosition.camera.z})
         gsap.to(game.lookAtFocus, { duration: 2.5, x: camPosition.focus.x, y: camPosition.focus.y, z: camPosition.focus.z })
+
+        stageData.musicLoop.play()
     })
 
     document.getElementById("reset-save-button").addEventListener('click', () => {

+ 164 - 0
ui/marketui.js

@@ -0,0 +1,164 @@
+import gsap from "gsap"
+
+export function marketUI(game, stageData) {
+    document.getElementById("market-container").addEventListener('click', () => {
+        closeMarketUI(game, stageData)
+    })
+
+    document.getElementById('market-panel').addEventListener('click', (event) => {
+        event.stopPropagation()
+    })
+
+    document.getElementById('buy-ingredients').addEventListener('click', (event) => {
+        stageData.buyIngredients()
+    })
+}
+
+let cartTotal = 0
+
+export function populateMarketIngredients(game, stageData) {
+    const ingredientList = document.getElementById("ingredients-list")
+    ingredientList.innerHTML = ''
+
+    const groupedIngredientCount = stageData.ingredientInventory.groupCount()
+
+    for(let ingredientName in stageData.ingredientInfo) {
+        const info = stageData.ingredientInfo[ingredientName]
+
+        const sellContainer = document.createElement('div')
+        sellContainer.className = "ingredient-sell-container"
+        sellContainer.setAttribute('data-ingredient', ingredientName)
+
+        const iconContainer = document.createElement('div')
+        iconContainer.className = "ingredient-icon-container"
+
+        const ingredientIcon = document.createElement('img')
+        ingredientIcon.src = info.image
+
+        const inventoryCount = document.createElement('div')
+        inventoryCount.className = 'ingredient-inventory-count'
+        inventoryCount.innerHTML = groupedIngredientCount[ingredientName] ?? 0
+
+        iconContainer.appendChild(ingredientIcon)
+        iconContainer.appendChild(inventoryCount)
+
+        const descriptionContainer = document.createElement('div')
+        descriptionContainer.className = 'ingredient-description'
+        const title = document.createElement('h2')
+        title.innerHTML = info.title
+
+        const ingredientPrice = document.createElement('div')
+        ingredientPrice.className = "ingredient-price"
+
+        const coinImage = document.createElement('img')
+        coinImage.src = "./images/coin.svg"
+
+        const priceValue = document.createElement('div')
+        priceValue.className = 'ingredient-price-value'
+        priceValue.innerHTML = info.cost
+
+        ingredientPrice.appendChild(coinImage)
+        ingredientPrice.appendChild(priceValue)
+
+        const callToAction = document.createElement('div')
+        callToAction.className = 'call-to-action'
+        callToAction.innerHTML = "Click to add to order"
+
+        descriptionContainer.appendChild(title)
+        descriptionContainer.appendChild(ingredientPrice)
+        descriptionContainer.appendChild(callToAction)
+
+        const inCartContainer = document.createElement('div')
+        inCartContainer.className = 'ingredient-in-cart-container'
+
+        const inCartAmount = document.createElement('div')
+        inCartAmount.className = 'ingredient-in-cart-amount'
+        inCartAmount.innerHTML = 0
+
+        inCartContainer.appendChild(inCartAmount)
+
+        sellContainer.appendChild(iconContainer)
+
+        iconContainer.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)
+        })
+
+        inCartContainer.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)
+            }
+        })
+        sellContainer.appendChild(descriptionContainer)
+        sellContainer.appendChild(inCartContainer)
+
+        ingredientList.appendChild(sellContainer)
+
+        
+    }
+    updateCartTotal(game, stageData)
+}
+
+export function updateCartTotal(game, stageData) {
+    cartTotal = 0
+    stageData.cartItems.forEach((ingredientName) => {
+        const info = stageData.ingredientInfo[ingredientName]
+        cartTotal += info.cost
+    })
+
+    document.getElementById("total-amount-value").innerHTML = cartTotal
+
+ 
+}
+
+export function openMarketUI(game, stageData) {
+    if(stageData.sellingSkeleton.marketMenuOpen) {
+        return
+    }
+    const marketContainer = document.getElementById("market-container")
+    marketContainer.style.display = "block"
+
+    populateMarketIngredients(game, stageData)
+
+    stageData.soundEffects['audio/drawKnife2.ogg'].play()
+    const marketPanel = document.getElementById("market-panel")
+    gsap.to(marketPanel, {
+        x: 0, duration: 0.8, onComplete: () => {
+            stageData.sellingSkeleton.marketMenuOpen = true
+        }
+    })
+}
+
+export function closeMarketUI(game, stageData) {
+    if(!stageData.sellingSkeleton.marketMenuOpen) {
+        return
+    }
+    stageData.sellingSkeleton.marketMenuOpen = false
+
+    stageData.cartItems = []
+
+    stageData.soundEffects['audio/drawKnife2.ogg'].play()
+    const marketPanel = document.getElementById("market-panel")
+    gsap.to(marketPanel, {
+        x: 1024, duration: 0.8, onComplete: () => {
+            const marketContainer = document.getElementById("market-container")
+            marketContainer.style.display = "none"
+
+        }
+    })
+}

+ 4 - 9
ui/shopui.js

@@ -42,14 +42,7 @@ function displayPotionInventory(game, stageData) {
     const container = document.getElementById("shop-potions-container")
     container.innerHTML = ''
 
-    const groupedPotionInventory = {}
-    stageData.potionInventory.forEach(potionName => {
-        if (!groupedPotionInventory[potionName]) {
-            groupedPotionInventory[potionName] = 1
-        } else {
-            groupedPotionInventory[potionName]++
-        }
-    })
+    const groupedPotionInventory = stageData.potionInventory.groupCount()
 
 
     Object.keys(groupedPotionInventory).forEach((potionName) => {
@@ -63,7 +56,7 @@ function displayPotionInventory(game, stageData) {
         potionImage.title = potionToDisplay.title
         const potionCount = document.createElement('div')
         potionCount.className = 'potion-inventory-count'
-        potionCount.innerHTML = `${groupedPotionInventory[potionName]}`
+        potionCount.innerHTML = `${groupedPotionInventory[potionName] ?? 0}`
 
         potionContainer.appendChild(potionImage)
         potionContainer.appendChild(potionCount)
@@ -169,6 +162,7 @@ export async function openShopUI(game, stageData) {
 
     displayPotionInventory(game, stageData)
     displayStockedPotions(game, stageData)
+    stageData.soundEffects['audio/drawKnife2.ogg'].play()
 
     const shopPanel = document.getElementById("shop-panel")
     gsap.to(shopPanel, {
@@ -181,6 +175,7 @@ export async function openShopUI(game, stageData) {
 export async function closeShopUI(game, stageData) {
     stageData.chest.isStocking = false
 
+    stageData.soundEffects['audio/drawKnife2.ogg'].play()
     const shopPanel = document.getElementById("shop-panel")
     gsap.to(shopPanel, {
         x: 1024, duration: 0.8, onComplete: () => {