game.js 61 KB

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