///
///
///
///
'use strict';
import Vector3 = THREE.Vector3;
import Face3 = THREE.Face3;
import Material = THREE.Material;
import Geometry = THREE.Geometry;
import CanvasRenderer = THREE.CanvasRenderer;
import WebGLRenderer = THREE.WebGLRenderer;
//wtf fix..
Physijs.scripts.worker = "physi_js/physijs_worker.js";
Physijs.scripts.ammo = "ammo.js";
class PointerLock {
hasLock:boolean = false;
constructor(private game:Game, private blocker:HTMLElement, private instructions:HTMLElement, private overlay:HTMLElement) {
}
gain() {
let havePointerLock = 'pointerLockElement' in document || 'mozPointerLockElement' in document || 'webkitPointerLockElement' in document;
if (!havePointerLock) {
return;
}
document.addEventListener('pointerlockchange', this.onChange, false);
document.addEventListener('mozpointerlockchange', this.onChange, false);
document.addEventListener('webkitpointerlockchange', this.onChange, false);
document.addEventListener('pointerlockerror', this.onError, false);
document.addEventListener('mozpointerlockerror', this.onError, false);
document.addEventListener('webkitpointerlockerror', this.onError, false);
this.blocker.addEventListener("click", this.onClick, false)
}
onChange = (event) => {
let element = document.body;
let doc:any = document;
if (doc.pointerLockElement === element || doc.mozPointerLockElement === element || doc.webkitPointerLockElement === element) {
//gained
this.hasLock = true;
this.blocker.style.display = "none";
if (this.game.state == GameState.INITIALIZED || this.game.state == GameState.PAUSED) {
this.game.start();
}
} else {
//lost
this.hasLock = false;
this.blocker.style.display = '-webkit-box';
this.blocker.style.display = '-moz-box';
this.blocker.style.display = 'box';
this.instructions.style.display = "";
if (this.game.state == GameState.STARTED) {
this.game.pause();
}
}
};
onError = (event) => {
this.instructions.style.display = "";
};
onClick = (event) => {
let element:any = document.body;
element.requestPointerLock = element.requestPointerLock || element.mozRequestPointerLock || element.webkitRequestPointerLock;
this.instructions.style.display = "none";
element.requestPointerLock();
};
}
class Keyboard {
/*
Credit to: https://github.com/stemkoski
*/
static k = {
8: "backspace", 9: "tab", 13: "enter", 16: "shift",
17: "ctrl", 18: "alt", 27: "esc", 32: "space",
33: "pageup", 34: "pagedown", 35: "end", 36: "home",
37: "left", 38: "up", 39: "right", 40: "down",
45: "insert", 46: "delete", 186: ";", 187: "=",
188: ",", 189: "-", 190: ".", 191: "/",
219: "[", 220: "\\", 221: "]", 222: "'"
};
private status = {};
constructor() {
}
update():void {
for (var key in this.status) {
// insure that every keypress has "down" status exactly once
if (!this.status[key].updatedPreviously) {
this.status[key].down = true;
this.status[key].pressed = true;
this.status[key].updatedPreviously = true;
}
else // updated previously
{
this.status[key].down = false;
}
// key has been flagged as "up" since last update
if (this.status[key].up) {
delete this.status[key];
continue; // move on to next key
}
if (!this.status[key].pressed) // key released
this.status[key].up = true;
}
}
onKeyDown = (event:KeyboardEvent) => {
var key = Keyboard.keyName(event.keyCode);
if (!this.status[key])
this.status[key] = {down: false, pressed: false, up: false, updatedPreviously: false};
};
onKeyUp = (event:KeyboardEvent) => {
var key = Keyboard.keyName(event.keyCode);
if (this.status[key])
this.status[key].pressed = false;
};
down(key):boolean {
return (this.status[key] && this.status[key].down);
}
pressed(key):boolean {
return (this.status[key] && this.status[key].pressed);
}
up(key):boolean {
return (this.status[key] && this.status[key].up);
}
register():void {
document.addEventListener("keydown", this.onKeyDown, false);
document.addEventListener("keyup", this.onKeyUp, false);
}
unregister():void {
document.removeEventListener("keydown", this.onKeyDown, false);
document.removeEventListener("keyup", this.onKeyUp, false);
}
static keyName(keyCode) {
return ( Keyboard.k[keyCode] != null ) ?
Keyboard.k[keyCode] :
String.fromCharCode(keyCode);
}
}
/**
*
*/
class Mouse {
x:number;
y:number;
xMovement:number = 0;
yMovement:number = 0;
private buttons = {};
constructor(private player:Player) {
}
onMouseMove = (event:MouseEvent) => {
this.x = event.screenX;
this.xMovement = event.movementX;
this.y = event.screenY;
this.yMovement = event.movementY;
this.player.rotate(event.movementX);
this.player.look(event.movementY);
};
onMouseDown = (event:MouseEvent) => {
this.buttons[event.button] = true;
this.player.click(event.button);
};
onMouseUp = (event:MouseEvent) => {
this.buttons[event.button] = false;
};
pressed(button:number):boolean {
return this.buttons[button];
}
register() {
document.addEventListener("mousemove", this.onMouseMove, false);
document.addEventListener("mousedown", this.onMouseDown, false);
document.addEventListener("mouseup", this.onMouseUp, false);
}
unregister() {
document.removeEventListener("mousemove", this.onMouseMove, false);
document.removeEventListener("mousedown", this.onMouseDown, false);
document.removeEventListener("mouseup", this.onMouseUp, false);
}
}
class Poly extends Physijs.PlaneMesh {
static red:Physijs.Material = Physijs.createMaterial(new THREE.MeshBasicMaterial({
color: 0xa01000
}),
1,
1
);
static blue:Physijs.Material = Physijs.createMaterial(new THREE.MeshBasicMaterial({
color: 0x00a0b0
}),
1,
1
);
constructor(pos:Vector3, public polarity:number) {
super(Poly.generateGeometry(), polarity > 0 ? Poly.blue : Poly.red, 0.3);
this.addEventListener("ready", () => this.init());
this.position.copy(pos);
}
init():void {
//launch the poly into space
this.setLinearVelocity(Poly.generateDirection().normalize());
}
static generateDirection():Vector3 {
let verts = [];
for (let i = 0; i < 3; i++) {
verts.push(Math.random() * (Math.random() > 0.5 ? 1 : -1));
}
verts[1] = Math.abs(verts[1]);
return new Vector3().fromArray(verts);
}
static generateGeometry():THREE.Geometry {
//generate two random verts, construct a triangle
let geom = new THREE.Geometry();
geom.vertices.push(new Vector3());
geom.vertices.push(Poly.generateDirection());
geom.vertices.push(Poly.generateDirection());
geom.faces.push(new THREE.Face3(0, 1, 2));
return geom;
}
collides(other:Morph):boolean {
return this.position.clone().sub(other.position).lengthSq() <= ((other.radius) ^ 2);
}
dispose() {
this.geometry.dispose()
}
}
class Morph extends Physijs.SphereMesh {
radius:number;
static levels:number[] = [4, 6, 12, 20];
constructor(pos:Vector3, public level:number, material?:THREE.Material, mass?:number) {
super(Morph.generateGeometry(level), material, mass);
this.radius = this.geometry.boundingSphere.radius;
this.position.copy(pos);
this.addEventListener("ready", () => this.init());
}
init():void {
this.castShadow = true;
}
static generateGeometry(level:number):THREE.Geometry {
let numFaces = Morph.levels[level];
switch (numFaces) {
case 4:
return new THREE.TetrahedronGeometry();
case 6:
return new THREE.BoxGeometry(1, 1, 1, 1, 1, 1);
case 12:
return new THREE.IcosahedronGeometry(1, 0);
case 20:
return new THREE.DodecahedronGeometry(1, 0);
default:
return new THREE.TetrahedronGeometry();
}
}
private updateGeometry():void {
this.geometry.dispose();
this.geometry = Morph.generateGeometry(this.level);
this.geometry.computeBoundingSphere();
this.radius = this.geometry.boundingSphere.radius;
}
shrink():void {
if (this.level > 0) {
this.level--;
this.updateGeometry();
}
}
grow():void {
if (this.level < 3) {
this.level++;
this.updateGeometry();
}
}
collides(other:Morph):boolean {
return this.position.clone().sub(other.position).length() <= (this.radius + other.radius);
}
dispose():void {
this.geometry.dispose();
}
}
class Projectile extends Morph {
time:number = 0;
static mat:Physijs.Material = Physijs.createMaterial(new THREE.MeshBasicMaterial({
color: 0x303030
}),
0.5,
0.3
);
constructor(private pos:Vector3, private dir:Vector3, level:number) {
super(pos.clone().add(dir.clone().setLength(2)), level, Projectile.mat, 0.05);
}
init():void {
this.launch();
};
launch():void {
this.setLinearVelocity(this.dir);
}
tick(delta):void {
this.time += delta;
}
}
class LiveMorph extends Morph {
life:number = 100;
speeds:number[];
damage(by:number):void {
if (this.isAlive())
this.life -= by;
}
isAlive():boolean {
return this.life > 0;
}
getSpeed():number {
return this.speeds[this.level];
}
}
class Mob extends LiveMorph {
speeds:number[] = [25.1, 23, 21, 19];
static mat:Physijs.Material = Physijs.createMaterial(
new THREE.MeshBasicMaterial({
color: 0xa01000
}),
.8,
.6);
constructor(pos:Vector3, level:number) {
super(pos, level, Mob.mat, 2);
}
approach(player:Player) {
let toPlayer = player.position.clone().sub(this.position).normalize();
this.setLinearVelocity(toPlayer.setLength(this.getSpeed()));
}
die():Poly[] {
let polys = [];
let amount = Math.floor(Math.random() * 10) + 3;
for (let i = 0; i < amount; i++) {
let poly = new Poly(this.position, Math.random() > 0.5 ? 1 : -1);
polys.push(poly);
}
return polys;
}
}
class Player extends LiveMorph {
minus:number = 5;
plus:number = 5;
forward:Vector3 = new Vector3(0, 0, -1);
upward:Vector3 = new Vector3(0, 1, 0);
camera:Vector3 = new Vector3(0, 6, 10);
heading:number = 0;
pitch:number = 0;
score:number = 0;
projectiles:Projectile[] = [];
listener:THREE.AudioListener;
speeds:number[] = [25, 24, 22, 20];
constructor(pos:Vector3) {
super(pos, 0, Physijs.createMaterial(
new THREE.MeshBasicMaterial({
color: 0x00a0b0
}),
1,
0.1
),
0.5);
this.listener = new THREE.AudioListener();
this.add(this.listener);
}
init():void {
this.castShadow = true;
this.setDamping(0.05, 0.05);
}
jump():void {
this.applyCentralImpulse(new Vector3(0, 8, 0));
}
rotate(xMovement:number):void {
this.heading -= xMovement * 0.002;
}
look(yMovement:number):void {
this.pitch -= yMovement * 0.002;
}
click(button:number):void {
if (button == THREE.MOUSE.LEFT) {
this.projectiles.push(new Projectile(this.position, this.getDirection().multiplyScalar(35), this.level));
}
}
getRight():Vector3 {
return this.getForward().cross(this.upward).normalize();
}
getDirection():Vector3 {
return this.getForward().applyAxisAngle(this.getRight(), this.pitch);
}
getForward():Vector3 {
return this.forward.clone().applyAxisAngle(this.upward, this.heading);
}
getCamera():Vector3 {
return this.camera.clone().applyAxisAngle(this.upward, this.heading).applyAxisAngle(this.getRight(), this.pitch);
}
}
class Level extends Physijs.Scene {
private mobs:Mob[] = [];
private projectiles:Projectile[] = [];
private polygons:Poly[] = [];
private ground:Physijs.BoxMesh;
private time:number = 0;
static durations:number[] = [25, 30, 45, Infinity];
static numLevels:number = Level.durations.length;
static mat:Physijs.Material = Physijs.createMaterial(
new THREE.MeshBasicMaterial({color: 0xcacaca}),
1,
1
);
constructor(private player:Player, public level:number) {
super();
this.setGravity(new THREE.Vector3(0, -40, 0));
this.add(player);
for (let i = 0; i < 3; i++) {
this.spawn(20, 20);
}
let groundGeometry = new THREE.BoxGeometry(1200, 1, 1200);
this.ground = new Physijs.BoxMesh(groundGeometry, Level.mat, 0);
this.add(this.ground);
}
random(start?:number, range?:number):Vector3 {
let a = Math.random() > 0.5 ? -1 : 1;
let b = Math.random() > 0.5 ? -1 : 1;
let x = Math.floor(Math.random() * range + start);
let z = Math.floor(Math.random() * range + start);
return new Vector3(a * x, 2, b * z);
}
spawnMob(where:Vector3, size:number):void {
let mob = new Mob(where, size);
this.add(mob);
this.mobs.push(mob);
}
spawnGroup(where:Vector3, amount:number, size:number):void {
for (let i = 0; i < amount; i++) {
this.spawnMob(where, size);
}
}
spawn(start:number, range:number, size?:number) {
if (!size) {
size = Math.floor(Math.random() * 4);
}
this.spawnMob(this.player.position.clone().add(this.random(start, range)), size);
}
tick(delta:number):void {
this.time += delta;
//push projectiles queued from player into the world.
while (this.player.projectiles.length > 0) {
let projectile = this.player.projectiles.pop();
this.projectiles.push(projectile);
this.add(projectile);
}
//enemy movement
this.mobs.forEach((mob) => {
mob.approach(this.player);
if (mob.collides(this.player)) {
//collide?
this.player.damage((mob.level + 1));
}
});
//tick projectiles and remove them if time out/on hit
this.projectiles = this.projectiles.filter((projectile) => {
projectile.tick(delta);
let keep = projectile.time < 10 * 1000;
let collided:boolean = false;
if (!keep) {
this.remove(projectile);
} else {
for (let mob of this.mobs) {
if (mob.collides(projectile)) {
if (mob.level == projectile.level) {
collided = true;
mob.damage(34);
break;
}
}
}
}
if (collided) {
this.remove(projectile);
}
return keep && !collided;
});
this.mobs = this.mobs.filter((mob) => {
let alive = mob.isAlive();
if (!alive) {
this.player.score += mob.level + 1;
let polys = mob.die();
polys.forEach((poly) => {
this.add(poly);
poly.init();
this.polygons.push(poly);
});
this.remove(mob);
}
return alive;
});
this.polygons = this.polygons.filter((poly) => {
if (poly.collides(this.player)) {
if (poly.polarity > 0) {
this.player.plus += 1;
} else {
this.player.minus += 1;
}
this.remove(poly);
return false;
}
return true;
});
//spawn new mob?
let amount = 0.004 * ((this.level+1)/2+0.8);
if(this.level == Level.numLevels-1){
amount+=(this.time/1000)/2000;
}
if (Math.random() < amount) {
let size = Math.floor(Math.random() * 4);
let pos = this.random(20, 10);
this.spawnGroup(pos, 3, size);
}
//physijs
this.simulate(delta, 1);
}
timeLeft():number {
return Level.durations[this.level] - (this.time / 1000);
}
dispose():void {
this.remove(this.ground);
this.ground.geometry.dispose();
this.mobs.forEach((obj) => {
this.remove(obj);
obj.dispose();
});
this.projectiles.forEach((obj) => {
this.remove(obj);
obj.dispose();
});
this.polygons.forEach((obj) => {
this.remove(obj);
obj.dispose();
});
}
//unused
static generateGeometry(level:number):THREE.Geometry[] {
switch (level) {
default:
case 0:
let vertices = [
1, 1, 1, -1, -1, 1, -1, 1, -1, 1, -1, -1
];
let verts = [];
for (let i = 0; i < 4; i++) {
verts.push(new Vector3().fromArray(vertices, i * 3));
}
let indices = [
0, 1, 2, 2, 3, 0, 0, 3, 1, 1, 3, 2
];
let faces = [];
for (let i = 0; i < 4; i++) {
faces.push(new Face3(indices[i * 3], indices[i * 3 + 1], indices[i * 3 + 2]));
}
return [new THREE.PolyhedronGeometry(verts, faces, 200)];
}
}
}
enum GameState {
INITIALIZED,
STARTED,
PAUSED,
STOPPED
}
class Game {
private renderer:THREE.Renderer;
private camera:THREE.PerspectiveCamera;
private overlay:HTMLDivElement;
private player:Player;
private level:Level;
private keyboard:Keyboard;
private mouse:Mouse;
private audio:THREE.Audio;
state:GameState;
private ticks:number = 0;
private delta:number = 0;
private lastFrame:number = 0;
private timestep:number = 1000 / 60;
private maxFPS:number = 60;
private keepRunning:boolean;
constructor() {
if (Detector.webgl) {
this.renderer = new THREE.WebGLRenderer({antialias: true});
let rendr = this.renderer;
rendr.setClearColor(0xcacaca);
rendr.setPixelRatio(window.devicePixelRatio);
rendr.setSize(window.innerWidth, window.innerHeight);
} else {
this.renderer = new THREE.CanvasRenderer();
let rendr = this.renderer;
rendr.setClearColor(0xcacaca);
rendr.setPixelRatio(window.devicePixelRatio);
rendr.setSize(window.innerWidth, window.innerHeight);
}
document.body.appendChild(this.renderer.domElement);
window.addEventListener("resize", this.onWindowResize, false);
this.overlay = document.getElementById("overlay");
this.camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 1, 1000);
}
init():void {
//init player
this.player = new Player(new Vector3(0, 2, 0));
//init audio
this.audio = new THREE.Audio(this.player.listener);
this.audio.load("background.ogg");
this.audio.autoplay = true;
this.audio.setLoop(true);
this.audio.setVolume(0.5);
//init keyboard and mouse
this.keyboard = new Keyboard();
this.mouse = new Mouse(this.player);
this.state = GameState.INITIALIZED;
this.newLevel(0);
}
newLevel(num:number):void {
//init level
this.level = new Level(this.player, num);
//init camera
this.camera.position.addVectors(this.player.position, this.player.camera);
this.camera.lookAt(this.player.position);
this.updateOverlay();
}
updateOverlay():void {
this.overlay.querySelector("#score").innerHTML = "Score: " + this.player.score;
this.overlay.querySelector("#time").innerHTML = "Time left: " + this.level.timeLeft().toFixed(0) + "s";
this.overlay.querySelector("#level").innerHTML = "Level: " + (this.level.level+1) + "/" + Level.numLevels;
this.overlay.querySelector("#life").innerHTML = "Life: " + this.player.life + "%";
this.overlay.querySelector("#positive").innerHTML = "Pos polygons(E): " + this.player.plus;
this.overlay.querySelector("#negative").innerHTML = "Neg polygons(Q): " + this.player.minus;
}
onWindowResize = () => {
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight);
};
/**
* Just render the scene.
*/
render():void {
this.renderer.render(this.level, this.camera);
}
/**
* Update logic based on @param delta.
* @param delta
*/
tick(delta:number):void {
this.ticks++;
if (this.ticks % 60 == 0) {
this.updateOverlay();
}
this.keyboard.update();
//camera
this.camera.position.addVectors(this.player.position, this.player.getCamera());
this.camera.lookAt(this.player.position);
//player movement
let playerSpeed = this.player.getSpeed();
let forward = this.player.getForward();
forward.setLength(playerSpeed);
let right = forward.clone().cross(this.player.upward);
right.setLength(playerSpeed);
if (this.keyboard.pressed("W")) {
this.player.applyCentralForce(forward);
}
if (this.keyboard.pressed("S")) {
this.player.applyCentralForce(forward.negate());
}
if (this.keyboard.pressed("D")) {
this.player.applyCentralForce(right);
}
if (this.keyboard.pressed("A")) {
this.player.applyCentralForce(right.negate());
}
//clamp speed, TODO into a method
let velocity = this.player.getLinearVelocity().clampLength(-playerSpeed, playerSpeed);
this.player.setLinearVelocity(velocity);
//morph!
if (this.keyboard.down("Q")) {
if (this.player.minus > 0 && this.player.level > 0) {
this.player.shrink();
this.player.minus--;
}
} else if (this.keyboard.down("E")) {
if (this.player.plus > 0 && this.player.level < 3) {
this.player.grow();
this.player.plus--;
}
}
//jump!
if (this.keyboard.down("space")) {
this.player.jump();
}
//debug shoot
if (this.keyboard.down("C")) {
this.player.click(THREE.MOUSE.LEFT);
}
this.level.tick(delta);
//die!
if (!this.player.isAlive()) {
this.stop(false);
}
//next level
if (this.level.timeLeft() < 0) {
this.stop(true);
}
}
run(timestamp?):void {
if (!timestamp) {
timestamp = performance.now();
}
if (timestamp < this.lastFrame + (1000 / this.maxFPS)) {
if (this.keepRunning) {
requestAnimationFrame(() => this.run());
}
return;
}
this.delta += timestamp - this.lastFrame;
this.lastFrame = timestamp;
var numUpdateSteps = 0;
while (this.delta >= this.timestep) {
this.tick(this.timestep);
this.delta -= this.timestep;
if (++numUpdateSteps >= 240) {
// panic here, reset delta
this.delta = 0;
break;
}
}
this.render();
if (this.keepRunning) {
requestAnimationFrame((time) => this.run(time));
}
}
start() {
this.state = GameState.STARTED;
this.keepRunning = true;
this.lastFrame = performance.now();
this.keyboard.register();
this.mouse.register();
this.run();
}
pause() {
this.updateOverlay();
this.state = GameState.PAUSED;
this.keyboard.unregister();
this.mouse.unregister();
this.keepRunning = false;
}
stop(result:boolean = false) {
this.pause();
this.state = GameState.STOPPED;
if (this.level.level != Level.numLevels - 1) {
if (result) {
//next level, shit!
this.level.dispose();
this.newLevel(this.level.level + 1);
this.start();
return;
}
} else {
result = true;//fix last level not winning
}
this.level.dispose();
window.removeEventListener("resize", this.onWindowResize, false);
let blocker = document.getElementById("block");
blocker.style.display = '-webkit-box';
blocker.style.display = '-moz-box';
blocker.style.display = 'box';
let instructions = document.getElementById("instructions");
instructions.style.fontSize = "40px";
instructions.innerHTML = result ? "You won!" : "You lost!";
instructions.style.display = "";
if (Detector.webgl) {
(this.renderer).dispose();
}
}
}
window.onload = () => {
let game = new Game();
game.init();
//make sure we have pointerlock here
//from three.js example(PointerLock), thanks
let block = document.getElementById("block");
let instructions = document.getElementById("instructions");
let overlay = document.getElementById("overlay");
let plock = new PointerLock(game, block, instructions, overlay);
plock.gain();
};