├── src
├── react-app-env.d.ts
├── components
│ ├── index.tsx
│ ├── end-screen.tsx
│ ├── bullet.tsx
│ ├── point-wrapper.tsx
│ ├── ship.tsx
│ └── asteroid.tsx
├── index.tsx
├── game
│ ├── utils.ts
│ ├── bullet.ts
│ ├── asteroid.ts
│ ├── ship.ts
│ ├── moving-object.ts
│ └── index.ts
├── App.css
├── index.css
└── App.tsx
├── .gitattributes
├── public
├── favicon.ico
├── screenshot.png
├── manifest.json
└── index.html
├── README.md
├── tsconfig.json
├── package.json
├── LICENSE
└── .gitignore
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/steveruizok/react-motion-asteroids/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/steveruizok/react-motion-asteroids/HEAD/public/screenshot.png
--------------------------------------------------------------------------------
/src/components/index.tsx:
--------------------------------------------------------------------------------
1 | export { Asteroid } from './asteroid'
2 | export { Ship } from './ship'
3 | export { Bullet } from './bullet'
4 | export { EndScreen } from './end-screen'
5 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import './index.css'
4 | import App from './App'
5 |
6 | ReactDOM.render(, document.getElementById('root'))
7 |
--------------------------------------------------------------------------------
/src/game/utils.ts:
--------------------------------------------------------------------------------
1 | export function getOffsetPoint(x = 0, y = 0, angle = 0, length = 0) {
2 | angle = (angle * Math.PI) / 180 // if you're using degrees instead of radians
3 | return [length * Math.cos(angle) + x, length * Math.sin(angle) + y]
4 | }
5 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css?family=VT323&display=swap');
2 |
3 | .App {
4 | text-align: center;
5 | }
6 |
7 | a {
8 | font-size: 0.7em;
9 | color: #41ff02;
10 | }
11 |
12 | a::hover {
13 | color: #41ff02;
14 | }
15 | a::visited {
16 | color: #41ff02;
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/end-screen.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | export const EndScreen: React.FC<{
4 | onClick: any
5 | }> = ({ onClick }) => {
6 | return (
7 |
8 |
Game Over
9 |
12 |
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-motion-asteroids
2 |
3 | [](https://optimistic-williams-d8811f.netlify.com/)
4 |
5 | 👾 [Play the Demo](https://optimistic-williams-d8811f.netlify.com/)
6 |
7 | 👾 Made with React and [Framer Motion](https://www.framer.com/motion/).
8 |
9 | 👾 Contact [@steveruizok](https://twitter.com/steveruizok)
10 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "noEmit": true,
20 | "jsx": "preserve"
21 | },
22 | "include": [
23 | "src"
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/bullet.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { MotionValue } from 'framer-motion'
3 | import { PointWrapper } from './point-wrapper'
4 |
5 | export const Bullet: React.FC<{
6 | x: MotionValue
7 | y: MotionValue
8 | angle: MotionValue
9 | radius: number
10 | }> = ({ x, y, angle, radius }) => {
11 | return (
12 |
13 |
22 |
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/src/components/point-wrapper.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { motion, MotionValue } from 'framer-motion'
3 |
4 | export const PointWrapper: React.FC<{
5 | x: MotionValue
6 | y: MotionValue
7 | angle: MotionValue
8 | initial?: any
9 | exit?: any
10 | }> = ({ x, y, angle, children, ...rest }) => {
11 | return (
12 |
26 | {children}
27 |
28 | )
29 | }
30 |
--------------------------------------------------------------------------------
/src/game/bullet.ts:
--------------------------------------------------------------------------------
1 | import MovingObject from './moving-object'
2 | import { game } from './index'
3 |
4 | export default class Bullet extends MovingObject {
5 | radius = 2.5
6 | maxVelocity = 12
7 | wraps = false
8 |
9 | constructor(options = {}) {
10 | super(options)
11 | }
12 |
13 | update = () => {
14 | let dead = false
15 | game.asteroids.forEach((asteroid) => {
16 | if (dead) return
17 | if (asteroid.hitTest(this.x.get(), this.y.get())) {
18 | dead = true
19 | game.hits += 1
20 | asteroid.break()
21 | }
22 | })
23 | if (dead) {
24 | this.remove()
25 | }
26 | }
27 |
28 | remove = () => {
29 | game.shots += 1
30 | game.bullets.delete(this)
31 | // game.setBullets(new Set(game.bullets))
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/ship.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { MotionValue } from 'framer-motion'
3 | import { PointWrapper } from './point-wrapper'
4 |
5 | export const Ship: React.FC<{
6 | x: MotionValue
7 | y: MotionValue
8 | angle: MotionValue
9 | radius: number
10 | }> = ({ x, y, angle, radius }) => {
11 | return (
12 |
13 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "asteroids",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@types/jest": "24.0.16",
7 | "@types/lodash": "^4.14.136",
8 | "@types/node": "12.6.9",
9 | "@types/react": "16.8.24",
10 | "@types/react-dom": "16.8.5",
11 | "framer-motion": "^1.5.0",
12 | "lodash": "^4.17.15",
13 | "mobx": "^5.13.0",
14 | "mobx-react-lite": "^1.4.1",
15 | "react": "^16.8.6",
16 | "react-dom": "^16.8.6",
17 | "react-scripts": "3.0.1",
18 | "typescript": "3.5.3"
19 | },
20 | "scripts": {
21 | "start": "react-scripts start",
22 | "build": "react-scripts build",
23 | "test": "react-scripts test",
24 | "eject": "react-scripts eject"
25 | },
26 | "eslintConfig": {
27 | "extends": "react-app"
28 | },
29 | "browserslist": {
30 | "production": [
31 | ">0.2%",
32 | "not dead",
33 | "not op_mini all"
34 | ],
35 | "development": [
36 | "last 1 chrome version",
37 | "last 1 firefox version",
38 | "last 1 safari version"
39 | ]
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Steve
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 |
--------------------------------------------------------------------------------
/src/game/asteroid.ts:
--------------------------------------------------------------------------------
1 | import MovingObject from './moving-object'
2 | import { defaults, random } from 'lodash'
3 | import { game } from './index'
4 |
5 | export default class Asteroid extends MovingObject {
6 | constructor(options = {}) {
7 | super(
8 | defaults(options, {
9 | velocity: random(0.2, 0.5),
10 | angle: random(360),
11 | radius: random(32, 114),
12 | })
13 | )
14 | }
15 |
16 | break = () => {
17 | this.remove()
18 | if (this.radius > 32) {
19 | const r = random(0.25, 0.75)
20 | game.asteroids.add(this.getChunk(r))
21 | game.asteroids.add(this.getChunk(1 - r))
22 | // game.setAsteroids(new Set(game.asteroids))
23 | }
24 | }
25 |
26 | getChunk = (size = 0.5) => {
27 | const { radius, velocity } = this
28 | const [x, y] = this.getOffsetPoint(
29 | random(this.radius / 4, this.radius / 2),
30 | Math.random() * 360
31 | )
32 | return new Asteroid({
33 | x,
34 | y,
35 | radius: radius * size,
36 | velocity: velocity * 1.5,
37 | })
38 | }
39 |
40 | update = () => {}
41 |
42 | remove = () => {
43 | game.asteroids.delete(this)
44 | // game.setAsteroids(new Set(game.asteroids))
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/game/ship.ts:
--------------------------------------------------------------------------------
1 | import MovingObject from './moving-object'
2 | import Bullet from './bullet'
3 | import { game } from './index'
4 |
5 | export default class Ship extends MovingObject {
6 | dead = false
7 | friction = 0.08
8 | maxVelocity = 6
9 |
10 | start = () => {
11 | game.inputs.set('w', this.accelerate)
12 | game.inputs.set('a', () => this.turn('left'))
13 | game.inputs.set('d', () => this.turn('right'))
14 | game.presses.set(' ', this.shoot)
15 | }
16 |
17 | update = () => {
18 | if (this.dead) return
19 | game.asteroids.forEach((asteroid) => {
20 | if (this.dead) return
21 | if (asteroid.hitTest(this.x.get(), this.y.get())) {
22 | this.dead = true
23 | asteroid.break()
24 | }
25 | })
26 |
27 | if (this.dead) {
28 | game.handlePlayerDeath()
29 | }
30 | }
31 |
32 | shoot = () => {
33 | const { angle, velocity, dead } = this
34 | if (dead) return
35 |
36 | const [x, y] = this.getOffsetPoint(this.radius + 3)
37 |
38 | game.bullets.add(
39 | new Bullet({
40 | x,
41 | y,
42 | angle: angle.get(),
43 | velocity: velocity + 10,
44 | })
45 | )
46 |
47 | // game.setBullets(new Set(game.bullets))
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/components/asteroid.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { MotionValue } from 'framer-motion'
3 | import { PointWrapper } from './point-wrapper'
4 | import { range, random } from 'lodash'
5 |
6 | export const Asteroid: React.FC<{
7 | x: MotionValue
8 | y: MotionValue
9 | angle: MotionValue
10 | radius: number
11 | }> = ({ x, y, angle, radius = 100 }) => {
12 | const path = React.useMemo(() => {
13 | const num = 3 + Math.floor(radius / 3)
14 | const gap = 360 / num
15 |
16 | const points = range(num).map((i) => {
17 | const offset = random(0.82, 1) * radius
18 | const angle = (i * gap * Math.PI) / 180 // degress -> radians
19 | return [
20 | offset * Math.cos(angle) + radius,
21 | offset * Math.sin(angle) + radius,
22 | ]
23 | })
24 |
25 | const [ox, oy] = points[0]
26 |
27 | return `M ${ox},${oy} L ${points
28 | .slice(1)
29 | .map((p) => p.join(','))
30 | .join(' ')} Z`
31 | }, [radius])
32 |
33 | return (
34 |
35 |
45 |
46 | )
47 | }
48 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Optional REPL history
57 | .node_repl_history
58 |
59 | # Output of 'npm pack'
60 | *.tgz
61 |
62 | # Yarn Integrity file
63 | .yarn-integrity
64 |
65 | # dotenv environment variables file
66 | .env
67 | .env.test
68 |
69 | # parcel-bundler cache (https://parceljs.org/)
70 | .cache
71 |
72 | # next.js build output
73 | .next
74 |
75 | # nuxt.js build output
76 | .nuxt
77 |
78 | # vuepress build output
79 | .vuepress/dist
80 |
81 | # Serverless directories
82 | .serverless/
83 |
84 | # FuseBox cache
85 | .fusebox/
86 |
87 | # DynamoDB Local files
88 | .dynamodb/
89 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
22 | React App
23 |
24 |
25 |
26 |
27 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { motion } from 'framer-motion'
3 | import { range, random } from 'lodash'
4 | import { observer } from 'mobx-react-lite'
5 |
6 | import { screen, game } from './game/index'
7 | import { Ship, Asteroid, Bullet, EndScreen } from './components/index'
8 | import './App.css'
9 |
10 | const App: React.FC = observer(() => {
11 | // Observables
12 | const { asteroids, bullets, lives, accuracy } = game
13 |
14 | // Start Game
15 | React.useEffect(() => {
16 | game.start()
17 | }, [])
18 |
19 | // Generate stars on each life change
20 | const stars = React.useMemo(() => {
21 | return range(100 - lives).map((i) => (
22 |
32 | ))
33 | }, [lives])
34 |
35 | return (
36 |
51 |
60 | {stars}
61 | {// ship
62 | lives > 0 && (
63 |
69 | )}
70 | {// bullets
71 | Array.from(bullets).map(({ id, x, y, angle, radius }, i) => (
72 |
73 | ))}
74 | {// asteroids
75 | Array.from(asteroids).map(({ id, x, y, angle, radius }, i) => (
76 |
77 | ))}
78 |
79 | {accuracy ? (accuracy * 100).toFixed() + '%' : '100%'} |{' '}
80 | {lives <= 0 ? : lives}
81 |
82 |
83 |
84 | W thrust | A left | D right | SPACE shoot
85 |
86 |
89 |
90 | )
91 | })
92 |
93 | export default App
94 |
--------------------------------------------------------------------------------
/src/game/moving-object.ts:
--------------------------------------------------------------------------------
1 | import { motionValue, MotionValue } from 'framer-motion'
2 | import { getOffsetPoint } from './utils'
3 | import { screen } from './index'
4 | import { uniqueId } from 'lodash'
5 |
6 | export interface MovingObjectOptions {
7 | x?: number
8 | y?: number
9 | velocity?: number
10 | angle?: number
11 | radius?: number
12 | wraps?: boolean
13 | }
14 |
15 | export default class MovingObject {
16 | id = uniqueId()
17 | dead = false
18 | x: MotionValue
19 | y: MotionValue
20 | angle: MotionValue
21 | velocity: number
22 | radius: number
23 | wraps = true
24 | maxVelocity = 10
25 | friction = 0
26 |
27 | constructor(options = {} as MovingObjectOptions) {
28 | const {
29 | x = 0,
30 | y = 0,
31 | angle = 0,
32 | velocity = 0,
33 | radius = 12,
34 | wraps = true,
35 | } = options
36 |
37 | this.x = motionValue(x)
38 | this.y = motionValue(y)
39 | this.angle = motionValue(angle)
40 | this.radius = radius
41 | this.velocity = velocity
42 | this.wraps = wraps
43 | }
44 |
45 | wrap = (x: number, y: number) => {
46 | const { radius: r } = this
47 | const { width, height } = screen
48 |
49 | if (x - r > width) {
50 | x = -r
51 | }
52 | if (x + r < 0) {
53 | x = width + r
54 | }
55 | if (y - r > height) {
56 | y = -r
57 | }
58 | if (y + r < 0) {
59 | y = height + r
60 | }
61 |
62 | return [x, y]
63 | }
64 |
65 | offScreen = (x: number, y: number) => {
66 | const { radius: r } = this
67 | const { width, height } = screen
68 | return x - r > width || x + r < 0 || y - r > height || y + r < 0
69 | }
70 |
71 | getOffsetPoint = (distance: number, angle = this.angle.get()) => {
72 | const { x, y } = this
73 | return getOffsetPoint(x.get(), y.get(), angle, distance)
74 | }
75 |
76 | hitTest = (x: number, y: number) => {
77 | return Math.hypot(this.y.get() - y, this.x.get() - x) < this.radius
78 | }
79 |
80 | turn = (direction: 'left' | 'right') => {
81 | if (direction === 'left') {
82 | this.angle.set(this.angle.get() - 4)
83 | }
84 | if (direction === 'right') {
85 | this.angle.set(this.angle.get() + 4)
86 | }
87 | }
88 |
89 | accelerate = () => {
90 | if (this.velocity < this.maxVelocity) {
91 | this.velocity += 1
92 | }
93 | }
94 |
95 | move = () => {
96 | const { wrap, friction, wraps, x, y, velocity, maxVelocity } = this
97 | let [nx, ny] = this.getOffsetPoint(velocity)
98 |
99 | if (velocity > maxVelocity) {
100 | this.velocity *= 0.98
101 | }
102 |
103 | if (friction) {
104 | this.velocity *= 1 - friction
105 | }
106 |
107 | if (wraps) {
108 | ;[nx, ny] = wrap(nx, ny)
109 | } else {
110 | if (this.offScreen(nx, ny)) {
111 | this.remove()
112 | }
113 | }
114 |
115 | x.set(nx)
116 | y.set(ny)
117 | }
118 |
119 | start = () => {}
120 | update = () => {}
121 | remove = () => {}
122 |
123 | render = () => {
124 | if (this.dead) return
125 | this.move()
126 | this.update()
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/src/game/index.ts:
--------------------------------------------------------------------------------
1 | import { range, random } from 'lodash'
2 | import { decorate, computed, observable } from 'mobx'
3 |
4 | import Ship from './ship'
5 | import Bullet from './bullet'
6 | import Asteroid from './asteroid'
7 |
8 | export const screen = {
9 | width: 600,
10 | height: 400,
11 | }
12 |
13 | export class Game {
14 | constructor() {
15 | window.addEventListener('keypress', this.handleKeyPress)
16 | window.addEventListener('keydown', this.handleKeyDown)
17 | window.addEventListener('keyup', this.handleKeyUp)
18 |
19 | this.ship = new Ship({
20 | x: screen.width / 2,
21 | y: screen.height / 2,
22 | })
23 | this.bullets = new Set([])
24 | this.asteroids = new Set(
25 | range(4).map((i) => {
26 | const [x, y] = this.ship.getOffsetPoint(
27 | random(128, 250, false),
28 | random(360)
29 | )
30 | return new Asteroid({ x, y })
31 | })
32 | )
33 | }
34 |
35 | /* ------------------------------- User Input ------------------------------- */
36 |
37 | presses = new Map void>()
38 | inputs = new Map void>()
39 | keyBuffer = new Set([])
40 |
41 | handleKeyPress = (event: KeyboardEvent) => {
42 | const action = this.presses.get(event.key)
43 | if (action) action()
44 | }
45 |
46 | handleKeyDown = (event: KeyboardEvent) => {
47 | this.keyBuffer.add(event.key)
48 | }
49 |
50 | handleKeyUp = (event: KeyboardEvent) => {
51 | this.keyBuffer.delete(event.key)
52 | }
53 |
54 | handleKey = (key: string) => {
55 | const action = this.inputs.get(key)
56 | if (action) action()
57 | }
58 |
59 | /* ------------------------------- Observables ------------------------------ */
60 |
61 | lives = 3
62 |
63 | shots = 0
64 |
65 | hits = 0
66 |
67 | ship: Ship
68 |
69 | bullets: Set
70 |
71 | asteroids: Set
72 |
73 | /* ------------------------------- Game Events ------------------------------ */
74 |
75 | start = () => {
76 | this.ship.start()
77 | this.bullets.forEach((asteroid) => asteroid.start())
78 | this.asteroids.forEach((asteroid) => asteroid.start())
79 |
80 | this.loop()
81 | }
82 |
83 | handlePlayerDeath = () => {
84 | this.lives -= 1
85 | if (this.lives >= 0) {
86 | this.resetLevel()
87 | }
88 | }
89 |
90 | restart = () => {
91 | this.lives = 3
92 | this.resetLevel()
93 | }
94 |
95 | resetLevel = () => {
96 | this.bullets = new Set([])
97 | this.asteroids = new Set(
98 | range(4).map((i) => {
99 | const [x, y] = this.ship.getOffsetPoint(
100 | random(128, 250, false),
101 | random(360)
102 | )
103 | return new Asteroid({ x, y })
104 | })
105 | )
106 |
107 | this.ship.dead = false
108 | this.ship.x.set(screen.width / 2)
109 | this.ship.y.set(screen.height / 2)
110 | this.ship.velocity = 0
111 | }
112 |
113 | get accuracy() {
114 | return this.hits / this.shots
115 | }
116 |
117 | /* ------------------------------- Main loop ------------------------------ */
118 |
119 | loop = () => {
120 | this.keyBuffer.forEach(this.handleKey)
121 |
122 | this.ship.render()
123 | this.bullets.forEach((asteroid) => asteroid.render())
124 | this.asteroids.forEach((asteroid) => asteroid.render())
125 |
126 | window.requestAnimationFrame(this.loop)
127 |
128 | if (this.asteroids.size <= 0) {
129 | this.resetLevel()
130 | }
131 | }
132 | }
133 |
134 | // Add observables
135 | decorate(Game, {
136 | asteroids: observable,
137 | bullets: observable,
138 | lives: observable,
139 | hits: observable,
140 | shots: observable,
141 | accuracy: computed,
142 | })
143 |
144 | // Create game
145 | export const game = new Game()
146 |
--------------------------------------------------------------------------------