├── .babelrc
├── .gitignore
├── .prettierrc
├── LICENSE
├── README.md
├── package.json
├── public
└── index.html
├── screenshot.png
├── src
├── App.scss
├── Types.ts
├── components
│ ├── Defeat.tsx
│ ├── Display.tsx
│ └── Overlay.tsx
├── constants
│ ├── ActionType.ts
│ └── index.ts
├── functions
│ ├── addOverlay.tsx
│ ├── addWindowEvents.ts
│ ├── createShader.ts
│ └── tick.ts
├── img
│ └── money.png
├── index.d.ts
├── index.ts
├── reducers
│ └── gameState.ts
└── shaders
│ ├── fog.frag
│ ├── index.ts
│ ├── scanlines.frag
│ └── vignette.frag
├── tsconfig.json
├── webpack.config.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-env",
4 | "@babel/preset-react"
5 | ],
6 | "plugins": [
7 | "@babel/plugin-proposal-class-properties"
8 | ]
9 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | yarn-error.log
2 | npm-error.log
3 | node_modules
4 | dist
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 2,
3 | "singleQuote": true,
4 | "semi": true,
5 | "arrowParens": "avoid"
6 | }
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2019-2020, Mat Sz
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted (subject to the limitations in the disclaimer
6 | below) provided that the following conditions are met:
7 |
8 | * Redistributions of source code must retain the above copyright notice,
9 | this list of conditions and the following disclaimer.
10 |
11 | * Redistributions in binary form must reproduce the above copyright
12 | notice, this list of conditions and the following disclaimer in the
13 | documentation and/or other materials provided with the distribution.
14 |
15 | * Neither the name of the copyright holder nor the names of its
16 | contributors may be used to endorse or promote products derived from this
17 | software without specific prior written permission.
18 |
19 | NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
20 | THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
21 | CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
23 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
24 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
25 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
26 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
27 | BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
28 | IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30 | POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # flight
2 |
3 |
4 |
5 |
6 |
7 | A simple game made so I can test a few things out.
8 |
9 | three.js, TypeScript, React, Redux and GLSL shaders at once.
10 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "flight",
3 | "version": "1.0.0",
4 | "main": "index.ts",
5 | "scripts": {
6 | "start": "NODE_ENV=development webpack-dev-server --mode development --open --hot --host 0.0.0.0",
7 | "build": "NODE_ENV=production webpack --mode production"
8 | },
9 | "husky": {
10 | "hooks": {
11 | "pre-commit": "lint-staged"
12 | }
13 | },
14 | "lint-staged": {
15 | "src/**/*.{js,jsx,ts,tsx,json,css,scss,md}": [
16 | "prettier --write"
17 | ],
18 | "__tests__/**/*.{js,jsx,ts,tsx,json,css,scss,md}": [
19 | "prettier --write"
20 | ]
21 | },
22 | "devDependencies": {
23 | "@babel/core": "^7.6.4",
24 | "@babel/plugin-proposal-class-properties": "^7.5.5",
25 | "@babel/preset-env": "^7.6.3",
26 | "@babel/preset-react": "^7.7.4",
27 | "babel-loader": "^8.0.6",
28 | "css-loader": "^3.2.0",
29 | "file-loader": "^5.0.2",
30 | "html-webpack-plugin": "^3.2.0",
31 | "husky": "^4.2.5",
32 | "image-webpack-loader": "^6.0.0",
33 | "lint-staged": "^10.2.11",
34 | "node-sass": "^4.13.0",
35 | "prettier": "^2.0.5",
36 | "raw-loader": "^4.0.0",
37 | "sass-loader": "^8.0.0",
38 | "style-loader": "^1.0.0",
39 | "ts-loader": "^6.2.1",
40 | "typescript": "^3.7.2",
41 | "webpack": "^4.41.2",
42 | "webpack-cli": "^3.3.9",
43 | "webpack-dev-server": "^3.9.0"
44 | },
45 | "dependencies": {
46 | "@types/classnames": "^2.2.9",
47 | "@types/react": "^16.9.15",
48 | "@types/react-dom": "^16.9.4",
49 | "@types/react-redux": "^7.1.5",
50 | "@types/redux": "^3.6.0",
51 | "classnames": "^2.2.6",
52 | "react": "^16.12.0",
53 | "react-device-detect": "^1.11.14",
54 | "react-dom": "^16.12.0",
55 | "react-redux": "^7.1.3",
56 | "redux": "^4.0.4",
57 | "three": "^0.111.0"
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Flight
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mat-sz/flight/83dc25110f0167fc6df7f9c7ff0f96eb8ff0934c/screenshot.png
--------------------------------------------------------------------------------
/src/App.scss:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | overflow: hidden;
4 | }
5 |
6 | body {
7 | position: fixed;
8 | color: white;
9 | font-family: monospace;
10 |
11 | display: flex;
12 | align-items: center;
13 | justify-content: center;
14 | flex-direction: column;
15 | width: 100%;
16 | margin: 0;
17 | }
18 |
19 | button {
20 | background: rgba(255, 255, 255, 0.75);
21 | border: none;
22 | color: black;
23 | font-size: 20px;
24 | padding: 10px 15px;
25 | cursor: pointer;
26 | transition: 0.2s ease-in-out all;
27 |
28 | &:hover {
29 | background: rgba(255, 255, 255, 1);
30 | }
31 | }
32 |
33 | .github-corner {
34 | z-index: 500;
35 |
36 | &:hover .octo-arm {
37 | animation: octocat-wave 560ms ease-in-out;
38 | }
39 | }
40 |
41 | @keyframes octocat-wave {
42 | 0%,
43 | 100% {
44 | transform: rotate(0);
45 | }
46 |
47 | 20%,
48 | 60% {
49 | transform: rotate(-25deg);
50 | }
51 |
52 | 40%,
53 | 80% {
54 | transform: rotate(10deg);
55 | }
56 | }
57 |
58 | .display {
59 | background: rgba(255, 255, 255, 0.05);
60 | color: white;
61 | margin: 10px;
62 | padding: 10px;
63 | width: 150px;
64 |
65 | &__title {
66 | color: #999;
67 | font-size: 12px;
68 | }
69 |
70 | &__value {
71 | font-size: 24px;
72 | will-change: contents;
73 | }
74 | }
75 |
76 | .overlay {
77 | position: absolute;
78 | top: 0;
79 | left: 0;
80 | right: 0;
81 | bottom: 0;
82 |
83 | .score {
84 | position: absolute;
85 | left: 5%;
86 | top: 5%;
87 | font-size: 20px;
88 | }
89 |
90 | .money {
91 | position: absolute;
92 | top: 5%;
93 | right: 5%;
94 | font-size: 20px;
95 |
96 | display: flex;
97 | justify-content: flex-end;
98 |
99 | img {
100 | margin-bottom: -1px;
101 | max-height: 20px;
102 | }
103 | }
104 |
105 | .help {
106 | position: absolute;
107 | bottom: 5%;
108 | font-size: 16px;
109 | left: 5%;
110 | right: 5%;
111 | text-align: center;
112 | }
113 |
114 | .defeat.hidden {
115 | opacity: 0;
116 | pointer-events: none;
117 | }
118 |
119 | .defeat {
120 | color: white;
121 | position: absolute;
122 | top: 0;
123 | left: 0;
124 | right: 0;
125 | bottom: 0;
126 |
127 | display: flex;
128 | justify-content: center;
129 | align-items: center;
130 | flex-direction: column;
131 |
132 | background: rgba(255, 255, 255, 0.05);
133 | transition: 0.2s ease-in-out all;
134 |
135 | .text {
136 | mix-blend-mode: difference;
137 | font-weight: bold;
138 | font-size: 90px;
139 | }
140 | }
141 | }
142 |
143 | @media screen and (min-width: 1000px) {
144 | .overlay {
145 | .score {
146 | left: 10%;
147 | top: 10%;
148 | }
149 |
150 | .money {
151 | top: 10%;
152 | right: 10%;
153 | }
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/src/Types.ts:
--------------------------------------------------------------------------------
1 | import ActionType from './constants/ActionType';
2 |
3 | export interface Action {
4 | type: ActionType;
5 | value?: number | boolean;
6 | }
7 |
8 | export interface GameState {
9 | score: number;
10 | highScore: number;
11 | lane: number;
12 | money: number;
13 | defeat: boolean;
14 | }
15 |
16 | export interface SavedState {
17 | highScore: number;
18 | money: number;
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/Defeat.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useSelector } from 'react-redux';
3 | import classNames from 'classnames';
4 |
5 | import { GameState } from '../Types';
6 |
7 | const Defeat = ({ onReset }: { onReset: () => void }) => {
8 | const defeat = useSelector((state: GameState) => state.defeat);
9 |
10 | return (
11 |
16 |
Defeat
17 |
18 | onReset()}>New Game
19 |
20 |
21 | );
22 | };
23 |
24 | export default Defeat;
25 |
--------------------------------------------------------------------------------
/src/components/Display.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Display = ({ title, value }: { title: string; value: any }) => {
4 | return (
5 |
6 |
{title}:
7 |
{value}
8 |
9 | );
10 | };
11 |
12 | export default Display;
13 |
--------------------------------------------------------------------------------
/src/components/Overlay.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useSelector } from 'react-redux';
3 | import { MobileView, BrowserView } from 'react-device-detect';
4 |
5 | import moneyImage from '../img/money.png';
6 |
7 | import { GameState } from '../Types';
8 | import Defeat from './Defeat';
9 | import Display from './Display';
10 |
11 | const Overlay = ({ onReset }: { onReset: () => void }) => {
12 | const score = useSelector((state: GameState) => state.score);
13 | const highScore = useSelector((state: GameState) => state.highScore);
14 | const money = useSelector((state: GameState) => state.money);
15 | const defeat = useSelector((state: GameState) => state.defeat);
16 |
17 | return (
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
29 | {Math.round(money)}
30 | >
31 | }
32 | />
33 |
34 |
35 |
36 | Tap on left and right sides of the screen to move.
37 |
38 |
39 | {defeat
40 | ? 'Press Space to restart.'
41 | : 'Use left and right arrow keys to move.'}
42 |
43 |
44 |
45 | );
46 | };
47 |
48 | export default Overlay;
49 |
--------------------------------------------------------------------------------
/src/constants/ActionType.ts:
--------------------------------------------------------------------------------
1 | enum ActionType {
2 | SET_SCORE,
3 | SET_MONEY,
4 | SET_DEFEAT,
5 | SET_LANE,
6 | ADD_SCORE,
7 | ADD_MONEY,
8 | }
9 |
10 | export default ActionType;
11 |
--------------------------------------------------------------------------------
/src/constants/index.ts:
--------------------------------------------------------------------------------
1 | export const laneWidth = 0.4;
2 |
--------------------------------------------------------------------------------
/src/functions/addOverlay.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { Provider } from 'react-redux';
4 |
5 | import Overlay from '../components/Overlay';
6 |
7 | export default function addOverlay(store: any, onReset: () => void) {
8 | ReactDOM.render(
9 |
10 |
11 | ,
12 | document.getElementById('overlay')
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/src/functions/addWindowEvents.ts:
--------------------------------------------------------------------------------
1 | import { PerspectiveCamera, WebGLRenderer } from 'three';
2 | import { GameState } from '../Types';
3 | import { Store, Action } from 'redux';
4 | import ActionType from '../constants/ActionType';
5 |
6 | export default function addWindowEvents(
7 | camera: PerspectiveCamera,
8 | renderer: WebGLRenderer,
9 | store: Store,
10 | onReset: () => void
11 | ) {
12 | window.addEventListener('resize', () => {
13 | camera.aspect = window.innerWidth / window.innerHeight;
14 | camera.updateProjectionMatrix();
15 | renderer.setSize(window.innerWidth, window.innerHeight);
16 | });
17 |
18 | window.addEventListener('keydown', event => {
19 | const state = store.getState();
20 | switch (event.key) {
21 | case 'ArrowLeft':
22 | if (state.lane > -1) {
23 | store.dispatch({ type: ActionType.SET_LANE, value: state.lane - 1 });
24 | }
25 | break;
26 | case 'ArrowRight':
27 | if (state.lane < 1) {
28 | store.dispatch({ type: ActionType.SET_LANE, value: state.lane + 1 });
29 | }
30 | break;
31 | case ' ':
32 | if (state.defeat) {
33 | onReset();
34 | }
35 | break;
36 | }
37 | });
38 |
39 | window.addEventListener('touchstart', event => {
40 | const planeLane = store.getState().lane;
41 | const touch = event.touches[0];
42 |
43 | if (touch.pageX < window.innerWidth / 2) {
44 | if (planeLane > -1)
45 | store.dispatch({ type: ActionType.SET_LANE, value: planeLane - 1 });
46 | } else {
47 | if (planeLane < 1)
48 | store.dispatch({ type: ActionType.SET_LANE, value: planeLane + 1 });
49 | }
50 | });
51 | }
52 |
--------------------------------------------------------------------------------
/src/functions/createShader.ts:
--------------------------------------------------------------------------------
1 | import {
2 | WebGLRenderer,
3 | WebGLRenderTarget,
4 | Vector2,
5 | Vector3,
6 | OrthographicCamera,
7 | ShaderMaterial,
8 | Mesh,
9 | PlaneBufferGeometry,
10 | Scene,
11 | Uniform,
12 | } from 'three';
13 |
14 | /**
15 | * Creates a function that allows shaders to be easily applied.
16 | * @param renderer
17 | * @param fragmentShader
18 | * @param vertexShader
19 | */
20 | export default function createShader(
21 | renderer: WebGLRenderer,
22 | fragmentShader?: string,
23 | vertexShader?: string,
24 | customUniforms?: { [key: string]: Uniform }
25 | ) {
26 | const camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1);
27 | const scene = new Scene();
28 |
29 | let uniforms = {
30 | iTime: new Uniform(0),
31 | iResolution: new Uniform(new Vector3()),
32 | iTexture: new Uniform(null),
33 | ...customUniforms,
34 | };
35 |
36 | const shaderMaterial: ShaderMaterial = new ShaderMaterial({
37 | uniforms,
38 | fragmentShader,
39 | vertexShader,
40 | });
41 |
42 | const shaderMesh = new Mesh(new PlaneBufferGeometry(2, 2), shaderMaterial);
43 | shaderMesh.frustumCulled = false;
44 | const outputBuffer: WebGLRenderTarget = new WebGLRenderTarget(1, 1);
45 |
46 | scene.add(shaderMesh);
47 |
48 | return (inputBuffer: WebGLRenderTarget, time: number, finalPass = false) => {
49 | uniforms.iResolution.value.set(
50 | renderer.domElement.width,
51 | renderer.domElement.height,
52 | 1
53 | );
54 | uniforms.iTime.value = time * 0.001;
55 | uniforms.iTexture.value = inputBuffer.texture;
56 |
57 | const size = renderer.getDrawingBufferSize(new Vector2());
58 | outputBuffer.setSize(size.width, size.height);
59 | const output = finalPass ? null : outputBuffer;
60 |
61 | renderer.setRenderTarget(output);
62 | renderer.render(scene, camera);
63 |
64 | return output;
65 | };
66 | }
67 |
--------------------------------------------------------------------------------
/src/functions/tick.ts:
--------------------------------------------------------------------------------
1 | import {
2 | PerspectiveCamera,
3 | Scene,
4 | Mesh,
5 | IcosahedronGeometry,
6 | BoxGeometry,
7 | MeshNormalMaterial,
8 | Vector3,
9 | Geometry,
10 | } from 'three';
11 | import { Store, Action } from 'redux';
12 |
13 | import { laneWidth } from '../constants';
14 | import ActionType from '../constants/ActionType';
15 | import { GameState } from '../Types';
16 |
17 | let boxMeshes: Mesh[] = [];
18 | let pickupMeshes: Mesh[] = [];
19 |
20 | let lastX = 0;
21 | let speed = 0.02;
22 |
23 | let spawnCycle = 0;
24 | let spawnMode = 0;
25 |
26 | function spawnGeometry(
27 | geometry: Geometry,
28 | trackingArray: Mesh[],
29 | camera: PerspectiveCamera,
30 | scene: Scene,
31 | lane: number
32 | ) {
33 | const material = new MeshNormalMaterial();
34 | let mesh = new Mesh(geometry, material);
35 | mesh.position.x = Math.floor(camera.position.x / 0.3) * 0.3 + 6;
36 | mesh.position.y = 0;
37 | mesh.position.z = lane * laneWidth;
38 |
39 | trackingArray.push(mesh);
40 |
41 | scene.add(mesh);
42 | }
43 |
44 | function spawnPickup(camera: PerspectiveCamera, scene: Scene, lane: number) {
45 | const geometry = new IcosahedronGeometry(0.1, 0);
46 | spawnGeometry(geometry, pickupMeshes, camera, scene, lane);
47 | }
48 |
49 | function spawnBox(camera: PerspectiveCamera, scene: Scene, lane: number) {
50 | const geometry = new BoxGeometry(0.2, 0.2, 0.2);
51 | spawnGeometry(geometry, boxMeshes, camera, scene, lane);
52 | }
53 |
54 | function spawn(camera: PerspectiveCamera, scene: Scene) {
55 | spawnCycle++;
56 |
57 | switch (spawnMode) {
58 | case 0:
59 | if (spawnCycle < 4) {
60 | spawnBox(camera, scene, 0);
61 | spawnBox(camera, scene, 1);
62 | }
63 | if (spawnCycle == 4) spawnPickup(camera, scene, 0);
64 | break;
65 | case 1:
66 | if (spawnCycle < 4) {
67 | spawnBox(camera, scene, -1);
68 | spawnBox(camera, scene, 1);
69 | }
70 | if (spawnCycle == 4) spawnPickup(camera, scene, 0);
71 | break;
72 | case 2:
73 | if (spawnCycle % 3 != 2)
74 | spawnBox(camera, scene, Math.round(Math.random() * 2) - 1);
75 | break;
76 | case 3:
77 | if (spawnCycle % 3 != 2) spawnBox(camera, scene, (spawnCycle % 2) - 1);
78 | break;
79 | }
80 |
81 | // Get rid of the ones we don't see anyway.
82 | boxMeshes = boxMeshes.filter(mesh => {
83 | if (mesh.position.x < camera.position.x) {
84 | scene.remove(mesh);
85 | return false;
86 | }
87 |
88 | return true;
89 | });
90 |
91 | pickupMeshes = pickupMeshes.filter(mesh => {
92 | if (mesh.position.x < camera.position.x) {
93 | scene.remove(mesh);
94 | return false;
95 | }
96 |
97 | return true;
98 | });
99 |
100 | if (spawnCycle == 5) {
101 | spawnCycle = 0;
102 | spawnMode = Math.round(Math.random() * 3);
103 | }
104 | }
105 |
106 | export function reset(
107 | camera: PerspectiveCamera,
108 | scene: Scene,
109 | planeMesh: Mesh,
110 | gameStateStore: Store
111 | ) {
112 | gameStateStore.dispatch({ type: ActionType.SET_SCORE, value: 0 });
113 | camera.position.x = 0;
114 | planeMesh.position.x = 1;
115 | camera.lookAt(new Vector3(camera.position.x + 1, 1, 0));
116 |
117 | lastX = 0;
118 | speed = 0.02;
119 | spawnCycle = 0;
120 | spawnMode = 0;
121 |
122 | boxMeshes = boxMeshes.filter(mesh => {
123 | scene.remove(mesh);
124 | return false;
125 | });
126 |
127 | pickupMeshes = pickupMeshes.filter(mesh => {
128 | scene.remove(mesh);
129 | return false;
130 | });
131 |
132 | gameStateStore.dispatch({ type: ActionType.SET_DEFEAT, value: false });
133 | }
134 |
135 | export function spawnTick(camera: PerspectiveCamera, scene: Scene) {
136 | const currentX = Math.floor(camera.position.x / 0.3);
137 | if (currentX > lastX) {
138 | if (lastX !== 0) spawn(camera, scene);
139 |
140 | lastX = currentX;
141 | speed += 0.0001;
142 | }
143 | }
144 |
145 | export default function tick(
146 | camera: PerspectiveCamera,
147 | scene: Scene,
148 | planeMesh: Mesh,
149 | gameStateStore: Store,
150 | timeDifference: number
151 | ) {
152 | const state = gameStateStore.getState();
153 | if (state.defeat) return;
154 |
155 | // 16.667 = 60 FPS tick target
156 | camera.position.x += speed * (timeDifference / 16.667);
157 | camera.lookAt(new Vector3(camera.position.x + 1, 1, 0));
158 |
159 | const targetZ = state.lane * laneWidth;
160 | planeMesh.position.x = camera.position.x + 1;
161 |
162 | if (planeMesh.position.z != targetZ) {
163 | if (targetZ > planeMesh.position.z) {
164 | planeMesh.position.z += 0.04;
165 | } else {
166 | planeMesh.position.z -= 0.04;
167 | }
168 |
169 | if (Math.abs(planeMesh.position.z - targetZ) < 0.04) {
170 | planeMesh.position.z = targetZ;
171 | }
172 | }
173 |
174 | for (let pickupMesh of pickupMeshes) {
175 | pickupMesh.rotation.x += 0.01;
176 | pickupMesh.rotation.z -= 0.02;
177 | }
178 |
179 | pickupMeshes = pickupMeshes.filter(mesh => {
180 | if (
181 | Math.abs(planeMesh.position.x - mesh.position.x) < 0.15 &&
182 | Math.abs(planeMesh.position.z - mesh.position.z) < 0.2
183 | ) {
184 | // Dumb collision detection.
185 | gameStateStore.dispatch({ type: ActionType.ADD_MONEY, value: 1 });
186 | scene.remove(mesh);
187 | return false;
188 | } else {
189 | return true;
190 | }
191 | });
192 |
193 | for (let mesh of boxMeshes) {
194 | if (
195 | Math.abs(planeMesh.position.x - mesh.position.x) < 0.15 &&
196 | Math.abs(planeMesh.position.z - mesh.position.z) < 0.2
197 | ) {
198 | gameStateStore.dispatch({ type: ActionType.SET_DEFEAT, value: true });
199 | }
200 | }
201 | }
202 |
--------------------------------------------------------------------------------
/src/img/money.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mat-sz/flight/83dc25110f0167fc6df7f9c7ff0f96eb8ff0934c/src/img/money.png
--------------------------------------------------------------------------------
/src/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.png';
2 | declare module '*.frag';
3 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | PerspectiveCamera,
3 | Scene,
4 | WebGLRenderer,
5 | Mesh,
6 | ConeGeometry,
7 | MeshNormalMaterial,
8 | Vector2,
9 | WebGLRenderTarget,
10 | } from 'three';
11 | import { createStore } from 'redux';
12 |
13 | import './App.scss';
14 |
15 | import gameState from './reducers/gameState';
16 | import addOverlay from './functions/addOverlay';
17 | import addWindowEvents from './functions/addWindowEvents';
18 | import tick, { reset, spawnTick } from './functions/tick';
19 | import createShaders from './shaders';
20 | import ActionType from './constants/ActionType';
21 |
22 | const gameStateStore = createStore(gameState);
23 |
24 | const camera: PerspectiveCamera = new PerspectiveCamera(
25 | 70,
26 | window.innerWidth / window.innerHeight,
27 | 0.01,
28 | 10
29 | );
30 | camera.position.x = 1;
31 | camera.position.z = 0;
32 | camera.position.y = 2;
33 |
34 | const scene: Scene = new Scene();
35 | const renderer: WebGLRenderer = new WebGLRenderer({ antialias: true });
36 |
37 | // The cone.
38 | const material = new MeshNormalMaterial();
39 | const geometry = new ConeGeometry(0.15, 0.3, 8);
40 | const planeMesh = new Mesh(geometry, material);
41 | planeMesh.rotateZ(-Math.PI / 2);
42 | scene.add(planeMesh);
43 |
44 | const shaders = createShaders(renderer);
45 |
46 | // ...and here we start rendering things.
47 | renderer.setSize(window.innerWidth, window.innerHeight);
48 | document.body.appendChild(renderer.domElement);
49 |
50 | const outputBuffer: WebGLRenderTarget = new WebGLRenderTarget(1, 1);
51 | outputBuffer.texture.generateMipmaps = false;
52 |
53 | let timeLast = 0;
54 | render(0);
55 |
56 | function render(time: number) {
57 | const timeDifference = time - timeLast;
58 | timeLast = time;
59 |
60 | tick(camera, scene, planeMesh, gameStateStore, timeDifference);
61 |
62 | const size = renderer.getDrawingBufferSize(new Vector2());
63 | outputBuffer.setSize(size.width, size.height);
64 |
65 | renderer.setRenderTarget(outputBuffer);
66 | renderer.render(scene, camera);
67 |
68 | shaders.reduce(
69 | (buffer, current, i) => current(buffer, time, i === shaders.length - 1),
70 | outputBuffer
71 | );
72 |
73 | requestAnimationFrame(render);
74 | }
75 |
76 | setInterval(() => {
77 | gameStateStore.dispatch({
78 | type: ActionType.SET_SCORE,
79 | value: camera.position.x,
80 | });
81 | }, 250);
82 |
83 | setInterval(() => {
84 | spawnTick(camera, scene);
85 | }, 100);
86 |
87 | const onReset = () => {
88 | reset(camera, scene, planeMesh, gameStateStore);
89 | };
90 |
91 | addWindowEvents(camera, renderer, gameStateStore, onReset);
92 | addOverlay(gameStateStore, onReset);
93 |
--------------------------------------------------------------------------------
/src/reducers/gameState.ts:
--------------------------------------------------------------------------------
1 | import ActionType from '../constants/ActionType';
2 | import { Action, GameState, SavedState } from '../Types';
3 |
4 | let savedState: SavedState = {
5 | highScore: 0,
6 | money: 0,
7 | };
8 |
9 | const savedStateSerialized = localStorage.getItem('savedState');
10 | if (savedStateSerialized) {
11 | try {
12 | savedState = JSON.parse(savedStateSerialized) as SavedState;
13 | } catch {}
14 | }
15 |
16 | const initialState: GameState = {
17 | score: 0,
18 | highScore: savedState.highScore,
19 | defeat: false,
20 | lane: 0,
21 | money: savedState.money,
22 | };
23 |
24 | export default function gameState(state = initialState, action: Action) {
25 | const newState = { ...state };
26 | switch (action.type) {
27 | case ActionType.SET_DEFEAT:
28 | newState.defeat = action.value as boolean;
29 |
30 | savedState.highScore = newState.highScore;
31 | savedState.money = newState.money;
32 |
33 | localStorage.setItem('savedState', JSON.stringify(savedState));
34 | break;
35 | case ActionType.SET_SCORE:
36 | newState.score = action.value as number;
37 | break;
38 | case ActionType.SET_LANE:
39 | newState.lane = action.value as number;
40 | break;
41 | case ActionType.SET_MONEY:
42 | newState.money = action.value as number;
43 | break;
44 | case ActionType.ADD_SCORE:
45 | newState.score += action.value as number;
46 | break;
47 | case ActionType.ADD_MONEY:
48 | newState.money += action.value as number;
49 | break;
50 | default:
51 | return state;
52 | }
53 |
54 | if (newState.score > newState.highScore) {
55 | newState.highScore = newState.score;
56 | }
57 |
58 | return newState;
59 | }
60 |
--------------------------------------------------------------------------------
/src/shaders/fog.frag:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | uniform sampler2D iTexture;
4 | uniform vec3 iResolution;
5 |
6 | vec4 fog(vec2 coord, vec4 screen)
7 | {
8 | float dy = coord.y;
9 | return screen * (0.75 - dy * dy);
10 | }
11 |
12 | void main()
13 | {
14 | vec2 p = gl_FragCoord.xy / iResolution.xy;
15 | gl_FragColor = texture2D(iTexture, p);
16 | gl_FragColor = fog(p, gl_FragColor);
17 | }
--------------------------------------------------------------------------------
/src/shaders/index.ts:
--------------------------------------------------------------------------------
1 | import { WebGLRenderer } from 'three';
2 |
3 | import scanlines from './scanlines.frag';
4 | import vignette from './vignette.frag';
5 | import fog from './fog.frag';
6 |
7 | import createShader from '../functions/createShader';
8 |
9 | export const createShaders = (renderer: WebGLRenderer) => [
10 | createShader(renderer, fog),
11 | createShader(renderer, scanlines),
12 | createShader(renderer, vignette),
13 | ];
14 |
15 | export default createShaders;
16 |
--------------------------------------------------------------------------------
/src/shaders/scanlines.frag:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | uniform sampler2D iTexture;
4 | uniform vec3 iResolution;
5 | uniform float iTime;
6 |
7 | vec2 fisheye(vec2 coord, float str)
8 | {
9 | vec2 neg1to1 = coord;
10 | neg1to1 = (neg1to1 - 0.5) * 2.0;
11 |
12 | vec2 offset;
13 | offset.x = ( pow(neg1to1.y,2.0)) * str * (neg1to1.x);
14 | offset.y = ( pow(neg1to1.x,2.0)) * str * (neg1to1.y);
15 |
16 | return coord + offset;
17 | }
18 |
19 | vec4 scanline(vec2 coord, vec4 screen)
20 | {
21 | const float scale = .0025;
22 | const float amt = 0.01;
23 | const float spd = 1.0;
24 |
25 | screen.rgb += sin((coord.y / scale - (iTime * spd * 6.28))) * amt;
26 | return screen;
27 | }
28 |
29 | void main()
30 | {
31 | vec2 p = gl_FragCoord.xy / iResolution.xy;
32 | p = fisheye(p, 0.1);
33 | gl_FragColor = texture2D(iTexture, p);
34 |
35 | gl_FragColor = scanline(p, gl_FragColor);
36 | gl_FragColor.a = 0.5;
37 | }
--------------------------------------------------------------------------------
/src/shaders/vignette.frag:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | uniform sampler2D iTexture;
4 | uniform vec3 iResolution;
5 |
6 | vec4 vignette(vec2 coord, vec4 screen)
7 | {
8 | float dx = 1.3 * abs(coord.x - .5);
9 | float dy = 1.3 * abs(coord.y - .5);
10 | return screen * (1.0 - dx * dx - dy * dy);
11 | }
12 |
13 | void main()
14 | {
15 | vec2 p = gl_FragCoord.xy / iResolution.xy;
16 | gl_FragColor = texture2D(iTexture, p);
17 | gl_FragColor = vignette(p, gl_FragColor);
18 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": [
4 | "dom",
5 | "dom.iterable",
6 | "esnext"
7 | ],
8 | "outDir": "./dist/",
9 | "noImplicitAny": true,
10 | "module": "es6",
11 | "target": "es6",
12 | "allowJs": true,
13 | "allowSyntheticDefaultImports": true,
14 | "moduleResolution": "node",
15 | "jsx": "react",
16 | },
17 | "include": ["src/index.d.ts"]
18 | }
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | const webpack = require("webpack");
3 | const HtmlWebpackPlugin = require("html-webpack-plugin");
4 |
5 | module.exports = {
6 | entry: "./src/index.ts",
7 | output: {
8 | path: path.join(__dirname, "/dist"),
9 | filename: "index-bundle.js"
10 | },
11 | module: {
12 | rules: [
13 | {
14 | test: /\.tsx?$/,
15 | exclude: /node_modules/,
16 | use: ['ts-loader'],
17 | },
18 | {
19 | test: /\.js$/,
20 | exclude: /node_modules/,
21 | use: ['babel-loader']
22 | },
23 | {
24 | test: /\.css$/i,
25 | use: ['style-loader', 'css-loader']
26 | },
27 | {
28 | test: /\.scss$/i,
29 | use: ['style-loader', 'css-loader', 'sass-loader']
30 | },
31 | {
32 | test: /\.(frag|vert)$/i,
33 | use: ['raw-loader'],
34 | },
35 | {
36 | test: /\.(gif|png|jpe?g|svg)$/i,
37 | use: [
38 | 'file-loader',
39 | {
40 | loader: 'image-webpack-loader',
41 | options: {
42 | bypassOnDebug: true, // webpack@1.x
43 | disable: true, // webpack@2.x and newer
44 | },
45 | },
46 | ],
47 | }
48 | ]
49 | },
50 | resolve: {
51 | extensions: [ '.tsx', '.ts', '.js' ],
52 | },
53 | plugins: [
54 | new HtmlWebpackPlugin({
55 | template: './public/index.html',
56 | }),
57 | new webpack.DefinePlugin({
58 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development')
59 | }),
60 | ]
61 | };
--------------------------------------------------------------------------------