import * as THREE from 'three' import gsap from 'gsap' import { Howl, Howler } from 'howler' import LevelLoader from './library/levelloader.js' import { loadGltf } from './library/loadgltf.js' import { loadRgbeBackground } from './library/loadbackground.js' import { getForwardVector, getRightVector, drawDebugLine } from './library/mathhelpers.js' import Ingredient from './library/ingredient.js' import './library/arrayhelpers.js' // Shaders import firefliesVertex from './shaders/firefly/vertex.glsl' import firefliesFragment from './shaders/firefly/fragment.glsl' import cauldronVertex from './shaders/cauldron/vertex.glsl' import cauldronFragment from './shaders/cauldron/fragment.glsl' // UI import { brewTutorialPrompt, closeMainMenuUI, mainMenuUI, openMainMenuUI } from './ui/mainmenuui.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' // Game Data import * as levelData from './data/level.json' import * as ingredientInfo from './data/ingredients.json' import * as potionInfo from './data/potions.json' 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' 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() let game, jackolantern, doorway, directionalLight, doorway2, lollipop, candle, candleLit, book const ingredients = [] const soundEffects = {} const stageData = {} export const ROOM_SHOP = 1; export const ROOM_BREW = 2; export const ROOM_MARKET = 3; const shelfSlots = [ { "position": new THREE.Vector3(-3.5, 1.5, -3.5), "motionPath": [{ x: -3, y: 1.3, z: -3.5 }, { x: -1.5, y: 2, z: 0 }, { x: -0.5, y: 2, z: 0 }, { x: 0, y: 0.5, z: 0 }] }, { "position": new THREE.Vector3(-4.5, 1.5, -3.5), "motionPath": [{ x: -3, y: 1.3, z: -3.5 }, { x: -1.5, y: 2, z: 0 }, { x: -0.5, y: 2, z: 0 }, { x: 0, y: 0.5, z: 0 }] }, { "position": new THREE.Vector3(-4.5, 0, -3.0), "motionPath": [{ x: -3, y: 1.3, z: -3.5 }, { x: -1.5, y: 2, z: 0 }, { x: -0.5, y: 2, z: 0 }, { x: 0, y: 0.5, z: 0 }] }, { "position": new THREE.Vector3(-3.3, 3, -3.5), "motionPath": [{ x: -3, y: 1.3, z: -3.5 }, { x: -1.5, y: 2, z: 0 }, { x: -0.5, y: 2, z: 0 }, { x: 0, y: 0.5, z: 0 }] } ] const storeShelfSlots = [ { "position": new THREE.Vector3(-12.5, 2.75, -3.25), "motionPath": [{ x: -11.5, y: 2.75, z: -3.25 }, { x: -12, y: 2, z: -2 }, { x: -13, y: 0.75, z: 2.5 }] }, { "position": new THREE.Vector3(-11.5, 2.75, -3.25), "motionPath": [{ x: -11.5, y: 2.75, z: -3.25 }, { x: -12, y: 2, z: -2 }, { x: -13, y: 0.75, z: 2.5 }] }, { "position": new THREE.Vector3(-8.5, 2.75, -3.25), "motionPath": [{ x: -11.5, y: 2.75, z: -3.25 }, { x: -12, y: 2, z: -2 }, { x: -13, y: 0.75, z: 2.5 }] }, { "position": new THREE.Vector3(-7.5, 2.75, -3.25), "motionPath": [{ x: -11.5, y: 2.75, z: -3.25 }, { x: -12, y: 2, z: -2 }, { x: -13, y: 0.75, z: 2.5 }] }, { "position": new THREE.Vector3(-8.5, 1.25, -3.25), "motionPath": [{ x: -11.5, y: 2.75, z: -3.25 }, { x: -12, y: 2, z: -2 }, { x: -13, y: 0.75, z: 2.5 }] }, { "position": new THREE.Vector3(-7.5, 1.25, -3.25), "motionPath": [{ x: -11.5, y: 2.75, z: -3.25 }, { x: -12, y: 2, z: -2 }, { x: -13, y: 0.75, z: 2.5 }] }, ] const customerSlots = [ { "position": new THREE.Vector3(-12.9, 0.1, 3) }, { "position": new THREE.Vector3(-13.1, 0.1, 4) }, { "position": new THREE.Vector3(-12.7, 0.1, 5) }, { "position": new THREE.Vector3(-13.1, 0.1, 6) }, { "position": new THREE.Vector3(-12.9, 0.1, 7) }, { "position": new THREE.Vector3(-13, 0.1, 8) }, ] const bubblePositions = [] const bubbleVelocity = [] const bubbleLifespan = [] const bubbleScale = [] const bubbleCount = 15 async function beginBrew(game, stageData) { if (!stageData.potionToBrew) { 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 closeBrewUI(game, stageData) stageData.brewWitch.stir = true stageData.brewWitch.bounce = true stageData.spoon.visible = true stageData.selectedIngredients.forEach(ingredientName => { ingredients.find((ingredient => ingredient.getName() == ingredientName)).beginBrew() }) stageData.bottle1 = await loadGltf(game, stageData.potionToBrew.model) stageData.bottle1.position.y = -1.5 game.scene.add(stageData.bottle1) stageData.soundEffects['audio/click1.ogg'].play() const nextColorData = stageData.potionToBrew.color stageData.cauldronUniforms.uNextPotionColor.value = new THREE.Color(nextColorData.r, nextColorData.g, nextColorData.b) stageData.cauldronUniforms.uBlendTime.value = 0.0 setTimeout(() => { stageData.soundEffects['audio/bubbling.mp3'].play() stageData.bubbleParticles.visible = true gsap.to(stageData.cauldronUniforms.uBlendTime, { value: 1, duration: 4.5, onComplete: () => { stageData.cauldronUniforms.uPotionColor.value = stageData.cauldronUniforms.uNextPotionColor.value stageData.cauldronUniforms.uBlendTime.value = 0.0 } }) }, 900) 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 = [] stageData.bottle1.position.set(0, -1.5, 0) 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 stageData.bubbleParticles.visible = false updateIngredients(game, stageData) gsap.to(stageData.bottle1.position, { duration: 3, y: 1.5, ease: "elastic", onComplete: () => { setTimeout(() => { game.scene.remove(stageData.bottle1) //TODO play potion particle effect stageData.cauldron.isBrewing = false }, 1000) } }) }, 5000) } async function customerBuilder(randomPotion, delay) { const shopperPosition = customerSlots[stageData.customers.length].position const randomIndex = Math.floor(Math.random() * 3) let shopper switch (randomIndex) { case 0: shopper = new Shopper(game, 'characters/assembled_character_1.gltf.glb', randomPotion, new THREE.Vector3(3.5, 3.5, 3.5), shopperPosition, Math.PI, function (elapsedTime) { this.object3d.position.y = 0.0625 * Math.sin(2 * elapsedTime) + 0.5 }) break; case 1: shopper = new Shopper(game, 'characters/character_skeleton_minion.gltf', randomPotion, new THREE.Vector3(1, 1, 1), shopperPosition, Math.PI, function (elapsedTime) { this.object3d.scale.x = 1 + (0.03125 * Math.sin(11.8 * elapsedTime)) this.object3d.scale.y = 1 + (-0.03125 * Math.sin(11.8 * elapsedTime)) this.object3d.scale.z = 1 + (0.03125 * Math.sin(11.8 * elapsedTime)) }) break; case 2: shopper = new Shopper(game, 'characters/ghost_1.gltf.glb', randomPotion, new THREE.Vector3(3.5, 3.5, 3.5), shopperPosition, Math.PI, function (elapsedTime) { this.object3d.position.y = 0.0625 * Math.sin(2.2 * elapsedTime) + 0.5 }) break; } await shopper.init(delay) return shopper } export function sellTutorialPrompt(stageData) { if(stageData.shopTutorial2.alreadySeen) { return } stageData.shopInactivityHandle = setTimeout(() => { gsap.to(stageData.shopTutorial2.material, {duration: 1.5, opacity: 1, onStart: () => { stageData.shopTutorial2.castShadow = true }}) }, 5000) } async function beginSell(game, stageData) { if (stageData.potionStocked.length < 1) { stageData.soundEffects['audio/witch_cackle1.ogg'].play() return } stageData.soundEffects['audio/click1.ogg'].play() stageData.isSellingPotions = true sellTutorialPrompt(stageData) let randomPotions = [] randomPotions.push(...stageData.potionStocked) randomPotions.push(...stageData.potionInventory) randomPotions.push(...stageData.potionInfo.map(info => info.name)) randomPotions.shuffle() let numberOfCustomers = Math.min(randomPotions.length, Math.floor(Math.random() * 3) + 3) for (let i = 0; i < numberOfCustomers; i++) { const desiredPotion = randomPotions.shift() const potionInfo = stageData.potionInfo.find(info => info.name == desiredPotion) stageData.customers.push(await customerBuilder(potionInfo, i)) } setTimeout(() => { stageData.soundEffects['audio/store-entrance-bell.ogg'].play() }, 500) stageData.customers[0].showDesire() closeShopUI(game, stageData) } export async function updatePotionShelfDisplay() { let storeShelfSpot = 0 for (let i = 0; i < storeShelfSlots.length; i++) { if (stageData.displayedPotions[i]) { game.entities.remove(stageData.displayedPotions[i]) game.scene.remove(stageData.displayedPotions[i]) stageData.displayedPotions[i] = null } const currentPotionInfo = stageData.potionInfo.find(potion => stageData.potionStocked[i] == potion.name) if (!currentPotionInfo) { continue } const currentPotion = await loadGltf(game, currentPotionInfo.model) currentPotion.position.copy(storeShelfSlots[storeShelfSpot++].position) currentPotion.potionData = currentPotionInfo stageData.displayedPotions[i] = currentPotion game.entities.push(currentPotion) game.scene.add(currentPotion) } } export function addValueToSave(container, key, value) { let containerString = localStorage.getItem(`${GAME_SAVE_KEY}-${key}`) if (!containerString) { container = [value] } else { container.push(value) } localStorage.setItem(`${GAME_SAVE_KEY}-${key}`, JSON.stringify(container)) } export function addAmountToSave(key, value) { let amount = parseInt(localStorage.getItem(`${GAME_SAVE_KEY}-${key}`)) ?? 0 amount += value localStorage.setItem(`${GAME_SAVE_KEY}-${key}`, amount) return amount } export function removeValueFromSave(container, key, value) { let containerString = localStorage.getItem(`${GAME_SAVE_KEY}-${key}`) if (!containerString) { container = [] } else { container.remove(value) } localStorage.setItem(`${GAME_SAVE_KEY}-${key}`, JSON.stringify(container)) } export function clearSaveData(stageData) { localStorage.removeItem(`${GAME_SAVE_KEY}-potion-inventory`) localStorage.removeItem(`${GAME_SAVE_KEY}-potion-stocked`) stageData.potionInventory = [] 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)) updateGameStatusUI(game, stageData) } export function loadSaveData(stageData) { let potionInventoryString = localStorage.getItem(`${GAME_SAVE_KEY}-potion-inventory`) if (!potionInventoryString) { localStorage.setItem(`${GAME_SAVE_KEY}-potion-inventory`, JSON.stringify([])) stageData.potionInventory = [] } else { stageData.potionInventory = JSON.parse(potionInventoryString) stageData.potionInventory.sort() } let potionsStockedString = localStorage.getItem(`${GAME_SAVE_KEY}-potion-stocked`) if (!potionsStockedString) { localStorage.setItem(`${GAME_SAVE_KEY}-potion-stocked`, JSON.stringify([])) stageData.potionStocked = [] } else { stageData.potionStocked = JSON.parse(potionsStockedString) stageData.potionStocked.sort() } stageData.currency = localStorage.getItem(`${GAME_SAVE_KEY}-currency`) if (!stageData.currency) { localStorage.setItem(`${GAME_SAVE_KEY}-currency`, 100) stageData.currency = 100 } stageData.currentDay = localStorage.getItem(`${GAME_SAVE_KEY}-currentday`) if (!stageData.currentDay) { 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 = ["mushroom", "mushroom", "pumpkin", "tomato", "tomato", "lettuce", "lettuce"] } 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) { game = inGame loadSaveData(stageData) stageData.currentRoom = 2 stageData.soundEffects = soundEffects stageData.selectedIngredients = [] stageData.cartItems = [] stageData.cameraPositions = [ { "camera": new THREE.Vector3(-15, 5, 7), "focus": new THREE.Vector3(-13, 0.1, 0) }, { "camera": new THREE.Vector3(1, 5, 7), "focus": new THREE.Vector3(0, 0.1, 0) }, { "camera": new THREE.Vector3(12, 5, 7), "focus": new THREE.Vector3(18, 0.1, 0) }, { "camera": new THREE.Vector3(1.37, 2.41, 3.77), "focus": new THREE.Vector3(-1.15, 0.79, -0.19) } ] const soundFilePaths = [ 'audio/click1.ogg', 'audio/sinkWater1.ogg', 'audio/doorOpen_1.ogg', 'audio/doorClose_4.ogg', 'audio/drawKnife2.ogg', 'audio/witch_cackle1.ogg', 'audio/bubbling.mp3', 'audio/chest_close_creak.ogg', 'audio/chest_open_creak.ogg', 'audio/handleCoins.ogg', 'audio/impactGlass_medium_000.ogg', 'audio/impactGlass_medium_001.ogg', 'audio/impactGlass_medium_002.ogg', 'audio/impactGlass_medium_003.ogg', 'audio/impactGlass_medium_004.ogg', 'audio/impactWood_heavy_002.ogg', 'audio/impactWood_heavy_004.ogg', 'audio/impactSoft_medium_002.ogg', 'audio/impactSoft_medium_004.ogg', 'audio/cash-register.ogg', 'audio/store-entrance-bell.ogg' ] soundFilePaths.forEach(path => { soundEffects[path] = new Howl({ src: [path], preload: true, }) }) 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) soundEffects['audio/bubbling.mp3'].volume(0.5) soundEffects['audio/store-entrance-bell.ogg'].volume(0.5) let levelLoader = new LevelLoader(game) game.camera.position.copy(stageData.cameraPositions[3].camera) game.lookAtFocus = stageData.cameraPositions[3].focus.clone() await loadRgbeBackground(game, 'kloppenheim_02_puresky_1k.hdr') game.scene.background = null game.scene.fog = new THREE.Fog(0x000000, 12, 20) let instancedMeshes = await levelLoader.load(levelData.default) instancedMeshes.forEach(instancedMesh => { game.scene.add(instancedMesh) }) stageData.ingredientInfo = ingredientInfo.default stageData.potionInfo = potionInfo.default Object.values(stageData.ingredientInfo).forEach(info => { ingredients.push(new Ingredient(info, game)) }) /////////////// /// Brewing /// /////////////// let shelfSpot = 0 ingredients.forEach(async ingredient => { await ingredient.spawn(shelfSlots[shelfSpot++]) }) stageData.candycorn = await loadGltf(game, 'models/candycorn.gltf') stageData.candycorn.position.x = -4.5 stageData.candycorn.position.z = -3.5 game.scene.add(stageData.candycorn) stageData.pumpkin2 = await loadGltf(game, 'models/pumpkin_orange.gltf') game.entities.push(stageData.pumpkin2) stageData.pumpkin2.position.x = -10 stageData.pumpkin2.position.y = 1 stageData.pumpkin2.position.z = 0.5 game.scene.add(stageData.pumpkin2) jackolantern = await loadGltf(game, 'models/pumpkin_orange_jackolantern.gltf') game.entities.push(jackolantern) jackolantern.spin = 0 jackolantern.position.set(3, 0.1, 1) jackolantern.rotateOnAxis(THREE.Object3D.DEFAULT_UP, -Math.PI / 4) game.scene.add(jackolantern) candle = await loadGltf(game, 'models/candle_thin.gltf.glb') game.entities.push(candle) candle.position.x = 0 candle.position.y = 1 candle.position.z = -2.5 candle.isLit = false game.scene.add(candle) candleLit = await loadGltf(game, 'models/candle_thin_lit.gltf.glb') game.entities.push(candleLit) candleLit.position.x = 0 candleLit.position.y = 1 candleLit.position.z = -2.5 candleLit.visible = false game.scene.add(candleLit) 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") cauldronBell.material.side = THREE.DoubleSide const cauldronTop = stageData.cauldron.getObjectByName("Sphere015_1") const cauldronTopMaterial = new THREE.ShaderMaterial({ fragmentShader: cauldronFragment, vertexShader: cauldronVertex, uniforms: stageData.cauldronUniforms, transparent: true, depthWrite: false, side: THREE.DoubleSide, lights: true, }) cauldronTop.material = cauldronTopMaterial game.entities.push(stageData.cauldron) stageData.cauldron.position.y = 0.1 game.scene.add(stageData.cauldron) stageData.brewTutorial1 = buildCanvasText("Click to Brew", { font: "86px Alice" }) stageData.brewTutorial1.position.z = 2.4 stageData.brewTutorial1.position.y = 0.2 stageData.brewTutorial1.rotateOnAxis(new THREE.Vector3(1, 0, 0), -Math.PI / 2) stageData.brewTutorial1.alreadySeen = false stageData.brewTutorial1.material.opacity = 0 stageData.brewTutorial1.castShadow = false game.scene.add(stageData.brewTutorial1) stageData.brewWitch = await loadGltf(game, 'characters/character_witch.gltf') game.entities.push(stageData.brewWitch) stageData.brewWitch.position.x = 1 stageData.brewWitch.position.y = 0.1 stageData.brewWitch.position.z = -1 stageData.brewWitch.stir = false stageData.brewWitch.bounce = false stageData.brewWitch.lookAt(stageData.cauldron.position) game.scene.add(stageData.brewWitch) stageData.shopWitch = await loadGltf(game, 'characters/character_witch.gltf') game.entities.push(stageData.shopWitch) stageData.shopWitch.position.x = -13 stageData.shopWitch.position.y = 0.1 stageData.shopWitch.position.z = -2 game.scene.add(stageData.shopWitch) stageData.marketWitch = await loadGltf(game, 'characters/character_witch.gltf') game.entities.push(stageData.marketWitch) stageData.marketWitch.position.x = 20 stageData.marketWitch.position.y = 0.1 stageData.marketWitch.position.z = 2.5 stageData.marketWitch.rotateOnAxis(THREE.Object3D.DEFAULT_UP, Math.PI) game.scene.add(stageData.marketWitch) stageData.spoon = await loadGltf(game, 'models/spoon.gltf') game.entities.push(stageData.spoon) stageData.spoon.rotateOnAxis(new THREE.Vector3(1, 0, 0), Math.PI) stageData.spoon.position.y = 1.25 stageData.spoon.visible = false game.scene.add(stageData.spoon) book = await loadGltf(game, 'models/book_grey.gltf.glb') game.entities.push(book) book.rotateOnAxis(THREE.Object3D.DEFAULT_UP, Math.PI) book.position.x = 4.6 book.position.y = 1.9 book.position.z = -3.9 game.scene.add(book) const bubbleGeometry = new THREE.SphereGeometry(0.1, 24, 24) const bubbleMaterial = cauldronTopMaterial //bubbleMaterial.uniforms.uTransparency.value = 0.3 stageData.bubbleParticles = new THREE.InstancedMesh(bubbleGeometry, bubbleMaterial, bubbleCount) stageData.bubbleParticles.instanceMatrix.setUsage(THREE.DynamicDrawUsage) stageData.bubbleParticles.visible = false game.scene.add(stageData.bubbleParticles) const matrix = new THREE.Matrix4() for (let index = 0; index < bubbleCount; index++) { bubblePositions[index] = new THREE.Vector3(0.5 * (Math.random() - 0.5), 0.5, 0.5 * (Math.random() - 0.5)) bubbleLifespan[index] = 7 - (index * 0.5) bubbleScale[index] = 0.2 bubbleVelocity[index] = new THREE.Vector3(0, 0.01, 0) } const quaternion = new THREE.Quaternion() const scaleVector = new THREE.Vector3(1, 1, 1) for (let index = 0; index < bubbleCount; index++) { quaternion.setFromAxisAngle(THREE.Object3D.DEFAULT_UP, 0) const scale = scaleVector.set(bubbleScale[index], bubbleScale[index], bubbleScale[index]) matrix.compose(bubblePositions[index], quaternion, scale) stageData.bubbleParticles.setMatrixAt(index, matrix) } doorway = await loadGltf(game, 'models/wall_doorway.glb') doorway.position.set(-6, 0, -2) doorway.castShadow = false doorway.traverse((child) => { if (child.isMesh) { child.castShadow = false } }) doorway.rotateOnAxis(THREE.Object3D.DEFAULT_UP, Math.PI / 2) doorway.isOpen = false doorway.isMoving = false game.entities.push(doorway) game.scene.add(doorway) doorway2 = await loadGltf(game, 'models/wall_doorway.glb') doorway2.position.set(6, 0, 2) doorway2.castShadow = false doorway2.traverse((child) => { if (child.isMesh) { child.castShadow = false } }) doorway2.rotateOnAxis(THREE.Object3D.DEFAULT_UP, Math.PI / 2) doorway2.isOpen = false doorway2.isMoving = false game.entities.push(doorway2) game.scene.add(doorway2) //////////// /// Shop /// //////////// // const debugSphereGeometry = new THREE.SphereGeometry(0.1) // const debugMaterial = new THREE.MeshBasicMaterial({color: "magenta"}) // const debugSphere = new THREE.Mesh(debugSphereGeometry, debugMaterial) // customerSlots.forEach(slot => { // debugSphere.position.copy(slot.position) // game.scene.add(debugSphere.clone()) // }) stageData.displayedPotions = [] updatePotionShelfDisplay() lollipop = await loadGltf(game, 'models/lollipop_green.gltf') lollipop.position.x = -11.5 lollipop.position.y = 1.28 lollipop.position.z = -3.4 lollipop.rotateOnAxis(new THREE.Vector3(1, 0, 0), Math.PI / 2) lollipop.rotateOnAxis(new THREE.Vector3(0, 0, 1), Math.PI / 2 + -0.3) game.entities.push(lollipop) game.scene.add(lollipop) stageData.chest = await loadGltf(game, 'models/chest_large.glb') stageData.chest.scale.set(0.5, 0.5, 0.5) stageData.chest.position.x = -15.5 stageData.chest.position.y = 1 stageData.chest.position.z = 0 stageData.chest.rotateOnAxis(THREE.Object3D.DEFAULT_UP, Math.PI / 4) game.entities.push(stageData.chest) game.scene.add(stageData.chest) stageData.shopTutorial1 = buildCanvasText("Click to Sell", { font: "100px Alice" }) stageData.shopTutorial1.position.x = -15.5 stageData.shopTutorial1.position.z = 0.2 stageData.shopTutorial1.position.y = 1.8 stageData.shopTutorial1.alreadySeen = false stageData.shopTutorial1.scale.set(0.6, 0.6, 0.6) stageData.shopTutorial1.material.opacity = 0 stageData.shopTutorial1.castShadow = false game.scene.add(stageData.shopTutorial1) stageData.shopTutorial2 = buildCanvasText("Pick the correct potion", { font: "72px Alice" }) stageData.shopTutorial2.position.x = -10 stageData.shopTutorial2.position.z = -3.4 stageData.shopTutorial2.position.y = 1.5 stageData.shopTutorial2.alreadySeen = false stageData.shopTutorial2.scale.set(1, 1, 1) stageData.shopTutorial2.material.opacity = 0 stageData.shopTutorial2.castShadow = false game.scene.add(stageData.shopTutorial2) stageData.coin = await loadGltf(game, 'models/coin.gltf.glb') stageData.coin.position.x = -12 stageData.coin.position.y = 1.1 stageData.coin.position.z = 0 game.entities.push(stageData.coin) game.scene.add(stageData.coin) stageData.customers = [] ////////////// /// Market /// ////////////// stageData.sellingSkeleton = await loadGltf(game, 'characters/character_skeleton_mage.gltf') stageData.sellingSkeleton.position.x = 20 stageData.sellingSkeleton.position.y = 0.1 stageData.sellingSkeleton.position.z = -1 stageData.sellingSkeleton.rotateOnAxis(THREE.Object3D.DEFAULT_UP, -Math.PI / 4) game.entities.push(stageData.sellingSkeleton) game.scene.add(stageData.sellingSkeleton) stageData.sign = await loadGltf(game, 'models/sign_left.gltf') stageData.sign.position.x = 14 stageData.sign.position.y = 0.1 stageData.sign.position.z = 2 game.entities.push(stageData.sign) game.scene.add(stageData.sign) 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 coffin.position.z = 6 coffin.rotateOnAxis(THREE.Object3D.DEFAULT_UP, -Math.PI / 4) game.scene.add(coffin) const candy = await loadGltf(game, 'models/candy_orange_A.gltf') candy.position.x = 21 candy.position.y = 0.25 candy.position.z = 6 candy.rotateOnAxis(THREE.Object3D.DEFAULT_UP, -Math.PI / 4) game.scene.add(candy) const candyBucket = await loadGltf(game, 'models/candy_bucket_B_decorated.gltf') candyBucket.position.x = 21.5 candyBucket.position.y = 0.1 candyBucket.position.z = 5.5 candyBucket.rotateOnAxis(THREE.Object3D.DEFAULT_UP, -Math.PI / 2) game.scene.add(candyBucket) /** * Fireflies */ stageData.firefliesUniforms = { uTime: { value: 0 }, uPixelRatio: { value: Math.min(window.devicePixelRatio, 2) } } const firefliesMaterial = new THREE.ShaderMaterial({ fragmentShader: firefliesFragment, vertexShader: firefliesVertex, uniforms: stageData.firefliesUniforms, transparent: true, depthWrite: false, blending: THREE.AdditiveBlending }) const firefliesGeometry = new THREE.BufferGeometry() const firefliesCount = 50 const positionArray = new Float32Array(firefliesCount * 3) const scaleArray = new Float32Array(firefliesCount) for (let i = 0; i < firefliesCount; i++) { positionArray[i * 3 + 0] = 24 * (Math.random() - 0.5) + 18 positionArray[i * 3 + 1] = 4 * Math.random() + 2 positionArray[i * 3 + 2] = 16 * (Math.random() - 0.5) scaleArray[i] = 0.8 * Math.random() + 1.2 } firefliesGeometry.setAttribute('position', new THREE.BufferAttribute(positionArray, 3)) firefliesGeometry.setAttribute('aScale', new THREE.BufferAttribute(scaleArray, 1)) const fireflies = new THREE.Points(firefliesGeometry, firefliesMaterial) game.scene.add(fireflies) //////////////// /// Lighting /// //////////////// const ambientLight = new THREE.AmbientLight(0xFFFFFF, 1) game.scene.add(ambientLight) const shadowSize = 24 directionalLight = new THREE.DirectionalLight(0xFFFFFF, 0.9) directionalLight.position.set(8, 10, 6) directionalLight.castShadow = true directionalLight.shadow.camera.left = -shadowSize directionalLight.shadow.camera.right = shadowSize directionalLight.shadow.camera.top = -shadowSize directionalLight.shadow.camera.bottom = shadowSize directionalLight.shadow.camera.far = 28 directionalLight.shadow.mapSize.width = Math.min(game.renderer.capabilities.maxTextureSize, 2048) directionalLight.shadow.mapSize.height = Math.min(game.renderer.capabilities.maxTextureSize, 2048) directionalLight.shadow.bias = -0.005 directionalLight.shadow.radius = 6 directionalLight.cameraOffset = new THREE.Vector3() directionalLight.cameraOffset.copy(directionalLight.position) directionalLight.cameraOffset.sub(new THREE.Vector3(0, 0, 0)) directionalLight.target = game.camera directionalLight.update = function () { const currentOffset = new THREE.Vector3() currentOffset.copy(directionalLight.cameraOffset).add(game.camera.position) directionalLight.position.set(currentOffset.x, currentOffset.y, currentOffset.z) if (directionalLight.shadow) { directionalLight.shadow.camera.position.set(currentOffset.x, currentOffset.y, currentOffset.z) } } game.scene.add(directionalLight) // const shadowHelper = new THREE.CameraHelper( directionalLight.shadow.camera ) // game.scene.add(shadowHelper) mainMenuUI(game, stageData) optionsUI(game, stageData) creditsUI(game, stageData) navigationUI(game, stageData) brewUI(game, stageData) shopUI(game, stageData) marketUI(game, stageData) gameOverUI(game, stageData) updateGameStatusUI(game, stageData) stageData.beginBrew = () => { beginBrew(game, stageData) } stageData.beginSell = () => { beginSell(game, stageData) } stageData.buyIngredients = () => { buyIngredients(game, stageData)} stageData.adjustMasterVolume = (volume) => { Howler.volume(volume / 100) } stageData.adjustMusicVolume = (volume) => { stageData.musicLoop.volume(volume / 100) } /////////// // DEBUG // /////////// // moveToRoom(ROOM_SHOP) // closeMainMenuUI(game, stageData) // const planeGeometry = new THREE.PlaneGeometry(1,1) // const debugPlane = new THREE.Mesh(planeGeometry, textBubbleMaterial) // debugPlane.position.x = 2 // debugPlane.position.y = 1 // debugPlane.scale.set(2,2,2) // game.scene.add(debugPlane) } export function update(game) { let elapsedTime = game.clock.getElapsedTime() ingredients.forEach(ingredient => { ingredient.wobble() }) stageData.candycorn.position.y = 0.125 * Math.sin(4.1 * elapsedTime) + 3.0 stageData.candycorn.rotateOnAxis(THREE.Object3D.DEFAULT_UP, -0.02) directionalLight.update() raycast.setFromCamera(game.mousePosition, game.camera) Howler.pos(game.camera.position.x, game.camera.position.y, game.camera.position.z) Howler.orientation(game.camera.position.x, game.camera.position.y, game.camera.position.z, 0, 1, 0) const quaternion = new THREE.Quaternion() const scaleVector = new THREE.Vector3(1, 1, 1) const matrix = new THREE.Matrix4() for (let index = 0; index < bubbleCount; index++) { bubblePositions[index].add(bubbleVelocity[index]) bubbleScale[index] = Math.min(bubbleScale[index] + 0.01, 1) bubbleLifespan[index] -= 0.1 if (bubbleLifespan[index] <= 0) { bubbleLifespan[index] = 7 bubblePositions[index].set(0.5 * (Math.random() - 0.5), 0.5, 0.5 * (Math.random() - 0.5)) bubbleScale[index] = 0.2 } //quaternion.setFromAxisAngle(THREE.Object3D.DEFAULT_UP, 0) const scale = scaleVector.set(bubbleScale[index], bubbleScale[index], bubbleScale[index]) matrix.compose(bubblePositions[index], quaternion, scale) stageData.bubbleParticles.setMatrixAt(index, matrix) } stageData.bubbleParticles.instanceMatrix.needsUpdate = true if (stageData.brewWitch.stir) { stageData.spoon.position.x = 0.125 * Math.sin(4 * elapsedTime) stageData.spoon.position.z = 0.125 * Math.cos(4 * elapsedTime) } if (stageData.brewWitch.bounce) { stageData.brewWitch.scale.x = 1 + (0.03125 * Math.sin(12 * elapsedTime)) stageData.brewWitch.scale.y = 1 + (-0.03125 * Math.sin(12 * elapsedTime)) stageData.brewWitch.scale.z = 1 + (0.03125 * Math.sin(12 * elapsedTime)) } else { stageData.brewWitch.scale.x = 1 stageData.brewWitch.scale.y = 1 stageData.brewWitch.scale.z = 1 } if (jackolantern.spin > 0) { jackolantern.spin -= 0.05 jackolantern.rotateOnAxis(THREE.Object3D.DEFAULT_UP, -0.1) if (jackolantern.spin < 0) { jackolantern.spin = 0 } } stageData.sellingSkeleton.scale.x = 1 + (0.03125 * Math.sin(12 * elapsedTime)) stageData.sellingSkeleton.scale.y = 1 + (-0.03125 * Math.sin(12 * elapsedTime)) stageData.sellingSkeleton.scale.z = 1 + (0.03125 * Math.sin(12 * elapsedTime)) stageData.firefliesUniforms.uTime.value = elapsedTime stageData.cauldronUniforms.uTime.value = elapsedTime stageData.customers.forEach(customer => { customer.update(elapsedTime) }) candle.position.y = 0.625 * Math.sin(0.5 * elapsedTime) + 1.3 candleLit.position.y = 0.625 * Math.sin(0.5 * elapsedTime) + 1.3 let intersects = raycast.intersectObjects(game.entities.filter(entity => entity.visible)) game.outlinePass.selectedObjects = [] document.body.style.cursor = "default" if (intersects.length > 0) { document.body.style.cursor = "pointer" if(!stageData.chest.isStocking && !stageData.isSellingPotions && !stageData.cauldron.isBrewing) { highlightParents(intersects, [stageData.cauldron, stageData.shopWitch, stageData.sellingSkeleton]) } else if(stageData.isSellingPotions) { highlightParents(intersects, stageData.displayedPotions) } } } function highlightParents(intersects, entities) { intersects.forEach(intersect => { entities.forEach(entity => { if(isAChildOf(entity, intersect.object)) { game.outlinePass.selectedObjects = [entity] return } }) }) } function isAChildOf(parent, childToCheck) { if (parent == childToCheck) return true if (childToCheck.parent != null) { return isAChildOf(parent, childToCheck.parent) } return false } export function onClick() { //drawDebugLine(game, witch.position, getForwardVector(witch).multiplyScalar(10), 0xff0000, 1000) //drawDebugLine(game, witch.position, getRightVector(witch).multiplyScalar(10), 0x00ff00, 1000) let intersects = raycast.intersectObjects(game.entities.filter(entity => entity.visible)) if (intersects.length > 0) { intersects.every(intersect => { switch (stageData.currentRoom) { case ROOM_SHOP: //SHOP CLICK ACTIONS HERE if (isAChildOf(stageData.chest, intersect.object)) { if (stageData.isSellingPotions) { return false } if (stageData.shopInactivityHandle) { clearTimeout(stageData.shopInactivityHandle) stageData.shopInactivityHandle = 0 } if (stageData.chest.isStocking) { closeShopUI(game, stageData) } else { openShopUI(game, stageData) if (stageData.shopTutorial1.material.opacity > 0) { gsap.to(stageData.shopTutorial1.material, { duration: 1.5, opacity: 0, onComplete: () => { stageData.shopTutorial1.castShadow = false stageData.shopTutorial1.alreadySeen = true } }) } } return false } if (isAChildOf(stageData.shopWitch, intersect.object)) { if (stageData.isSellingPotions) { return false } if (stageData.shopInactivityHandle) { clearTimeout(stageData.shopInactivityHandle) stageData.shopInactivityHandle = 0 } if (stageData.chest.isStocking) { closeShopUI(game, stageData) } else { openShopUI(game, stageData) if (stageData.shopTutorial1.material.opacity > 0) { gsap.to(stageData.shopTutorial1.material, { duration: 1.5, opacity: 0, onComplete: () => { stageData.shopTutorial1.castShadow = false stageData.shopTutorial1.alreadySeen = true } }) } } return false } if (isAChildOf(stageData.coin, intersect.object)) { let coinMotionPath = [{ x: -12, y: 1.1, z: 1 }, { x: -12, y: 2, z: 2 }, { x: -15.5, y: 2, z: 2 }, { x: -15.5, y: 1.2, z: 0 }] gsap.to(stageData.coin.position, { duration: 3.5, motionPath: coinMotionPath, onComplete: () => { stageData.soundEffects['audio/handleCoins.ogg'].play() closeChest(stageData.chest, () => { stageData.coin.scale.set(0, 0, 0) stageData.coin.position.set(-12, 1.1, 0) gsap.to(stageData.coin.scale, { ease: "elastic", duration: 0.7, x: 1, y: 1, z: 1, }) }) } }) setTimeout(() => { openChest(stageData.chest) }, 700) return false } stageData.displayedPotions.forEach((potion) => { if (isAChildOf(potion, intersect.object)) { if (stageData.isSellingPotions) { if (stageData.sellInactivityHandle) { clearTimeout(stageData.sellInactivityHandle) stageData.sellInactivityHandle = 0 } if (stageData.customers[0].isMatchingPotion(potion.potionData)) { if (stageData.shopTutorial2.material.opacity > 0) { gsap.to(stageData.shopTutorial2.material, { duration: 1.5, opacity: 0, onComplete: () => { stageData.shopTutorial2.castShadow = false stageData.shopTutorial2.alreadySeen = true } }) } const firstCustomer = stageData.customers[0] firstCustomer.hideDesire() const potionMotionPath = [{ x: -11.5, y: 2.75, z: -3.25 }, { x: -12, y: 2, z: -2 }, { x: -13, y: 0.75, z: 2.5 }] gsap.to(potion.position, { duration: 2, motionPath: potionMotionPath, onComplete: () => { removeValueFromSave(stageData.potionStocked, 'potion-stocked', potion.potionData.name) updatePotionShelfDisplay() firstCustomer.acceptPotion() stageData.soundEffects['audio/cash-register.ogg'].play() stageData.currency = addAmountToSave("currency", potion.potionData.value) updateGameStatusUI(game, stageData) //move money from customer to chest //add money to save data //move customer out of room nextCustomer() //display next customer desire //if no more customers or no more valid potions, end selling } }) } else { //TODO: potion does not match playBottleClink() gsap.fromTo(potion.rotation, { duration: 0.1, z: 0.2 }, { duration: 0.1, z: 0 }) stageData.customers[0].rejectPotion() if (stageData.customers[0].rejectCount >= 3) { stageData.customers[0].showSad() nextCustomer() } } } else { playBottleClink() gsap.fromTo(potion.rotation, { duration: 0.1, z: 0.2 }, { duration: 0.1, z: 0 }) } } }) if (isAChildOf(stageData.pumpkin2, intersect.object)) { if (stageData.pumpkin2.isMoving) { return } stageData.pumpkin2.isMoving = true gsap.to(stageData.pumpkin2.position, { duration: 2, y: 3, onComplete: () => { setTimeout(() => { stageData.soundEffects['audio/impactWood_heavy_002.ogg'].play() }, 400) setTimeout(() => { stageData.soundEffects['audio/impactSoft_medium_002.ogg'].play() }, 800) gsap.to(stageData.pumpkin2.position, { duration: 1, y: 1, ease: "bounce", onComplete: () => { stageData.pumpkin2.isMoving = false } }) } }) return false } break; case ROOM_BREW: //BREW CLICK ACTIONS HERE if (isAChildOf(jackolantern, intersect.object)) { soundEffects['audio/drawKnife2.ogg'].play() jackolantern.spin += Math.PI return false } if (isAChildOf(book, intersect.object)) { //soundEffects['audio/drawKnife2.ogg'].play() //jackolantern.spin += Math.PI let bookMotionPath = [{ x: 4.6, y: 1.9, z: 0 }, { x: 4.6, y: 1.9, z: -3.9 }] gsap.to(book.position, { duration: 6, motionPath: bookMotionPath, onComplete: () => { } }) return false } if (isAChildOf(stageData.brewWitch, intersect.object)) { if (stageData.cauldron.isBrewing) { return false } if (stageData.brewInactivityHandle) { clearTimeout(stageData.brewInactivityHandle) stageData.brewInactivityHandle = 0 } if (stageData.cauldron.brewMenuOpen) { closeBrewUI(game, stageData) } else { openBrewUI(game, stageData) //cauldron.isBrewing = true if (stageData.brewTutorial1.material.opacity > 0) { gsap.to(stageData.brewTutorial1.material, { duration: 1.5, opacity: 0, onComplete: () => { stageData.brewTutorial1.castShadow = false stageData.brewTutorial1.alreadySeen = true } }) } } return false } if (isAChildOf(stageData.cauldron, intersect.object)) { if (stageData.cauldron.isBrewing) { return false } if (stageData.brewInactivityHandle) { clearTimeout(stageData.brewInactivityHandle) stageData.brewInactivityHandle = 0 } if (stageData.cauldron.brewMenuOpen) { closeBrewUI(game, stageData) } else { openBrewUI(game, stageData) if (stageData.brewTutorial1.material.opacity > 0) { gsap.to(stageData.brewTutorial1.material, { duration: 1.5, opacity: 0, onComplete: () => { stageData.brewTutorial1.castShadow = false stageData.brewTutorial1.alreadySeen = true } }) } //cauldron.isBrewing = true } // let soundId = soundEffects['audio/sinkWater1.ogg'].play() // soundEffects['audio/sinkWater1.ogg'].once('play', () => { // soundEffects['audio/sinkWater1.ogg'].volume(0.5, soundId) // // soundEffects['audio/sinkWater1.ogg'].pos(cauldron.position.x, cauldron.position.y, cauldron.position.z, soundId) // // soundEffects['audio/sinkWater1.ogg'].pannerAttr({ // // panningModel: 'HRTF', // // refDistance: 1.0, // // rolloffFactor: 0.8, // // distanceModel: 'exponential', // // }, soundId) // soundEffects['audio/sinkWater1.ogg'].stereo(1, soundId) // //console.log(soundEffects['audio/sinkWater1.ogg']) // }, soundId) return false } if (isAChildOf(candle, intersect.object)) { candle.isLit = true candleLit.visible = true candle.visible = false return false } if (isAChildOf(candleLit, intersect.object)) { candle.isLit = false candleLit.visible = false candle.visible = true return false } break; case ROOM_MARKET: //MARKET CLICK ACTIONS HERE 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; } if (isAChildOf(doorway, intersect.object)) { if (stageData.currentRoom == ROOM_BREW) { previousRoom(game, stageData) } else if (stageData.currentRoom == ROOM_SHOP) { nextRoom(game, stageData) } return false } if (isAChildOf(doorway2, intersect.object)) { if (stageData.currentRoom == ROOM_BREW) { nextRoom(game, stageData) } else if (stageData.currentRoom == ROOM_MARKET) { previousRoom(game, stageData) } return false } }) } } function nextCustomer() { const firstCustomer = stageData.customers.shift() if (!firstCustomer) { return } 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() }, 3000) gsap.to(firstCustomer.object3d.position, { duration: 8, motionPath: customerMotionPath, onComplete: () => { game.scene.remove(firstCustomer.object3d) } }) gsap.to(firstCustomer.object3d.rotation, { duration: 1, y: Math.PI, onComplete: () => { }, onUpdate: () => { firstCustomer.requestBillboard.lookAt(game.camera.position) } }) //const shopperPosition = customerSlots[stageData.customers.length].position //move other customers forward for (let i = 0; i < stageData.customers.length; i++) { const customer = stageData.customers[i] const shopperPosition = customerSlots[i].position gsap.to(customer.object3d.position, { duration: 1.5, x: shopperPosition.x, y: shopperPosition.y, z: shopperPosition.z }) } if (stageData.customers.length == 0) { stageData.isSellingPotions = false stageData.currentDay = addAmountToSave("currentday", 1) updateGameStatusUI(game, stageData) if(stageData.currency >= 1000) { showGameOverUI(game, stageData) } } else { stageData.customers[0].showDesire() if (stageData.potionStocked.length < 1) { stageData.customers[0].rejectPotion(3) stageData.customers[0].showSad() nextCustomer() } } } export function playBottleClink() { setTimeout(() => { let randomSound2 = Math.floor(5 * Math.random()) stageData.soundEffects[`audio/impactGlass_medium_00${randomSound2}.ogg`].play() }, 50) let randomSound = Math.floor(5 * Math.random()) stageData.soundEffects[`audio/impactGlass_medium_00${randomSound}.ogg`].play() } function openChest(chest) { let lid = chest.getObjectByName("chest_large_lid") if (chest.isOpen) { return } if (!chest.isMoving) { soundEffects['audio/chest_open_creak.ogg'].play() chest.isMoving = true gsap.to(lid.rotation, { duration: 2.5, x: -(Math.PI / 2) + (Math.PI / 8), ease: "elastic", onComplete: () => { chest.isOpen = true chest.isMoving = false } }) } } function closeChest(chest, onComplete = () => { }) { let lid = chest.getObjectByName("chest_large_lid") if (!chest.isOpen) { return } if (!chest.isMoving) { soundEffects['audio/chest_close_creak.ogg'].play() chest.isMoving = true gsap.to(lid.rotation, { duration: 1.5, x: 0, ease: "bounce", onComplete: () => { chest.isOpen = false chest.isMoving = false onComplete() } }) } } function openDoorway(doorway) { let door = doorway.getObjectByName("wall_doorway_door") if (doorway.isOpen) { return } if (!doorway.isMoving) { soundEffects['audio/doorOpen_1.ogg'].play() doorway.isMoving = true gsap.to(door.rotation, { duration: 2.5, y: -Math.PI / 2, ease: "elastic", onComplete: () => { doorway.isOpen = true doorway.isMoving = false } }) } } function closeDoorway(doorway) { let door = doorway.getObjectByName("wall_doorway_door") if (!doorway.isOpen) { return } if (!doorway.isMoving) { soundEffects['audio/doorClose_4.ogg'].play() doorway.isMoving = true gsap.to(door.rotation, { duration: 2.5, y: 0, ease: "elastic", onComplete: () => { doorway.isOpen = false doorway.isMoving = false } }) } } function moveToShop() { closeBrewUI(game, stageData) openDoorway(doorway) const camPosition = stageData.cameraPositions[0] gsap.to(game.camera.position, { duration: 2.5, x: camPosition.camera.x, y: camPosition.camera.y, z: camPosition.camera.z, onComplete: () => { closeDoorway(doorway) } }) gsap.to(game.lookAtFocus, { duration: 2.5, x: camPosition.focus.x, y: camPosition.focus.y, z: camPosition.focus.z }) stageData.currentRoom = ROOM_SHOP shopTutorialPrompt(stageData) } function moveToBrew() { let doorwayToOpen = doorway if (stageData.currentRoom == ROOM_MARKET) { doorwayToOpen = doorway2 } openDoorway(doorwayToOpen) 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, onComplete: () => { closeDoorway(doorwayToOpen) } }) gsap.to(game.lookAtFocus, { duration: 2.5, x: camPosition.focus.x, y: camPosition.focus.y, z: camPosition.focus.z }) stageData.currentRoom = ROOM_BREW brewTutorialPrompt(stageData) } function moveToMarket() { closeBrewUI(game, stageData) openDoorway(doorway2) const camPosition = stageData.cameraPositions[2] gsap.to(game.camera.position, { duration: 2.5, x: camPosition.camera.x, y: camPosition.camera.y, z: camPosition.camera.z, onComplete: () => { closeDoorway(doorway2) } }) gsap.to(game.lookAtFocus, { duration: 2.5, x: camPosition.focus.x, y: camPosition.focus.y, z: camPosition.focus.z }) stageData.currentRoom = ROOM_MARKET } export function moveToRoom(roomId) { if (roomId == stageData.currentRoom) { return } switch (roomId) { case ROOM_SHOP: moveToShop() break; case ROOM_BREW: moveToBrew() break; case ROOM_MARKET: moveToMarket() break; } } let keyHistory = [] export function onKeyPress(code) { if (game.keyboard['Digit1'] && stageData.currentRoom == ROOM_BREW) { moveToRoom(ROOM_SHOP) keyHistory = [] } if (game.keyboard['Digit2'] && stageData.currentRoom != ROOM_BREW) { moveToRoom(ROOM_BREW) keyHistory = [] } if (game.keyboard['Digit3'] && stageData.currentRoom == ROOM_BREW) { moveToRoom(ROOM_MARKET) keyHistory = [] } if (game.keyboard['Digit9'] || game.keyboard['F9']) { game.orbitControls.enabled = !game.orbitControls.enabled if (!game.orbitControls.enabled) { console.log(`position:`, game.camera.position) console.log(`focus:`, game.orbitControls.target) moveToRoom(stageData.currentRoom) keyHistory = [] } } if (game.keyboard['Escape']) { returnToMainMenu(game, stageData) keyHistory = [] } keyHistory.push(code.replace("Key", "")) const cheatcode = keyHistory.join("").toLowerCase() if(cheatcode.includes("ghostbux")) { keyHistory = [] stageData.currency += 1000 addAmountToSave("currency", 1000) updateGameStatusUI(game, stageData) stageData.soundEffects['audio/witch_cackle1.ogg'].play() } if(cheatcode.includes("awesomesauce")) { keyHistory = [] stageData.potionInventory.push(...stageData.potionInfo.map(info => info.name)) addValueToSave(stageData.potionInventory, "potion-inventory", "spooksauce") stageData.soundEffects['audio/witch_cackle1.ogg'].play() } if(cheatcode.includes("yeschef")) { keyHistory = [] //TODO: grant all ingredients stageData.soundEffects['audio/witch_cackle1.ogg'].play() } } 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() stageData.brewTutorial1.material.opacity = 0 stageData.brewTutorial1.castShadow = false stageData.brewTutorial1.alreadySeen = false stageData.shopTutorial1.material.opacity = 0 stageData.shopTutorial1.castShadow = false stageData.shopTutorial1.alreadySeen = false stageData.shopTutorial2.material.opacity = 0 stageData.shopTutorial2.castShadow = false stageData.shopTutorial2.alreadySeen = false updatePotionShelfDisplay() updateGameStatusUI(game, stageData) }