├── .gitignore ├── LICENSE ├── README.md ├── osawards-badge.png ├── package.json ├── public ├── cuberun-logo.png ├── favicon.png ├── fonts.css ├── fonts │ ├── Road_Rage.otf │ └── commando.ttf ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json ├── regular.PNG ├── robots.txt └── tunnelred.PNG ├── src ├── audio │ ├── intro-loop.mp3 │ ├── main-nodrums.mp3 │ ├── main-onlydrums.mp3 │ └── speedup.mp3 ├── components │ ├── Arch.js │ ├── CubeTunnel.js │ ├── CubeWorld.js │ ├── Cubes.js │ ├── Effects.js │ ├── FixedCubes.js │ ├── GameState.js │ ├── GlobalColor.js │ ├── Ground.js │ ├── Hyperspace.js │ ├── KeyboardControls.js │ ├── Music.js │ ├── Ship.js │ ├── Skybox.js │ ├── Sound.js │ ├── Walls.js │ └── html │ │ ├── Author.js │ │ ├── CustomLoader.js │ │ ├── GameOverScreen.js │ │ ├── Hud.js │ │ └── Overlay.js ├── constants │ └── index.js ├── fonts │ ├── Road_Rage.otf │ └── commando.ttf ├── index.js ├── models │ └── spaceship.gltf ├── state │ └── useStore.js ├── styles │ ├── author.css │ ├── gameMenu.css │ ├── hud.css │ ├── index.css │ └── normalize.css ├── textures │ ├── cuberun-logo.png │ ├── enginetextureflip.png │ ├── galaxy.jpg │ ├── galaxyTextureBW.png │ ├── grid-blue.png │ ├── grid-green.png │ ├── grid-orange.png │ ├── grid-pink.png │ ├── grid-purple.png │ ├── grid-rainbow.png │ ├── grid-red.png │ └── noise.png └── util │ ├── distance2D.js │ ├── generateFixedCubes.js │ └── randomInRange.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Adam Karlsten 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![Cuberun](./src/textures/cuberun-logo.png) 3 | 4 |

5 | Avoid the cubes while the speed progressively increases! Can you beat the rainbow level? 6 |

7 | 8 | ---- 9 | 10 |
11 | 12 |
13 |

14 | Winner of the 2022 React Open Source Awards in the category Fun Side Project of the Year! 15 |

16 |
17 |
18 | 19 | --- 20 | 21 | The game is inspired by an old flash game I used to play in the late 2000s called Cubefield. My version is in full 3D and built with React, THREE.js and react-three-fiber to glue them together. 22 | 23 | I went for a synthwave aesthetic, including some self-composed music, which the visual effects are synced to (so turn the music on!). 24 | 25 | Also features high scores stored locally. 26 | 27 | The development process will be detailed on my [website](https://adamkarlsten.com). 28 | 29 | ## Screenshots 30 | 31 | ![](./public/regular.PNG) 32 | ![](./public/tunnelred.PNG) 33 | 34 | ## Controls 35 | 36 | * Left: A, LeftArrow 37 | 38 | * Right: D, RightArrow 39 | 40 | Touch devices have on-screen controls. 41 | -------------------------------------------------------------------------------- /osawards-badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akarlsten/cuberun/b3581ac0fbaa4293c571d8935c11cb454b29be03/osawards-badge.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cubeworld", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@react-three/drei": "^5.3.1", 7 | "@react-three/fiber": "^6.2.2", 8 | "react": "^18.0.0-alpha-ed6c091fe-20210701", 9 | "react-device-detect": "^1.17.0", 10 | "react-dom": "^18.0.0-alpha-ed6c091fe-20210701", 11 | "react-scripts": "4.0.3", 12 | "three": "^0.129.0", 13 | "zustand": "^3.5.2" 14 | }, 15 | "scripts": { 16 | "start": "HOST=0.0.0.0 react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject" 20 | }, 21 | "eslintConfig": { 22 | "extends": [ 23 | "react-app", 24 | "react-app/jest" 25 | ] 26 | }, 27 | "browserslist": { 28 | "production": [ 29 | ">0.2%", 30 | "not dead", 31 | "not op_mini all" 32 | ], 33 | "development": [ 34 | "last 1 chrome version", 35 | "last 1 firefox version", 36 | "last 1 safari version" 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /public/cuberun-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akarlsten/cuberun/b3581ac0fbaa4293c571d8935c11cb454b29be03/public/cuberun-logo.png -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akarlsten/cuberun/b3581ac0fbaa4293c571d8935c11cb454b29be03/public/favicon.png -------------------------------------------------------------------------------- /public/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Road Rage'; 3 | src: local('Road Rage'), url(/fonts/Road_Rage.otf) format('opentype'); 4 | } 5 | 6 | @font-face { 7 | font-family: 'Commando'; 8 | src: local('Commando'), url(/fonts/commando.ttf) format('truetype'); 9 | } -------------------------------------------------------------------------------- /public/fonts/Road_Rage.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akarlsten/cuberun/b3581ac0fbaa4293c571d8935c11cb454b29be03/public/fonts/Road_Rage.otf -------------------------------------------------------------------------------- /public/fonts/commando.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akarlsten/cuberun/b3581ac0fbaa4293c571d8935c11cb454b29be03/public/fonts/commando.ttf -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | CubeRun 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akarlsten/cuberun/b3581ac0fbaa4293c571d8935c11cb454b29be03/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akarlsten/cuberun/b3581ac0fbaa4293c571d8935c11cb454b29be03/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "CubeRun", 3 | "name": "CubeRun", 4 | "icons": [ 5 | { 6 | "src": "favicon.png", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "fullscreen", 23 | "theme_color": "#fe2079", 24 | "background_color": "#141622" 25 | } -------------------------------------------------------------------------------- /public/regular.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akarlsten/cuberun/b3581ac0fbaa4293c571d8935c11cb454b29be03/public/regular.PNG -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/tunnelred.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akarlsten/cuberun/b3581ac0fbaa4293c571d8935c11cb454b29be03/public/tunnelred.PNG -------------------------------------------------------------------------------- /src/audio/intro-loop.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akarlsten/cuberun/b3581ac0fbaa4293c571d8935c11cb454b29be03/src/audio/intro-loop.mp3 -------------------------------------------------------------------------------- /src/audio/main-nodrums.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akarlsten/cuberun/b3581ac0fbaa4293c571d8935c11cb454b29be03/src/audio/main-nodrums.mp3 -------------------------------------------------------------------------------- /src/audio/main-onlydrums.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akarlsten/cuberun/b3581ac0fbaa4293c571d8935c11cb454b29be03/src/audio/main-onlydrums.mp3 -------------------------------------------------------------------------------- /src/audio/speedup.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akarlsten/cuberun/b3581ac0fbaa4293c571d8935c11cb454b29be03/src/audio/speedup.mp3 -------------------------------------------------------------------------------- /src/components/Arch.js: -------------------------------------------------------------------------------- 1 | import { useFrame } from '@react-three/fiber' 2 | import { useRef } from 'react' 3 | 4 | import { useStore, mutation } from '../state/useStore' 5 | import { PLANE_SIZE, COLORS, LEVEL_SIZE } from '../constants' 6 | 7 | export default function Arch() { 8 | const ship = useStore((s) => s.ship) 9 | const level = useStore(s => s.level) 10 | 11 | const arches = useRef() 12 | 13 | const arch1 = useRef() 14 | const arch2 = useRef() 15 | const arch3 = useRef() 16 | const arch4 = useRef() 17 | const arch5 = useRef() 18 | const arch6 = useRef() 19 | 20 | // latter arches 21 | const arch7 = useRef() 22 | const arch8 = useRef() 23 | const arch9 = useRef() 24 | const arch10 = useRef() 25 | 26 | const levelColor = (base) => { 27 | return (base + level) % LEVEL_SIZE 28 | } 29 | 30 | useFrame((state, delta) => { 31 | if (ship.current) { 32 | if (mutation.shouldShiftItems) { 33 | arches.current.position.z = mutation.currentLevelLength - PLANE_SIZE * (LEVEL_SIZE - 2) - 300 34 | arch7.current.visible = true 35 | arch8.current.visible = true 36 | arch9.current.visible = true 37 | arch10.current.visible = true 38 | } 39 | } 40 | 41 | 42 | // TODO: maybe set arches to globalcolor always 43 | if (mutation.colorLevel === 6) { 44 | arch1.current.material.color = mutation.globalColor 45 | arch2.current.material.color = mutation.globalColor 46 | arch3.current.material.color = mutation.globalColor 47 | arch4.current.material.color = mutation.globalColor 48 | arch5.current.material.color = mutation.globalColor 49 | arch6.current.material.color = mutation.globalColor 50 | arch7.current.material.color = mutation.globalColor 51 | arch8.current.material.color = mutation.globalColor 52 | arch9.current.material.color = mutation.globalColor 53 | arch10.current.material.color = mutation.globalColor 54 | } else { 55 | arch1.current.material.color = COLORS[levelColor(0)].three 56 | arch2.current.material.color = COLORS[levelColor(1)].three 57 | arch3.current.material.color = COLORS[levelColor(2)].three 58 | arch4.current.material.color = COLORS[levelColor(3)].three 59 | arch5.current.material.color = COLORS[levelColor(4)].three 60 | arch6.current.material.color = COLORS[levelColor(5)].three 61 | 62 | arch7.current.material.color = COLORS[levelColor(0)].three 63 | arch8.current.material.color = COLORS[levelColor(0)].three 64 | arch9.current.material.color = COLORS[levelColor(0)].three 65 | arch10.current.material.color = COLORS[levelColor(0)].three 66 | } 67 | 68 | 69 | const scaleFactor = mutation.currentMusicLevel 70 | 71 | if (scaleFactor > 0.8 && arches.current.scale.x > 0.95) { 72 | arches.current.scale.x -= scaleFactor * delta * 1 73 | arches.current.scale.y -= scaleFactor * delta * 1 74 | } else if (arches.current.scale.x < 1) { 75 | arches.current.scale.x += scaleFactor * delta * 0.5 76 | arches.current.scale.y += scaleFactor * delta * 0.5 77 | } 78 | }) 79 | 80 | return ( 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | ) 124 | } -------------------------------------------------------------------------------- /src/components/CubeTunnel.js: -------------------------------------------------------------------------------- 1 | import FixedCubes from './FixedCubes' 2 | import Arch from './Arch' 3 | import Hyperspace from './Hyperspace' 4 | 5 | export default function CubeTunnel() { 6 | 7 | 8 | return ( 9 | <> 10 | 11 | 12 | 13 | 14 | ) 15 | } -------------------------------------------------------------------------------- /src/components/CubeWorld.js: -------------------------------------------------------------------------------- 1 | import { Canvas } from '@react-three/fiber' 2 | import { Suspense } from 'react' 3 | import { Preload } from '@react-three/drei' 4 | 5 | import { useStore } from '../state/useStore' 6 | 7 | // THREE components 8 | import Ship from './Ship' 9 | import Ground from './Ground' 10 | import Skybox from './Skybox' 11 | import Cubes from './Cubes' 12 | import Walls from './Walls' 13 | import CubeTunnel from './CubeTunnel' 14 | import Effects from './Effects' 15 | 16 | // State/dummy components 17 | import KeyboardControls from './KeyboardControls' 18 | import GameState from './GameState' 19 | import GlobalColor from './GlobalColor' 20 | import Music from './Music' 21 | import Sound from './Sound' 22 | 23 | // HTML components 24 | import Overlay from './html/Overlay' 25 | import Hud from './html/Hud' 26 | import GameOverScreen from './html/GameOverScreen' 27 | 28 | 29 | export default function CubeWorld({ color, bgColor }) { 30 | const directionalLight = useStore((s) => s.directionalLight) 31 | 32 | return ( 33 | <> 34 | 35 | 36 | 37 | 38 | 43 | 44 | 45 | {directionalLight.current && } 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | ) 64 | } 65 | 66 | -------------------------------------------------------------------------------- /src/components/Cubes.js: -------------------------------------------------------------------------------- 1 | import { Object3D } from 'three' 2 | import { useRef, useMemo } from 'react' 3 | import { useFrame } from '@react-three/fiber' 4 | 5 | import { CUBE_AMOUNT, CUBE_SIZE, PLANE_SIZE, COLORS, WALL_RADIUS, LEVEL_SIZE, LEFT_BOUND, RIGHT_BOUND } from '../constants' 6 | import { useStore, mutation } from '../state/useStore' 7 | 8 | import randomInRange from '../util/randomInRange' 9 | import distance2D from '../util/distance2D' 10 | 11 | const negativeBound = LEFT_BOUND + WALL_RADIUS / 2 12 | const positiveBound = RIGHT_BOUND - WALL_RADIUS / 2 13 | 14 | export default function InstancedCubes() { 15 | const mesh = useRef() 16 | const material = useRef() 17 | 18 | const ship = useStore(s => s.ship) 19 | const level = useStore(s => s.level) 20 | 21 | const dummy = useMemo(() => new Object3D(), []) 22 | const cubes = useMemo(() => { 23 | // Setup initial cube positions 24 | const temp = [] 25 | for (let i = 0; i < CUBE_AMOUNT; i++) { 26 | const x = randomInRange(negativeBound, positiveBound) 27 | const y = 10 28 | const z = -900 + randomInRange(-400, 400) 29 | 30 | temp.push({ x, y, z }) 31 | } 32 | return temp 33 | }, []) 34 | 35 | const diamondStart = useMemo(() => -(level * PLANE_SIZE * LEVEL_SIZE) - PLANE_SIZE * (LEVEL_SIZE - 2.6), [level]) 36 | const diamondEnd = useMemo(() => -(level * PLANE_SIZE * LEVEL_SIZE) - PLANE_SIZE * (LEVEL_SIZE), [level]) 37 | 38 | useFrame((state, delta) => { 39 | let isOutsideDiamond = false 40 | if (ship.current) { 41 | if (ship.current.position.z > diamondStart || ship.current.position.z < diamondEnd) { 42 | isOutsideDiamond = true 43 | } 44 | } 45 | 46 | cubes.forEach((cube, i) => { 47 | if (ship.current) { 48 | if (cube.z - ship.current.position.z > -15) { // No need to run the rather expensive distance function if the ship is too far away 49 | if (cube.x - ship.current.position.x > -15 || cube.x - ship.current.position.x < 15) { 50 | const distanceToShip = distance2D(ship.current.position.x, ship.current.position.z, cube.x, cube.z) 51 | 52 | if (distanceToShip < 12) { 53 | mutation.gameSpeed = 0 54 | mutation.gameOver = true 55 | } 56 | } 57 | } 58 | 59 | if (cube.z - ship.current.position.z > 15) { 60 | if (isOutsideDiamond) { 61 | cube.z = ship.current.position.z - PLANE_SIZE + randomInRange(-200, 0) 62 | cube.y = -CUBE_SIZE 63 | cube.x = randomInRange(negativeBound, positiveBound) 64 | } else { 65 | cube.z = ship.current.position.z - (PLANE_SIZE * 3.1) + randomInRange(-200, 0) 66 | cube.y = -CUBE_SIZE 67 | cube.x = randomInRange(negativeBound, positiveBound) 68 | } 69 | } 70 | 71 | if (cube.y < CUBE_SIZE / 2) { 72 | if (cube.y + delta * 100 > CUBE_SIZE / 2) { 73 | cube.y = CUBE_SIZE / 2 74 | } else { 75 | cube.y += delta * 100 76 | } 77 | } 78 | } 79 | 80 | material.current.color = mutation.globalColor 81 | 82 | dummy.position.set( 83 | cube.x, 84 | cube.y, 85 | cube.z 86 | ) 87 | 88 | // apply changes to dummy and to the instanced matrix 89 | dummy.updateMatrix() 90 | mesh.current.setMatrixAt(i, dummy.matrix) 91 | }) 92 | 93 | // Tells THREE to draw the updated matrix, I guess? 94 | mesh.current.instanceMatrix.needsUpdate = true 95 | }) 96 | 97 | return ( 98 | 99 | 100 | 101 | 102 | ) 103 | } -------------------------------------------------------------------------------- /src/components/Effects.js: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect } from 'react' 2 | import { extend, useThree, useFrame } from '@react-three/fiber' 3 | import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer' 4 | import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass' 5 | import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass' 6 | import { SSAOPass } from 'three/examples/jsm/postprocessing/SSAOPass' 7 | import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass' 8 | import { AfterimagePass } from 'three/examples/jsm/postprocessing/AfterimagePass' 9 | import { FXAAShader } from 'three/examples/jsm/shaders/FXAAShader' 10 | 11 | import { useStore, mutation } from '../state/useStore' 12 | 13 | extend({ EffectComposer, ShaderPass, RenderPass, UnrealBloomPass, SSAOPass, AfterimagePass }) 14 | 15 | 16 | 17 | 18 | export default function Effects() { 19 | const composer = useRef() 20 | const { scene, gl, size, camera } = useThree() 21 | 22 | const bloomFactor = useRef(0) 23 | 24 | const musicEnabled = useStore(s => s.musicEnabled) 25 | 26 | useEffect(() => void composer.current.setSize(size.width, size.height), [size]) 27 | useFrame((state, delta) => { 28 | if (musicEnabled) { 29 | const bloom = composer.current.passes[1] 30 | 31 | // const bloomFactor = mutation.currentMusicLevel 32 | // console.log(bloomFactor) 33 | 34 | if (mutation.currentMusicLevel > bloomFactor.current) { 35 | bloomFactor.current = mutation.currentMusicLevel 36 | } else { 37 | bloomFactor.current -= delta * 0.5 38 | } 39 | 40 | bloom.strength = bloomFactor.current > 0.8 ? bloomFactor.current : 0.8 41 | // bloom.radius = bloomFactor + 0.2 > 1 ? bloomFactor + 0.2 : 1 42 | } 43 | composer.current.render() 44 | }, 1) 45 | 46 | 47 | return ( 48 | 49 | 50 | 51 | 52 | 53 | ) 54 | } -------------------------------------------------------------------------------- /src/components/FixedCubes.js: -------------------------------------------------------------------------------- 1 | import { Object3D } from 'three' 2 | import { useRef, useMemo } from 'react' 3 | import { useFrame } from '@react-three/fiber' 4 | 5 | import { PLANE_SIZE, COLORS, LEVEL_SIZE } from '../constants' 6 | import { useStore, mutation } from '../state/useStore' 7 | 8 | import distance2D from '../util/distance2D' 9 | import { generateCubeTunnel, generateDiamond } from '../util/generateFixedCubes' 10 | 11 | 12 | export default function InstancedCubes() { 13 | const mesh = useRef() 14 | const material = useRef() 15 | 16 | const ship = useStore(s => s.ship) 17 | const level = useStore(s => s.level) 18 | 19 | const tunnelCoords = useMemo(() => generateCubeTunnel(), []) 20 | const diamondCoords = useMemo(() => generateDiamond(), []) 21 | 22 | const dummy = useMemo(() => new Object3D(), []) 23 | const cubes = useMemo(() => { 24 | // Setup initial cube positions 25 | const temp = [] 26 | for (let i = 0; i < diamondCoords.length; i++) { 27 | const x = tunnelCoords[i]?.x || 0 28 | const y = 0 29 | const z = 300 + tunnelCoords[i]?.z || 10 30 | 31 | temp.push({ x, y, z }) 32 | } 33 | return temp 34 | }, [diamondCoords, tunnelCoords]) 35 | 36 | const currentLevelMinusDiamondStart = useMemo(() => -(level * PLANE_SIZE * LEVEL_SIZE) - PLANE_SIZE * (LEVEL_SIZE - 2), [level]) 37 | 38 | useFrame((state, delta) => { 39 | cubes.forEach((cube, i) => { 40 | if (ship.current) { 41 | if (cube.z - ship.current.position.z > -15) { 42 | if (cube.x - ship.current.position.x > -15 || cube.x - ship.current.position.x < 15) { 43 | const distanceToShip = distance2D(ship.current.position.x, ship.current.position.z, cube.x, cube.z) 44 | 45 | if (distanceToShip < 12) { 46 | mutation.gameSpeed = 0 47 | mutation.gameOver = true 48 | } 49 | } 50 | } 51 | 52 | if (mutation.shouldShiftItems) { // 4 53 | cube.x = diamondCoords[i].x 54 | cube.y = 0 55 | cube.z = currentLevelMinusDiamondStart + diamondCoords[i].z 56 | } 57 | 58 | } 59 | 60 | material.current.color = mutation.globalColor 61 | 62 | dummy.position.set( 63 | cube.x, 64 | cube.y, 65 | cube.z 66 | ) 67 | 68 | // apply changes to dummy and to the instanced matrix 69 | dummy.updateMatrix() 70 | mesh.current.setMatrixAt(i, dummy.matrix) 71 | }) 72 | 73 | // Tells THREE to draw the updated matrix, I guess? 74 | mesh.current.instanceMatrix.needsUpdate = true 75 | }) 76 | 77 | return ( 78 | 79 | 80 | 81 | 82 | ) 83 | } -------------------------------------------------------------------------------- /src/components/GameState.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import { useFrame } from '@react-three/fiber' 3 | 4 | import { useStore, mutation } from '../state/useStore' 5 | import { INITIAL_GAME_SPEED, PLANE_SIZE, LEVEL_SIZE } from '../constants' 6 | 7 | // this is supposedly a performance improvement 8 | const shipSelector = s => s.ship 9 | const setScoreSelector = s => s.setScore 10 | const gameStartedSelector = s => s.gameStarted 11 | const setIsSpeedingUpSelector = s => s.setIsSpeedingUp 12 | const setGameOverSelector = s => s.setGameOver 13 | 14 | export default function GameState() { 15 | const ship = useStore(shipSelector) 16 | const setScore = useStore(setScoreSelector) 17 | const gameStarted = useStore(gameStartedSelector) 18 | const setIsSpeedingUp = useStore(setIsSpeedingUpSelector) 19 | const setGameOver = useStore(setGameOverSelector) 20 | 21 | const level = useStore(s => s.level) 22 | 23 | useEffect(() => { 24 | mutation.currentLevelLength = -(level * PLANE_SIZE * LEVEL_SIZE) 25 | }, [level]) 26 | 27 | useEffect(() => { 28 | if (gameStarted) { 29 | mutation.desiredSpeed = INITIAL_GAME_SPEED 30 | } 31 | }, [gameStarted]) 32 | 33 | 34 | 35 | useFrame((state, delta) => { 36 | 37 | // acceleration logic 38 | const accelDelta = 1 * delta * 0.15 39 | if (gameStarted && !mutation.gameOver) { 40 | if (mutation.gameSpeed < mutation.desiredSpeed) { 41 | setIsSpeedingUp(true) 42 | if (mutation.gameSpeed + accelDelta > mutation.desiredSpeed) { 43 | mutation.gameSpeed = mutation.desiredSpeed 44 | } else { 45 | mutation.gameSpeed += accelDelta 46 | } 47 | } else { 48 | setIsSpeedingUp(false) 49 | } 50 | } 51 | 52 | if (ship.current) { 53 | // sets the score counter in the hud 54 | mutation.score = Math.abs(ship.current.position.z) - 10 55 | 56 | // optimization, instead of calculating this for all elements we do it once per frame here 57 | mutation.shouldShiftItems = ship.current.position.z < -400 && ship.current.position.z < mutation.currentLevelLength - 400 && ship.current.position.z > mutation.currentLevelLength - 1000 58 | } 59 | 60 | if (gameStarted && mutation.gameOver) { 61 | setScore(Math.abs(ship.current.position.z) - 10) 62 | setGameOver(true) 63 | } 64 | }) 65 | 66 | return null 67 | } -------------------------------------------------------------------------------- /src/components/GlobalColor.js: -------------------------------------------------------------------------------- 1 | import { useFrame } from '@react-three/fiber' 2 | import { useRef } from 'react' 3 | 4 | import { COLORS } from '../constants' 5 | 6 | import { mutation } from '../state/useStore' 7 | 8 | // handles gracefully fading all objects to the correct color depending on level 9 | // mutates state directly for performance reasons 10 | export default function GlobalColor({ materialRef }) { 11 | const colorAlpha = useRef(0) 12 | const previousLevel = useRef(0) 13 | 14 | const rainbowAlpha1 = useRef(0) 15 | const rainbowAlpha2 = useRef(0) 16 | const rainbowAlpha3 = useRef(0) 17 | const rainbowAlpha4 = useRef(0) 18 | const rainbowAlpha5 = useRef(0) 19 | 20 | useFrame((state, delta) => { 21 | // Make sure the first level is red 22 | if (mutation.colorLevel === 0) { 23 | mutation.globalColor.set(COLORS[0].three) 24 | } 25 | 26 | // Rainbow Level 27 | if (mutation.colorLevel === 6) { 28 | const rainbowSpeed = delta * 3 29 | 30 | if (rainbowAlpha1.current < 1) { 31 | rainbowAlpha1.current += rainbowSpeed 32 | mutation.globalColor.lerpColors(COLORS[0].three, COLORS[2].three, rainbowAlpha1.current) 33 | } else if (rainbowAlpha2.current < 1) { 34 | rainbowAlpha2.current += rainbowSpeed 35 | mutation.globalColor.lerpColors(COLORS[2].three, COLORS[3].three, rainbowAlpha2.current) 36 | } else if (rainbowAlpha3.current < 1) { 37 | rainbowAlpha3.current += rainbowSpeed 38 | mutation.globalColor.lerpColors(COLORS[3].three, COLORS[1].three, rainbowAlpha3.current) 39 | } else if (rainbowAlpha4.current < 1) { 40 | rainbowAlpha4.current += rainbowSpeed 41 | mutation.globalColor.lerpColors(COLORS[1].three, COLORS[5].three, rainbowAlpha4.current) 42 | } else if (rainbowAlpha5.current < 1) { 43 | rainbowAlpha5.current += rainbowSpeed 44 | mutation.globalColor.lerpColors(COLORS[5].three, COLORS[0].three, rainbowAlpha5.current) 45 | } else { 46 | rainbowAlpha1.current = 0 47 | rainbowAlpha2.current = 0 48 | rainbowAlpha3.current = 0 49 | rainbowAlpha4.current = 0 50 | rainbowAlpha5.current = 0 51 | } 52 | 53 | previousLevel.current = 0 54 | 55 | // regular levels 56 | } else if (mutation.colorLevel > previousLevel.current) { 57 | mutation.globalColor.lerpColors(COLORS[previousLevel.current].three, COLORS[mutation.colorLevel].three, colorAlpha.current) 58 | 59 | if (colorAlpha.current + (delta * mutation.gameSpeed) * 0.5 > 1) { 60 | colorAlpha.current = 1 61 | } else { 62 | colorAlpha.current += (delta * mutation.gameSpeed) * 0.5 63 | } 64 | 65 | if (colorAlpha.current === 1) { 66 | previousLevel.current = mutation.colorLevel 67 | colorAlpha.current = 0 68 | } 69 | } 70 | }) 71 | 72 | return null 73 | } -------------------------------------------------------------------------------- /src/components/Ground.js: -------------------------------------------------------------------------------- 1 | import { Color, RepeatWrapping } from 'three' 2 | import React, { useRef, Suspense, useLayoutEffect } from 'react' 3 | import { useFrame } from '@react-three/fiber' 4 | import { useTexture } from '@react-three/drei' 5 | import { useStore, mutation } from '../state/useStore' 6 | 7 | import { PLANE_SIZE, GAME_SPEED_MULTIPLIER } from '../constants' 8 | 9 | import gridRed from '../textures/grid-red.png' 10 | import gridOrange from '../textures/grid-orange.png' 11 | import gridGreen from '../textures/grid-green.png' 12 | import gridBlue from '../textures/grid-blue.png' 13 | import gridPurple from '../textures/grid-purple.png' 14 | import gridPink from '../textures/grid-pink.png' 15 | import gridRainbow from '../textures/grid-rainbow.png' 16 | 17 | const TEXTURE_SIZE = PLANE_SIZE * 0.05 // 0.075 18 | const MOVE_DISTANCE = PLANE_SIZE * 2 19 | 20 | const color = new Color(0x000000) 21 | 22 | function Ground() { 23 | const ground = useRef() 24 | const groundTwo = useRef() 25 | 26 | const plane = useRef() 27 | const planeTwo = useRef() 28 | 29 | const textures = useTexture([gridPink, gridRed, gridOrange, gridGreen, gridBlue, gridPurple, gridRainbow]) 30 | 31 | const ship = useStore(s => s.ship) 32 | const incrementLevel = useStore(s => s.incrementLevel) 33 | 34 | useLayoutEffect(() => { 35 | textures.forEach(texture => { 36 | texture.wrapS = texture.wrapT = RepeatWrapping 37 | texture.repeat.set(TEXTURE_SIZE, TEXTURE_SIZE) 38 | texture.anisotropy = 16 39 | }) 40 | }, [textures]) 41 | 42 | const moveCounter = useRef(1) 43 | const lastMove = useRef(0) 44 | 45 | useFrame((state, delta) => { 46 | 47 | if (ship.current) { 48 | // Alternates moving the two ground planes when we've just passed over onto a new plane, with logic to make sure it only happens once per pass 49 | // Checks if weve moved 10 meters into the new plane (-10) (so the old plane is no longer visible) 50 | if (Math.round(ship.current.position.z) + PLANE_SIZE * moveCounter.current + 10 < -10) { 51 | 52 | // Ensures we only move the plane once per pass 53 | if (moveCounter.current === 1 || Math.abs(ship.current.position.z) - Math.abs(lastMove.current) <= 10) { 54 | 55 | // change the level every 4 moves or 4000 meters 56 | if (moveCounter.current % 6 === 0) { 57 | incrementLevel() 58 | mutation.colorLevel++ 59 | mutation.desiredSpeed += GAME_SPEED_MULTIPLIER 60 | 61 | if (mutation.colorLevel >= textures.length) { 62 | mutation.colorLevel = 0 63 | } 64 | } 65 | 66 | if (moveCounter.current % 2 === 0) { 67 | groundTwo.current.position.z -= MOVE_DISTANCE 68 | lastMove.current = groundTwo.current.position.z 69 | planeTwo.current.material.map = textures[mutation.colorLevel] 70 | } else { 71 | ground.current.position.z -= MOVE_DISTANCE 72 | lastMove.current = ground.current.position.z 73 | plane.current.material.map = textures[mutation.colorLevel] 74 | } 75 | } 76 | 77 | moveCounter.current++ 78 | } 79 | } 80 | 81 | 82 | // handles changing ground color between levels (really interpolating the emissiveness and changing the emissive map around) 83 | if (mutation.colorLevel > 0) { 84 | if (plane.current.material.map.uuid !== planeTwo.current.material.map.uuid) { 85 | if (plane.current.material.emissiveIntensity < 1) { 86 | if (plane.current.material.emissiveIntensity + delta * mutation.gameSpeed > 1) { 87 | plane.current.material.emissiveIntensity = 1 88 | } else { 89 | plane.current.material.emissiveIntensity += delta * mutation.gameSpeed 90 | } 91 | } else { 92 | plane.current.material.map = textures[mutation.colorLevel] 93 | if (mutation.colorLevel === textures.length - 1) { 94 | plane.current.material.emissiveMap = textures[0] 95 | } else { 96 | plane.current.material.emissiveMap = textures[mutation.colorLevel + 1] 97 | } 98 | plane.current.material.emissiveIntensity = 0 99 | } 100 | } 101 | } 102 | }) 103 | 104 | 105 | return ( 106 | <> 107 | 108 | 114 | 115 | 126 | 127 | 128 | 129 | 135 | 136 | 144 | 145 | 146 | 147 | ) 148 | } 149 | 150 | function LoadingGround() { 151 | return ( 152 | 158 | 159 | 166 | 167 | ) 168 | } 169 | 170 | export default function CompleteGround() { 171 | 172 | return ( 173 | }> 174 | 175 | 176 | ) 177 | } -------------------------------------------------------------------------------- /src/components/Hyperspace.js: -------------------------------------------------------------------------------- 1 | import { useFrame, useThree } from '@react-three/fiber' 2 | import { useTexture } from '@react-three/drei' 3 | import { useRef, useLayoutEffect, useState, Suspense, useMemo } from 'react' 4 | import { MirroredRepeatWrapping, Vector2, BackSide } from 'three' 5 | 6 | import { useStore, mutation } from '../state/useStore' 7 | import { PLANE_SIZE, COLORS, LEVEL_SIZE } from '../constants' 8 | 9 | import galaxyTexture from '../textures/galaxyTextureBW.png' 10 | 11 | function HyperspaceTunnel() { 12 | const texture = useTexture(galaxyTexture) 13 | 14 | const ship = useStore((s) => s.ship) 15 | const level = useStore(s => s.level) 16 | 17 | const { clock } = useThree() 18 | 19 | const tunnel = useRef() 20 | 21 | const tunnel2 = useRef() 22 | 23 | const tunnels = useRef() 24 | 25 | const repeatX = useRef(10) 26 | const repeatY = useRef(4) 27 | 28 | 29 | const [lathe] = useState(() => { 30 | const points = [ 31 | new Vector2(100, 150), 32 | new Vector2(90, 200), 33 | new Vector2(80, 250), 34 | new Vector2(70, 300), 35 | new Vector2(60, 350), 36 | new Vector2(50, 400), 37 | 38 | ] 39 | 40 | return points 41 | }) 42 | 43 | const [lathe2] = useState(() => { 44 | const points = [ 45 | new Vector2(50, 400), 46 | new Vector2(300, 600), // 6 47 | new Vector2(300, 1000), // 7 48 | new Vector2(300, 1600), // 8 49 | new Vector2(100, 1720), // 9 50 | new Vector2(50, 1790), 51 | new Vector2(50, 1980)] 52 | 53 | return points 54 | }) 55 | 56 | useLayoutEffect(() => { 57 | texture.wrapS = texture.wrapT = MirroredRepeatWrapping 58 | texture.repeat.set(10, 4) 59 | texture.anisotropy = 16 60 | }, [texture]) 61 | 62 | const lowerBound = useMemo(() => -4250 - PLANE_SIZE * (level * LEVEL_SIZE), [level]) 63 | const upperBound = useMemo(() => -6500 - PLANE_SIZE * (level * LEVEL_SIZE), [level]) 64 | const prevLowerBound = useMemo(() => -4250 - PLANE_SIZE * ((level - 1) * LEVEL_SIZE), [level]) 65 | const prevUpperBound = useMemo(() => -6500 - PLANE_SIZE * ((level - 1) * LEVEL_SIZE), [level]) 66 | 67 | useFrame((state, delta) => { 68 | if (ship.current) { 69 | if (mutation.shouldShiftItems) { 70 | tunnel.current.position.z = lowerBound - 50 71 | tunnel2.current.position.z = lowerBound - 50 72 | } 73 | } 74 | 75 | if (ship.current) { 76 | if ((ship.current.position.z < lowerBound && ship.current.position.z > upperBound) || 77 | (ship.current.position.z < prevLowerBound && ship.current.position.z > prevUpperBound)) { 78 | tunnel2.current.visible = true 79 | } else { 80 | tunnel2.current.visible = false 81 | } 82 | } 83 | 84 | 85 | repeatY.current = 0.3 + (Math.sin(clock.getElapsedTime() / 3)) * 1.5 86 | repeatX.current = 6 + (Math.sin(clock.getElapsedTime() / 2)) * 4 87 | 88 | tunnel.current.material.map.offset.x += 0.01 * delta * 165 89 | tunnel.current.material.map.offset.y += 0.005 * delta * 165 90 | tunnel.current.material.map.repeat.set(repeatX.current, repeatY.current) 91 | tunnel.current.material.emissive = mutation.globalColor 92 | tunnel2.current.material.map.offset.x += 0.01 * delta * 165 93 | tunnel2.current.material.map.offset.y += 0.005 * delta * 165 94 | tunnel2.current.material.map.repeat.set(repeatX.current, repeatY.current) 95 | tunnel2.current.material.emissive = mutation.globalColor 96 | 97 | 98 | const scaleFactor = mutation.currentMusicLevel 99 | 100 | if (scaleFactor > 0.8 && tunnels.current.scale.x > 0.95) { 101 | tunnels.current.scale.x -= scaleFactor * delta * 1 102 | tunnels.current.scale.y -= scaleFactor * delta * 1 103 | } else if (tunnels.current.scale.x < 1) { 104 | tunnels.current.scale.x += scaleFactor * delta * 0.5 105 | tunnels.current.scale.y += scaleFactor * delta * 0.5 106 | } 107 | }) 108 | 109 | return ( 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | ) 121 | } 122 | 123 | export default function SuspendedHyperspaceTunnel() { 124 | return ( 125 | 126 | 127 | 128 | ) 129 | } -------------------------------------------------------------------------------- /src/components/KeyboardControls.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import { useStore } from '../state/useStore' 3 | 4 | const pressed = [] 5 | 6 | function useKeys(target, event, up = true) { 7 | useEffect(() => { 8 | const downHandler = (e) => { 9 | if (target.indexOf(e.key) !== -1) { 10 | const isRepeating = !!pressed[e.keyCode] 11 | pressed[e.keyCode] = true 12 | if (up || !isRepeating) event(true) 13 | } 14 | } 15 | 16 | const upHandler = (e) => { 17 | if (target.indexOf(e.key) !== -1) { 18 | pressed[e.keyCode] = false 19 | if (up) event(false) 20 | } 21 | } 22 | 23 | window.addEventListener('keydown', downHandler, { passive: true }) 24 | window.addEventListener('keyup', upHandler, { passive: true }) 25 | return () => { 26 | window.removeEventListener('keydown', downHandler) 27 | window.removeEventListener('keyup', upHandler) 28 | } 29 | }, [target, event, up]) 30 | } 31 | 32 | export default function KeyboardControls() { 33 | const set = useStore((state) => state.set) 34 | useKeys(['ArrowLeft', 'a', 'A'], (left) => set((state) => ({ ...state, controls: { ...state.controls, left } }))) 35 | useKeys(['ArrowRight', 'd', 'D'], (right) => set((state) => ({ ...state, controls: { ...state.controls, right } }))) 36 | 37 | return null 38 | } -------------------------------------------------------------------------------- /src/components/Music.js: -------------------------------------------------------------------------------- 1 | import { AudioListener, AudioLoader, AudioAnalyser } from 'three' 2 | import { useRef, useEffect, useState, Suspense } from 'react' 3 | import { useLoader, useFrame } from '@react-three/fiber' 4 | import { MathUtils } from 'three' 5 | 6 | 7 | import { mutation, useStore } from '../state/useStore' 8 | 9 | import introSong from '../audio/intro-loop.mp3' 10 | 11 | import mainSong from '../audio/main-nodrums.mp3' 12 | import mainSongDrums from '../audio/main-onlydrums.mp3' 13 | 14 | function Music() { 15 | const introPlayer = useRef() 16 | const themePlayer = useRef() 17 | const drumPlayer = useRef() 18 | 19 | const soundOrigin = useRef() 20 | 21 | const musicEnabled = useStore(s => s.musicEnabled) 22 | const gameStarted = useStore(s => s.gameStarted) 23 | const gameOver = useStore(s => s.gameOver) 24 | const camera = useStore(s => s.camera) 25 | const level = useStore(s => s.level) 26 | const hasInteracted = useStore(s => s.hasInteracted) 27 | 28 | const [listener] = useState(() => new AudioListener()) 29 | 30 | const introTheme = useLoader(AudioLoader, introSong) 31 | const mainTheme = useLoader(AudioLoader, mainSong) 32 | const mainThemeDrums = useLoader(AudioLoader, mainSongDrums) 33 | 34 | const themeFilter = useRef() 35 | const audioAnalyzer = useRef() 36 | 37 | const introVolume = useRef(1) 38 | const themeVolume = useRef(0) 39 | const drumVolume = useRef(0) 40 | 41 | const introPlaying = useRef(true) 42 | const startCrossfade = useRef(false) 43 | 44 | useEffect(() => { 45 | if (hasInteracted && musicEnabled) { 46 | introPlayer.current.context.resume() 47 | } 48 | }, [hasInteracted, musicEnabled]) 49 | 50 | useEffect(() => { 51 | introPlayer.current.setBuffer(introTheme) 52 | }, [introTheme]) 53 | 54 | // creates a lowpass filter with the browser audio API, also an audio analyzer 55 | useEffect(() => { 56 | themePlayer.current.setBuffer(mainTheme) 57 | themeFilter.current = themePlayer.current.context.createBiquadFilter() 58 | themeFilter.current.type = "lowpass" 59 | themeFilter.current.frequency.value = 0 60 | themePlayer.current.setFilter(themeFilter.current) 61 | 62 | }, [mainTheme]) 63 | 64 | useEffect(() => { 65 | drumPlayer.current.setBuffer(mainThemeDrums) 66 | 67 | audioAnalyzer.current = new AudioAnalyser(drumPlayer.current, 32) 68 | }, [mainThemeDrums]) 69 | 70 | useEffect(() => { 71 | if (!musicEnabled) { 72 | if (introPlayer.current?.isPlaying) { 73 | introPlayer.current.stop() 74 | } 75 | 76 | if (themePlayer.current?.isPlaying) { 77 | themePlayer.current.stop() 78 | drumPlayer.current.stop() 79 | } 80 | 81 | } 82 | }, [musicEnabled]) 83 | 84 | useEffect(() => { 85 | if (musicEnabled && !gameOver) { 86 | if (!introPlayer.current.isPlaying) { 87 | introPlayer.current.play() 88 | introPlaying.current = true 89 | } 90 | } else { 91 | if (introPlayer.current?.isPlaying) { 92 | introPlayer.current.stop() 93 | } 94 | } 95 | 96 | introPlayer.current.setLoop(true) 97 | themePlayer.current.setLoop(true) 98 | drumPlayer.current.setLoop(true) 99 | 100 | if (camera.current) { 101 | const cam = camera.current 102 | cam.add(listener) 103 | return () => cam.remove(listener) 104 | } 105 | 106 | }, [musicEnabled, introTheme, mainTheme, mainThemeDrums, gameStarted, gameOver, camera, listener]) 107 | 108 | useEffect(() => { 109 | if (level > 0 && level % 2 === 0) { 110 | themePlayer.current.setPlaybackRate(1 + level * 0.02) 111 | drumPlayer.current.setPlaybackRate(1 + level * 0.02) 112 | } else if (level === 0) { 113 | themePlayer.current.setPlaybackRate(1) 114 | drumPlayer.current.setPlaybackRate(1) 115 | } 116 | }, [level]) 117 | 118 | useFrame((state, delta) => { 119 | if (musicEnabled) { 120 | 121 | if (audioAnalyzer.current) { 122 | const audioLevel = MathUtils.inverseLerp(0, 255, audioAnalyzer.current.getFrequencyData()[0]) 123 | mutation.currentMusicLevel = audioLevel 124 | } 125 | 126 | // start playing main theme "on the beat" when game starts 127 | if (gameStarted && !themePlayer.current.isPlaying) { 128 | if (introPlayer.current.context.currentTime.toFixed(1) % 9.6 === 0) { 129 | startCrossfade.current = true 130 | themePlayer.current.play() 131 | drumPlayer.current.play() 132 | themePlayer.current.setVolume(0) 133 | drumPlayer.current.setVolume(0) 134 | } 135 | } 136 | 137 | // crossfade intro music to main theme when game starts 138 | if (gameStarted && !gameOver && themeVolume.current < 1) { 139 | if (!themePlayer.current.isPlaying) { 140 | themePlayer.current.play() 141 | drumPlayer.current.play() 142 | } 143 | 144 | themeFilter.current.frequency.value += delta * 4000 145 | 146 | if (themeVolume.current + delta * 0.2 > 1) { 147 | themeVolume.current = 1 148 | drumVolume.current = 1 149 | } else { 150 | themeVolume.current += delta * 0.2 151 | drumVolume.current += delta * 0.2 152 | } 153 | 154 | if (introVolume.current - delta * 0.2 < 0) { 155 | introVolume.current = 0 156 | } else { 157 | introVolume.current -= delta * 0.2 158 | } 159 | 160 | introPlayer.current.setVolume(introVolume.current) 161 | themePlayer.current.setVolume(themeVolume.current) 162 | drumPlayer.current.setVolume(drumVolume.current) 163 | } 164 | 165 | 166 | // Crossfade main theme back to intro on game over 167 | if (gameOver && introVolume.current < 1) { 168 | if (!introPlayer.current.isPlaying) { 169 | introPlayer.current.play() 170 | } 171 | 172 | themeFilter.current.frequency.value -= delta * 4000 173 | 174 | if (themeVolume.current - delta * 0.2 < 0) { 175 | themeVolume.current = 0 176 | drumVolume.current = 0 177 | } else { 178 | themeVolume.current -= delta * 0.2 179 | drumVolume.current -= delta * 0.2 180 | } 181 | 182 | if (introVolume.current + delta * 0.2 > 1) { 183 | introVolume.current = 1 184 | } else { 185 | introVolume.current += delta * 0.2 186 | } 187 | 188 | introPlayer.current.setVolume(introVolume.current) 189 | themePlayer.current.setVolume(themeVolume.current) 190 | drumPlayer.current.setVolume(drumVolume.current) 191 | } 192 | } 193 | }) 194 | 195 | return ( 196 | 197 | 201 | ) 202 | } 203 | 204 | export default function SuspenseMusic() { 205 | 206 | return ( 207 | 208 | 209 | 210 | ) 211 | } -------------------------------------------------------------------------------- /src/components/Ship.js: -------------------------------------------------------------------------------- 1 | import React, { useRef, useLayoutEffect, useEffect, Suspense, useState } from 'react' 2 | import { useFrame, useThree } from '@react-three/fiber' 3 | import { useGLTF, PerspectiveCamera, useTexture } from '@react-three/drei' 4 | import { MirroredRepeatWrapping, Vector2, Vector3 } from 'three' 5 | 6 | import shipModel from '../models/spaceship.gltf' 7 | 8 | import noiseTexture from '../textures/noise.png' 9 | import engineTexture from '../textures/enginetextureflip.png' 10 | 11 | 12 | import { useStore, mutation } from '../state/useStore' 13 | 14 | const v = new Vector3() 15 | 16 | function ShipModel(props, { children }) { 17 | const { nodes, materials } = useGLTF(shipModel, "https://www.gstatic.com/draco/versioned/decoders/1.4.0/") 18 | // tie ship and camera ref to store to allow getting at them elsewhere 19 | const ship = useStore((s) => s.ship) 20 | const camera = useStore((s) => s.camera) 21 | 22 | const pointLight = useRef() 23 | 24 | const innerConeExhaust = useRef() 25 | const coneExhaust = useRef() 26 | const outerConeExhaust = useRef() 27 | 28 | const noise = useTexture(noiseTexture) 29 | const exhaust = useTexture(engineTexture) 30 | 31 | const leftWingTrail = useRef() 32 | const rightWingTrail = useRef() 33 | 34 | const bodyDetail = useRef() 35 | 36 | const { clock } = useThree() 37 | 38 | const gameStarted = useStore(s => s.gameStarted) 39 | const gameOver = useStore(s => s.gameOver) 40 | 41 | // subscribe to controller updates on mount 42 | const controlsRef = useRef(useStore.getState().controls) 43 | useEffect(() => useStore.subscribe( 44 | controls => (controlsRef.current = controls), 45 | state => state.controls 46 | ), []) 47 | 48 | useLayoutEffect(() => { 49 | camera.current.rotation.set(0, Math.PI, 0) 50 | camera.current.position.set(0, 4, -9) // 0, 1.5, -8 51 | camera.current.lookAt(v.set(ship.current.position.x, ship.current.position.y, ship.current.position.z + 10)) // modify the camera tracking to look above the center of the ship 52 | 53 | camera.current.rotation.z = Math.PI 54 | ship.current.rotation.y = Math.PI 55 | }, [ship, camera]) 56 | 57 | // turn off movement related parts when we arent moving 58 | useLayoutEffect(() => { 59 | if (!gameStarted || gameOver) { 60 | innerConeExhaust.current.material.visible = false 61 | coneExhaust.current.material.visible = false 62 | outerConeExhaust.current.material.visible = false 63 | leftWingTrail.current.material.visible = false 64 | rightWingTrail.current.material.visible = false 65 | pointLight.current.visible = false 66 | } else { 67 | innerConeExhaust.current.material.visible = true 68 | coneExhaust.current.material.visible = true 69 | outerConeExhaust.current.material.visible = true 70 | leftWingTrail.current.material.visible = true 71 | rightWingTrail.current.material.visible = true 72 | pointLight.current.visible = true 73 | } 74 | }, [gameStarted, gameOver]) 75 | 76 | useLayoutEffect(() => { 77 | noise.wrapS = noise.wrapT = MirroredRepeatWrapping 78 | noise.repeat.set(1, 1) 79 | noise.anisotropy = 16 80 | 81 | exhaust.wrapS = exhaust.wrapT = MirroredRepeatWrapping 82 | exhaust.repeat.set(1, 1) 83 | exhaust.anisotropy = 16 84 | }, [noise, exhaust]) 85 | 86 | const [innerLathe] = useState(() => { 87 | const points = [ 88 | new Vector2(0.2, 0.8), 89 | new Vector2(0.1, 0), 90 | new Vector2(0.3, 1.5), 91 | new Vector2(0.4, 1.9), 92 | new Vector2(0.01, 7)] 93 | 94 | return points 95 | }) 96 | 97 | const [mediumLathe] = useState(() => { 98 | const points = [ 99 | new Vector2(0.2, 0), 100 | new Vector2(0.5, 2), 101 | new Vector2(0.01, 8)] 102 | 103 | return points 104 | }) 105 | 106 | const [lathe] = useState(() => { 107 | const points = [ 108 | new Vector2(0.01, 0), 109 | new Vector2(0.3, 0.8), 110 | new Vector2(0.4, 1.5), 111 | new Vector2(0.5, 1.9), 112 | new Vector2(0.01, 9)] 113 | 114 | return points 115 | }) 116 | 117 | const innerConeScaleFactor = useRef(0.7) 118 | 119 | useFrame((state, delta) => { 120 | const accelDelta = 1 * delta * 2 // 1.5 121 | 122 | const time = clock.getElapsedTime() 123 | 124 | const slowSine = Math.sin(time * 5) 125 | const medSine = Math.sin(time * 10) 126 | const fastSine = Math.sin(time * 15) 127 | 128 | const { left, right } = controlsRef.current 129 | 130 | rightWingTrail.current.scale.x = fastSine / 50 131 | rightWingTrail.current.scale.y = medSine / 50 132 | leftWingTrail.current.scale.x = fastSine / 50 133 | leftWingTrail.current.scale.y = medSine / 50 134 | 135 | // Forward Movement 136 | ship.current.position.z -= mutation.gameSpeed * delta * 165 137 | 138 | 139 | // Lateral Movement 140 | if (mutation.gameOver) { 141 | mutation.horizontalVelocity = 0 142 | } 143 | ship.current.position.x += mutation.horizontalVelocity * delta * 165 144 | 145 | // Curving during turns 146 | ship.current.rotation.z = mutation.horizontalVelocity * 1.5 147 | ship.current.rotation.y = Math.PI - mutation.horizontalVelocity * 0.4 148 | ship.current.rotation.x = -Math.abs(mutation.horizontalVelocity) / 10 // max/min velocity is -0.5/0.5, divide by ten to get our desired max rotation of 0.05 149 | 150 | // Ship Jitter - small incidental movements 151 | ship.current.position.y -= slowSine / 200 152 | ship.current.rotation.x += slowSine / 100 153 | ship.current.rotation.z += Math.sin(time * 4) / 100 154 | 155 | // pointLight follow along 156 | pointLight.current.position.z = ship.current.position.z + 1 157 | pointLight.current.position.x = ship.current.position.x 158 | pointLight.current.position.y -= slowSine / 80 159 | 160 | // uncomment to unlock camera 161 | camera.current.position.z = ship.current.position.z + 13.5 // + 13.5 162 | camera.current.position.y = ship.current.position.y + 5 // 5 163 | camera.current.position.x = ship.current.position.x 164 | 165 | camera.current.rotation.y = Math.PI 166 | 167 | if ((left && right) || (!left && !right)) { 168 | if (mutation.horizontalVelocity < 0) { 169 | if (mutation.horizontalVelocity + accelDelta > 0) { 170 | mutation.horizontalVelocity = 0 171 | } else { 172 | mutation.horizontalVelocity += accelDelta 173 | } 174 | } 175 | 176 | if (mutation.horizontalVelocity > 0) { 177 | if (mutation.horizontalVelocity - accelDelta < 0) { 178 | mutation.horizontalVelocity = 0 179 | } else { 180 | mutation.horizontalVelocity -= accelDelta 181 | } 182 | } 183 | } 184 | 185 | if (!mutation.gameOver && mutation.gameSpeed > 0) { 186 | if ((left && !right)) { 187 | mutation.horizontalVelocity = Math.max(-0.7 /* -0.5 */, mutation.horizontalVelocity - accelDelta) 188 | 189 | // wing trail 190 | rightWingTrail.current.scale.x = fastSine / 30 191 | rightWingTrail.current.scale.y = slowSine / 30 192 | leftWingTrail.current.scale.x = fastSine / 200 193 | leftWingTrail.current.scale.y = slowSine / 200 194 | } 195 | 196 | if ((!left && right)) { 197 | mutation.horizontalVelocity = Math.min(0.7 /* 0.7 */, mutation.horizontalVelocity + accelDelta) 198 | 199 | // wing trail 200 | leftWingTrail.current.scale.x = fastSine / 30 201 | leftWingTrail.current.scale.y = slowSine / 30 202 | rightWingTrail.current.scale.x = fastSine / 200 203 | rightWingTrail.current.scale.y = slowSine / 200 204 | } 205 | } 206 | 207 | pointLight.current.intensity = 20 - (fastSine / 15) 208 | 209 | outerConeExhaust.current.material.map.offset.y += 0.01 * (0.4 + mutation.gameSpeed) * delta * 165 210 | coneExhaust.current.material.map.offset.y -= 0.01 * (0.4 + mutation.gameSpeed) * delta * 165 211 | 212 | if (mutation.desiredSpeed > mutation.gameSpeed) { 213 | pointLight.current.intensity = 30 - (fastSine / 15) 214 | 215 | if (innerConeScaleFactor.current < 0.95) { 216 | if (innerConeScaleFactor.current + 0.005 * delta * 165 > 0.95) { 217 | innerConeScaleFactor.current = 0.95 218 | } else { 219 | innerConeScaleFactor.current += 0.005 * delta * 165 220 | } 221 | } 222 | } else { 223 | if (innerConeScaleFactor.current > 0.7) { 224 | if (innerConeScaleFactor.current - 0.005 * delta * 165 < 0.7) { 225 | innerConeScaleFactor.current = 0.7 226 | } else { 227 | innerConeScaleFactor.current -= 0.005 * delta * 165 228 | } 229 | } 230 | } 231 | 232 | 233 | const scaleFactor = mutation.currentMusicLevel > 0.8 ? mutation.currentMusicLevel + 0.2 : 1 234 | 235 | innerConeExhaust.current.scale.z = (fastSine / 15) 236 | innerConeExhaust.current.scale.x = (innerConeScaleFactor.current + fastSine / 15) * scaleFactor 237 | coneExhaust.current.scale.z = (fastSine / 15) 238 | coneExhaust.current.scale.x = (0.85 + fastSine / 15) * scaleFactor 239 | outerConeExhaust.current.scale.z = (0.9 + fastSine / 15) 240 | outerConeExhaust.current.scale.x = (0.9 + fastSine / 15) * scaleFactor 241 | 242 | bodyDetail.current.material.color = mutation.globalColor 243 | }) 244 | 245 | return ( 246 | <> 247 | 248 | 249 | 250 | {children} 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | ) 285 | } 286 | 287 | 288 | useGLTF.preload(shipModel, "https://www.gstatic.com/draco/versioned/decoders/1.4.0/") 289 | 290 | function Loading() { 291 | return ( 292 | 293 | 294 | 302 | 303 | ) 304 | } 305 | 306 | 307 | export default function Ship({ children }) { 308 | 309 | return ( 310 | }> 311 | 312 | {children} 313 | 314 | 315 | ) 316 | } -------------------------------------------------------------------------------- /src/components/Skybox.js: -------------------------------------------------------------------------------- 1 | import { Suspense, useRef, useMemo, useLayoutEffect } from 'react' 2 | import { useThree, useFrame } from '@react-three/fiber' 3 | import { useTexture, Stars } from '@react-three/drei' 4 | import { Color, BackSide, MirroredRepeatWrapping } from 'three' 5 | 6 | import galaxyTexture from '../textures/galaxy.jpg' 7 | 8 | import { mutation, useStore } from '../state/useStore' 9 | 10 | import { COLORS } from '../constants' 11 | 12 | function Sun() { 13 | const { clock } = useThree() 14 | 15 | const sun = useStore((s) => s.sun) 16 | const ship = useStore((s) => s.ship) 17 | 18 | const sunColor = useMemo(() => new Color(1, 0.694, 0.168), []) 19 | 20 | useFrame((state, delta) => { 21 | if (ship.current) { 22 | sun.current.position.z = ship.current.position.z - 2000 23 | sun.current.position.x = ship.current.position.x 24 | } 25 | 26 | const scaleFactor = mutation.currentMusicLevel 27 | 28 | sun.current.scale.x += Math.sin(clock.getElapsedTime() * 3) / 3000 29 | sun.current.scale.y += Math.sin(clock.getElapsedTime() * 3) / 3000 30 | 31 | if (scaleFactor >= 0.8 && sun.current.scale.x < 1.05) { 32 | sun.current.scale.x += scaleFactor * delta * 2 33 | sun.current.scale.y += scaleFactor * delta * 2 34 | } else if (sun.current.scale.x > 1) { 35 | sun.current.scale.x -= scaleFactor * delta * 0.5 36 | sun.current.scale.y -= scaleFactor * delta * 0.5 37 | } 38 | }) 39 | 40 | return ( 41 | 42 | 43 | 44 | 45 | ) 46 | } 47 | 48 | function Sky() { 49 | const texture = useTexture(galaxyTexture) 50 | const sky = useRef() 51 | const stars = useRef() 52 | 53 | const ship = useStore((s) => s.ship) 54 | 55 | useLayoutEffect(() => { 56 | texture.wrapS = texture.wrapT = MirroredRepeatWrapping 57 | texture.repeat.set(1.8, 1.8) 58 | texture.anisotropy = 16 59 | }, [texture]) 60 | 61 | 62 | useFrame((state, delta) => { 63 | sky.current.rotation.z += delta * 0.02 * mutation.gameSpeed 64 | stars.current.rotation.z += delta * 0.02 * mutation.gameSpeed 65 | sky.current.emissive = mutation.globalColor 66 | 67 | if (ship.current) { 68 | sky.current.position.x = ship.current.position.x 69 | stars.current.position.x = ship.current.position.x 70 | sky.current.position.z = ship.current.position.z 71 | stars.current.position.z = ship.current.position.z 72 | } 73 | }) 74 | 75 | 76 | return ( 77 | <> 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | ) 86 | } 87 | 88 | function Fog() { 89 | const fog = useRef() 90 | 91 | useFrame((state, delta) => { 92 | fog.current.near = 100 93 | fog.current.far = 800 94 | fog.current.color = mutation.globalColor 95 | }) 96 | 97 | return ( 98 | 99 | ) 100 | } 101 | 102 | 103 | export default function Skybox() { 104 | 105 | return ( 106 | 107 | 108 | 109 | 110 | 111 | ) 112 | } -------------------------------------------------------------------------------- /src/components/Sound.js: -------------------------------------------------------------------------------- 1 | import { AudioListener, AudioLoader } from 'three' 2 | import { useRef, useEffect, useState, Suspense } from 'react' 3 | import { useLoader } from '@react-three/fiber' 4 | 5 | import { useStore } from '../state/useStore' 6 | 7 | import speedUp from '../audio/speedup.mp3' 8 | 9 | function Sound() { 10 | const sound = useRef() 11 | const soundOrigin = useRef() 12 | 13 | 14 | const camera = useStore(s => s.camera) 15 | const musicEnabled = useStore(s => s.musicEnabled) 16 | const level = useStore(s => s.level) 17 | const gameStarted = useStore(s => s.gameStarted) 18 | 19 | const [listener] = useState(() => new AudioListener()) 20 | 21 | const speedUpSound = useLoader(AudioLoader, speedUp) 22 | 23 | 24 | useEffect(() => { 25 | sound.current.setBuffer(speedUpSound) 26 | 27 | if (musicEnabled) { 28 | sound.current.setVolume(0.5) 29 | } else { 30 | sound.current.setVolume(0) 31 | } 32 | 33 | if (camera.current) { 34 | const cam = camera.current 35 | cam.add(listener) 36 | return () => cam.remove(listener) 37 | } 38 | }, [speedUpSound, musicEnabled, camera, listener]) 39 | 40 | useEffect(() => { 41 | if (gameStarted && level > 0) { 42 | sound.current.setBuffer(speedUpSound) 43 | sound.current.play() 44 | } 45 | }, [gameStarted, level, speedUpSound]) 46 | 47 | return ( 48 | 49 | 51 | ) 52 | } 53 | 54 | export default function SuspenseSound() { 55 | 56 | return ( 57 | 58 | 59 | 60 | ) 61 | } -------------------------------------------------------------------------------- /src/components/Walls.js: -------------------------------------------------------------------------------- 1 | import { Cone } from '@react-three/drei' 2 | import { useFrame } from '@react-three/fiber' 3 | import { useRef } from 'react' 4 | 5 | import { useStore, mutation } from '../state/useStore' 6 | import { PLANE_SIZE, WALL_RADIUS, COLORS, LEFT_BOUND, RIGHT_BOUND } from '../constants' 7 | 8 | export default function Walls() { 9 | const ship = useStore((s) => s.ship) 10 | 11 | const rightWall = useRef() 12 | const leftWall = useRef() 13 | 14 | useFrame((state, delta) => { 15 | if (ship.current) { 16 | rightWall.current.position.z = ship.current.position.z 17 | leftWall.current.position.z = ship.current.position.z 18 | 19 | if (ship.current.position.x <= LEFT_BOUND + (WALL_RADIUS / 2) || ship.current.position.x >= RIGHT_BOUND - (WALL_RADIUS / 2)) { 20 | mutation.gameSpeed = 0 21 | mutation.gameOver = true 22 | } 23 | } 24 | 25 | leftWall.current.material.color = mutation.globalColor 26 | rightWall.current.material.color = mutation.globalColor 27 | }) 28 | 29 | return ( 30 | <> 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | ) 39 | } -------------------------------------------------------------------------------- /src/components/html/Author.js: -------------------------------------------------------------------------------- 1 | import '../../styles/author.css' 2 | 3 | export default function Author() { 4 | return ( 5 | 8 | ) 9 | } -------------------------------------------------------------------------------- /src/components/html/CustomLoader.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | const defaultDataInterpolation = (p) => `Loading: ${p.toFixed(0)}%` 4 | 5 | export default function CustomLoader({ active, progress, dataInterpolation = defaultDataInterpolation }) { 6 | const progressRef = React.useRef(0) 7 | const rafRef = React.useRef(0) 8 | const progressSpanRef = React.useRef(null) 9 | 10 | const updateProgress = React.useCallback(() => { 11 | if (!progressSpanRef.current) return 12 | progressRef.current += (progress - progressRef.current) / 2 13 | if (progressRef.current > 0.95 * progress || progress === 100) progressRef.current = progress 14 | progressSpanRef.current.innerText = dataInterpolation(progressRef.current) 15 | if (progressRef.current < progress) rafRef.current = requestAnimationFrame(updateProgress) 16 | }, [dataInterpolation, progress]) 17 | 18 | React.useEffect(() => { 19 | updateProgress() 20 | return () => cancelAnimationFrame(rafRef.current) 21 | }, [updateProgress]) 22 | 23 | return ( 24 |
25 |
26 |
27 |
28 |
29 | 30 |
31 |
32 |
33 |
34 | ) 35 | } 36 | 37 | const styles = { 38 | container: { 39 | display: 'flex', 40 | alignItems: 'center', 41 | justifyContent: 'center', 42 | transition: 'opacity 300ms ease', 43 | zIndex: 1000, 44 | }, 45 | inner: { 46 | width: 206, 47 | height: 26, 48 | textAlign: 'center', 49 | borderRadius: '5px', 50 | boxShadow: '0 0 20px 0px #fe2079', 51 | border: '3px solid #fe2079' 52 | }, 53 | bar: { 54 | height: 20, 55 | width: '100%', 56 | background: '#fe2079', 57 | transition: 'transform 200ms', 58 | transformOrigin: 'left center', 59 | boxShadow: '0 0 20px 0px #fe2079' 60 | }, 61 | data: { 62 | textAlign: 'center', 63 | fontVariantNumeric: 'tabular-nums', 64 | marginTop: '2rem', 65 | color: '#f0f0f0', 66 | fontSize: '2em', 67 | fontFamily: `'Commando', mono, monospace, -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", "Helvetica Neue", Helvetica, Arial, Roboto, Ubuntu, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`, 68 | whiteSpace: 'nowrap', 69 | textShadow: '0 0 20px #fe2079' 70 | }, 71 | } 72 | -------------------------------------------------------------------------------- /src/components/html/GameOverScreen.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | 3 | import cubeRunLogo from '../../textures/cuberun-logo.png' 4 | 5 | import '../../styles/gameMenu.css' 6 | 7 | import { useStore } from '../../state/useStore' 8 | 9 | const GameOverScreen = () => { 10 | const previousScores = localStorage.getItem('highscores') ? JSON.parse(localStorage.getItem('highscores')) : [...Array(3).fill(0)] 11 | const [shown, setShown] = useState(false) 12 | const [opaque, setOpaque] = useState(false) 13 | const [highScores, setHighscores] = useState(previousScores) 14 | 15 | const gameOver = useStore(s => s.gameOver) 16 | const score = useStore(s => s.score) 17 | 18 | useEffect(() => { 19 | let t 20 | if (gameOver !== opaque) t = setTimeout(() => setOpaque(gameOver), 500) 21 | return () => clearTimeout(t) 22 | }, [gameOver, opaque]) 23 | 24 | useEffect(() => { 25 | if (gameOver) { 26 | setShown(true) 27 | } else { 28 | setShown(false) 29 | } 30 | }, [gameOver]) 31 | 32 | useEffect(() => { 33 | if (gameOver) { 34 | if (highScores.some(previousScore => score > previousScore)) { 35 | const sortedScores = highScores.sort((a, b) => a - b) 36 | sortedScores[0] = score.toFixed(0) 37 | const resortedScores = sortedScores.sort((a, b) => b - a) 38 | 39 | setHighscores(resortedScores) 40 | localStorage.setItem('highscores', JSON.stringify(resortedScores)) 41 | } 42 | } 43 | }, [gameOver, highScores, score]) 44 | 45 | const handleRestart = () => { 46 | window.location.reload() // TODO: make a proper restart 47 | } 48 | 49 | return shown ? ( 50 |
51 |
52 | Cuberun Logo 53 |

GAME OVER

54 |
55 |
56 |

SCORE

57 |

{score.toFixed(0)}

58 |
59 |
60 |

HIGH SCORES

61 | {highScores.map((newScore, i) => ( 62 |
63 | {i + 1} 64 | {newScore > 0 ? newScore : '-'} 65 |
66 | ))} 67 |
68 |
69 | 70 |
71 |
72 | ) : null 73 | } 74 | 75 | export default GameOverScreen -------------------------------------------------------------------------------- /src/components/html/Hud.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useRef } from 'react' 2 | import { isMobile } from 'react-device-detect' 3 | import { addEffect } from '@react-three/fiber' 4 | 5 | import { useStore, mutation } from '../../state/useStore' 6 | 7 | import '../../styles/hud.css' 8 | 9 | const getSpeed = () => `${(mutation.gameSpeed * 400).toFixed(0)}` 10 | const getScore = () => `${mutation.score.toFixed(0)}` 11 | 12 | 13 | export default function Hud() { 14 | const set = useStore((state) => state.set) 15 | const level = useStore(s => s.level) 16 | 17 | const gameOver = useStore(s => s.gameOver) 18 | const gameStarted = useStore(s => s.gameStarted) 19 | const isSpeedingUp = useStore(s => s.isSpeedingUp) 20 | 21 | const [shown, setShown] = useState(false) 22 | 23 | const [showControls, setShowControls] = useState(false) 24 | const [left, setLeftPressed] = useState(false) 25 | const [right, setRightPressed] = useState(false) 26 | 27 | 28 | // performance optimization for the rapidly updating speedometer and score - see https://github.com/pmndrs/racing-game/blob/main/src/ui/Speed/Text.tsx 29 | let then = Date.now() 30 | 31 | const speedRef = useRef() 32 | const scoreRef = useRef() 33 | 34 | let currentSpeed = getSpeed() 35 | let currentScore = getScore() 36 | 37 | useEffect(() => addEffect(() => { 38 | const now = Date.now() 39 | 40 | if (now - then > 33.3333) { // throttle these to a max of 30 updates/sec 41 | if (speedRef.current) { 42 | speedRef.current.innerText = getSpeed() 43 | } 44 | 45 | if (scoreRef.current) { 46 | scoreRef.current.innerText = getScore() 47 | } 48 | 49 | // eslint-disable-next-line 50 | then = now 51 | } 52 | })) 53 | 54 | useEffect(() => { 55 | if (showControls) { 56 | window.oncontextmenu = function (event) { 57 | event.preventDefault(); 58 | event.stopPropagation(); 59 | return false; 60 | } 61 | } 62 | }, [showControls]) 63 | 64 | useEffect(() => { 65 | if (gameStarted && !gameOver) { 66 | setShown(true) 67 | } else { 68 | setShown(false) 69 | } 70 | }, [gameStarted, gameOver]) 71 | 72 | useEffect(() => { 73 | if (isMobile) { 74 | setShowControls(true) 75 | } else { 76 | setShowControls(false) 77 | } 78 | }, []) 79 | 80 | useEffect(() => { 81 | set((state) => ({ ...state, controls: { ...state.controls, left } })) 82 | }, [set, left]) 83 | 84 | useEffect(() => { 85 | set((state) => ({ ...state, controls: { ...state.controls, right } })) 86 | }, [set, right]) 87 | 88 | return shown ? ( 89 |
90 | {level > 0 && isSpeedingUp && ( 91 |
92 |

SPEED UP

93 |
94 | )} 95 | {showControls && ( 96 |
97 | 98 | 99 |
100 | )} 101 |
102 |
103 |

LEVEL

104 |

{level + 1}

105 |

KM/H

106 |

{currentSpeed}

107 |

SCORE

108 |

{currentScore}

109 |
110 |
111 |
112 | ) : null 113 | } -------------------------------------------------------------------------------- /src/components/html/Overlay.js: -------------------------------------------------------------------------------- 1 | import { useProgress } from '@react-three/drei' 2 | import { useState, useEffect } from 'react' 3 | 4 | import Loader from './CustomLoader' 5 | import Author from './Author' 6 | 7 | import '../../styles/gameMenu.css' 8 | 9 | import { useStore } from '../../state/useStore' 10 | 11 | const Overlay = () => { 12 | const [shown, setShown] = useState(true) 13 | const [opaque, setOpaque] = useState(true) 14 | const [hasLoaded, setHasLoaded] = useState(false) 15 | const { active, progress } = useProgress() 16 | 17 | const gameStarted = useStore(s => s.gameStarted) 18 | const gameOver = useStore(s => s.gameOver) 19 | const setGameStarted = useStore(s => s.setGameStarted) 20 | const musicEnabled = useStore(s => s.musicEnabled) 21 | const enableMusic = useStore(s => s.enableMusic) 22 | const setHasInteracted = useStore(s => s.setHasInteracted) 23 | 24 | useEffect(() => { 25 | if (gameStarted || gameOver) { 26 | setShown(false) 27 | } else if (!gameStarted) { 28 | setShown(true) 29 | } 30 | }, [gameStarted, active, gameOver]) 31 | 32 | useEffect(() => { 33 | let t 34 | if (hasLoaded === opaque) t = setTimeout(() => setOpaque(!hasLoaded), 300) 35 | return () => clearTimeout(t) 36 | }, [hasLoaded, opaque]) 37 | 38 | useEffect(() => { 39 | localStorage.setItem('musicEnabled', JSON.stringify(musicEnabled)) 40 | }, [musicEnabled]) 41 | 42 | useEffect(() => { 43 | if (progress >= 100) { 44 | setHasLoaded(true) 45 | } 46 | }, [progress]) 47 | 48 | const handleStart = () => { 49 | setGameStarted(true) 50 | } 51 | 52 | const handleMusic = () => { 53 | enableMusic(!musicEnabled) 54 | } 55 | 56 | return shown ? ( 57 |
setHasInteracted()} className={`game__container`} style={{ opacity: shown ? 1 : 0, background: opaque ? '#141622FF' : '#141622CC' }}> 58 |
59 | Cuberun Logo 60 |
61 | {!hasLoaded ? ( 62 | 63 | ) : ( 64 | <> 65 | 66 |
67 |
75 | 76 | )} 77 |
78 |
79 |
80 | ) : null 81 | } 82 | 83 | export default Overlay -------------------------------------------------------------------------------- /src/constants/index.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | 3 | /** 4 | * The size of each ground plane in meters, 1000 is fine 5 | */ 6 | export const PLANE_SIZE = 1000 7 | 8 | /** 9 | * How many ground planes must we traverse per level, default 6 10 | */ 11 | export const LEVEL_SIZE = 6 12 | 13 | export const LEFT_BOUND = (-PLANE_SIZE / 2) * 0.6 14 | export const RIGHT_BOUND = (PLANE_SIZE / 2) * 0.6 15 | 16 | export const CUBE_SIZE = 20 17 | 18 | export const CUBE_AMOUNT = 60 19 | 20 | export const INITIAL_GAME_SPEED = 0.8 21 | 22 | export const GAME_SPEED_MULTIPLIER = 0.2 23 | 24 | export const WALL_RADIUS = 40 25 | 26 | export const COLORS = [ 27 | { 28 | name: 'pink', 29 | hex: '#ff69b4', 30 | three: new THREE.Color(0xff2190) 31 | }, 32 | { 33 | name: 'red', 34 | hex: '#ff2919', 35 | three: new THREE.Color(0xff2919) // 0xff3021 #ff1e0d 36 | }, 37 | { 38 | name: 'orange', 39 | hex: '#bd4902', 40 | three: new THREE.Color(0xbd4902) //0xcc4e00 41 | }, 42 | { 43 | name: 'green', 44 | hex: '#26a300', 45 | three: new THREE.Color(0x26a300) // 0x2ec200 46 | }, 47 | { 48 | name: 'blue', 49 | hex: '#217aff', 50 | three: new THREE.Color(0x2069d6) 51 | }, 52 | { 53 | name: 'purple', 54 | hex: '#9370D8', 55 | three: new THREE.Color(0x6942b8) 56 | }, 57 | { 58 | name: 'white', 59 | hex: '#ffffff', 60 | three: new THREE.Color(0x6b6b6b) // 0x828282 61 | }, 62 | { 63 | name: 'black', 64 | hex: '#000000', 65 | three: new THREE.Color(0xCCCCCC) 66 | } 67 | ] 68 | -------------------------------------------------------------------------------- /src/fonts/Road_Rage.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akarlsten/cuberun/b3581ac0fbaa4293c571d8935c11cb454b29be03/src/fonts/Road_Rage.otf -------------------------------------------------------------------------------- /src/fonts/commando.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akarlsten/cuberun/b3581ac0fbaa4293c571d8935c11cb454b29be03/src/fonts/commando.ttf -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import './styles/normalize.css' 5 | import './styles/index.css'; 6 | 7 | import CubeWorld from './components/CubeWorld'; 8 | 9 | // ReactDOM.render( 10 | // 11 | // 12 | // , 13 | // document.getElementById('root') 14 | // ); 15 | 16 | ReactDOM.createRoot(document.getElementById('root')).render( 17 | 18 | 19 | 20 | ) -------------------------------------------------------------------------------- /src/models/spaceship.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "asset" : { 3 | "generator" : "Khronos glTF Blender I/O v1.6.16", 4 | "version" : "2.0" 5 | }, 6 | "extensionsUsed" : [ 7 | "KHR_draco_mesh_compression" 8 | ], 9 | "extensionsRequired" : [ 10 | "KHR_draco_mesh_compression" 11 | ], 12 | "scene" : 0, 13 | "scenes" : [ 14 | { 15 | "name" : "Scene", 16 | "nodes" : [ 17 | 0 18 | ] 19 | } 20 | ], 21 | "nodes" : [ 22 | { 23 | "mesh" : 0, 24 | "name" : "Ship", 25 | "scale" : [ 26 | -1, 27 | 1, 28 | 1 29 | ], 30 | "translation" : [ 31 | 0, 32 | 0.867340087890625, 33 | 0 34 | ] 35 | } 36 | ], 37 | "materials" : [ 38 | { 39 | "doubleSided" : true, 40 | "name" : "Cockpit", 41 | "pbrMetallicRoughness" : { 42 | "baseColorFactor" : [ 43 | 0.009049390442669392, 44 | 0.01064073946326971, 45 | 0.01108749583363533, 46 | 1 47 | ], 48 | "metallicFactor" : 0.8217821717262268, 49 | "roughnessFactor" : 0 50 | } 51 | }, 52 | { 53 | "doubleSided" : true, 54 | "name" : "Chassis", 55 | "pbrMetallicRoughness" : { 56 | "baseColorFactor" : [ 57 | 0, 58 | 0, 59 | 0, 60 | 1 61 | ], 62 | "roughnessFactor" : 0.7522123456001282 63 | } 64 | }, 65 | { 66 | "doubleSided" : true, 67 | "emissiveFactor" : [ 68 | 0, 69 | 1, 70 | 1 71 | ], 72 | "name" : "Engine Glow", 73 | "pbrMetallicRoughness" : { 74 | "baseColorFactor" : [ 75 | 0.001870652660727501, 76 | 0.8000000715255737, 77 | 0.8000000715255737, 78 | 1 79 | ], 80 | "metallicFactor" : 0, 81 | "roughnessFactor" : 0.5 82 | } 83 | }, 84 | { 85 | "doubleSided" : true, 86 | "name" : "Gray Metal", 87 | "pbrMetallicRoughness" : { 88 | "baseColorFactor" : [ 89 | 0.013410485349595547, 90 | 0.013410485349595547, 91 | 0.013410485349595547, 92 | 1 93 | ], 94 | "metallicFactor" : 0.5707964301109314, 95 | "roughnessFactor" : 0.787610650062561 96 | } 97 | }, 98 | { 99 | "doubleSided" : true, 100 | "emissiveFactor" : [ 101 | 0.07498929500749796, 102 | 1, 103 | 1 104 | ], 105 | "name" : "Chassis 2", 106 | "pbrMetallicRoughness" : { 107 | "baseColorFactor" : [ 108 | 0.7642737030982971, 109 | 0.7294957041740417, 110 | 0.8000000715255737, 111 | 1 112 | ], 113 | "metallicFactor" : 0, 114 | "roughnessFactor" : 0.5 115 | } 116 | } 117 | ], 118 | "meshes" : [ 119 | { 120 | "name" : "Ship Body", 121 | "primitives" : [ 122 | { 123 | "attributes" : { 124 | "POSITION" : 0, 125 | "NORMAL" : 1, 126 | "TEXCOORD_0" : 2 127 | }, 128 | "extensions" : { 129 | "KHR_draco_mesh_compression" : { 130 | "bufferView" : 0, 131 | "attributes" : { 132 | "POSITION" : 0, 133 | "NORMAL" : 1, 134 | "TEXCOORD_0" : 2 135 | } 136 | } 137 | }, 138 | "indices" : 3, 139 | "material" : 0, 140 | "mode" : 4 141 | }, 142 | { 143 | "attributes" : { 144 | "POSITION" : 4, 145 | "NORMAL" : 5, 146 | "TEXCOORD_0" : 6 147 | }, 148 | "extensions" : { 149 | "KHR_draco_mesh_compression" : { 150 | "bufferView" : 1, 151 | "attributes" : { 152 | "POSITION" : 0, 153 | "NORMAL" : 1, 154 | "TEXCOORD_0" : 2 155 | } 156 | } 157 | }, 158 | "indices" : 7, 159 | "material" : 1, 160 | "mode" : 4 161 | }, 162 | { 163 | "attributes" : { 164 | "POSITION" : 8, 165 | "NORMAL" : 9, 166 | "TEXCOORD_0" : 10 167 | }, 168 | "extensions" : { 169 | "KHR_draco_mesh_compression" : { 170 | "bufferView" : 2, 171 | "attributes" : { 172 | "POSITION" : 0, 173 | "NORMAL" : 1, 174 | "TEXCOORD_0" : 2 175 | } 176 | } 177 | }, 178 | "indices" : 11, 179 | "material" : 2, 180 | "mode" : 4 181 | }, 182 | { 183 | "attributes" : { 184 | "POSITION" : 12, 185 | "NORMAL" : 13, 186 | "TEXCOORD_0" : 14 187 | }, 188 | "extensions" : { 189 | "KHR_draco_mesh_compression" : { 190 | "bufferView" : 3, 191 | "attributes" : { 192 | "POSITION" : 0, 193 | "NORMAL" : 1, 194 | "TEXCOORD_0" : 2 195 | } 196 | } 197 | }, 198 | "indices" : 15, 199 | "material" : 3, 200 | "mode" : 4 201 | }, 202 | { 203 | "attributes" : { 204 | "POSITION" : 16, 205 | "NORMAL" : 17, 206 | "TEXCOORD_0" : 18 207 | }, 208 | "extensions" : { 209 | "KHR_draco_mesh_compression" : { 210 | "bufferView" : 4, 211 | "attributes" : { 212 | "POSITION" : 0, 213 | "NORMAL" : 1, 214 | "TEXCOORD_0" : 2 215 | } 216 | } 217 | }, 218 | "indices" : 19, 219 | "material" : 4, 220 | "mode" : 4 221 | } 222 | ] 223 | } 224 | ], 225 | "accessors" : [ 226 | { 227 | "componentType" : 5126, 228 | "count" : 24, 229 | "max" : [ 230 | 0.35420510172843933, 231 | 0.2025671750307083, 232 | 2.253599166870117 233 | ], 234 | "min" : [ 235 | -0.35420510172843933, 236 | -0.3314523994922638, 237 | 0.5512999296188354 238 | ], 239 | "type" : "VEC3" 240 | }, 241 | { 242 | "componentType" : 5126, 243 | "count" : 24, 244 | "type" : "VEC3" 245 | }, 246 | { 247 | "componentType" : 5126, 248 | "count" : 24, 249 | "type" : "VEC2" 250 | }, 251 | { 252 | "componentType" : 5123, 253 | "count" : 36, 254 | "type" : "SCALAR" 255 | }, 256 | { 257 | "componentType" : 5126, 258 | "count" : 816, 259 | "max" : [ 260 | 1.5997267961502075, 261 | 0.5065528154373169, 262 | 3.2650928497314453 263 | ], 264 | "min" : [ 265 | -1.5997267961502075, 266 | -0.7808818817138672, 267 | -3.893253803253174 268 | ], 269 | "type" : "VEC3" 270 | }, 271 | { 272 | "componentType" : 5126, 273 | "count" : 816, 274 | "type" : "VEC3" 275 | }, 276 | { 277 | "componentType" : 5126, 278 | "count" : 816, 279 | "type" : "VEC2" 280 | }, 281 | { 282 | "componentType" : 5123, 283 | "count" : 1224, 284 | "type" : "SCALAR" 285 | }, 286 | { 287 | "componentType" : 5126, 288 | "count" : 24, 289 | "max" : [ 290 | 0.3309778571128845, 291 | -0.05325804278254509, 292 | -0.5913770794868469 293 | ], 294 | "min" : [ 295 | -0.3309778571128845, 296 | -0.4751274585723877, 297 | -1.5824956893920898 298 | ], 299 | "type" : "VEC3" 300 | }, 301 | { 302 | "componentType" : 5126, 303 | "count" : 24, 304 | "type" : "VEC3" 305 | }, 306 | { 307 | "componentType" : 5126, 308 | "count" : 24, 309 | "type" : "VEC2" 310 | }, 311 | { 312 | "componentType" : 5123, 313 | "count" : 36, 314 | "type" : "SCALAR" 315 | }, 316 | { 317 | "componentType" : 5126, 318 | "count" : 406, 319 | "max" : [ 320 | 0.7808820009231567, 321 | 0.4827719032764435, 322 | 1.883481740951538 323 | ], 324 | "min" : [ 325 | -0.7808820009231567, 326 | -0.7808818817138672, 327 | -3.576046943664551 328 | ], 329 | "type" : "VEC3" 330 | }, 331 | { 332 | "componentType" : 5126, 333 | "count" : 406, 334 | "type" : "VEC3" 335 | }, 336 | { 337 | "componentType" : 5126, 338 | "count" : 406, 339 | "type" : "VEC2" 340 | }, 341 | { 342 | "componentType" : 5123, 343 | "count" : 744, 344 | "type" : "SCALAR" 345 | }, 346 | { 347 | "componentType" : 5126, 348 | "count" : 242, 349 | "max" : [ 350 | 1.2640650272369385, 351 | 0.5065528154373169, 352 | 2.506472587585449 353 | ], 354 | "min" : [ 355 | -1.2640650272369385, 356 | -0.42414695024490356, 357 | -3.1151609420776367 358 | ], 359 | "type" : "VEC3" 360 | }, 361 | { 362 | "componentType" : 5126, 363 | "count" : 242, 364 | "type" : "VEC3" 365 | }, 366 | { 367 | "componentType" : 5126, 368 | "count" : 242, 369 | "type" : "VEC2" 370 | }, 371 | { 372 | "componentType" : 5123, 373 | "count" : 366, 374 | "type" : "SCALAR" 375 | } 376 | ], 377 | "bufferViews" : [ 378 | { 379 | "buffer" : 0, 380 | "byteLength" : 370, 381 | "byteOffset" : 0 382 | }, 383 | { 384 | "buffer" : 0, 385 | "byteLength" : 5623, 386 | "byteOffset" : 372 387 | }, 388 | { 389 | "buffer" : 0, 390 | "byteLength" : 381, 391 | "byteOffset" : 5996 392 | }, 393 | { 394 | "buffer" : 0, 395 | "byteLength" : 3473, 396 | "byteOffset" : 6380 397 | }, 398 | { 399 | "buffer" : 0, 400 | "byteLength" : 1824, 401 | "byteOffset" : 9856 402 | } 403 | ], 404 | "buffers" : [ 405 | { 406 | "byteLength" : 11680, 407 | "uri" : "data:application/octet-stream;base64,RFJBQ08CAgEBAAAAGAwCDAAABd/3fd8H/wJmQP8CZkD/AmZAA/8AAAAAAAEAAAEACQMAAAIBAQkDAAEDAQMJAgACAgEBAQAPK60KAQgBEFUdCNX3nqS87nmCygmaiZ8ZKj9jRJohgJAONocQuKH/xt9lPIDb7zG+hsAAVfBswYaCqzZE1R+LDTk6600AwASleTWEsmyrXECjRFqo/IwRKZShzMVvCIEbspH2hXnzv+33GC/KQBm5L7ZgQ8F1DcGBn9eQo7PelDKUufgMGTDbAAAAAP8/AABfWrW+IbSpvv4hDT/x5Nk/DgADAQALAwEwD1UFA60CAwEIBgBiPXrNhN5vAH4BtoMPeF01gF+A7eAD/wMAAP8BAAAKAQEBAA0DARgjARABGAcAtoeCLayK/x8A/heA/H8A/L8ASAAGAAAKgPQ/AP4XQP8BgAACwP8LwP8DAP4HAAAKwP8BAAAAAAD/DwAA//8vP/7/nz4IAIA9DAAARFJBQ08CAgEBAAAAsAaYAwKYAwAAmQHf933f933f933f933f933f933f933f933f933f933f933f933f933f933f933f933f933f933f933f933f933f933f933f933f933f933f933f933f933f933f933f933f933f933f933f933f933f933f933f933f933f933f933f933f933f933f933f933f933f933f933/AihU/wIoVP8CKFQD/wAAAAAAAQAAAQAJAwAAAgEBCQMAAQMBAwkCAAICAQEBAA8D3A/wuQFhA00HoQgNDq0OEQdpBasCzAHNc9g0MUzvUJMWFD6NnMFKRZqMAi5ARva8K/SlYFCuZqdsp0B4+V0h1PqQii0sgC7LlQRgpfSXSL9v+zieGgR4C+8IM5aEbwNznh8VQ6det1QQiI87TLokFGnYQOi+chC1LMH+bTzOqvrD2xJ/nlBAufEnAsO1t1ZZkIbtsCtwE8gsjE82k8tiQydYwm9eoTdkZgavdNja9DvoZ0tBuCVTOmhJSCY5+gbg8qVUPbwwVhvrfY3H7TdP/eJ29yhHNt8oMprl8aEHorTeIWa/bSMNJ/9yqfkgKvx48CfofhUxjm2U4vSAQMIpSK5kEjuQgUZ+bjIgGy5cCVBPROqn4dQmyKedLUgvQxAL14y3UxpvVhvlARxCckxP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K7IV7YpwgTWimJFnA6TM1FavBQLeiISFKGLg9wALSbFgJum4wYdRIb9CJnVEM40eKvCodsnzkDGvJh6FoM23PBFdP3uYwKOijEiIlRLBYMYDYKj0xAz2rlEGsE2EgcYAGJLm2WffPBECnE3iMUINpFtRvCI/u6Jt5KjEtbDpwHbaoctE8Ndoh6NKEQb8LsCFScOQlWChfArl7Gk1/4KMBKEw8gxQC9zcHdIIem1lFvHi1OQT4YIkQcqEBN3baxTdD0Xg/qHwFeV+RGQFpezhCPcVQErOFMpTQRG2SfO5v3p6JzPQMS6lB+DXp4yEsJCzJJ3C8RoxYAEXnXFgeFMkRYKCBJIyTEPG7QUQ2lglm2HQjAbRpw1DBwAAAAD/PwAA4syhv8op2b7MXkfAbOSzQA4AAwEACwMdMAuIiBEBmQFE6Qsl/qdNe+bCmHo15RuyKsYMGHYHEpVQJHYNq0BA0oFU3LW3zvYMg7QvAY8fAhAgAPkKECsA6O0DllcOjE098H4HgEaA4X8DjFMFQELqQADoA37PAHj5ACCA/kDwjwIqVA4AIQLyhwBIhECGLBBEw0Af4wOPcwBoQDUAttdAx48DpIcOpC8/8PghAAECkK8AsQKA3j5geeXA2NQD73cAaAQY/jfAOFUAJKQOBIA+4PcMgJcPAALoDwT/KKBC5QAQIiB/CIBECGTIAkE0DPQxPvA4B4AGVANgew10/DhAeugA/wMAAP8BAAAKAQEBAQXHHaUa/////98dAtkBQQKZAf/////f5QQlB////////////8+1A10E/////+Mg/////+eIdQEDIP/////fIGT/////3/0BuQH////////////PqCADRP/////jRP/////fRKhkZMQBotowzyqdU9vRtXtF2ohslT7tbzmODj/dR1WLjd+/twAK3/b1fvSmzhS5R7u8dTtAD6uLstPT6jILve4KqKhL6nU860bND20QOSVZtSA6Mw1PESusFJvUCbJyKzZmLvAZjBvRoccex4pbwg1A22suQ6tLTblPB0mnMs1Q5dNrV7T6djxtJutJD1pC5SbbEvxgbrxl5oQXCk37/qwZRX+6ggptNoPderqP9p8w4vyuErH9lmoayaD5PBL1GVGlkaz+0MX/hgAAAAD/DwAA/v8PPwAAmD4AAFA+DA==" 408 | } 409 | ] 410 | } 411 | -------------------------------------------------------------------------------- /src/state/useStore.js: -------------------------------------------------------------------------------- 1 | import { Color } from 'three' 2 | import { createRef } from 'react' 3 | import create from 'zustand' 4 | 5 | const useStore = create((set, get) => { 6 | 7 | return { 8 | set, 9 | get, 10 | score: 0, 11 | level: 0, 12 | gameOver: false, 13 | gameStarted: false, 14 | musicEnabled: JSON.parse(localStorage.getItem('musicEnabled')) ?? false, 15 | isSpeedingUp: false, 16 | controls: { 17 | left: false, 18 | right: false, 19 | }, 20 | directionalLight: createRef(), 21 | camera: createRef(), 22 | ship: createRef(), 23 | sun: createRef(), 24 | sfx: createRef(), 25 | hasInteracted: false, 26 | setHasInteracted: () => set(state => ({ hasInteracted: true })), 27 | setIsSpeedingUp: (speedingUp) => set(state => ({ isSpeedingUp: speedingUp })), 28 | incrementLevel: () => set(state => ({ level: state.level + 1 })), 29 | setScore: (score) => set(state => ({ score: score })), 30 | setGameStarted: (started) => set(state => ({ gameStarted: started })), 31 | setGameOver: (over) => set(state => ({ gameOver: over })), 32 | enableMusic: (enabled) => set(state => ({ musicEnabled: enabled })) 33 | } 34 | }) 35 | 36 | const mutation = { 37 | gameOver: false, 38 | score: 0, 39 | gameSpeed: 0.0, 40 | desiredSpeed: 0.0, 41 | horizontalVelocity: 0, 42 | colorLevel: 0, 43 | shouldShiftItems: false, 44 | currentMusicLevel: 0, 45 | currentLevelLength: 0, 46 | globalColor: new Color() 47 | } 48 | 49 | export { useStore, mutation } -------------------------------------------------------------------------------- /src/styles/author.css: -------------------------------------------------------------------------------- 1 | 2 | .author { 3 | margin-top: 1rem; 4 | color: #fe2079; 5 | font-family: 'Road Rage', Inconsolata, monospace; 6 | text-shadow: 0 0 40px #fe2079; 7 | font-size: 2rem; 8 | } 9 | 10 | .author > a { 11 | color: #fe2079; 12 | text-decoration: none; 13 | } 14 | 15 | .author > a:hover { 16 | color: #eee; 17 | } 18 | 19 | 20 | @media screen and (max-width: 992px) { 21 | .author { 22 | text-shadow: 0 0 15px #fe2079; 23 | } 24 | } 25 | 26 | @media screen and (max-width: 600px) { 27 | .author { 28 | font-size: 1.5rem; 29 | text-shadow: 0 0 15px #fe2079; 30 | } 31 | } -------------------------------------------------------------------------------- /src/styles/gameMenu.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Road Rage'; 3 | src: local('Road Rage'), url(../fonts/Road_Rage.otf) format('opentype'); 4 | } 5 | 6 | @font-face { 7 | font-family: 'Commando'; 8 | src: local('Commando'), url(../fonts/commando.ttf) format('truetype'); 9 | } 10 | 11 | 12 | .game__container { 13 | position: absolute; 14 | top: 0; 15 | left: 0; 16 | width: 100%; 17 | height: 100%; 18 | background: #141622CC; 19 | display: flex; 20 | align-items: center; 21 | justify-content: center; 22 | transition: opacity 300ms ease; 23 | transition: background-color 1000ms ease; 24 | z-index: 1000; 25 | } 26 | 27 | .game__subcontainer { 28 | margin: 2rem; 29 | text-align: center; 30 | display: flex; 31 | flex-direction: column; 32 | align-items: center; 33 | justify-content: space-around; 34 | height: 100%; 35 | } 36 | 37 | .game__logo { 38 | max-width: 100%; 39 | max-height: 100%; 40 | width: 1024px; 41 | margin-top: 2rem; 42 | } 43 | 44 | .game__logo-small { 45 | height: 256px; 46 | width: 512px; 47 | } 48 | 49 | .game__menu { 50 | display: flex; 51 | align-items: center; 52 | justify-content: center; 53 | flex-direction: column; 54 | height: 100%; 55 | } 56 | 57 | .game__menu-options { 58 | display: flex; 59 | flex-direction: column; 60 | margin-top: 1rem; 61 | } 62 | 63 | .game__score-gameover { 64 | color: #fe2079; 65 | font-family: 'Commando', Inconsolata, monospace; 66 | font-size: 12rem; 67 | text-shadow: 0 0 10px #fe2079; 68 | margin: 0; 69 | margin-bottom: 2rem; 70 | } 71 | 72 | .game__score-title { 73 | color: #fe2079; 74 | font-family: 'Road Rage', Inconsolata, monospace; 75 | font-weight: 500; 76 | font-size: 4rem; 77 | text-align: center; 78 | text-shadow: 0 0 40px #fe2079; 79 | margin: 0; 80 | margin-bottom: 1rem; 81 | } 82 | 83 | .game__score { 84 | color: #eee; 85 | font-family: 'Commando', Inconsolata, monospace; 86 | font-size: 10rem; 87 | font-weight: 500; 88 | text-align: center; 89 | text-shadow: 0 0 20px #fe2079; 90 | margin: 0; 91 | } 92 | 93 | .game__scorecontainer { 94 | display: flex; 95 | flex-direction: row; 96 | justify-content: space-between; 97 | text-shadow: 0 0 20px #fe2079; 98 | margin-bottom: 2rem; 99 | } 100 | 101 | .game__score-highscore { 102 | font-size: 3rem; 103 | width: 100%; 104 | display: flex; 105 | justify-content: center; 106 | padding-left: 2rem; 107 | padding-right: 2rem; 108 | color: #eee; 109 | font-family: 'Commando', Inconsolata, monospace; 110 | } 111 | 112 | .game__score-number { 113 | color: #fe2079; 114 | margin-right: 1rem; 115 | } 116 | 117 | .game__score-left { 118 | margin-right: 10rem; 119 | display: flex; 120 | flex-direction: column; 121 | align-items: center; 122 | } 123 | 124 | .game__score-right { 125 | display: flex; 126 | flex-direction: column; 127 | align-items: center; 128 | } 129 | 130 | .game__menu-button { 131 | background: none; 132 | border-radius: 20px; 133 | border: 0; 134 | font-family: 'Road Rage', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 135 | font-size: 6rem; 136 | color: #fe2079; 137 | text-shadow: 0 0 40px #fe2079; 138 | } 139 | 140 | .game__menu-button-music { 141 | font-size: 3.5rem; 142 | } 143 | 144 | .game__menu-warning { 145 | margin-top: 8rem; 146 | font-family: 'Commando', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 147 | font-size: 2rem; 148 | color: #fe2079; 149 | text-shadow: 0 0 20px #fe2079; 150 | } 151 | 152 | .game__menu-musicinfo { 153 | margin-top: 0.5rem; 154 | margin-bottom: 0.5rem; 155 | font-family: 'Commando', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 156 | font-size: 1.5rem; 157 | color: #fe2079; 158 | text-shadow: 0 0 20px #fe2079; 159 | } 160 | 161 | .game__menu-controls { 162 | margin-top: 2rem; 163 | margin-bottom: 0.5rem; 164 | font-family: 'Road Rage', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 165 | font-size: 2rem; 166 | color: #fe2079; 167 | text-shadow: 0 0 20px #fe2079; 168 | } 169 | 170 | .game__menu-controls > p { 171 | margin: 0; 172 | } 173 | 174 | .game__menu-button:hover { 175 | color: #FFF; 176 | cursor: pointer; 177 | } 178 | 179 | 180 | @media screen and (max-width: 992px), (max-height: 1080px) { 181 | .game__logo { 182 | height: 360px; 183 | width: 720px; 184 | } 185 | 186 | .game__logo-small { 187 | height: 175px; 188 | width: 350px; 189 | } 190 | 191 | .game__menu-button { 192 | font-size: 4rem; 193 | text-shadow: 0 0 15px #fe2079; 194 | } 195 | 196 | .game__menu-button-music { 197 | font-size: 2.5rem; 198 | } 199 | 200 | .game__menu-warning { 201 | text-shadow: 0 0 15px #fe2079; 202 | } 203 | 204 | .game__score-gameover { 205 | font-size: 8rem; 206 | text-shadow: 0 0 10px #fe2079; 207 | } 208 | 209 | .game__score-title { 210 | font-size: 3rem; 211 | } 212 | 213 | .game__score { 214 | font-size: 6rem; 215 | } 216 | 217 | .game__menu-warning { 218 | font-size: 1.5rem; 219 | margin-top: 0; 220 | } 221 | 222 | .game__menu-musicinfo { 223 | font-size: 1.2rem; 224 | } 225 | .game__menu-controls { 226 | font-size: 1.5rem 227 | } 228 | 229 | .game__subcontainer { 230 | margin-bottom: 1rem; 231 | } 232 | } 233 | 234 | @media screen and (max-width: 600px), (max-height: 700px) { 235 | .game__menu-musicinfo { 236 | font-size: 1rem; 237 | } 238 | 239 | .game__subcontainer { 240 | margin: 0; 241 | } 242 | .game__menu-controls { 243 | margin-top: 0.5rem; 244 | margin-bottom: 0.5rem; 245 | font-size: 1.2rem 246 | } 247 | .game__scorecontainer { 248 | flex-direction: column; 249 | justify-content: center; 250 | } 251 | 252 | .game__score-left { 253 | margin-right: 0; 254 | margin-bottom: 1rem; 255 | } 256 | 257 | .game__score-highscore { 258 | font-size: 2.5rem; 259 | } 260 | 261 | .game__logo { 262 | height: 128px; 263 | width: 256px; 264 | } 265 | 266 | .game__logo-small { 267 | height: 128px; 268 | width: 256px; 269 | } 270 | 271 | .game__menu-button { 272 | font-size: 3rem; 273 | text-shadow: 0 0 15px #fe2079; 274 | } 275 | 276 | .game__menu-button-music { 277 | font-size: 2rem; 278 | } 279 | 280 | .game__menu-warning { 281 | font-size: 1rem; 282 | text-shadow: 0 0 15px #fe2079; 283 | } 284 | 285 | .game__score-gameover { 286 | font-size: 4rem; 287 | text-shadow: 0 0 10px #fe2079; 288 | margin-bottom: 1rem; 289 | } 290 | 291 | .game__score-title { 292 | font-size: 2rem; 293 | text-shadow: 0 0 5px #fe2079; 294 | } 295 | 296 | .game__score { 297 | font-size: 4rem; 298 | text-shadow: 0 0 10px #fe2079; 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /src/styles/hud.css: -------------------------------------------------------------------------------- 1 | .hud { 2 | font-family: 'Commando', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 3 | text-shadow: 0 0 40px #fe2079; 4 | color: #eee; 5 | font-size: 8rem; 6 | font-weight: 700; 7 | } 8 | 9 | @keyframes pulse { 10 | 0% { 11 | color: #eee; 12 | opacity: 1; 13 | } 14 | 50% { 15 | color: #fe2079; 16 | opacity: 0.7; 17 | } 18 | 100% { 19 | color: #eee; 20 | opacity: 1; 21 | } 22 | } 23 | 24 | @keyframes wiggle { 25 | 0% { 26 | transform: translateY(0%); 27 | } 28 | 29 | 50% { 30 | transform: translateY(10%); 31 | } 32 | 33 | 100% { 34 | transform: translateY(0%); 35 | } 36 | } 37 | 38 | .center { 39 | position: absolute; 40 | top: 0; 41 | left: 0; 42 | width: 100%; 43 | height: 100%; 44 | display: flex; 45 | align-items: center; 46 | justify-content: center; 47 | } 48 | 49 | .center__speedup { 50 | color: #eee; 51 | animation: pulse 0.5s ease-in-out infinite, wiggle 0.3s ease-in infinite; 52 | } 53 | 54 | .bottomLeft { 55 | position: absolute; 56 | bottom: 2vh; 57 | left: 4vh; 58 | transform: skew(0deg, -10deg); 59 | width: 200px; 60 | margin: 0; 61 | } 62 | 63 | .score { 64 | opacity: 1; 65 | -webkit-user-select: none; /* Safari */ 66 | -moz-user-select: none; /* Firefox */ 67 | -ms-user-select: none; /* IE10+/Edge */ 68 | user-select: none; /* Standard */ 69 | } 70 | 71 | .score__number { 72 | font-size: 8rem; 73 | font-weight: 500; 74 | margin: 0; 75 | } 76 | 77 | .score__title { 78 | color: #fe2079; 79 | font-size: 3rem; 80 | font-weight: 500; 81 | margin: 0; 82 | margin-top: 0.5rem; 83 | font-family: 'Commando', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 84 | } 85 | 86 | .score__withcontrols { 87 | margin-bottom: 10rem; 88 | } 89 | 90 | .controls { 91 | position: absolute; 92 | width: 100vw; 93 | display: flex; 94 | bottom: 2vh; 95 | justify-content: space-between; 96 | padding-left: 2vh; 97 | padding-right: 2vh; 98 | z-index: 1001; 99 | } 100 | 101 | .control { 102 | opacity: 0.3; 103 | padding-left: 2rem; 104 | padding-right: 2rem; 105 | color: #fe2079; 106 | border: none; 107 | display: flex; 108 | align-items: center; 109 | justify-content: center; 110 | font-size: 6rem; 111 | border-radius: 20px; 112 | background-color: transparent; 113 | cursor: default; 114 | -webkit-user-select: none; /* Safari */ 115 | -moz-user-select: none; /* Firefox */ 116 | -ms-user-select: none; /* IE10+/Edge */ 117 | user-select: none; /* Standard */ 118 | box-shadow: 0 0 40px #fe2079; 119 | text-shadow: 0 0 40px #fe2079; 120 | } 121 | 122 | .control-active { 123 | opacity: 1; 124 | } 125 | 126 | @media screen and (max-width: 992px) { 127 | .bottomLeft { 128 | left: 2vh; 129 | } 130 | 131 | .score__number { 132 | font-size: 4rem; 133 | } 134 | .score__title { 135 | font-size: 2rem; 136 | } 137 | .hud { 138 | font-size: 5rem; 139 | } 140 | } 141 | 142 | @media screen and (max-width: 600px) { 143 | .center { 144 | font-size: 3rem; 145 | } 146 | 147 | .bottomLeft { 148 | left: 2vh; 149 | } 150 | .score__withcontrols { 151 | margin-bottom: 7rem; 152 | } 153 | .score__number { 154 | font-size: 2rem; 155 | } 156 | .score__title { 157 | font-size: 1rem; 158 | } 159 | .hud { 160 | text-shadow: 0 0 15px #fe2079; 161 | font-size: 5rem; 162 | } 163 | 164 | .control { 165 | padding-left: 1rem; 166 | padding-right: 1rem; 167 | font-size: 4rem; 168 | box-shadow: 0 0 10px #fe2079; 169 | } 170 | } -------------------------------------------------------------------------------- /src/styles/index.css: -------------------------------------------------------------------------------- 1 | /*styles.css*/ 2 | * { 3 | box-sizing: border-box; 4 | } 5 | 6 | html, 7 | body, 8 | #root { 9 | width: 100%; 10 | height: 100%; 11 | margin: 0; 12 | padding: 0; 13 | background: #141622; 14 | color: #141622; 15 | font-size: 16px; 16 | } 17 | 18 | body { 19 | margin: 0; 20 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 21 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 22 | sans-serif; 23 | -webkit-font-smoothing: antialiased; 24 | -moz-osx-font-smoothing: grayscale; 25 | } 26 | -------------------------------------------------------------------------------- /src/styles/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in iOS. 9 | */ 10 | 11 | html { 12 | line-height: 1; /* 1 */ 13 | -webkit-text-size-adjust: 100%; /* 2 */ 14 | } 15 | 16 | /* Sections 17 | ========================================================================== */ 18 | 19 | /** 20 | * Remove the margin in all browsers. 21 | */ 22 | 23 | body { 24 | margin: 0; 25 | } 26 | 27 | /** 28 | * Render the `main` element consistently in IE. 29 | */ 30 | 31 | main { 32 | display: block; 33 | } 34 | 35 | /** 36 | * Correct the font size and margin on `h1` elements within `section` and 37 | * `article` contexts in Chrome, Firefox, and Safari. 38 | */ 39 | 40 | h1 { 41 | font-size: 2em; 42 | margin: 0.67em 0; 43 | } 44 | 45 | /* Grouping content 46 | ========================================================================== */ 47 | 48 | /** 49 | * 1. Add the correct box sizing in Firefox. 50 | * 2. Show the overflow in Edge and IE. 51 | */ 52 | 53 | hr { 54 | box-sizing: content-box; /* 1 */ 55 | height: 0; /* 1 */ 56 | overflow: visible; /* 2 */ 57 | } 58 | 59 | /** 60 | * 1. Correct the inheritance and scaling of font size in all browsers. 61 | * 2. Correct the odd `em` font sizing in all browsers. 62 | */ 63 | 64 | pre { 65 | font-family: monospace, monospace; /* 1 */ 66 | font-size: 1em; /* 2 */ 67 | } 68 | 69 | /* Text-level semantics 70 | ========================================================================== */ 71 | 72 | /** 73 | * Remove the gray background on active links in IE 10. 74 | */ 75 | 76 | a { 77 | background-color: transparent; 78 | } 79 | 80 | /** 81 | * 1. Remove the bottom border in Chrome 57- 82 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 83 | */ 84 | 85 | abbr[title] { 86 | border-bottom: none; /* 1 */ 87 | text-decoration: underline; /* 2 */ 88 | text-decoration: underline dotted; /* 2 */ 89 | } 90 | 91 | /** 92 | * Add the correct font weight in Chrome, Edge, and Safari. 93 | */ 94 | 95 | b, 96 | strong { 97 | font-weight: bolder; 98 | } 99 | 100 | /** 101 | * 1. Correct the inheritance and scaling of font size in all browsers. 102 | * 2. Correct the odd `em` font sizing in all browsers. 103 | */ 104 | 105 | code, 106 | kbd, 107 | samp { 108 | font-family: monospace, monospace; /* 1 */ 109 | font-size: 1em; /* 2 */ 110 | } 111 | 112 | /** 113 | * Add the correct font size in all browsers. 114 | */ 115 | 116 | small { 117 | font-size: 80%; 118 | } 119 | 120 | /** 121 | * Prevent `sub` and `sup` elements from affecting the line height in 122 | * all browsers. 123 | */ 124 | 125 | sub, 126 | sup { 127 | font-size: 75%; 128 | line-height: 0; 129 | position: relative; 130 | vertical-align: baseline; 131 | } 132 | 133 | sub { 134 | bottom: -0.25em; 135 | } 136 | 137 | sup { 138 | top: -0.5em; 139 | } 140 | 141 | /* Embedded content 142 | ========================================================================== */ 143 | 144 | /** 145 | * Remove the border on images inside links in IE 10. 146 | */ 147 | 148 | img { 149 | border-style: none; 150 | } 151 | 152 | /* Forms 153 | ========================================================================== */ 154 | 155 | /** 156 | * 1. Change the font styles in all browsers. 157 | * 2. Remove the margin in Firefox and Safari. 158 | */ 159 | 160 | button, 161 | input, 162 | optgroup, 163 | select, 164 | textarea { 165 | font-family: inherit; /* 1 */ 166 | font-size: 100%; /* 1 */ 167 | line-height: 1.15; /* 1 */ 168 | margin: 0; /* 2 */ 169 | } 170 | 171 | /** 172 | * Show the overflow in IE. 173 | * 1. Show the overflow in Edge. 174 | */ 175 | 176 | button, 177 | input { /* 1 */ 178 | overflow: visible; 179 | } 180 | 181 | /** 182 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 183 | * 1. Remove the inheritance of text transform in Firefox. 184 | */ 185 | 186 | button, 187 | select { /* 1 */ 188 | text-transform: none; 189 | } 190 | 191 | /** 192 | * Correct the inability to style clickable types in iOS and Safari. 193 | */ 194 | 195 | button, 196 | [type="button"], 197 | [type="reset"], 198 | [type="submit"] { 199 | -webkit-appearance: button; 200 | } 201 | 202 | /** 203 | * Remove the inner border and padding in Firefox. 204 | */ 205 | 206 | button::-moz-focus-inner, 207 | [type="button"]::-moz-focus-inner, 208 | [type="reset"]::-moz-focus-inner, 209 | [type="submit"]::-moz-focus-inner { 210 | border-style: none; 211 | padding: 0; 212 | } 213 | 214 | /** 215 | * Restore the focus styles unset by the previous rule. 216 | */ 217 | 218 | button:-moz-focusring, 219 | [type="button"]:-moz-focusring, 220 | [type="reset"]:-moz-focusring, 221 | [type="submit"]:-moz-focusring { 222 | outline: 1px dotted ButtonText; 223 | } 224 | 225 | /** 226 | * Correct the padding in Firefox. 227 | */ 228 | 229 | fieldset { 230 | padding: 0.35em 0.75em 0.625em; 231 | } 232 | 233 | /** 234 | * 1. Correct the text wrapping in Edge and IE. 235 | * 2. Correct the color inheritance from `fieldset` elements in IE. 236 | * 3. Remove the padding so developers are not caught out when they zero out 237 | * `fieldset` elements in all browsers. 238 | */ 239 | 240 | legend { 241 | box-sizing: border-box; /* 1 */ 242 | color: inherit; /* 2 */ 243 | display: table; /* 1 */ 244 | max-width: 100%; /* 1 */ 245 | padding: 0; /* 3 */ 246 | white-space: normal; /* 1 */ 247 | } 248 | 249 | /** 250 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 251 | */ 252 | 253 | progress { 254 | vertical-align: baseline; 255 | } 256 | 257 | /** 258 | * Remove the default vertical scrollbar in IE 10+. 259 | */ 260 | 261 | textarea { 262 | overflow: auto; 263 | } 264 | 265 | /** 266 | * 1. Add the correct box sizing in IE 10. 267 | * 2. Remove the padding in IE 10. 268 | */ 269 | 270 | [type="checkbox"], 271 | [type="radio"] { 272 | box-sizing: border-box; /* 1 */ 273 | padding: 0; /* 2 */ 274 | } 275 | 276 | /** 277 | * Correct the cursor style of increment and decrement buttons in Chrome. 278 | */ 279 | 280 | [type="number"]::-webkit-inner-spin-button, 281 | [type="number"]::-webkit-outer-spin-button { 282 | height: auto; 283 | } 284 | 285 | /** 286 | * 1. Correct the odd appearance in Chrome and Safari. 287 | * 2. Correct the outline style in Safari. 288 | */ 289 | 290 | [type="search"] { 291 | -webkit-appearance: textfield; /* 1 */ 292 | outline-offset: -2px; /* 2 */ 293 | } 294 | 295 | /** 296 | * Remove the inner padding in Chrome and Safari on macOS. 297 | */ 298 | 299 | [type="search"]::-webkit-search-decoration { 300 | -webkit-appearance: none; 301 | } 302 | 303 | /** 304 | * 1. Correct the inability to style clickable types in iOS and Safari. 305 | * 2. Change font properties to `inherit` in Safari. 306 | */ 307 | 308 | ::-webkit-file-upload-button { 309 | -webkit-appearance: button; /* 1 */ 310 | font: inherit; /* 2 */ 311 | } 312 | 313 | /* Interactive 314 | ========================================================================== */ 315 | 316 | /* 317 | * Add the correct display in Edge, IE 10+, and Firefox. 318 | */ 319 | 320 | details { 321 | display: block; 322 | } 323 | 324 | /* 325 | * Add the correct display in all browsers. 326 | */ 327 | 328 | summary { 329 | display: list-item; 330 | } 331 | 332 | /* Misc 333 | ========================================================================== */ 334 | 335 | /** 336 | * Add the correct display in IE 10+. 337 | */ 338 | 339 | template { 340 | display: none; 341 | } 342 | 343 | /** 344 | * Add the correct display in IE 10. 345 | */ 346 | 347 | [hidden] { 348 | display: none; 349 | } -------------------------------------------------------------------------------- /src/textures/cuberun-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akarlsten/cuberun/b3581ac0fbaa4293c571d8935c11cb454b29be03/src/textures/cuberun-logo.png -------------------------------------------------------------------------------- /src/textures/enginetextureflip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akarlsten/cuberun/b3581ac0fbaa4293c571d8935c11cb454b29be03/src/textures/enginetextureflip.png -------------------------------------------------------------------------------- /src/textures/galaxy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akarlsten/cuberun/b3581ac0fbaa4293c571d8935c11cb454b29be03/src/textures/galaxy.jpg -------------------------------------------------------------------------------- /src/textures/galaxyTextureBW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akarlsten/cuberun/b3581ac0fbaa4293c571d8935c11cb454b29be03/src/textures/galaxyTextureBW.png -------------------------------------------------------------------------------- /src/textures/grid-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akarlsten/cuberun/b3581ac0fbaa4293c571d8935c11cb454b29be03/src/textures/grid-blue.png -------------------------------------------------------------------------------- /src/textures/grid-green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akarlsten/cuberun/b3581ac0fbaa4293c571d8935c11cb454b29be03/src/textures/grid-green.png -------------------------------------------------------------------------------- /src/textures/grid-orange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akarlsten/cuberun/b3581ac0fbaa4293c571d8935c11cb454b29be03/src/textures/grid-orange.png -------------------------------------------------------------------------------- /src/textures/grid-pink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akarlsten/cuberun/b3581ac0fbaa4293c571d8935c11cb454b29be03/src/textures/grid-pink.png -------------------------------------------------------------------------------- /src/textures/grid-purple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akarlsten/cuberun/b3581ac0fbaa4293c571d8935c11cb454b29be03/src/textures/grid-purple.png -------------------------------------------------------------------------------- /src/textures/grid-rainbow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akarlsten/cuberun/b3581ac0fbaa4293c571d8935c11cb454b29be03/src/textures/grid-rainbow.png -------------------------------------------------------------------------------- /src/textures/grid-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akarlsten/cuberun/b3581ac0fbaa4293c571d8935c11cb454b29be03/src/textures/grid-red.png -------------------------------------------------------------------------------- /src/textures/noise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akarlsten/cuberun/b3581ac0fbaa4293c571d8935c11cb454b29be03/src/textures/noise.png -------------------------------------------------------------------------------- /src/util/distance2D.js: -------------------------------------------------------------------------------- 1 | export default function distance2D(p1x, p1y, p2x, p2y) { 2 | const a = p2x - p1x; 3 | const b = p2y - p1y; 4 | 5 | return Math.sqrt(a * a + b * b); 6 | } -------------------------------------------------------------------------------- /src/util/generateFixedCubes.js: -------------------------------------------------------------------------------- 1 | import { PLANE_SIZE, CUBE_SIZE, WALL_RADIUS, LEFT_BOUND } from '../constants/index' 2 | 3 | const segments = (PLANE_SIZE - WALL_RADIUS / 2) / CUBE_SIZE 4 | const negativeBound = -(PLANE_SIZE / 2) + WALL_RADIUS / 2 5 | 6 | export function makeWall(hasGap = true, gapSize = 3) { 7 | const wallCoordinates = [...Array(segments)].map((cube, index) => { 8 | return { 9 | x: negativeBound + index * CUBE_SIZE, // -500, -480, -460, etc 10 | y: CUBE_SIZE / 2, 11 | z: index <= segments / 2 ? -(index * CUBE_SIZE) : -(segments * CUBE_SIZE) + index * CUBE_SIZE, // pyramid formation 12 | } 13 | }) 14 | 15 | if (hasGap) { 16 | wallCoordinates.splice(segments / 2 - Math.floor(gapSize / 2), gapSize) // TODO: rethink this math when not tired 17 | } 18 | 19 | if (LEFT_BOUND < PLANE_SIZE / 2) { 20 | const spliceFactor = (PLANE_SIZE / 2 + LEFT_BOUND) / CUBE_SIZE - 1 21 | wallCoordinates.splice(0, spliceFactor) 22 | wallCoordinates.splice(-spliceFactor, spliceFactor) 23 | } 24 | 25 | return wallCoordinates 26 | } 27 | 28 | function makeTunnel(tunnelLength = 10, gapSize = 3) { 29 | 30 | // const tunnelCoordinates = [...Array(tunnelLength * 2)].map((cube, index) => { 31 | // return { 32 | // x: index % 2 === 0 ? CUBE_SIZE * 2 : -CUBE_SIZE * 2, 33 | // y: CUBE_SIZE / 2, 34 | // z: -(((segments / 2 - 2) * CUBE_SIZE) + (index * CUBE_SIZE * 0.7)) 35 | // } 36 | // }) 37 | 38 | const coords = [ 39 | { x: 40, y: 10, z: -450 }, 40 | { x: -40, y: 10, z: -450 }, 41 | { x: 40, y: 10, z: -478 }, 42 | { x: -40, y: 10, z: -478 }, 43 | { x: 40, y: 10, z: -506 }, 44 | { x: -40, y: 10, z: -506 }, 45 | { x: 40, y: 10, z: -534 }, 46 | { x: -40, y: 10, z: -534 }, 47 | { x: 40, y: 10, z: -562 }, 48 | { x: -40, y: 10, z: -562 }, 49 | { x: 40, y: 10, z: -590 }, 50 | { x: -40, y: 10, z: -590 }, 51 | { x: 40, y: 10, z: -618 }, 52 | { x: -40, y: 10, z: -618 }, 53 | { x: 40, y: 10, z: -646 }, 54 | { x: -40, y: 10, z: -646 }, 55 | { x: 40, y: 10, z: -674 }, 56 | { x: -40, y: 10, z: -674 }, 57 | { x: 40, y: 10, z: -702 }, 58 | { x: -40, y: 10, z: -702 }] 59 | 60 | return coords 61 | } 62 | 63 | function makeDiamond(size = 21, tunnelLength = 10) { 64 | const wallEndOffset = -((segments / 2 - 2) * CUBE_SIZE) 65 | const tunnelEndOffset = tunnelLength * CUBE_SIZE * 0.7 66 | 67 | const outerLeftWall = [...Array(size)].map((cube, index) => { 68 | return { 69 | x: index === 0 ? -70 : index <= size / 2 ? -CUBE_SIZE * 3 - index * CUBE_SIZE : (-CUBE_SIZE * 3 - size * CUBE_SIZE) + index * CUBE_SIZE, 70 | y: CUBE_SIZE / 2, 71 | z: wallEndOffset - tunnelEndOffset - index * CUBE_SIZE * 1.75 72 | } 73 | }) 74 | 75 | const outerRightWall = [...Array(size)].map((cube, index) => { 76 | return { 77 | x: index === 0 ? 70 : index <= size / 2 ? CUBE_SIZE * 3 + index * CUBE_SIZE : (CUBE_SIZE * 3 + size * CUBE_SIZE) - index * CUBE_SIZE, 78 | y: CUBE_SIZE / 2, 79 | z: wallEndOffset - tunnelEndOffset - index * CUBE_SIZE * 1.75 80 | } 81 | }) 82 | 83 | const innerSize = Math.floor(size / 2) - 2 // TODO: maybe remove -2 84 | 85 | const innerLeftWall = [...Array(innerSize)].map((cube, index) => { 86 | return { 87 | x: index === 1 ? (-CUBE_SIZE / 2) - index * CUBE_SIZE * 1.1 : index < innerSize / 2 ? (-CUBE_SIZE / 2) - index * CUBE_SIZE : (-CUBE_SIZE / 2) - (innerSize * CUBE_SIZE) + index * CUBE_SIZE, 88 | y: CUBE_SIZE / 2, 89 | z: wallEndOffset - tunnelEndOffset - (Math.floor(size / 1.5) * CUBE_SIZE) - index * CUBE_SIZE * 1.75 90 | } 91 | }) 92 | 93 | const innerRightWall = [...Array(innerSize)].map((cube, index) => { 94 | return { 95 | x: index === 1 ? (CUBE_SIZE / 2) + index * CUBE_SIZE * 1.1 : index < innerSize / 2 ? (CUBE_SIZE / 2) + index * CUBE_SIZE : (CUBE_SIZE / 2) + (innerSize * CUBE_SIZE) - index * CUBE_SIZE, 96 | y: CUBE_SIZE / 2, 97 | z: wallEndOffset - tunnelEndOffset - (Math.floor(size / 1.5) * CUBE_SIZE) - index * CUBE_SIZE * 1.75 98 | } 99 | }) 100 | 101 | const middlePiece = { x: 0, y: CUBE_SIZE / 2, z: wallEndOffset - tunnelEndOffset - (Math.floor(size / 1.5) * CUBE_SIZE) + CUBE_SIZE } 102 | 103 | const firstDiamond = [...outerLeftWall, ...outerRightWall, middlePiece, ...innerLeftWall, ...innerRightWall] 104 | const secondDiamond = firstDiamond.map((coordinates, index) => ({ ...coordinates, z: index >= 42 ? coordinates.z - 700 /* 735 */ : coordinates.z - (size * CUBE_SIZE * 1.75) })) 105 | 106 | const finalTunnel = [ 107 | { x: 60, y: 10, z: -2045 }, 108 | { x: -60, y: 10, z: -2045 }, 109 | { x: 40, y: 10, z: -2065 }, 110 | { x: -40, y: 10, z: -2065 }, 111 | { x: 40, y: 10, z: -2085 }, 112 | { x: -40, y: 10, z: -2085 }, 113 | { x: 40, y: 10, z: -2105 }, 114 | { x: -40, y: 10, z: -2105 }, 115 | { x: 40, y: 10, z: -2125 }, 116 | { x: -40, y: 10, z: -2125 }, 117 | { x: 40, y: 10, z: -2145 }, 118 | { x: -40, y: 10, z: -2145 }, 119 | { x: 40, y: 10, z: -2165 }, 120 | { x: -40, y: 10, z: -2165 }, 121 | { x: 40, y: 10, z: -2185 }, 122 | { x: -40, y: 10, z: -2185 }, 123 | { x: 40, y: 10, z: -2205 }, 124 | { x: -40, y: 10, z: -2205 }, 125 | { x: 40, y: 10, z: -2225 }, 126 | { x: -40, y: 10, z: -2225 }, 127 | { x: 40, y: 10, z: -2245 }, 128 | { x: -40, y: 10, z: -2245 }, 129 | { x: 40, y: 10, z: -2265 }, 130 | { x: -40, y: 10, z: -2265 }, 131 | ] 132 | 133 | return [...firstDiamond, ...secondDiamond, ...finalTunnel] 134 | } 135 | 136 | export function generateDiamond() { 137 | const initialWall = makeWall() 138 | const tunnel = makeTunnel() 139 | const diamond = makeDiamond() 140 | 141 | return [...initialWall, ...tunnel, ...diamond] 142 | } 143 | 144 | export function generateCubeTunnel() { 145 | const initialWall = makeWall() 146 | const tunnel = makeTunnel() 147 | 148 | return [...initialWall, ...tunnel] 149 | } -------------------------------------------------------------------------------- /src/util/randomInRange.js: -------------------------------------------------------------------------------- 1 | const randomInRange = (from, to) => Math.floor(Math.random() * (to - from + 1)) - to 2 | 3 | export default randomInRange --------------------------------------------------------------------------------