Skip to content

Commit 2ce05d3

Browse files
eshaan7Github Pages Overwriter
authored and
Github Pages Overwriter
committed
This commit will be automatically published
0 parents  commit 2ce05d3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

64 files changed

+6064
-0
lines changed

.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.vscode
2+
npm-debug*.*
3+
node_modules
4+
build/

README.md

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
2+
This is an attempt to recreate the classic arcade game, Asteroids.
3+
4+
[You can give it a try here](https://jphamilton.github.io/asteroids/)
5+
6+
## Controls
7+
8+
* Tap, click, press any key to START
9+
* Rotate: Left/Right arrow keys or A and D, pan left/right to rotate on mobile
10+
* Thrust: Up arrow or W, pan up to thrust on mobile
11+
* Fire: CTRL, tap
12+
* Hyperspace: Space bar, pinch out for hyperspace
13+
* Debug Mode: Z (during game only). Primarily shows collision related information.
14+
* Monitor Burn Effect: toggle on/off with B
15+
* God Mode: G
16+
17+
## Screenshots
18+
19+
![Sceenshot 1](https://raw.githubusercontent.com/jphamilton/asteroids/master/assets/1.png)
20+
![Sceenshot 2](https://raw.githubusercontent.com/jphamilton/asteroids/master/assets/2.png)
21+
![Sceenshot 3](https://raw.githubusercontent.com/jphamilton/asteroids/master/assets/3.png)
22+
23+
## About
24+
25+
This is my "re-imagining" of the classic arcade game, Asteroids. I tried to stay true to the spirit of the original as much as possible, while adding a more modern "game feel"
26+
(e.g. camera shake, kick back, bigger explosions, faster movement, etc). While the original was designed to encourage you to part with your quarters, this version is designed for you to
27+
blow lots of stuff up. It's a little more forgiving, too. Your ship has a little shielding that can protect you from damage for a bit and you can fire more bullets.
28+
29+
Like the arcade upright, the game will cycle between the highscore screen and 'attraction mode' every 15 seconds. Attraction mode will continue to play itself out until a new game is started.
30+
After a game is completed, attraction mode will continue, using the state of the last game. High scores are tracked and saved in local storage.
31+
32+
Collision detection occurs in 3 stages. First, a quadtree is used to determine potential collision candidates. Second, candidates are checked using Axis-Aligned Bounding Boxes (AABB).
33+
Finally, if the ship is involved, a point in polygon routine (credit Lascha Lagidse http://alienryderflex.com/polygon/) is used to determine if an actual collision has taken place.
34+
Using point in poly for all collisions didn't "feel right". Bullet collisions (from the ship) also the Cohen-Sutherland line clipping algorithm - the result being that rocks in between 2 bullets
35+
will be destroyed. This vastly speeds up the game and gives the player the illusion that they are actually better than they are :) Basically, everything in the game is favored to the player.
36+
This entire process is visualized in debug mode (hit F1 during game to view).
37+
38+
I only used two libraries for the game: [howler.js](https://howlerjs.com/) for sound and [hammer.js](http://hammerjs.github.io/) for touch support. I have to say, both of the libraries are amazing.
39+
I can't believe how quickly I was able to put them to use. As for the rest of the game, it was important for me to do all the dirty work myself. I had almost zero exposure to the HTML 5 canvas before
40+
starting this project.

assets/1.png

206 KB
Loading

assets/2.png

86.4 KB
Loading

assets/3.png

75.4 KB
Loading

assets/Hyperspace.otf

21.4 KB
Binary file not shown.

assets/explode1.wav

85.9 KB
Binary file not shown.

assets/explode2.wav

84.7 KB
Binary file not shown.

assets/explode3.wav

84.8 KB
Binary file not shown.

assets/fire.wav

24.2 KB
Binary file not shown.

assets/getpowerup.wav

41.1 KB
Binary file not shown.

assets/life.wav

117 KB
Binary file not shown.

assets/lsaucer.wav

15.1 KB
Binary file not shown.

assets/powerup.wav

185 KB
Binary file not shown.

assets/sfire.wav

26.5 KB
Binary file not shown.

assets/ssaucer.wav

10.5 KB
Binary file not shown.

assets/thrust.wav

7.99 KB
Binary file not shown.

assets/thumphi.wav

10.4 KB
Binary file not shown.

assets/thumplo.wav

8.8 KB
Binary file not shown.

comets.d.ts

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import {Screen} from './game/screen';
2+
3+
interface IGameState {
4+
update: (step: number, inputs?: VirtualInput) => void;
5+
render: (screen: Screen, dt?: number) => void;
6+
}
7+
8+
type VirtualInput = { [key: string]: boolean };
9+
10+
interface Point {
11+
x: number,
12+
y: number
13+
}
14+
15+
interface Rect extends Point {
16+
width: number;
17+
height: number;
18+
}
19+
20+
interface IQuadtree {
21+
nodes: IQuadtree[];
22+
objects: Rect[];
23+
width2: number;
24+
height2: number;
25+
xmid: number;
26+
ymid: number;
27+
}
28+

game/achievement.ts

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { HEIGHT, WIDTH } from "./constants";
2+
import { Object2D } from "./object2d";
3+
import { Screen } from "./screen";
4+
5+
export class Achievement extends Object2D {
6+
life: number = 1; // in seconds
7+
fontSize: number;
8+
9+
heightText: number = HEIGHT / 4;
10+
heightScore: number = HEIGHT / 6;
11+
12+
constructor(private text: string, score: number) {
13+
super(WIDTH / 2, HEIGHT / 2);
14+
this.score = score;
15+
//this.velocity = new Vector(0, -1);
16+
}
17+
18+
update(dt: number) {
19+
this.life -= dt;
20+
this.fontSize -= 1;
21+
22+
if (this.life <= 0) {
23+
this.destroy();
24+
}
25+
}
26+
27+
render(screen: Screen) {
28+
this.fontSize = screen.font.xlarge * 2;
29+
30+
screen.draw.text3(this.text, this.fontSize, (width) => {
31+
return {
32+
x: screen.width2 - width / 2,
33+
y: this.heightText,
34+
};
35+
});
36+
37+
screen.draw.text3(`+${this.score}`, this.fontSize, (width) => {
38+
return {
39+
x: screen.width2 - width / 2,
40+
y: this.heightScore,
41+
};
42+
});
43+
}
44+
45+
destroy() {
46+
this.life = 0;
47+
this.trigger("expired");
48+
}
49+
}

game/alien.ts

+159
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import { Bullet } from "./bullet";
2+
import { HEIGHT, OBJECT_SCALE, WIDTH } from "./constants";
3+
import { Object2D } from "./object2d";
4+
import { Screen } from "./screen";
5+
import { Ship } from "./ship";
6+
import { random } from "./util";
7+
import { Vector } from "./vector";
8+
9+
const BIG_ALIEN_BULLET_SPEED: number = 600 * OBJECT_SCALE;
10+
const SMALL_ALIEN_BULLET_SPEED: number = 800 * OBJECT_SCALE;
11+
const BIG_ALIEN_SPEED: number = 225 * OBJECT_SCALE;
12+
const SMALL_ALIEN_SPEED: number = 250 * OBJECT_SCALE;
13+
14+
export abstract class Alien extends Object2D {
15+
moveTimer: number = 0;
16+
moveTime: number = 1;
17+
bulletTimer: number = 0;
18+
bulletTime: number = 0.7;
19+
20+
abstract fire(): void;
21+
abstract destroy(): void;
22+
23+
constructor(speed) {
24+
super(0, 0);
25+
26+
this.velocity.y = 0;
27+
28+
this.origin.y = random(100, HEIGHT - 100);
29+
30+
if (this.origin.y % 2 === 0) {
31+
this.origin.x = 40;
32+
this.velocity.x = speed;
33+
} else {
34+
this.origin.x = WIDTH - 40;
35+
this.velocity.x = -speed;
36+
}
37+
38+
this.points = [
39+
{ x: 0.5, y: -2 },
40+
{ x: 1, y: -1 },
41+
{ x: 2.5, y: 0 },
42+
{ x: 1, y: 1 },
43+
{ x: -1, y: 1 },
44+
{ x: -2.5, y: 0 },
45+
{ x: -1, y: -1 },
46+
{ x: -0.5, y: -2 },
47+
];
48+
}
49+
50+
update(dt: number) {
51+
this.move(dt);
52+
53+
if (this.origin.x >= WIDTH - 5 || this.origin.x <= 5) {
54+
this.trigger("expired");
55+
return;
56+
}
57+
58+
// direction changes
59+
this.moveTimer += dt;
60+
61+
if (this.moveTimer >= 1 && this.velocity.y !== 0) {
62+
this.velocity.y = 0;
63+
this.moveTimer = 0;
64+
}
65+
66+
if (this.moveTimer >= this.moveTime) {
67+
let move = random(1, 20) % 2 === 0;
68+
69+
if (move) {
70+
this.velocity.y =
71+
this.origin.x % 2 === 0 ? this.velocity.x : -this.velocity.x;
72+
}
73+
74+
this.moveTimer = 0;
75+
}
76+
77+
// firing
78+
this.bulletTimer += dt;
79+
80+
if (this.bulletTimer >= this.bulletTime) {
81+
this.fire();
82+
this.bulletTimer = 0;
83+
}
84+
}
85+
86+
render(screen: Screen) {
87+
this.draw(screen);
88+
}
89+
90+
draw(screen: Screen) {
91+
super.draw(screen);
92+
screen.draw.vectorShape(
93+
[this.points[1], this.points[6]],
94+
this.origin.x,
95+
this.origin.y
96+
);
97+
screen.draw.vectorShape(
98+
[this.points[2], this.points[5]],
99+
this.origin.x,
100+
this.origin.y
101+
);
102+
}
103+
}
104+
105+
// Mr. Bill
106+
export class BigAlien extends Alien {
107+
constructor() {
108+
super(BIG_ALIEN_SPEED);
109+
this.score = 200;
110+
this.scale(7);
111+
}
112+
113+
fire() {
114+
const v = Vector.fromAngle(random(1, 360), BIG_ALIEN_BULLET_SPEED);
115+
const bullet = new Bullet(this.origin, v);
116+
this.trigger("fire", bullet);
117+
}
118+
119+
destroy() {
120+
this.trigger("expired");
121+
}
122+
}
123+
124+
// Sluggo
125+
export class SmallAlien extends Alien {
126+
//score: number = 1000;
127+
bulletTime: number = 1;
128+
129+
constructor(private ship: Ship) {
130+
super(SMALL_ALIEN_SPEED);
131+
this.score = 1000;
132+
this.scale(4);
133+
}
134+
135+
fire() {
136+
let bullet;
137+
138+
if (this.ship) {
139+
// target ship
140+
const v = Vector.fromXY(
141+
this.ship.origin,
142+
this.origin,
143+
SMALL_ALIEN_BULLET_SPEED
144+
);
145+
bullet = new Bullet(this.origin, v, 2);
146+
} else {
147+
// random fire
148+
const v = Vector.fromAngle(random(1, 360), SMALL_ALIEN_BULLET_SPEED);
149+
bullet = new Bullet(this.origin, v, 2);
150+
}
151+
152+
this.trigger("fire", bullet);
153+
}
154+
155+
destroy() {
156+
this.ship = null;
157+
this.trigger("expired");
158+
}
159+
}

game/attractMode.ts

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { IGameState } from "../comets";
2+
import { fetchMruInfo, startGame } from "../rpc/api";
3+
import { addToStore, getFromStore, StorageKey } from "../rpc/storage";
4+
import { DemoMode } from "./demoMode";
5+
import { EventSource } from "./events";
6+
import { HighScoreMode } from "./highScoreMode";
7+
import { Key } from "./keys";
8+
import { Screen } from "./screen";
9+
import { Sound } from "./sounds";
10+
import { World } from "./world";
11+
12+
const ATTRACT_TIME = 15;
13+
14+
// combines DemoMode and HighscoreMode to attract people to part with their quarters
15+
export class AttractMode extends EventSource implements IGameState {
16+
private currentMode: IGameState;
17+
private modes: IGameState[];
18+
private isStarting = false;
19+
20+
constructor(world: World, lastScore: number) {
21+
super();
22+
23+
this.modes = [
24+
new HighScoreMode(lastScore),
25+
new DemoMode(world || new World()),
26+
];
27+
28+
this.currentMode = this.modes[0];
29+
30+
Sound.stop();
31+
Sound.off();
32+
}
33+
34+
update(step: number) {
35+
this.currentMode.update(step);
36+
37+
if (Key.isEnterPressed()) {
38+
if (!this.isStarting) {
39+
this.isStarting = true;
40+
startGame()
41+
.then((res) => {
42+
console.log("Game started", res.logs[0].value);
43+
addToStore(StorageKey.GAME_ID, res.logs[0].value);
44+
this.isStarting = false;
45+
this.trigger("done");
46+
})
47+
.catch((e) => {
48+
console.error("Error starting game", e.message);
49+
})
50+
.finally(() => {
51+
this.isStarting = false;
52+
// clears the keys to prevent the game from starting again
53+
Key.clear();
54+
});
55+
}
56+
}
57+
}
58+
59+
render(screen: Screen, dt?: number) {
60+
this.currentMode.render(screen, dt);
61+
}
62+
}

0 commit comments

Comments
 (0)