forked from jphamilton/asteroids
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This commit will be automatically published
- Loading branch information
0 parents
commit 8cc602e
Showing
64 changed files
with
6,017 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
.vscode | ||
npm-debug*.* | ||
node_modules | ||
build/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
Oops, something went wrong.