Skip to content

Commit

Permalink
This commit will be automatically published
Browse files Browse the repository at this point in the history
  • Loading branch information
aashutoshrathi authored and Github Pages Overwriter committed Aug 1, 2024
0 parents commit 8cc602e
Show file tree
Hide file tree
Showing 64 changed files with 6,017 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.vscode
npm-debug*.*
node_modules
build/
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@

This is an attempt to recreate the classic arcade game, Asteroids.

[You can give it a try here](https://jphamilton.github.io/asteroids/)

## Controls

* Tap, click, press any key to START
* Rotate: Left/Right arrow keys or A and D, pan left/right to rotate on mobile
* Thrust: Up arrow or W, pan up to thrust on mobile
* Fire: CTRL, tap
* Hyperspace: Space bar, pinch out for hyperspace
* Debug Mode: Z (during game only). Primarily shows collision related information.
* Monitor Burn Effect: toggle on/off with B
* God Mode: G

## Screenshots

![Sceenshot 1](https://raw.githubusercontent.com/jphamilton/asteroids/master/assets/1.png)
![Sceenshot 2](https://raw.githubusercontent.com/jphamilton/asteroids/master/assets/2.png)
![Sceenshot 3](https://raw.githubusercontent.com/jphamilton/asteroids/master/assets/3.png)

## About

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"
(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
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.

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

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).
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.
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
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.
This entire process is visualized in debug mode (hit F1 during game to view).

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.
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
starting this project.
Binary file added assets/1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/Hyperspace.otf
Binary file not shown.
Binary file added assets/explode1.wav
Binary file not shown.
Binary file added assets/explode2.wav
Binary file not shown.
Binary file added assets/explode3.wav
Binary file not shown.
Binary file added assets/fire.wav
Binary file not shown.
Binary file added assets/getpowerup.wav
Binary file not shown.
Binary file added assets/life.wav
Binary file not shown.
Binary file added assets/lsaucer.wav
Binary file not shown.
Binary file added assets/powerup.wav
Binary file not shown.
Binary file added assets/sfire.wav
Binary file not shown.
Binary file added assets/ssaucer.wav
Binary file not shown.
Binary file added assets/thrust.wav
Binary file not shown.
Binary file added assets/thumphi.wav
Binary file not shown.
Binary file added assets/thumplo.wav
Binary file not shown.
28 changes: 28 additions & 0 deletions comets.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {Screen} from './game/screen';

interface IGameState {
update: (step: number, inputs?: VirtualInput) => void;
render: (screen: Screen, dt?: number) => void;
}

type VirtualInput = { [key: string]: boolean };

interface Point {
x: number,
y: number
}

interface Rect extends Point {
width: number;
height: number;
}

interface IQuadtree {
nodes: IQuadtree[];
objects: Rect[];
width2: number;
height2: number;
xmid: number;
ymid: number;
}

49 changes: 49 additions & 0 deletions game/achievement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { HEIGHT, WIDTH } from "./constants";
import { Object2D } from "./object2d";
import { Screen } from "./screen";

export class Achievement extends Object2D {
life: number = 1; // in seconds
fontSize: number;

heightText: number = HEIGHT / 4;
heightScore: number = HEIGHT / 6;

constructor(private text: string, score: number) {
super(WIDTH / 2, HEIGHT / 2);
this.score = score;
//this.velocity = new Vector(0, -1);
}

update(dt: number) {
this.life -= dt;
this.fontSize -= 1;

if (this.life <= 0) {
this.destroy();
}
}

render(screen: Screen) {
this.fontSize = screen.font.xlarge * 2;

screen.draw.text3(this.text, this.fontSize, (width) => {
return {
x: screen.width2 - width / 2,
y: this.heightText,
};
});

screen.draw.text3(`+${this.score}`, this.fontSize, (width) => {
return {
x: screen.width2 - width / 2,
y: this.heightScore,
};
});
}

destroy() {
this.life = 0;
this.trigger("expired");
}
}
159 changes: 159 additions & 0 deletions game/alien.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { Bullet } from "./bullet";
import { HEIGHT, OBJECT_SCALE, WIDTH } from "./constants";
import { Object2D } from "./object2d";
import { Screen } from "./screen";
import { Ship } from "./ship";
import { random } from "./util";
import { Vector } from "./vector";

const BIG_ALIEN_BULLET_SPEED: number = 600 * OBJECT_SCALE;
const SMALL_ALIEN_BULLET_SPEED: number = 800 * OBJECT_SCALE;
const BIG_ALIEN_SPEED: number = 225 * OBJECT_SCALE;
const SMALL_ALIEN_SPEED: number = 250 * OBJECT_SCALE;

export abstract class Alien extends Object2D {
moveTimer: number = 0;
moveTime: number = 1;
bulletTimer: number = 0;
bulletTime: number = 0.7;

abstract fire(): void;
abstract destroy(): void;

constructor(speed) {
super(0, 0);

this.velocity.y = 0;

this.origin.y = random(100, HEIGHT - 100);

if (this.origin.y % 2 === 0) {
this.origin.x = 40;
this.velocity.x = speed;
} else {
this.origin.x = WIDTH - 40;
this.velocity.x = -speed;
}

this.points = [
{ x: 0.5, y: -2 },
{ x: 1, y: -1 },
{ x: 2.5, y: 0 },
{ x: 1, y: 1 },
{ x: -1, y: 1 },
{ x: -2.5, y: 0 },
{ x: -1, y: -1 },
{ x: -0.5, y: -2 },
];
}

update(dt: number) {
this.move(dt);

if (this.origin.x >= WIDTH - 5 || this.origin.x <= 5) {
this.trigger("expired");
return;
}

// direction changes
this.moveTimer += dt;

if (this.moveTimer >= 1 && this.velocity.y !== 0) {
this.velocity.y = 0;
this.moveTimer = 0;
}

if (this.moveTimer >= this.moveTime) {
let move = random(1, 20) % 2 === 0;

if (move) {
this.velocity.y =
this.origin.x % 2 === 0 ? this.velocity.x : -this.velocity.x;
}

this.moveTimer = 0;
}

// firing
this.bulletTimer += dt;

if (this.bulletTimer >= this.bulletTime) {
this.fire();
this.bulletTimer = 0;
}
}

render(screen: Screen) {
this.draw(screen);
}

draw(screen: Screen) {
super.draw(screen);
screen.draw.vectorShape(
[this.points[1], this.points[6]],
this.origin.x,
this.origin.y
);
screen.draw.vectorShape(
[this.points[2], this.points[5]],
this.origin.x,
this.origin.y
);
}
}

// Mr. Bill
export class BigAlien extends Alien {
constructor() {
super(BIG_ALIEN_SPEED);
this.score = 200;
this.scale(7);
}

fire() {
const v = Vector.fromAngle(random(1, 360), BIG_ALIEN_BULLET_SPEED);
const bullet = new Bullet(this.origin, v);
this.trigger("fire", bullet);
}

destroy() {
this.trigger("expired");
}
}

// Sluggo
export class SmallAlien extends Alien {
//score: number = 1000;
bulletTime: number = 1;

constructor(private ship: Ship) {
super(SMALL_ALIEN_SPEED);
this.score = 1000;
this.scale(4);
}

fire() {
let bullet;

if (this.ship) {
// target ship
const v = Vector.fromXY(
this.ship.origin,
this.origin,
SMALL_ALIEN_BULLET_SPEED
);
bullet = new Bullet(this.origin, v, 2);
} else {
// random fire
const v = Vector.fromAngle(random(1, 360), SMALL_ALIEN_BULLET_SPEED);
bullet = new Bullet(this.origin, v, 2);
}

this.trigger("fire", bullet);
}

destroy() {
this.ship = null;
this.trigger("expired");
}
}
62 changes: 62 additions & 0 deletions game/attractMode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { IGameState } from "../comets";
import { fetchMruInfo, startGame } from "../rpc/api";
import { addToStore, getFromStore, StorageKey } from "../rpc/storage";
import { DemoMode } from "./demoMode";
import { EventSource } from "./events";
import { HighScoreMode } from "./highScoreMode";
import { Key } from "./keys";
import { Screen } from "./screen";
import { Sound } from "./sounds";
import { World } from "./world";

const ATTRACT_TIME = 15;

// combines DemoMode and HighscoreMode to attract people to part with their quarters
export class AttractMode extends EventSource implements IGameState {
private currentMode: IGameState;
private modes: IGameState[];
private isStarting = false;

constructor(world: World, lastScore: number) {
super();

this.modes = [
new HighScoreMode(lastScore),
new DemoMode(world || new World()),
];

this.currentMode = this.modes[0];

Sound.stop();
Sound.off();
}

update(step: number) {
this.currentMode.update(step);

if (Key.isEnterPressed()) {
if (!this.isStarting) {
this.isStarting = true;
startGame()
.then((res) => {
console.log("Game started", res.logs[0].value);
addToStore(StorageKey.GAME_ID, res.logs[0].value);
this.isStarting = false;
this.trigger("done");
})
.catch((e) => {
console.error("Error starting game", e.message);
})
.finally(() => {
this.isStarting = false;
// clears the keys to prevent the game from starting again
Key.clear();
});
}
}
}

render(screen: Screen, dt?: number) {
this.currentMode.render(screen, dt);
}
}
Loading

0 comments on commit 8cc602e

Please sign in to comment.