game.js 56 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463
  1. import * as THREE from 'three'
  2. import gsap from 'gsap'
  3. import { Howl, Howler } from 'howler'
  4. import LevelLoader from './library/levelloader.js'
  5. import { loadGltf } from './library/loadgltf.js'
  6. import { loadRgbeBackground } from './library/loadbackground.js'
  7. import { getForwardVector, getRightVector, drawDebugLine } from './library/mathhelpers.js'
  8. import Ingredient from './library/ingredient.js'
  9. import './library/arrayhelpers.js'
  10. // Shaders
  11. import firefliesVertex from './shaders/firefly/vertex.glsl'
  12. import firefliesFragment from './shaders/firefly/fragment.glsl'
  13. import cauldronVertex from './shaders/cauldron/vertex.glsl'
  14. import cauldronFragment from './shaders/cauldron/fragment.glsl'
  15. // UI
  16. import { brewTutorialPrompt, closeMainMenuUI, mainMenuUI, openMainMenuUI } from './ui/mainmenuui.js'
  17. import { brewUI, openBrewUI, closeBrewUI } from './ui/gameui.js'
  18. import { closeShopUI, openShopUI, shopTutorialPrompt, shopUI } from './ui/shopui.js'
  19. import { navigationUI, hideNavigationUI, nextRoom, previousRoom } from './ui/navigationui.js'
  20. // Game Data
  21. import * as levelData from './data/level.json'
  22. import * as ingredientInfo from './data/ingredients.json'
  23. import * as potionInfo from './data/potions.json'
  24. import Shopper from './shopper.js'
  25. import { buildCanvasText } from './library/canvastext.js'
  26. import { hideGameStatusUI, updateGameStatusUI } from './ui/gamestatusui.js'
  27. import { gameOverUI, showGameOverUI } from './ui/gameoverui.js'
  28. export const GAME_SAVE_KEY = "spookonomics-v1"
  29. const raycast = new THREE.Raycaster()
  30. let game, jackolantern, doorway, directionalLight, doorway2, lollipop, candle, candleLit, book
  31. const ingredients = []
  32. const soundEffects = {}
  33. const stageData = {}
  34. export const ROOM_SHOP = 1;
  35. export const ROOM_BREW = 2;
  36. export const ROOM_MARKET = 3;
  37. const shelfSlots = [
  38. { "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 }] },
  39. { "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 }] },
  40. { "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 }] },
  41. { "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 }] }
  42. ]
  43. const storeShelfSlots = [
  44. { "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 }] },
  45. { "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 }] },
  46. { "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 }] },
  47. { "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 }] },
  48. { "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 }] },
  49. { "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 }] },
  50. ]
  51. const customerSlots = [
  52. { "position": new THREE.Vector3(-12.9, 0.1, 3) },
  53. { "position": new THREE.Vector3(-13.1, 0.1, 4) },
  54. { "position": new THREE.Vector3(-12.7, 0.1, 5) },
  55. { "position": new THREE.Vector3(-13.1, 0.1, 6) },
  56. { "position": new THREE.Vector3(-12.9, 0.1, 7) },
  57. { "position": new THREE.Vector3(-13, 0.1, 8) },
  58. ]
  59. const bubblePositions = []
  60. const bubbleVelocity = []
  61. const bubbleLifespan = []
  62. const bubbleScale = []
  63. const bubbleCount = 15
  64. async function beginBrew(game, stageData) {
  65. if (!stageData.potionToBrew) {
  66. stageData.soundEffects['audio/witch_cackle1.ogg'].play()
  67. return
  68. }
  69. stageData.cauldron.isBrewing = true
  70. closeBrewUI(game, stageData)
  71. stageData.brewWitch.stir = true
  72. stageData.brewWitch.bounce = true
  73. stageData.spoon.visible = true
  74. stageData.selectedIngredients.forEach(ingredientName => {
  75. ingredients.find((ingredient => ingredient.getName() == ingredientName)).beginBrew()
  76. })
  77. stageData.bottle1 = await loadGltf(game, stageData.potionToBrew.model)
  78. stageData.bottle1.position.y = -1.5
  79. game.scene.add(stageData.bottle1)
  80. stageData.selectedIngredients = []
  81. stageData.soundEffects['audio/click1.ogg'].play()
  82. const nextColorData = stageData.potionToBrew.color
  83. stageData.cauldronUniforms.uNextPotionColor.value = new THREE.Color(nextColorData.r, nextColorData.g, nextColorData.b)
  84. stageData.cauldronUniforms.uBlendTime.value = 0.0
  85. setTimeout(() => {
  86. stageData.soundEffects['audio/bubbling.mp3'].volume(0.5)
  87. stageData.soundEffects['audio/bubbling.mp3'].play()
  88. stageData.bubbleParticles.visible = true
  89. gsap.to(stageData.cauldronUniforms.uBlendTime, {
  90. value: 1, duration: 4.5, onComplete: () => {
  91. stageData.cauldronUniforms.uPotionColor.value = stageData.cauldronUniforms.uNextPotionColor.value
  92. stageData.cauldronUniforms.uBlendTime.value = 0.0
  93. }
  94. })
  95. }, 900)
  96. setTimeout(() => {
  97. addValueToSave(stageData.potionInventory, 'potion-inventory', stageData.potionToBrew.name)
  98. stageData.potionToBrew = null
  99. stageData.selectedIngredients = []
  100. stageData.bottle1.position.set(0, -1.5, 0)
  101. stageData.soundEffects['audio/bubbling.mp3'].fade(1, 0, 300)
  102. stageData.brewWitch.stir = false
  103. stageData.brewWitch.bounce = false
  104. stageData.spoon.visible = false
  105. stageData.bubbleParticles.visible = false
  106. gsap.to(stageData.bottle1.position, {
  107. duration: 3, y: 1.5, ease: "elastic", onComplete: () => {
  108. setTimeout(() => {
  109. game.scene.remove(stageData.bottle1)
  110. //TODO play potion particle effect
  111. stageData.cauldron.isBrewing = false
  112. }, 1000)
  113. }
  114. })
  115. }, 5000)
  116. }
  117. async function customerBuilder(randomPotion, delay) {
  118. const shopperPosition = customerSlots[stageData.customers.length].position
  119. const randomIndex = Math.floor(Math.random() * 3)
  120. let shopper
  121. switch (randomIndex) {
  122. case 0:
  123. 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) {
  124. this.object3d.position.y = 0.0625 * Math.sin(2 * elapsedTime) + 0.5
  125. })
  126. break;
  127. case 1:
  128. shopper = new Shopper(game, 'characters/character_skeleton_minion.gltf', randomPotion, new THREE.Vector3(1, 1, 1), shopperPosition, Math.PI, function (elapsedTime) {
  129. this.object3d.scale.x = 1 + (0.03125 * Math.sin(11.8 * elapsedTime))
  130. this.object3d.scale.y = 1 + (-0.03125 * Math.sin(11.8 * elapsedTime))
  131. this.object3d.scale.z = 1 + (0.03125 * Math.sin(11.8 * elapsedTime))
  132. })
  133. break;
  134. case 2:
  135. shopper = new Shopper(game, 'characters/ghost_1.gltf.glb', randomPotion, new THREE.Vector3(3.5, 3.5, 3.5), shopperPosition, Math.PI, function (elapsedTime) {
  136. this.object3d.position.y = 0.0625 * Math.sin(2.2 * elapsedTime) + 0.5
  137. })
  138. break;
  139. }
  140. await shopper.init(delay)
  141. return shopper
  142. }
  143. export function sellTutorialPrompt(stageData) {
  144. if(stageData.shopTutorial2.alreadySeen) {
  145. return
  146. }
  147. stageData.shopInactivityHandle = setTimeout(() => {
  148. gsap.to(stageData.shopTutorial2.material, {duration: 1.5, opacity: 1, onStart: () => {
  149. stageData.shopTutorial2.castShadow = true
  150. }})
  151. }, 5000)
  152. }
  153. async function beginSell(game, stageData) {
  154. if (stageData.potionStocked.length < 1) {
  155. stageData.soundEffects['audio/witch_cackle1.ogg'].play()
  156. return
  157. }
  158. stageData.soundEffects['audio/click1.ogg'].play()
  159. stageData.isSellingPotions = true
  160. sellTutorialPrompt(stageData)
  161. let randomPotions = []
  162. randomPotions.push(...stageData.potionStocked)
  163. randomPotions.push(...stageData.potionInventory)
  164. randomPotions.push(...stageData.potionInfo.map(info => info.name))
  165. randomPotions.shuffle()
  166. let numberOfCustomers = Math.min(randomPotions.length, Math.floor(Math.random() * 3) + 3)
  167. for (let i = 0; i < numberOfCustomers; i++) {
  168. const desiredPotion = randomPotions.shift()
  169. const potionInfo = stageData.potionInfo.find(info => info.name == desiredPotion)
  170. stageData.customers.push(await customerBuilder(potionInfo, i))
  171. }
  172. setTimeout(() => {
  173. stageData.soundEffects['audio/store-entrance-bell.ogg'].play()
  174. }, 500)
  175. stageData.customers[0].showDesire()
  176. closeShopUI(game, stageData)
  177. }
  178. export async function updatePotionShelfDisplay() {
  179. let storeShelfSpot = 0
  180. for (let i = 0; i < storeShelfSlots.length; i++) {
  181. if (stageData.displayedPotions[i]) {
  182. game.entities.remove(stageData.displayedPotions[i])
  183. game.scene.remove(stageData.displayedPotions[i])
  184. stageData.displayedPotions[i] = null
  185. }
  186. const currentPotionInfo = stageData.potionInfo.find(potion => stageData.potionStocked[i] == potion.name)
  187. if (!currentPotionInfo) {
  188. continue
  189. }
  190. const currentPotion = await loadGltf(game, currentPotionInfo.model)
  191. currentPotion.position.copy(storeShelfSlots[storeShelfSpot++].position)
  192. currentPotion.potionData = currentPotionInfo
  193. stageData.displayedPotions[i] = currentPotion
  194. game.entities.push(currentPotion)
  195. game.scene.add(currentPotion)
  196. }
  197. }
  198. export function addValueToSave(container, key, value) {
  199. let containerString = localStorage.getItem(`${GAME_SAVE_KEY}-${key}`)
  200. if (!containerString) {
  201. container = [value]
  202. } else {
  203. container.push(value)
  204. }
  205. localStorage.setItem(`${GAME_SAVE_KEY}-${key}`, JSON.stringify(container))
  206. }
  207. export function addAmountToSave(key, value) {
  208. let amount = parseInt(localStorage.getItem(`${GAME_SAVE_KEY}-${key}`)) ?? 0
  209. amount += value
  210. localStorage.setItem(`${GAME_SAVE_KEY}-${key}`, amount)
  211. return amount
  212. }
  213. export function removeValueFromSave(container, key, value) {
  214. let containerString = localStorage.getItem(`${GAME_SAVE_KEY}-${key}`)
  215. if (!containerString) {
  216. container = []
  217. } else {
  218. container.remove(value)
  219. }
  220. localStorage.setItem(`${GAME_SAVE_KEY}-${key}`, JSON.stringify(container))
  221. }
  222. export function clearSaveData(stageData) {
  223. localStorage.removeItem(`${GAME_SAVE_KEY}-potion-inventory`)
  224. localStorage.removeItem(`${GAME_SAVE_KEY}-potion-stocked`)
  225. localStorage.setItem(`${GAME_SAVE_KEY}-potion-inventory`, JSON.stringify([]))
  226. stageData.potionInventory = []
  227. localStorage.setItem(`${GAME_SAVE_KEY}-potion-stocked`, JSON.stringify([]))
  228. stageData.potionStocked = []
  229. stageData.currency = 100
  230. localStorage.setItem(`${GAME_SAVE_KEY}-currency`, stageData.currency)
  231. stageData.currentDay = 1
  232. localStorage.setItem(`${GAME_SAVE_KEY}-currentday`, stageData.currentDay)
  233. }
  234. export function loadSaveData(stageData) {
  235. let potionInventoryString = localStorage.getItem(`${GAME_SAVE_KEY}-potion-inventory`)
  236. if (!potionInventoryString) {
  237. localStorage.setItem(`${GAME_SAVE_KEY}-potion-inventory`, JSON.stringify([]))
  238. stageData.potionInventory = []
  239. } else {
  240. stageData.potionInventory = JSON.parse(potionInventoryString)
  241. stageData.potionInventory.sort()
  242. }
  243. let potionsStockedString = localStorage.getItem(`${GAME_SAVE_KEY}-potion-stocked`)
  244. if (!potionsStockedString) {
  245. localStorage.setItem(`${GAME_SAVE_KEY}-potion-stocked`, JSON.stringify([]))
  246. stageData.potionStocked = []
  247. } else {
  248. stageData.potionStocked = JSON.parse(potionsStockedString)
  249. stageData.potionStocked.sort()
  250. }
  251. stageData.currency = localStorage.getItem(`${GAME_SAVE_KEY}-currency`)
  252. if (!stageData.currency) {
  253. localStorage.setItem(`${GAME_SAVE_KEY}-currency`, 100)
  254. stageData.currency = 100
  255. }
  256. stageData.currentDay = localStorage.getItem(`${GAME_SAVE_KEY}-currentday`)
  257. if (!stageData.currentDay) {
  258. localStorage.setItem(`${GAME_SAVE_KEY}-currentday`, 1)
  259. stageData.currentDay = 1
  260. }
  261. }
  262. export async function init(inGame) {
  263. game = inGame
  264. loadSaveData(stageData)
  265. stageData.currentRoom = 2
  266. stageData.soundEffects = soundEffects
  267. stageData.selectedIngredients = []
  268. stageData.cameraPositions = [
  269. { "camera": new THREE.Vector3(-15, 5, 7), "focus": new THREE.Vector3(-13, 0.1, 0) },
  270. { "camera": new THREE.Vector3(1, 5, 7), "focus": new THREE.Vector3(0, 0.1, 0) },
  271. { "camera": new THREE.Vector3(12, 5, 7), "focus": new THREE.Vector3(18, 0.1, 0) },
  272. { "camera": new THREE.Vector3(1.37, 2.41, 3.77), "focus": new THREE.Vector3(-1.15, 0.79, -0.19) }
  273. ]
  274. const soundFilePaths = [
  275. 'audio/click1.ogg',
  276. 'audio/sinkWater1.ogg',
  277. 'audio/doorOpen_1.ogg',
  278. 'audio/doorClose_4.ogg',
  279. 'audio/drawKnife2.ogg',
  280. 'audio/witch_cackle1.ogg',
  281. 'audio/bubbling.mp3',
  282. 'audio/chest_close_creak.ogg',
  283. 'audio/chest_open_creak.ogg',
  284. 'audio/handleCoins.ogg',
  285. 'audio/impactGlass_medium_000.ogg',
  286. 'audio/impactGlass_medium_001.ogg',
  287. 'audio/impactGlass_medium_002.ogg',
  288. 'audio/impactGlass_medium_003.ogg',
  289. 'audio/impactGlass_medium_004.ogg',
  290. 'audio/impactWood_heavy_002.ogg',
  291. 'audio/impactWood_heavy_004.ogg',
  292. 'audio/impactSoft_medium_002.ogg',
  293. 'audio/impactSoft_medium_004.ogg',
  294. 'audio/cash-register.ogg',
  295. 'audio/store-entrance-bell.ogg'
  296. ]
  297. soundFilePaths.forEach(path => {
  298. soundEffects[path] = new Howl({
  299. src: [path],
  300. preload: true,
  301. })
  302. })
  303. soundEffects['audio/doorClose_4.ogg'].volume(0.5)
  304. soundEffects['audio/doorOpen_1.ogg'].volume(0.5)
  305. let levelLoader = new LevelLoader(game)
  306. game.camera.position.copy(stageData.cameraPositions[3].camera)
  307. game.lookAtFocus = stageData.cameraPositions[3].focus.clone()
  308. await loadRgbeBackground(game, 'kloppenheim_02_puresky_1k.hdr')
  309. game.scene.background = null
  310. game.scene.fog = new THREE.Fog(0x000000, 12, 20)
  311. let instancedMeshes = await levelLoader.load(levelData.default)
  312. instancedMeshes.forEach(instancedMesh => {
  313. game.scene.add(instancedMesh)
  314. })
  315. stageData.ingredientInfo = ingredientInfo.default
  316. stageData.potionInfo = potionInfo.default
  317. Object.values(stageData.ingredientInfo).forEach(info => {
  318. ingredients.push(new Ingredient(info, game))
  319. })
  320. ///////////////
  321. /// Brewing ///
  322. ///////////////
  323. let shelfSpot = 0
  324. ingredients.forEach(async ingredient => {
  325. await ingredient.spawn(shelfSlots[shelfSpot++])
  326. })
  327. stageData.candycorn = await loadGltf(game, 'models/candycorn.gltf')
  328. stageData.candycorn.position.x = -4.5
  329. stageData.candycorn.position.z = -3.5
  330. game.scene.add(stageData.candycorn)
  331. stageData.pumpkin2 = await loadGltf(game, 'models/pumpkin_orange.gltf')
  332. game.entities.push(stageData.pumpkin2)
  333. stageData.pumpkin2.position.x = -10
  334. stageData.pumpkin2.position.y = 1
  335. stageData.pumpkin2.position.z = 0.5
  336. game.scene.add(stageData.pumpkin2)
  337. jackolantern = await loadGltf(game, 'models/pumpkin_orange_jackolantern.gltf')
  338. game.entities.push(jackolantern)
  339. jackolantern.spin = 0
  340. jackolantern.position.set(3, 0.1, 1)
  341. jackolantern.rotateOnAxis(THREE.Object3D.DEFAULT_UP, -Math.PI / 4)
  342. game.scene.add(jackolantern)
  343. candle = await loadGltf(game, 'models/candle_thin.gltf.glb')
  344. game.entities.push(candle)
  345. candle.position.x = 0
  346. candle.position.y = 1
  347. candle.position.z = -2.5
  348. candle.isLit = false
  349. game.scene.add(candle)
  350. candleLit = await loadGltf(game, 'models/candle_thin_lit.gltf.glb')
  351. game.entities.push(candleLit)
  352. candleLit.position.x = 0
  353. candleLit.position.y = 1
  354. candleLit.position.z = -2.5
  355. candleLit.visible = false
  356. game.scene.add(candleLit)
  357. stageData.cauldronUniforms = {
  358. uTime: { value: 0 },
  359. uPixelRatio: { value: Math.min(window.devicePixelRatio, 2) },
  360. uBlendTime: { value: 0 },
  361. uPotionColor: { value: new THREE.Vector3(0, 0.8, 0.2) },
  362. uNextPotionColor: { value: new THREE.Vector3(0, 0.8, 0.2) },
  363. uTransparency: { value: 0.8 }
  364. }
  365. stageData.cauldron = await loadGltf(game, 'models/simple_cauldron.gltf.glb')
  366. const cauldronBell = stageData.cauldron.getObjectByName("Sphere015")
  367. cauldronBell.material.side = THREE.DoubleSide
  368. const cauldronTop = stageData.cauldron.getObjectByName("Sphere015_1")
  369. const cauldronTopMaterial = new THREE.ShaderMaterial({
  370. fragmentShader: cauldronFragment,
  371. vertexShader: cauldronVertex,
  372. uniforms: stageData.cauldronUniforms,
  373. transparent: true,
  374. depthWrite: false,
  375. side: THREE.DoubleSide,
  376. })
  377. cauldronTop.material = cauldronTopMaterial
  378. game.entities.push(stageData.cauldron)
  379. stageData.cauldron.position.y = 0.1
  380. game.scene.add(stageData.cauldron)
  381. stageData.brewTutorial1 = buildCanvasText("Click to Brew", { font: "86px Alice" })
  382. stageData.brewTutorial1.position.z = 2.4
  383. stageData.brewTutorial1.position.y = 0.2
  384. stageData.brewTutorial1.rotateOnAxis(new THREE.Vector3(1, 0, 0), -Math.PI / 2)
  385. stageData.brewTutorial1.alreadySeen = false
  386. stageData.brewTutorial1.material.opacity = 0
  387. stageData.brewTutorial1.castShadow = false
  388. game.scene.add(stageData.brewTutorial1)
  389. stageData.brewWitch = await loadGltf(game, 'characters/character_witch.gltf')
  390. game.entities.push(stageData.brewWitch)
  391. stageData.brewWitch.position.x = 1
  392. stageData.brewWitch.position.y = 0.1
  393. stageData.brewWitch.position.z = -1
  394. stageData.brewWitch.stir = false
  395. stageData.brewWitch.bounce = false
  396. stageData.brewWitch.lookAt(stageData.cauldron.position)
  397. game.scene.add(stageData.brewWitch)
  398. stageData.shopWitch = await loadGltf(game, 'characters/character_witch.gltf')
  399. game.entities.push(stageData.shopWitch)
  400. stageData.shopWitch.position.x = -13
  401. stageData.shopWitch.position.y = 0.1
  402. stageData.shopWitch.position.z = -2
  403. game.scene.add(stageData.shopWitch)
  404. stageData.marketWitch = await loadGltf(game, 'characters/character_witch.gltf')
  405. game.entities.push(stageData.marketWitch)
  406. stageData.marketWitch.position.x = 20
  407. stageData.marketWitch.position.y = 0.1
  408. stageData.marketWitch.position.z = 2.5
  409. stageData.marketWitch.rotateOnAxis(THREE.Object3D.DEFAULT_UP, Math.PI)
  410. game.scene.add(stageData.marketWitch)
  411. stageData.spoon = await loadGltf(game, 'models/spoon.gltf')
  412. game.entities.push(stageData.spoon)
  413. stageData.spoon.rotateOnAxis(new THREE.Vector3(1, 0, 0), Math.PI)
  414. stageData.spoon.position.y = 1.25
  415. stageData.spoon.visible = false
  416. game.scene.add(stageData.spoon)
  417. book = await loadGltf(game, 'models/book_grey.gltf.glb')
  418. game.entities.push(book)
  419. book.rotateOnAxis(THREE.Object3D.DEFAULT_UP, Math.PI)
  420. book.position.x = 4.6
  421. book.position.y = 1.9
  422. book.position.z = -3.9
  423. game.scene.add(book)
  424. const bubbleGeometry = new THREE.SphereGeometry(0.1, 24, 24)
  425. const bubbleMaterial = cauldronTopMaterial
  426. //bubbleMaterial.uniforms.uTransparency.value = 0.3
  427. stageData.bubbleParticles = new THREE.InstancedMesh(bubbleGeometry, bubbleMaterial, bubbleCount)
  428. stageData.bubbleParticles.instanceMatrix.setUsage(THREE.DynamicDrawUsage)
  429. stageData.bubbleParticles.visible = false
  430. game.scene.add(stageData.bubbleParticles)
  431. const matrix = new THREE.Matrix4()
  432. for (let index = 0; index < bubbleCount; index++) {
  433. bubblePositions[index] = new THREE.Vector3(0.5 * (Math.random() - 0.5), 0.5, 0.5 * (Math.random() - 0.5))
  434. bubbleLifespan[index] = 7 - (index * 0.5)
  435. bubbleScale[index] = 0.2
  436. bubbleVelocity[index] = new THREE.Vector3(0, 0.01, 0)
  437. }
  438. const quaternion = new THREE.Quaternion()
  439. const scaleVector = new THREE.Vector3(1, 1, 1)
  440. for (let index = 0; index < bubbleCount; index++) {
  441. quaternion.setFromAxisAngle(THREE.Object3D.DEFAULT_UP, 0)
  442. const scale = scaleVector.set(bubbleScale[index], bubbleScale[index], bubbleScale[index])
  443. matrix.compose(bubblePositions[index], quaternion, scale)
  444. stageData.bubbleParticles.setMatrixAt(index, matrix)
  445. }
  446. doorway = await loadGltf(game, 'models/wall_doorway.glb')
  447. doorway.position.set(-6, 0, -2)
  448. doorway.castShadow = false
  449. doorway.traverse((child) => {
  450. if (child.isMesh) {
  451. child.castShadow = false
  452. }
  453. })
  454. doorway.rotateOnAxis(THREE.Object3D.DEFAULT_UP, Math.PI / 2)
  455. doorway.isOpen = false
  456. doorway.isMoving = false
  457. game.entities.push(doorway)
  458. game.scene.add(doorway)
  459. doorway2 = await loadGltf(game, 'models/wall_doorway.glb')
  460. doorway2.position.set(6, 0, 2)
  461. doorway2.castShadow = false
  462. doorway2.traverse((child) => {
  463. if (child.isMesh) {
  464. child.castShadow = false
  465. }
  466. })
  467. doorway2.rotateOnAxis(THREE.Object3D.DEFAULT_UP, Math.PI / 2)
  468. doorway2.isOpen = false
  469. doorway2.isMoving = false
  470. game.entities.push(doorway2)
  471. game.scene.add(doorway2)
  472. ////////////
  473. /// Shop ///
  474. ////////////
  475. // const debugSphereGeometry = new THREE.SphereGeometry(0.1)
  476. // const debugMaterial = new THREE.MeshBasicMaterial({color: "magenta"})
  477. // const debugSphere = new THREE.Mesh(debugSphereGeometry, debugMaterial)
  478. // customerSlots.forEach(slot => {
  479. // debugSphere.position.copy(slot.position)
  480. // game.scene.add(debugSphere.clone())
  481. // })
  482. stageData.displayedPotions = []
  483. updatePotionShelfDisplay()
  484. lollipop = await loadGltf(game, 'models/lollipop_green.gltf')
  485. lollipop.position.x = -11.5
  486. lollipop.position.y = 1.28
  487. lollipop.position.z = -3.4
  488. lollipop.rotateOnAxis(new THREE.Vector3(1, 0, 0), Math.PI / 2)
  489. lollipop.rotateOnAxis(new THREE.Vector3(0, 0, 1), Math.PI / 2 + -0.3)
  490. game.entities.push(lollipop)
  491. game.scene.add(lollipop)
  492. stageData.chest = await loadGltf(game, 'models/chest_large.glb')
  493. stageData.chest.scale.set(0.5, 0.5, 0.5)
  494. stageData.chest.position.x = -15.5
  495. stageData.chest.position.y = 1
  496. stageData.chest.position.z = 0
  497. stageData.chest.rotateOnAxis(THREE.Object3D.DEFAULT_UP, Math.PI / 4)
  498. game.entities.push(stageData.chest)
  499. game.scene.add(stageData.chest)
  500. stageData.shopTutorial1 = buildCanvasText("Click to Sell", { font: "100px Alice" })
  501. stageData.shopTutorial1.position.x = -15.5
  502. stageData.shopTutorial1.position.z = 0.2
  503. stageData.shopTutorial1.position.y = 1.8
  504. stageData.shopTutorial1.alreadySeen = false
  505. stageData.shopTutorial1.scale.set(0.6, 0.6, 0.6)
  506. stageData.shopTutorial1.material.opacity = 0
  507. stageData.shopTutorial1.castShadow = false
  508. game.scene.add(stageData.shopTutorial1)
  509. stageData.shopTutorial2 = buildCanvasText("Pick the correct potion", { font: "72px Alice" })
  510. stageData.shopTutorial2.position.x = -10
  511. stageData.shopTutorial2.position.z = -3.4
  512. stageData.shopTutorial2.position.y = 1.5
  513. stageData.shopTutorial2.alreadySeen = false
  514. stageData.shopTutorial2.scale.set(1, 1, 1)
  515. stageData.shopTutorial2.material.opacity = 0
  516. stageData.shopTutorial2.castShadow = false
  517. game.scene.add(stageData.shopTutorial2)
  518. stageData.coin = await loadGltf(game, 'models/coin.gltf.glb')
  519. stageData.coin.position.x = -12
  520. stageData.coin.position.y = 1.1
  521. stageData.coin.position.z = 0
  522. game.entities.push(stageData.coin)
  523. game.scene.add(stageData.coin)
  524. stageData.customers = []
  525. //////////////
  526. /// Market ///
  527. //////////////
  528. stageData.sellingSkeleton = await loadGltf(game, 'characters/character_skeleton_mage.gltf')
  529. stageData.sellingSkeleton.position.x = 20
  530. stageData.sellingSkeleton.position.y = 0.1
  531. stageData.sellingSkeleton.position.z = -1
  532. stageData.sellingSkeleton.rotateOnAxis(THREE.Object3D.DEFAULT_UP, -Math.PI / 4)
  533. game.entities.push(stageData.sellingSkeleton)
  534. game.scene.add(stageData.sellingSkeleton)
  535. stageData.sign = await loadGltf(game, 'models/sign_left.gltf')
  536. stageData.sign.position.x = 14
  537. stageData.sign.position.y = 0.1
  538. stageData.sign.position.z = 2
  539. game.entities.push(stageData.sign)
  540. game.scene.add(stageData.sign)
  541. const mushroomCrate = await loadGltf(game, 'models/crate_mushrooms.gltf')
  542. mushroomCrate.scale.set(0.5, 0.5, 0.5)
  543. mushroomCrate.position.x = 19.5
  544. mushroomCrate.position.z = 0.5
  545. mushroomCrate.rotateOnAxis(THREE.Object3D.DEFAULT_UP, Math.PI / 4)
  546. game.entities.push(mushroomCrate)
  547. game.scene.add(mushroomCrate)
  548. const tomatoCrate = await loadGltf(game, 'models/crate_tomatoes.gltf')
  549. tomatoCrate.scale.set(0.5, 0.5, 0.5)
  550. tomatoCrate.position.x = 21
  551. game.entities.push(tomatoCrate)
  552. game.scene.add(tomatoCrate)
  553. const lettuceCrate = await loadGltf(game, 'models/crate_lettuce.gltf')
  554. lettuceCrate.scale.set(0.5, 0.5, 0.5)
  555. lettuceCrate.position.x = 18.5
  556. lettuceCrate.position.z = -0.5
  557. game.entities.push(lettuceCrate)
  558. game.scene.add(lettuceCrate)
  559. const coffin = await loadGltf(game, 'models/coffin_decorated.gltf')
  560. coffin.position.x = 23
  561. coffin.position.z = 6
  562. coffin.rotateOnAxis(THREE.Object3D.DEFAULT_UP, -Math.PI / 4)
  563. game.scene.add(coffin)
  564. const candy = await loadGltf(game, 'models/candy_orange_A.gltf')
  565. candy.position.x = 21
  566. candy.position.y = 0.25
  567. candy.position.z = 6
  568. candy.rotateOnAxis(THREE.Object3D.DEFAULT_UP, -Math.PI / 4)
  569. game.scene.add(candy)
  570. const candyBucket = await loadGltf(game, 'models/candy_bucket_B_decorated.gltf')
  571. candyBucket.position.x = 21.5
  572. candyBucket.position.y = 0.1
  573. candyBucket.position.z = 5.5
  574. candyBucket.rotateOnAxis(THREE.Object3D.DEFAULT_UP, -Math.PI / 2)
  575. game.scene.add(candyBucket)
  576. /**
  577. * Fireflies
  578. */
  579. stageData.firefliesUniforms = {
  580. uTime: { value: 0 },
  581. uPixelRatio: { value: Math.min(window.devicePixelRatio, 2) }
  582. }
  583. const firefliesMaterial = new THREE.ShaderMaterial({
  584. fragmentShader: firefliesFragment,
  585. vertexShader: firefliesVertex,
  586. uniforms: stageData.firefliesUniforms,
  587. transparent: true,
  588. depthWrite: false,
  589. blending: THREE.AdditiveBlending
  590. })
  591. const firefliesGeometry = new THREE.BufferGeometry()
  592. const firefliesCount = 50
  593. const positionArray = new Float32Array(firefliesCount * 3)
  594. const scaleArray = new Float32Array(firefliesCount)
  595. for (let i = 0; i < firefliesCount; i++) {
  596. positionArray[i * 3 + 0] = 24 * (Math.random() - 0.5) + 18
  597. positionArray[i * 3 + 1] = 4 * Math.random() + 2
  598. positionArray[i * 3 + 2] = 16 * (Math.random() - 0.5)
  599. scaleArray[i] = 0.8 * Math.random() + 1.2
  600. }
  601. firefliesGeometry.setAttribute('position', new THREE.BufferAttribute(positionArray, 3))
  602. firefliesGeometry.setAttribute('aScale', new THREE.BufferAttribute(scaleArray, 1))
  603. const fireflies = new THREE.Points(firefliesGeometry, firefliesMaterial)
  604. game.scene.add(fireflies)
  605. ////////////////
  606. /// Lighting ///
  607. ////////////////
  608. const ambientLight = new THREE.AmbientLight(0xFFFFFF, 1)
  609. game.scene.add(ambientLight)
  610. const shadowSize = 24
  611. directionalLight = new THREE.DirectionalLight(0xFFFFFF, 0.9)
  612. directionalLight.position.set(8, 10, 6)
  613. directionalLight.castShadow = true
  614. directionalLight.shadow.camera.left = -shadowSize
  615. directionalLight.shadow.camera.right = shadowSize
  616. directionalLight.shadow.camera.top = -shadowSize
  617. directionalLight.shadow.camera.bottom = shadowSize
  618. directionalLight.shadow.camera.far = 28
  619. directionalLight.shadow.mapSize.width = Math.min(game.renderer.capabilities.maxTextureSize, 2048)
  620. directionalLight.shadow.mapSize.height = Math.min(game.renderer.capabilities.maxTextureSize, 2048)
  621. directionalLight.shadow.bias = -0.005
  622. directionalLight.shadow.radius = 6
  623. directionalLight.cameraOffset = new THREE.Vector3()
  624. directionalLight.cameraOffset.copy(directionalLight.position)
  625. directionalLight.cameraOffset.sub(new THREE.Vector3(0, 0, 0))
  626. directionalLight.target = game.camera
  627. directionalLight.update = function () {
  628. const currentOffset = new THREE.Vector3()
  629. currentOffset.copy(directionalLight.cameraOffset).add(game.camera.position)
  630. directionalLight.position.set(currentOffset.x, currentOffset.y, currentOffset.z)
  631. if (directionalLight.shadow) {
  632. directionalLight.shadow.camera.position.set(currentOffset.x, currentOffset.y, currentOffset.z)
  633. }
  634. }
  635. game.scene.add(directionalLight)
  636. // const shadowHelper = new THREE.CameraHelper( directionalLight.shadow.camera )
  637. // game.scene.add(shadowHelper)
  638. mainMenuUI(game, stageData)
  639. navigationUI(game, stageData)
  640. brewUI(game, stageData)
  641. shopUI(game, stageData)
  642. gameOverUI(game, stageData)
  643. updateGameStatusUI(game, stageData)
  644. stageData.beginBrew = () => { beginBrew(game, stageData) }
  645. stageData.beginSell = () => { beginSell(game, stageData) }
  646. ///////////
  647. // DEBUG //
  648. ///////////
  649. // moveToRoom(ROOM_SHOP)
  650. // closeMainMenuUI(game, stageData)
  651. // const planeGeometry = new THREE.PlaneGeometry(1,1)
  652. // const debugPlane = new THREE.Mesh(planeGeometry, textBubbleMaterial)
  653. // debugPlane.position.x = 2
  654. // debugPlane.position.y = 1
  655. // debugPlane.scale.set(2,2,2)
  656. // game.scene.add(debugPlane)
  657. }
  658. export function update(game) {
  659. let elapsedTime = game.clock.getElapsedTime()
  660. ingredients.forEach(ingredient => {
  661. ingredient.wobble()
  662. })
  663. stageData.candycorn.position.y = 0.125 * Math.sin(4.1 * elapsedTime) + 3.0
  664. stageData.candycorn.rotateOnAxis(THREE.Object3D.DEFAULT_UP, -0.02)
  665. directionalLight.update()
  666. raycast.setFromCamera(game.mousePosition, game.camera)
  667. Howler.pos(game.camera.position.x, game.camera.position.y, game.camera.position.z)
  668. Howler.orientation(game.camera.position.x, game.camera.position.y, game.camera.position.z, 0, 1, 0)
  669. const quaternion = new THREE.Quaternion()
  670. const scaleVector = new THREE.Vector3(1, 1, 1)
  671. const matrix = new THREE.Matrix4()
  672. for (let index = 0; index < bubbleCount; index++) {
  673. bubblePositions[index].add(bubbleVelocity[index])
  674. bubbleScale[index] = Math.min(bubbleScale[index] + 0.01, 1)
  675. bubbleLifespan[index] -= 0.1
  676. if (bubbleLifespan[index] <= 0) {
  677. bubbleLifespan[index] = 7
  678. bubblePositions[index].set(0.5 * (Math.random() - 0.5), 0.5, 0.5 * (Math.random() - 0.5))
  679. bubbleScale[index] = 0.2
  680. }
  681. //quaternion.setFromAxisAngle(THREE.Object3D.DEFAULT_UP, 0)
  682. const scale = scaleVector.set(bubbleScale[index], bubbleScale[index], bubbleScale[index])
  683. matrix.compose(bubblePositions[index], quaternion, scale)
  684. stageData.bubbleParticles.setMatrixAt(index, matrix)
  685. }
  686. stageData.bubbleParticles.instanceMatrix.needsUpdate = true
  687. if (stageData.brewWitch.stir) {
  688. stageData.spoon.position.x = 0.125 * Math.sin(4 * elapsedTime)
  689. stageData.spoon.position.z = 0.125 * Math.cos(4 * elapsedTime)
  690. }
  691. if (stageData.brewWitch.bounce) {
  692. stageData.brewWitch.scale.x = 1 + (0.03125 * Math.sin(12 * elapsedTime))
  693. stageData.brewWitch.scale.y = 1 + (-0.03125 * Math.sin(12 * elapsedTime))
  694. stageData.brewWitch.scale.z = 1 + (0.03125 * Math.sin(12 * elapsedTime))
  695. } else {
  696. stageData.brewWitch.scale.x = 1
  697. stageData.brewWitch.scale.y = 1
  698. stageData.brewWitch.scale.z = 1
  699. }
  700. if (jackolantern.spin > 0) {
  701. jackolantern.spin -= 0.05
  702. jackolantern.rotateOnAxis(THREE.Object3D.DEFAULT_UP, -0.1)
  703. if (jackolantern.spin < 0) {
  704. jackolantern.spin = 0
  705. }
  706. }
  707. stageData.sellingSkeleton.scale.x = 1 + (0.03125 * Math.sin(12 * elapsedTime))
  708. stageData.sellingSkeleton.scale.y = 1 + (-0.03125 * Math.sin(12 * elapsedTime))
  709. stageData.sellingSkeleton.scale.z = 1 + (0.03125 * Math.sin(12 * elapsedTime))
  710. stageData.firefliesUniforms.uTime.value = elapsedTime
  711. stageData.cauldronUniforms.uTime.value = elapsedTime
  712. stageData.customers.forEach(customer => {
  713. customer.update(elapsedTime)
  714. })
  715. candle.position.y = 0.625 * Math.sin(0.5 * elapsedTime) + 1.3
  716. candleLit.position.y = 0.625 * Math.sin(0.5 * elapsedTime) + 1.3
  717. let intersects = raycast.intersectObjects(game.entities.filter(entity => entity.visible))
  718. if (intersects.length > 0) {
  719. document.body.style.cursor = "pointer"
  720. } else {
  721. document.body.style.cursor = "default"
  722. }
  723. }
  724. function isAChildOf(parent, childToCheck) {
  725. if (parent == childToCheck) return true
  726. if (childToCheck.parent != null) {
  727. return isAChildOf(parent, childToCheck.parent)
  728. }
  729. return false
  730. }
  731. export function onClick() {
  732. //drawDebugLine(game, witch.position, getForwardVector(witch).multiplyScalar(10), 0xff0000, 1000)
  733. //drawDebugLine(game, witch.position, getRightVector(witch).multiplyScalar(10), 0x00ff00, 1000)
  734. let intersects = raycast.intersectObjects(game.entities.filter(entity => entity.visible))
  735. if (intersects.length > 0) {
  736. intersects.every(intersect => {
  737. switch (stageData.currentRoom) {
  738. case ROOM_SHOP:
  739. //SHOP CLICK ACTIONS HERE
  740. if (isAChildOf(stageData.chest, intersect.object)) {
  741. if (stageData.isSellingPotions) {
  742. return false
  743. }
  744. if (stageData.shopInactivityHandle) {
  745. clearTimeout(stageData.shopInactivityHandle)
  746. stageData.shopInactivityHandle = 0
  747. }
  748. if (stageData.chest.isStocking) {
  749. closeShopUI(game, stageData)
  750. } else {
  751. openShopUI(game, stageData)
  752. if (stageData.shopTutorial1.material.opacity > 0) {
  753. gsap.to(stageData.shopTutorial1.material, {
  754. duration: 1.5, opacity: 0, onComplete: () => {
  755. stageData.shopTutorial1.castShadow = false
  756. stageData.shopTutorial1.alreadySeen = true
  757. }
  758. })
  759. }
  760. }
  761. return false
  762. }
  763. if (isAChildOf(stageData.shopWitch, intersect.object)) {
  764. if (stageData.isSellingPotions) {
  765. return false
  766. }
  767. if (stageData.shopInactivityHandle) {
  768. clearTimeout(stageData.shopInactivityHandle)
  769. stageData.shopInactivityHandle = 0
  770. }
  771. if (stageData.chest.isStocking) {
  772. closeShopUI(game, stageData)
  773. } else {
  774. openShopUI(game, stageData)
  775. if (stageData.shopTutorial1.material.opacity > 0) {
  776. gsap.to(stageData.shopTutorial1.material, {
  777. duration: 1.5, opacity: 0, onComplete: () => {
  778. stageData.shopTutorial1.castShadow = false
  779. stageData.shopTutorial1.alreadySeen = true
  780. }
  781. })
  782. }
  783. }
  784. return false
  785. }
  786. if (isAChildOf(stageData.coin, intersect.object)) {
  787. 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 }]
  788. gsap.to(stageData.coin.position, {
  789. duration: 3.5, motionPath: coinMotionPath, onComplete: () => {
  790. stageData.soundEffects['audio/handleCoins.ogg'].play()
  791. closeChest(stageData.chest, () => {
  792. stageData.coin.scale.set(0, 0, 0)
  793. stageData.coin.position.set(-12, 1.1, 0)
  794. gsap.to(stageData.coin.scale, {
  795. ease: "elastic",
  796. duration: 0.7, x: 1, y: 1, z: 1,
  797. })
  798. })
  799. }
  800. })
  801. setTimeout(() => {
  802. openChest(stageData.chest)
  803. }, 700)
  804. return false
  805. }
  806. stageData.displayedPotions.forEach((potion) => {
  807. if (isAChildOf(potion, intersect.object)) {
  808. if (stageData.isSellingPotions) {
  809. if (stageData.sellInactivityHandle) {
  810. clearTimeout(stageData.sellInactivityHandle)
  811. stageData.sellInactivityHandle = 0
  812. }
  813. if (stageData.customers[0].isMatchingPotion(potion.potionData)) {
  814. if (stageData.shopTutorial2.material.opacity > 0) {
  815. gsap.to(stageData.shopTutorial2.material, {
  816. duration: 1.5, opacity: 0, onComplete: () => {
  817. stageData.shopTutorial2.castShadow = false
  818. stageData.shopTutorial2.alreadySeen = true
  819. }
  820. })
  821. }
  822. const firstCustomer = stageData.customers[0]
  823. firstCustomer.hideDesire()
  824. 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 }]
  825. gsap.to(potion.position, {
  826. duration: 2, motionPath: potionMotionPath, onComplete: () => {
  827. removeValueFromSave(stageData.potionStocked, 'potion-stocked', potion.potionData.name)
  828. updatePotionShelfDisplay()
  829. firstCustomer.acceptPotion()
  830. stageData.soundEffects['audio/cash-register.ogg'].play()
  831. stageData.currency = addAmountToSave("currency", potion.potionData.value)
  832. updateGameStatusUI(game, stageData)
  833. //move money from customer to chest
  834. //add money to save data
  835. //move customer out of room
  836. nextCustomer()
  837. //display next customer desire
  838. //if no more customers or no more valid potions, end selling
  839. }
  840. })
  841. } else {
  842. //TODO: potion does not match
  843. playBottleClink()
  844. gsap.fromTo(potion.rotation, { duration: 0.1, z: 0.2 }, { duration: 0.1, z: 0 })
  845. stageData.customers[0].rejectPotion()
  846. if (stageData.customers[0].rejectCount >= 3) {
  847. stageData.customers[0].showSad()
  848. nextCustomer()
  849. }
  850. }
  851. } else {
  852. playBottleClink()
  853. gsap.fromTo(potion.rotation, { duration: 0.1, z: 0.2 }, { duration: 0.1, z: 0 })
  854. }
  855. }
  856. })
  857. if (isAChildOf(stageData.pumpkin2, intersect.object)) {
  858. if (stageData.pumpkin2.isMoving) {
  859. return
  860. }
  861. stageData.pumpkin2.isMoving = true
  862. gsap.to(stageData.pumpkin2.position, {
  863. duration: 2, y: 3, onComplete: () => {
  864. setTimeout(() => {
  865. stageData.soundEffects['audio/impactWood_heavy_002.ogg'].play()
  866. }, 400)
  867. setTimeout(() => {
  868. stageData.soundEffects['audio/impactSoft_medium_002.ogg'].play()
  869. }, 800)
  870. gsap.to(stageData.pumpkin2.position, {
  871. duration: 1, y: 1, ease: "bounce", onComplete: () => {
  872. stageData.pumpkin2.isMoving = false
  873. }
  874. })
  875. }
  876. })
  877. return false
  878. }
  879. break;
  880. case ROOM_BREW:
  881. //BREW CLICK ACTIONS HERE
  882. if (isAChildOf(jackolantern, intersect.object)) {
  883. soundEffects['audio/drawKnife2.ogg'].play()
  884. jackolantern.spin += Math.PI
  885. return false
  886. }
  887. if (isAChildOf(book, intersect.object)) {
  888. //soundEffects['audio/drawKnife2.ogg'].play()
  889. //jackolantern.spin += Math.PI
  890. let bookMotionPath = [{ x: 4.6, y: 1.9, z: 0 }, { x: 4.6, y: 1.9, z: -3.9 }]
  891. gsap.to(book.position, {
  892. duration: 6, motionPath: bookMotionPath, onComplete: () => {
  893. }
  894. })
  895. return false
  896. }
  897. if (isAChildOf(stageData.brewWitch, intersect.object)) {
  898. if (stageData.cauldron.isBrewing) {
  899. return false
  900. }
  901. if (stageData.brewInactivityHandle) {
  902. clearTimeout(stageData.brewInactivityHandle)
  903. stageData.brewInactivityHandle = 0
  904. }
  905. if (stageData.cauldron.brewMenuOpen) {
  906. closeBrewUI(game, stageData)
  907. } else {
  908. openBrewUI(game, stageData)
  909. //cauldron.isBrewing = true
  910. if (stageData.brewTutorial1.material.opacity > 0) {
  911. gsap.to(stageData.brewTutorial1.material, {
  912. duration: 1.5, opacity: 0, onComplete: () => {
  913. stageData.brewTutorial1.castShadow = false
  914. stageData.brewTutorial1.alreadySeen = true
  915. }
  916. })
  917. }
  918. }
  919. return false
  920. }
  921. if (isAChildOf(stageData.cauldron, intersect.object)) {
  922. if (stageData.cauldron.isBrewing) {
  923. return false
  924. }
  925. if (stageData.brewInactivityHandle) {
  926. clearTimeout(stageData.brewInactivityHandle)
  927. stageData.brewInactivityHandle = 0
  928. }
  929. if (stageData.cauldron.brewMenuOpen) {
  930. closeBrewUI(game, stageData)
  931. } else {
  932. openBrewUI(game, stageData)
  933. if (stageData.brewTutorial1.material.opacity > 0) {
  934. gsap.to(stageData.brewTutorial1.material, {
  935. duration: 1.5, opacity: 0, onComplete: () => {
  936. stageData.brewTutorial1.castShadow = false
  937. stageData.brewTutorial1.alreadySeen = true
  938. }
  939. })
  940. }
  941. //cauldron.isBrewing = true
  942. }
  943. // let soundId = soundEffects['audio/sinkWater1.ogg'].play()
  944. // soundEffects['audio/sinkWater1.ogg'].once('play', () => {
  945. // soundEffects['audio/sinkWater1.ogg'].volume(0.5, soundId)
  946. // // soundEffects['audio/sinkWater1.ogg'].pos(cauldron.position.x, cauldron.position.y, cauldron.position.z, soundId)
  947. // // soundEffects['audio/sinkWater1.ogg'].pannerAttr({
  948. // // panningModel: 'HRTF',
  949. // // refDistance: 1.0,
  950. // // rolloffFactor: 0.8,
  951. // // distanceModel: 'exponential',
  952. // // }, soundId)
  953. // soundEffects['audio/sinkWater1.ogg'].stereo(1, soundId)
  954. // //console.log(soundEffects['audio/sinkWater1.ogg'])
  955. // }, soundId)
  956. return false
  957. }
  958. if (isAChildOf(candle, intersect.object)) {
  959. candle.isLit = true
  960. candleLit.visible = true
  961. candle.visible = false
  962. return false
  963. }
  964. if (isAChildOf(candleLit, intersect.object)) {
  965. candle.isLit = false
  966. candleLit.visible = false
  967. candle.visible = true
  968. return false
  969. }
  970. break;
  971. case ROOM_MARKET:
  972. //MARKET CLICK ACTIONS HERE
  973. if (isAChildOf(stageData.sign, intersect.object)) {
  974. previousRoom(game, stageData)
  975. }
  976. break;
  977. }
  978. if (isAChildOf(doorway, intersect.object)) {
  979. if (stageData.currentRoom == ROOM_BREW) {
  980. previousRoom(game, stageData)
  981. } else if (stageData.currentRoom == ROOM_SHOP) {
  982. nextRoom(game, stageData)
  983. }
  984. return false
  985. }
  986. if (isAChildOf(doorway2, intersect.object)) {
  987. if (stageData.currentRoom == ROOM_BREW) {
  988. nextRoom(game, stageData)
  989. } else if (stageData.currentRoom == ROOM_MARKET) {
  990. previousRoom(game, stageData)
  991. }
  992. return false
  993. }
  994. })
  995. }
  996. }
  997. function nextCustomer() {
  998. const firstCustomer = stageData.customers.shift()
  999. if (!firstCustomer) {
  1000. return
  1001. }
  1002. const customerMotionPath = [{ x: -11, y: 0.1, z: 3 }, { x: -8, y: 0.1, z: 3 }, { x: -4, y: 0.1, z: 10 }]
  1003. setTimeout(() => {
  1004. stageData.soundEffects['audio/store-entrance-bell.ogg'].play()
  1005. }, 4000)
  1006. gsap.to(firstCustomer.object3d.position, {
  1007. duration: 8, motionPath: customerMotionPath, onComplete: () => {
  1008. game.scene.remove(firstCustomer.object3d)
  1009. }
  1010. })
  1011. gsap.to(firstCustomer.object3d.rotation, {
  1012. duration: 1, y: Math.PI, onComplete: () => {
  1013. }, onUpdate: () => {
  1014. firstCustomer.requestBillboard.lookAt(game.camera.position)
  1015. }
  1016. })
  1017. //const shopperPosition = customerSlots[stageData.customers.length].position
  1018. //move other customers forward
  1019. for (let i = 0; i < stageData.customers.length; i++) {
  1020. const customer = stageData.customers[i]
  1021. const shopperPosition = customerSlots[i].position
  1022. gsap.to(customer.object3d.position, { duration: 1.5, x: shopperPosition.x, y: shopperPosition.y, z: shopperPosition.z })
  1023. }
  1024. if (stageData.customers.length == 0) {
  1025. stageData.isSellingPotions = false
  1026. stageData.currentDay = addAmountToSave("currentday", 1)
  1027. updateGameStatusUI(game, stageData)
  1028. if(stageData.currency >= 1000) {
  1029. showGameOverUI(game, stageData)
  1030. }
  1031. } else {
  1032. stageData.customers[0].showDesire()
  1033. if (stageData.potionStocked.length < 1) {
  1034. stageData.customers[0].rejectPotion(3)
  1035. stageData.customers[0].showSad()
  1036. nextCustomer()
  1037. }
  1038. }
  1039. }
  1040. export function playBottleClink() {
  1041. setTimeout(() => {
  1042. let randomSound2 = Math.floor(5 * Math.random())
  1043. stageData.soundEffects[`audio/impactGlass_medium_00${randomSound2}.ogg`].play()
  1044. }, 50)
  1045. let randomSound = Math.floor(5 * Math.random())
  1046. stageData.soundEffects[`audio/impactGlass_medium_00${randomSound}.ogg`].play()
  1047. }
  1048. function openChest(chest) {
  1049. let lid = chest.getObjectByName("chest_large_lid")
  1050. if (chest.isOpen) {
  1051. return
  1052. }
  1053. if (!chest.isMoving) {
  1054. soundEffects['audio/chest_open_creak.ogg'].play()
  1055. chest.isMoving = true
  1056. gsap.to(lid.rotation, {
  1057. duration: 2.5, x: -(Math.PI / 2) + (Math.PI / 8), ease: "elastic", onComplete: () => {
  1058. chest.isOpen = true
  1059. chest.isMoving = false
  1060. }
  1061. })
  1062. }
  1063. }
  1064. function closeChest(chest, onComplete = () => { }) {
  1065. let lid = chest.getObjectByName("chest_large_lid")
  1066. if (!chest.isOpen) {
  1067. return
  1068. }
  1069. if (!chest.isMoving) {
  1070. soundEffects['audio/chest_close_creak.ogg'].play()
  1071. chest.isMoving = true
  1072. gsap.to(lid.rotation, {
  1073. duration: 1.5, x: 0, ease: "bounce", onComplete: () => {
  1074. chest.isOpen = false
  1075. chest.isMoving = false
  1076. onComplete()
  1077. }
  1078. })
  1079. }
  1080. }
  1081. function openDoorway(doorway) {
  1082. let door = doorway.getObjectByName("wall_doorway_door")
  1083. if (doorway.isOpen) {
  1084. return
  1085. }
  1086. if (!doorway.isMoving) {
  1087. soundEffects['audio/doorOpen_1.ogg'].play()
  1088. doorway.isMoving = true
  1089. gsap.to(door.rotation, {
  1090. duration: 2.5, y: -Math.PI / 2, ease: "elastic", onComplete: () => {
  1091. doorway.isOpen = true
  1092. doorway.isMoving = false
  1093. }
  1094. })
  1095. }
  1096. }
  1097. function closeDoorway(doorway) {
  1098. let door = doorway.getObjectByName("wall_doorway_door")
  1099. if (!doorway.isOpen) {
  1100. return
  1101. }
  1102. if (!doorway.isMoving) {
  1103. soundEffects['audio/doorClose_4.ogg'].play()
  1104. doorway.isMoving = true
  1105. gsap.to(door.rotation, {
  1106. duration: 2.5, y: 0, ease: "elastic", onComplete: () => {
  1107. doorway.isOpen = false
  1108. doorway.isMoving = false
  1109. }
  1110. })
  1111. }
  1112. }
  1113. function moveToShop() {
  1114. closeBrewUI(game, stageData)
  1115. openDoorway(doorway)
  1116. const camPosition = stageData.cameraPositions[0]
  1117. gsap.to(game.camera.position, {
  1118. duration: 2.5, x: camPosition.camera.x, y: camPosition.camera.y, z: camPosition.camera.z, onComplete: () => {
  1119. closeDoorway(doorway)
  1120. }
  1121. })
  1122. gsap.to(game.lookAtFocus, { duration: 2.5, x: camPosition.focus.x, y: camPosition.focus.y, z: camPosition.focus.z })
  1123. stageData.currentRoom = ROOM_SHOP
  1124. shopTutorialPrompt(stageData)
  1125. }
  1126. function moveToBrew() {
  1127. let doorwayToOpen = doorway
  1128. if (stageData.currentRoom == ROOM_MARKET) {
  1129. doorwayToOpen = doorway2
  1130. }
  1131. openDoorway(doorwayToOpen)
  1132. const camPosition = stageData.cameraPositions[1]
  1133. gsap.to(game.camera.position, {
  1134. duration: 2.5, x: camPosition.camera.x, y: camPosition.camera.y, z: camPosition.camera.z, onComplete: () => {
  1135. closeDoorway(doorwayToOpen)
  1136. }
  1137. })
  1138. gsap.to(game.lookAtFocus, { duration: 2.5, x: camPosition.focus.x, y: camPosition.focus.y, z: camPosition.focus.z })
  1139. stageData.currentRoom = ROOM_BREW
  1140. brewTutorialPrompt(stageData)
  1141. }
  1142. function moveToMarket() {
  1143. closeBrewUI(game, stageData)
  1144. openDoorway(doorway2)
  1145. const camPosition = stageData.cameraPositions[2]
  1146. gsap.to(game.camera.position, {
  1147. duration: 2.5, x: camPosition.camera.x, y: camPosition.camera.y, z: camPosition.camera.z, onComplete: () => {
  1148. closeDoorway(doorway2)
  1149. }
  1150. })
  1151. gsap.to(game.lookAtFocus, { duration: 2.5, x: camPosition.focus.x, y: camPosition.focus.y, z: camPosition.focus.z })
  1152. stageData.currentRoom = ROOM_MARKET
  1153. }
  1154. export function moveToRoom(roomId) {
  1155. if (roomId == stageData.currentRoom) {
  1156. return
  1157. }
  1158. switch (roomId) {
  1159. case ROOM_SHOP:
  1160. moveToShop()
  1161. break;
  1162. case ROOM_BREW:
  1163. moveToBrew()
  1164. break;
  1165. case ROOM_MARKET:
  1166. moveToMarket()
  1167. break;
  1168. }
  1169. }
  1170. let keyHistory = []
  1171. export function onKeyPress(code) {
  1172. if (game.keyboard['Digit1'] && stageData.currentRoom == ROOM_BREW) {
  1173. moveToRoom(ROOM_SHOP)
  1174. keyHistory = []
  1175. }
  1176. if (game.keyboard['Digit2'] && stageData.currentRoom != ROOM_BREW) {
  1177. moveToRoom(ROOM_BREW)
  1178. keyHistory = []
  1179. }
  1180. if (game.keyboard['Digit3'] && stageData.currentRoom == ROOM_BREW) {
  1181. moveToRoom(ROOM_MARKET)
  1182. keyHistory = []
  1183. }
  1184. if (game.keyboard['Digit9'] || game.keyboard['F9']) {
  1185. game.orbitControls.enabled = !game.orbitControls.enabled
  1186. if (!game.orbitControls.enabled) {
  1187. console.log(`position:`, game.camera.position)
  1188. console.log(`focus:`, game.orbitControls.target)
  1189. moveToRoom(stageData.currentRoom)
  1190. keyHistory = []
  1191. }
  1192. }
  1193. if (game.keyboard['Escape']) {
  1194. returnToMainMenu(game, stageData)
  1195. keyHistory = []
  1196. }
  1197. keyHistory.push(code.replace("Key", ""))
  1198. const cheatcode = keyHistory.join("").toLowerCase()
  1199. if(cheatcode.includes("ghostbux")) {
  1200. keyHistory = []
  1201. stageData.currency += 1000
  1202. addAmountToSave("currency", 1000)
  1203. updateGameStatusUI(game, stageData)
  1204. stageData.soundEffects['audio/witch_cackle1.ogg'].play()
  1205. }
  1206. if(cheatcode.includes("awesomesauce")) {
  1207. keyHistory = []
  1208. stageData.potionInventory.push(...stageData.potionInfo.map(info => info.name))
  1209. addValueToSave(stageData.potionInventory, "potion-inventory", "spooksauce")
  1210. stageData.soundEffects['audio/witch_cackle1.ogg'].play()
  1211. }
  1212. if(cheatcode.includes("yeschef")) {
  1213. keyHistory = []
  1214. //TODO: grant all ingredients
  1215. stageData.soundEffects['audio/witch_cackle1.ogg'].play()
  1216. }
  1217. }
  1218. export function returnToMainMenu(game, stageData) {
  1219. closeBrewUI(game, stageData)
  1220. closeShopUI(game, stageData)
  1221. hideNavigationUI(game, stageData)
  1222. openMainMenuUI(game, stageData)
  1223. hideGameStatusUI(game, stageData)
  1224. stageData.currentRoom = ROOM_BREW
  1225. game.camera.position.copy(stageData.cameraPositions[3].camera)
  1226. game.lookAtFocus = stageData.cameraPositions[3].focus.clone()
  1227. stageData.brewTutorial1.material.opacity = 0
  1228. stageData.brewTutorial1.castShadow = false
  1229. stageData.brewTutorial1.alreadySeen = false
  1230. stageData.shopTutorial1.material.opacity = 0
  1231. stageData.shopTutorial1.castShadow = false
  1232. stageData.shopTutorial1.alreadySeen = false
  1233. stageData.shopTutorial2.material.opacity = 0
  1234. stageData.shopTutorial2.castShadow = false
  1235. stageData.shopTutorial2.alreadySeen = false
  1236. updatePotionShelfDisplay()
  1237. updateGameStatusUI(game, stageData)
  1238. }