├── .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 | 
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 | 
32 | 
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 |
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 |
198 |
199 |
200 |
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 |
50 |
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 |
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 |

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 |

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
--------------------------------------------------------------------------------