Space shooter game in TypeScript

The name of the pictureThe name of the pictureThe name of the pictureClash Royale CLAN TAG#URR8PPP





.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty margin-bottom:0;







up vote
2
down vote

favorite












I recently started learning TypeScript and created a small space shooter. I would like to get some feedback on the structure of my code.
I have some classes with both static and instance methods with references back and forth and wonder if I could have done this cleaner somehow.



Link to full code: Github

Game: Firefly (has sound on by default)



RenderObject.ts



import IRenderObect from "../interfaces/IRenderObject";
import Point from "./Point";

const images: [imagePath: string]: HTMLImageElement = ;

let renderContext: CanvasRenderingContext2D;
const objectList: RenderObject = ;
export class RenderObject
public static setRenderContext(ctx: CanvasRenderingContext2D)
renderContext = ctx;

public static getObjectList()
return objectList as ReadonlyArray<RenderObject>;

public static setDimensionsForImage(imageSrc: string)
objectList
.filter(o => o.image.src === imageSrc)
.forEach(o => o.setDimensions());

public static displaceAll(pointA: Point, pointB: Point)
const displaceX = pointA.x - pointB.x;
const displaceY = pointA.y - pointB.y;
objectList.forEach(o =>
o.center = new Point(o.center.x + displaceX, o.center.y + displaceY);
);

public center: Point = new Point(0, 0);
public angle: number;
public width: number = 1;
public height: number = 1;
public image: HTMLImageElement;
protected maxLifeSpan: number;
private frameIndex: number = 0;
private lifeSpan: number = 0;
private ticksCurrentFrame: number = 0;

constructor(private options: IRenderObect) Infinity;

if (!images[options.imageSrc])
this.image = new Image();
this.image.src = options.imageSrc;
this.image.onload = this.onImageLoad.bind(this);
images[options.imageSrc] = this.image;
else
this.image = images[options.imageSrc];
this.setDimensions();


if (this.options.sprite && !this.options.sprite.loop)
this.maxLifeSpan =
this.options.sprite.frames * this.options.sprite.ticksPerFrame;


this.center = options.initialPosition;


public update()
if (this.lifeSpan > this.maxLifeSpan)
return this.destroy();


this.lifeSpan++;
this.spriteActions();
this.draw();


public destroy()
const index = objectList.indexOf(this);
objectList.splice(index, 1);


protected draw()
const x = this.center.x;
const y = this.center.y;
const degrees = this.angle + 90;
const angleInRadians = degrees * Math.PI / 180;

renderContext.translate(x, y);
renderContext.rotate(angleInRadians);

if (this.options.sprite)
renderContext.drawImage(
this.image,
this.frameIndex * this.width,
0,
this.width,
this.height,
-this.width / 2,
-this.height / 2,
this.width,
this.height
);
else
renderContext.drawImage(this.image, -this.width / 2, -this.height / 2);


renderContext.rotate(-angleInRadians);
renderContext.translate(-x, -y);


protected onImageLoad()
RenderObject.setDimensionsForImage(this.image.src);


private setDimensions()
this.width = this.options.sprite
? this.image.width / this.options.sprite.frames
: this.image.width;
this.height = this.image.height;


private spriteActions()
if (!this.options.sprite)
return;


this.ticksCurrentFrame++;

if (this.ticksCurrentFrame > this.options.sprite.ticksPerFrame)
this.ticksCurrentFrame = 0;
if (this.frameIndex < this.options.sprite.frames - 1)
this.frameIndex++;
else
this.frameIndex = 0;






Entity.ts



import * as Calculations from "../Calculations";
import IDirection from "../interfaces/IDirection";
import IStatus from "../interfaces/IStatus";
import Point from "./Point";
import RenderObject from "./RenderObject";

const entityList: Entity = ;
export class Entity extends RenderObject
public static getEntityList()
return entityList as ReadonlyArray<Entity>;


public static testCollision(entity1: Entity, entity2: Entity)
return entity1.isCollidingWith(entity2);


public health = Infinity;
public maxHealth = Infinity;
public speedX = 0;
public speedY = 0;
public impactDamage = 1;
public status: IStatus = ;
public faction?: string;
public owner?: Entity;
public acceleration = 0;
protected turnSpeed = 0;
protected initialized = false;

constructor(imageSrc: string, initialPosition: Point)
super( imageSrc, initialPosition );
entityList.push(this);


public update()
if (!this.initialized)
this.init();


if (this.health <= 0)
return this.destroy();


this.processStatus();

super.update();


public destroy()
const index = entityList.indexOf(this);
entityList.splice(index, 1);
super.destroy();


public isCollidingWith(entity: Entity)

public init()
if (!this.initialized)
this.initialized = true;
this.maxHealth = this.health;



protected move(directions: IDirection)
// Angle 0 is X-axis, direction is in radians.
const angle = this.angle * (Math.PI / 180);

const forward = directions.forward ? 1 : 0;
const back = directions.back ? -0.4 : 0;
const left = directions.left ? 0.5 : 0;
const right = directions.right ? -0.5 : 0;

// Forward and backward.
this.speedX =
this.speedX + (forward + back) * this.acceleration * Math.cos(angle);
this.speedY =
this.speedY + (forward + back) * this.acceleration * Math.sin(angle);

// Left and right.
this.speedX =
this.speedX +
(left + right) * this.acceleration * Math.cos(angle - Math.PI / 2);
this.speedY =
this.speedY +
(left + right) * this.acceleration * Math.sin(angle - Math.PI / 2);

// Friction.
this.speedX *= 0.97;
this.speedY *= 0.97;

this.center = new Point(
this.center.x + this.speedX,
this.center.y + this.speedY
);


protected turn(point: Point)
const targetAngle = Calculations.getAngle(this.center, point);
const turnDegrees =
Calculations.mod(targetAngle - this.angle + 180, 360) - 180;

if (turnDegrees > -4 && turnDegrees < 4)
this.angle = targetAngle;
else if (turnDegrees < 0)
this.angle -= this.turnSpeed;
else
this.angle += this.turnSpeed;



private processStatus()
if (this.status.firing)
this.status.firing--;
// tslint:disable-next-line:no-unused-expression
new RenderObject(
angle: this.angle,
imageSrc: "images/objects/GunFlare.png",
initialPosition: this.center,
lifeSpan: 1
);


if (this.status.takingFire)
this.status.takingFire--;
// tslint:disable-next-line:no-unused-expression
new RenderObject(
angle: this.angle,
imageSrc: "images/objects/BulletImpact.png",
initialPosition: this.center,
lifeSpan: 1
);





Ship.ts



import * as Calculations from "../Calculations";
import IDirection from "../interfaces/IDirection";
import Entity from "./Entity";
import ExplosionSprite from "./Explosion";
import doubleLaserShot from "./Laser";
import Point from "./Point";
import Projectile from "./Projectile";
import SoundPool from "./SoundPool";

const laserSound = new SoundPool("sound/effects/laser.wav", 0.02, 200);

export class Ship extends Entity
public health = 50;
public shield = 0;
public cooldown = 0;
public cooldownTime = 20;
public acceleration = 0.3;
public inaccuracy = 100;
public turnSpeed = 3;

constructor(imageSrc: string, initialPosition: Point)
super(imageSrc, initialPosition);


public canFire()
return this.cooldown === 0;


public update()
if (this.cooldown > 0)
this.cooldown--;

super.update();


public destroy()
// tslint:disable-next-line:no-unused-expression
new ExplosionSprite(this.center);
super.destroy();


protected firePrimary()
if (!this.canFire())
return;

this.status.firing = 1;
doubleLaserShot(this);


protected fireSecondary()
if (!this.canFire())
return;

this.status.firing = 1;

// const offset = this.width / 2.4;
// tslint:disable-next-line:no-unused-expression
new Projectile(this, "dumbFire");

this.cooldown = this.cooldownTime;
laserSound.play();




PlayerShip.ts



import MouseButtonMap from "../enums/MouseButtonMap";
import IDirection from "../interfaces/IDirection";
import InputController from "./InputController";
import Point from "./Point";
import RenderObject from "./RenderObject";
import Ship from "./Ship";

export class PlayerShip extends Ship
public health = 500;
public shield = 0;
public acceleration = 0.8;
public cooldownTime = 5;
public inaccuracy = 0;
public turnSpeed = 6;
public alive = true;

constructor()
super(
"images/objects/Firefly.png",
new Point(window.innerWidth / 2 - 40, window.innerHeight / 2 - 40)
);


public update()
this.turn(InputController.getMousePosition());

const keysDown = InputController.getKeysDown();
const directions: IDirection = keysDown.w,
left: keysDown.ArrowLeft ;
const currentPosition = this.center;
this.move(directions);
const newPosition = this.center;
RenderObject.displaceAll(currentPosition, newPosition);

if (InputController.getMouseDownButtons()[MouseButtonMap.LEFT])
this.firePrimary();


if (keysDown[" "])
this.fireSecondary();


super.update();


public destroy()
this.alive = false;
super.destroy();




EnemyShip.ts



import * as Calculations from "../Calculations";
import IDirection from "../interfaces/IDirection";
import Entity from "./Entity";
import PlayerShip from "./PlayerShip";
import Point from './Point';
import Ship from "./Ship";

export class EnemyShip extends Ship
public health = 50;
public shield = 0;

constructor(imageSrc = "images/objects/enemy1.png", initialPosition: Point)
super(imageSrc, initialPosition);


public update()
this.actions();
super.update();


private findTarget()
return Entity.getEntityList().find(p => p instanceof PlayerShip);


private actions()
if (this.findTarget())
return this.attack(this.findTarget() as PlayerShip);



private attack(target: Entity)
this.turn(target.center);

const playerFacing = Calculations.isFacing(target, this);
let directions: IDirection = ;
if (playerFacing)
directions =
forward: true,
left: playerFacing > 0,
right: playerFacing < 0
;
else if (Calculations.lineDistance(this.center, target.center) < 300)
directions =
back: true
;

this.move(directions);

if (Calculations.chance(0.5))
this.firePrimary();


if (Calculations.chance(0.5))
this.fireSecondary();





Projectile.ts



import getRandomInt from "../Calculations";
import Entity from "./Entity";
import ExplosionSprite from "./Explosion";
import Point from "./Point";

interface IProjectileType
impactDamage: number;
acceleration: number;
maxLifeSpan: number;
turnSpeed?: number;


const projectileTypes: [key: string]: IProjectileType =
dumbFire:
acceleration: 0.1,
impactDamage: 10,
maxLifeSpan: 100
,
homing:
acceleration: 0.1,
impactDamage: 10,
maxLifeSpan: 100,
turnSpeed: 0.8

;

const image = "images/objects/bolt1.png";
export class Projectile extends Entity
public target?: Entity;
constructor(owner: Entity, type: string, target?: Entity)

public update()
if (this.target)
super.turn(this.target.center);

super.move( forward: true );
super.update();


public destroy()
// tslint:disable-next-line:no-unused-expression
new ExplosionSprite(this.center);
super.destroy();








share|improve this question



























    up vote
    2
    down vote

    favorite












    I recently started learning TypeScript and created a small space shooter. I would like to get some feedback on the structure of my code.
    I have some classes with both static and instance methods with references back and forth and wonder if I could have done this cleaner somehow.



    Link to full code: Github

    Game: Firefly (has sound on by default)



    RenderObject.ts



    import IRenderObect from "../interfaces/IRenderObject";
    import Point from "./Point";

    const images: [imagePath: string]: HTMLImageElement = ;

    let renderContext: CanvasRenderingContext2D;
    const objectList: RenderObject = ;
    export class RenderObject
    public static setRenderContext(ctx: CanvasRenderingContext2D)
    renderContext = ctx;

    public static getObjectList()
    return objectList as ReadonlyArray<RenderObject>;

    public static setDimensionsForImage(imageSrc: string)
    objectList
    .filter(o => o.image.src === imageSrc)
    .forEach(o => o.setDimensions());

    public static displaceAll(pointA: Point, pointB: Point)
    const displaceX = pointA.x - pointB.x;
    const displaceY = pointA.y - pointB.y;
    objectList.forEach(o =>
    o.center = new Point(o.center.x + displaceX, o.center.y + displaceY);
    );

    public center: Point = new Point(0, 0);
    public angle: number;
    public width: number = 1;
    public height: number = 1;
    public image: HTMLImageElement;
    protected maxLifeSpan: number;
    private frameIndex: number = 0;
    private lifeSpan: number = 0;
    private ticksCurrentFrame: number = 0;

    constructor(private options: IRenderObect) Infinity;

    if (!images[options.imageSrc])
    this.image = new Image();
    this.image.src = options.imageSrc;
    this.image.onload = this.onImageLoad.bind(this);
    images[options.imageSrc] = this.image;
    else
    this.image = images[options.imageSrc];
    this.setDimensions();


    if (this.options.sprite && !this.options.sprite.loop)
    this.maxLifeSpan =
    this.options.sprite.frames * this.options.sprite.ticksPerFrame;


    this.center = options.initialPosition;


    public update()
    if (this.lifeSpan > this.maxLifeSpan)
    return this.destroy();


    this.lifeSpan++;
    this.spriteActions();
    this.draw();


    public destroy()
    const index = objectList.indexOf(this);
    objectList.splice(index, 1);


    protected draw()
    const x = this.center.x;
    const y = this.center.y;
    const degrees = this.angle + 90;
    const angleInRadians = degrees * Math.PI / 180;

    renderContext.translate(x, y);
    renderContext.rotate(angleInRadians);

    if (this.options.sprite)
    renderContext.drawImage(
    this.image,
    this.frameIndex * this.width,
    0,
    this.width,
    this.height,
    -this.width / 2,
    -this.height / 2,
    this.width,
    this.height
    );
    else
    renderContext.drawImage(this.image, -this.width / 2, -this.height / 2);


    renderContext.rotate(-angleInRadians);
    renderContext.translate(-x, -y);


    protected onImageLoad()
    RenderObject.setDimensionsForImage(this.image.src);


    private setDimensions()
    this.width = this.options.sprite
    ? this.image.width / this.options.sprite.frames
    : this.image.width;
    this.height = this.image.height;


    private spriteActions()
    if (!this.options.sprite)
    return;


    this.ticksCurrentFrame++;

    if (this.ticksCurrentFrame > this.options.sprite.ticksPerFrame)
    this.ticksCurrentFrame = 0;
    if (this.frameIndex < this.options.sprite.frames - 1)
    this.frameIndex++;
    else
    this.frameIndex = 0;






    Entity.ts



    import * as Calculations from "../Calculations";
    import IDirection from "../interfaces/IDirection";
    import IStatus from "../interfaces/IStatus";
    import Point from "./Point";
    import RenderObject from "./RenderObject";

    const entityList: Entity = ;
    export class Entity extends RenderObject
    public static getEntityList()
    return entityList as ReadonlyArray<Entity>;


    public static testCollision(entity1: Entity, entity2: Entity)
    return entity1.isCollidingWith(entity2);


    public health = Infinity;
    public maxHealth = Infinity;
    public speedX = 0;
    public speedY = 0;
    public impactDamage = 1;
    public status: IStatus = ;
    public faction?: string;
    public owner?: Entity;
    public acceleration = 0;
    protected turnSpeed = 0;
    protected initialized = false;

    constructor(imageSrc: string, initialPosition: Point)
    super( imageSrc, initialPosition );
    entityList.push(this);


    public update()
    if (!this.initialized)
    this.init();


    if (this.health <= 0)
    return this.destroy();


    this.processStatus();

    super.update();


    public destroy()
    const index = entityList.indexOf(this);
    entityList.splice(index, 1);
    super.destroy();


    public isCollidingWith(entity: Entity)

    public init()
    if (!this.initialized)
    this.initialized = true;
    this.maxHealth = this.health;



    protected move(directions: IDirection)
    // Angle 0 is X-axis, direction is in radians.
    const angle = this.angle * (Math.PI / 180);

    const forward = directions.forward ? 1 : 0;
    const back = directions.back ? -0.4 : 0;
    const left = directions.left ? 0.5 : 0;
    const right = directions.right ? -0.5 : 0;

    // Forward and backward.
    this.speedX =
    this.speedX + (forward + back) * this.acceleration * Math.cos(angle);
    this.speedY =
    this.speedY + (forward + back) * this.acceleration * Math.sin(angle);

    // Left and right.
    this.speedX =
    this.speedX +
    (left + right) * this.acceleration * Math.cos(angle - Math.PI / 2);
    this.speedY =
    this.speedY +
    (left + right) * this.acceleration * Math.sin(angle - Math.PI / 2);

    // Friction.
    this.speedX *= 0.97;
    this.speedY *= 0.97;

    this.center = new Point(
    this.center.x + this.speedX,
    this.center.y + this.speedY
    );


    protected turn(point: Point)
    const targetAngle = Calculations.getAngle(this.center, point);
    const turnDegrees =
    Calculations.mod(targetAngle - this.angle + 180, 360) - 180;

    if (turnDegrees > -4 && turnDegrees < 4)
    this.angle = targetAngle;
    else if (turnDegrees < 0)
    this.angle -= this.turnSpeed;
    else
    this.angle += this.turnSpeed;



    private processStatus()
    if (this.status.firing)
    this.status.firing--;
    // tslint:disable-next-line:no-unused-expression
    new RenderObject(
    angle: this.angle,
    imageSrc: "images/objects/GunFlare.png",
    initialPosition: this.center,
    lifeSpan: 1
    );


    if (this.status.takingFire)
    this.status.takingFire--;
    // tslint:disable-next-line:no-unused-expression
    new RenderObject(
    angle: this.angle,
    imageSrc: "images/objects/BulletImpact.png",
    initialPosition: this.center,
    lifeSpan: 1
    );





    Ship.ts



    import * as Calculations from "../Calculations";
    import IDirection from "../interfaces/IDirection";
    import Entity from "./Entity";
    import ExplosionSprite from "./Explosion";
    import doubleLaserShot from "./Laser";
    import Point from "./Point";
    import Projectile from "./Projectile";
    import SoundPool from "./SoundPool";

    const laserSound = new SoundPool("sound/effects/laser.wav", 0.02, 200);

    export class Ship extends Entity
    public health = 50;
    public shield = 0;
    public cooldown = 0;
    public cooldownTime = 20;
    public acceleration = 0.3;
    public inaccuracy = 100;
    public turnSpeed = 3;

    constructor(imageSrc: string, initialPosition: Point)
    super(imageSrc, initialPosition);


    public canFire()
    return this.cooldown === 0;


    public update()
    if (this.cooldown > 0)
    this.cooldown--;

    super.update();


    public destroy()
    // tslint:disable-next-line:no-unused-expression
    new ExplosionSprite(this.center);
    super.destroy();


    protected firePrimary()
    if (!this.canFire())
    return;

    this.status.firing = 1;
    doubleLaserShot(this);


    protected fireSecondary()
    if (!this.canFire())
    return;

    this.status.firing = 1;

    // const offset = this.width / 2.4;
    // tslint:disable-next-line:no-unused-expression
    new Projectile(this, "dumbFire");

    this.cooldown = this.cooldownTime;
    laserSound.play();




    PlayerShip.ts



    import MouseButtonMap from "../enums/MouseButtonMap";
    import IDirection from "../interfaces/IDirection";
    import InputController from "./InputController";
    import Point from "./Point";
    import RenderObject from "./RenderObject";
    import Ship from "./Ship";

    export class PlayerShip extends Ship
    public health = 500;
    public shield = 0;
    public acceleration = 0.8;
    public cooldownTime = 5;
    public inaccuracy = 0;
    public turnSpeed = 6;
    public alive = true;

    constructor()
    super(
    "images/objects/Firefly.png",
    new Point(window.innerWidth / 2 - 40, window.innerHeight / 2 - 40)
    );


    public update()
    this.turn(InputController.getMousePosition());

    const keysDown = InputController.getKeysDown();
    const directions: IDirection = keysDown.w,
    left: keysDown.ArrowLeft ;
    const currentPosition = this.center;
    this.move(directions);
    const newPosition = this.center;
    RenderObject.displaceAll(currentPosition, newPosition);

    if (InputController.getMouseDownButtons()[MouseButtonMap.LEFT])
    this.firePrimary();


    if (keysDown[" "])
    this.fireSecondary();


    super.update();


    public destroy()
    this.alive = false;
    super.destroy();




    EnemyShip.ts



    import * as Calculations from "../Calculations";
    import IDirection from "../interfaces/IDirection";
    import Entity from "./Entity";
    import PlayerShip from "./PlayerShip";
    import Point from './Point';
    import Ship from "./Ship";

    export class EnemyShip extends Ship
    public health = 50;
    public shield = 0;

    constructor(imageSrc = "images/objects/enemy1.png", initialPosition: Point)
    super(imageSrc, initialPosition);


    public update()
    this.actions();
    super.update();


    private findTarget()
    return Entity.getEntityList().find(p => p instanceof PlayerShip);


    private actions()
    if (this.findTarget())
    return this.attack(this.findTarget() as PlayerShip);



    private attack(target: Entity)
    this.turn(target.center);

    const playerFacing = Calculations.isFacing(target, this);
    let directions: IDirection = ;
    if (playerFacing)
    directions =
    forward: true,
    left: playerFacing > 0,
    right: playerFacing < 0
    ;
    else if (Calculations.lineDistance(this.center, target.center) < 300)
    directions =
    back: true
    ;

    this.move(directions);

    if (Calculations.chance(0.5))
    this.firePrimary();


    if (Calculations.chance(0.5))
    this.fireSecondary();





    Projectile.ts



    import getRandomInt from "../Calculations";
    import Entity from "./Entity";
    import ExplosionSprite from "./Explosion";
    import Point from "./Point";

    interface IProjectileType
    impactDamage: number;
    acceleration: number;
    maxLifeSpan: number;
    turnSpeed?: number;


    const projectileTypes: [key: string]: IProjectileType =
    dumbFire:
    acceleration: 0.1,
    impactDamage: 10,
    maxLifeSpan: 100
    ,
    homing:
    acceleration: 0.1,
    impactDamage: 10,
    maxLifeSpan: 100,
    turnSpeed: 0.8

    ;

    const image = "images/objects/bolt1.png";
    export class Projectile extends Entity
    public target?: Entity;
    constructor(owner: Entity, type: string, target?: Entity)

    public update()
    if (this.target)
    super.turn(this.target.center);

    super.move( forward: true );
    super.update();


    public destroy()
    // tslint:disable-next-line:no-unused-expression
    new ExplosionSprite(this.center);
    super.destroy();








    share|improve this question























      up vote
      2
      down vote

      favorite









      up vote
      2
      down vote

      favorite











      I recently started learning TypeScript and created a small space shooter. I would like to get some feedback on the structure of my code.
      I have some classes with both static and instance methods with references back and forth and wonder if I could have done this cleaner somehow.



      Link to full code: Github

      Game: Firefly (has sound on by default)



      RenderObject.ts



      import IRenderObect from "../interfaces/IRenderObject";
      import Point from "./Point";

      const images: [imagePath: string]: HTMLImageElement = ;

      let renderContext: CanvasRenderingContext2D;
      const objectList: RenderObject = ;
      export class RenderObject
      public static setRenderContext(ctx: CanvasRenderingContext2D)
      renderContext = ctx;

      public static getObjectList()
      return objectList as ReadonlyArray<RenderObject>;

      public static setDimensionsForImage(imageSrc: string)
      objectList
      .filter(o => o.image.src === imageSrc)
      .forEach(o => o.setDimensions());

      public static displaceAll(pointA: Point, pointB: Point)
      const displaceX = pointA.x - pointB.x;
      const displaceY = pointA.y - pointB.y;
      objectList.forEach(o =>
      o.center = new Point(o.center.x + displaceX, o.center.y + displaceY);
      );

      public center: Point = new Point(0, 0);
      public angle: number;
      public width: number = 1;
      public height: number = 1;
      public image: HTMLImageElement;
      protected maxLifeSpan: number;
      private frameIndex: number = 0;
      private lifeSpan: number = 0;
      private ticksCurrentFrame: number = 0;

      constructor(private options: IRenderObect) Infinity;

      if (!images[options.imageSrc])
      this.image = new Image();
      this.image.src = options.imageSrc;
      this.image.onload = this.onImageLoad.bind(this);
      images[options.imageSrc] = this.image;
      else
      this.image = images[options.imageSrc];
      this.setDimensions();


      if (this.options.sprite && !this.options.sprite.loop)
      this.maxLifeSpan =
      this.options.sprite.frames * this.options.sprite.ticksPerFrame;


      this.center = options.initialPosition;


      public update()
      if (this.lifeSpan > this.maxLifeSpan)
      return this.destroy();


      this.lifeSpan++;
      this.spriteActions();
      this.draw();


      public destroy()
      const index = objectList.indexOf(this);
      objectList.splice(index, 1);


      protected draw()
      const x = this.center.x;
      const y = this.center.y;
      const degrees = this.angle + 90;
      const angleInRadians = degrees * Math.PI / 180;

      renderContext.translate(x, y);
      renderContext.rotate(angleInRadians);

      if (this.options.sprite)
      renderContext.drawImage(
      this.image,
      this.frameIndex * this.width,
      0,
      this.width,
      this.height,
      -this.width / 2,
      -this.height / 2,
      this.width,
      this.height
      );
      else
      renderContext.drawImage(this.image, -this.width / 2, -this.height / 2);


      renderContext.rotate(-angleInRadians);
      renderContext.translate(-x, -y);


      protected onImageLoad()
      RenderObject.setDimensionsForImage(this.image.src);


      private setDimensions()
      this.width = this.options.sprite
      ? this.image.width / this.options.sprite.frames
      : this.image.width;
      this.height = this.image.height;


      private spriteActions()
      if (!this.options.sprite)
      return;


      this.ticksCurrentFrame++;

      if (this.ticksCurrentFrame > this.options.sprite.ticksPerFrame)
      this.ticksCurrentFrame = 0;
      if (this.frameIndex < this.options.sprite.frames - 1)
      this.frameIndex++;
      else
      this.frameIndex = 0;






      Entity.ts



      import * as Calculations from "../Calculations";
      import IDirection from "../interfaces/IDirection";
      import IStatus from "../interfaces/IStatus";
      import Point from "./Point";
      import RenderObject from "./RenderObject";

      const entityList: Entity = ;
      export class Entity extends RenderObject
      public static getEntityList()
      return entityList as ReadonlyArray<Entity>;


      public static testCollision(entity1: Entity, entity2: Entity)
      return entity1.isCollidingWith(entity2);


      public health = Infinity;
      public maxHealth = Infinity;
      public speedX = 0;
      public speedY = 0;
      public impactDamage = 1;
      public status: IStatus = ;
      public faction?: string;
      public owner?: Entity;
      public acceleration = 0;
      protected turnSpeed = 0;
      protected initialized = false;

      constructor(imageSrc: string, initialPosition: Point)
      super( imageSrc, initialPosition );
      entityList.push(this);


      public update()
      if (!this.initialized)
      this.init();


      if (this.health <= 0)
      return this.destroy();


      this.processStatus();

      super.update();


      public destroy()
      const index = entityList.indexOf(this);
      entityList.splice(index, 1);
      super.destroy();


      public isCollidingWith(entity: Entity)

      public init()
      if (!this.initialized)
      this.initialized = true;
      this.maxHealth = this.health;



      protected move(directions: IDirection)
      // Angle 0 is X-axis, direction is in radians.
      const angle = this.angle * (Math.PI / 180);

      const forward = directions.forward ? 1 : 0;
      const back = directions.back ? -0.4 : 0;
      const left = directions.left ? 0.5 : 0;
      const right = directions.right ? -0.5 : 0;

      // Forward and backward.
      this.speedX =
      this.speedX + (forward + back) * this.acceleration * Math.cos(angle);
      this.speedY =
      this.speedY + (forward + back) * this.acceleration * Math.sin(angle);

      // Left and right.
      this.speedX =
      this.speedX +
      (left + right) * this.acceleration * Math.cos(angle - Math.PI / 2);
      this.speedY =
      this.speedY +
      (left + right) * this.acceleration * Math.sin(angle - Math.PI / 2);

      // Friction.
      this.speedX *= 0.97;
      this.speedY *= 0.97;

      this.center = new Point(
      this.center.x + this.speedX,
      this.center.y + this.speedY
      );


      protected turn(point: Point)
      const targetAngle = Calculations.getAngle(this.center, point);
      const turnDegrees =
      Calculations.mod(targetAngle - this.angle + 180, 360) - 180;

      if (turnDegrees > -4 && turnDegrees < 4)
      this.angle = targetAngle;
      else if (turnDegrees < 0)
      this.angle -= this.turnSpeed;
      else
      this.angle += this.turnSpeed;



      private processStatus()
      if (this.status.firing)
      this.status.firing--;
      // tslint:disable-next-line:no-unused-expression
      new RenderObject(
      angle: this.angle,
      imageSrc: "images/objects/GunFlare.png",
      initialPosition: this.center,
      lifeSpan: 1
      );


      if (this.status.takingFire)
      this.status.takingFire--;
      // tslint:disable-next-line:no-unused-expression
      new RenderObject(
      angle: this.angle,
      imageSrc: "images/objects/BulletImpact.png",
      initialPosition: this.center,
      lifeSpan: 1
      );





      Ship.ts



      import * as Calculations from "../Calculations";
      import IDirection from "../interfaces/IDirection";
      import Entity from "./Entity";
      import ExplosionSprite from "./Explosion";
      import doubleLaserShot from "./Laser";
      import Point from "./Point";
      import Projectile from "./Projectile";
      import SoundPool from "./SoundPool";

      const laserSound = new SoundPool("sound/effects/laser.wav", 0.02, 200);

      export class Ship extends Entity
      public health = 50;
      public shield = 0;
      public cooldown = 0;
      public cooldownTime = 20;
      public acceleration = 0.3;
      public inaccuracy = 100;
      public turnSpeed = 3;

      constructor(imageSrc: string, initialPosition: Point)
      super(imageSrc, initialPosition);


      public canFire()
      return this.cooldown === 0;


      public update()
      if (this.cooldown > 0)
      this.cooldown--;

      super.update();


      public destroy()
      // tslint:disable-next-line:no-unused-expression
      new ExplosionSprite(this.center);
      super.destroy();


      protected firePrimary()
      if (!this.canFire())
      return;

      this.status.firing = 1;
      doubleLaserShot(this);


      protected fireSecondary()
      if (!this.canFire())
      return;

      this.status.firing = 1;

      // const offset = this.width / 2.4;
      // tslint:disable-next-line:no-unused-expression
      new Projectile(this, "dumbFire");

      this.cooldown = this.cooldownTime;
      laserSound.play();




      PlayerShip.ts



      import MouseButtonMap from "../enums/MouseButtonMap";
      import IDirection from "../interfaces/IDirection";
      import InputController from "./InputController";
      import Point from "./Point";
      import RenderObject from "./RenderObject";
      import Ship from "./Ship";

      export class PlayerShip extends Ship
      public health = 500;
      public shield = 0;
      public acceleration = 0.8;
      public cooldownTime = 5;
      public inaccuracy = 0;
      public turnSpeed = 6;
      public alive = true;

      constructor()
      super(
      "images/objects/Firefly.png",
      new Point(window.innerWidth / 2 - 40, window.innerHeight / 2 - 40)
      );


      public update()
      this.turn(InputController.getMousePosition());

      const keysDown = InputController.getKeysDown();
      const directions: IDirection = keysDown.w,
      left: keysDown.ArrowLeft ;
      const currentPosition = this.center;
      this.move(directions);
      const newPosition = this.center;
      RenderObject.displaceAll(currentPosition, newPosition);

      if (InputController.getMouseDownButtons()[MouseButtonMap.LEFT])
      this.firePrimary();


      if (keysDown[" "])
      this.fireSecondary();


      super.update();


      public destroy()
      this.alive = false;
      super.destroy();




      EnemyShip.ts



      import * as Calculations from "../Calculations";
      import IDirection from "../interfaces/IDirection";
      import Entity from "./Entity";
      import PlayerShip from "./PlayerShip";
      import Point from './Point';
      import Ship from "./Ship";

      export class EnemyShip extends Ship
      public health = 50;
      public shield = 0;

      constructor(imageSrc = "images/objects/enemy1.png", initialPosition: Point)
      super(imageSrc, initialPosition);


      public update()
      this.actions();
      super.update();


      private findTarget()
      return Entity.getEntityList().find(p => p instanceof PlayerShip);


      private actions()
      if (this.findTarget())
      return this.attack(this.findTarget() as PlayerShip);



      private attack(target: Entity)
      this.turn(target.center);

      const playerFacing = Calculations.isFacing(target, this);
      let directions: IDirection = ;
      if (playerFacing)
      directions =
      forward: true,
      left: playerFacing > 0,
      right: playerFacing < 0
      ;
      else if (Calculations.lineDistance(this.center, target.center) < 300)
      directions =
      back: true
      ;

      this.move(directions);

      if (Calculations.chance(0.5))
      this.firePrimary();


      if (Calculations.chance(0.5))
      this.fireSecondary();





      Projectile.ts



      import getRandomInt from "../Calculations";
      import Entity from "./Entity";
      import ExplosionSprite from "./Explosion";
      import Point from "./Point";

      interface IProjectileType
      impactDamage: number;
      acceleration: number;
      maxLifeSpan: number;
      turnSpeed?: number;


      const projectileTypes: [key: string]: IProjectileType =
      dumbFire:
      acceleration: 0.1,
      impactDamage: 10,
      maxLifeSpan: 100
      ,
      homing:
      acceleration: 0.1,
      impactDamage: 10,
      maxLifeSpan: 100,
      turnSpeed: 0.8

      ;

      const image = "images/objects/bolt1.png";
      export class Projectile extends Entity
      public target?: Entity;
      constructor(owner: Entity, type: string, target?: Entity)

      public update()
      if (this.target)
      super.turn(this.target.center);

      super.move( forward: true );
      super.update();


      public destroy()
      // tslint:disable-next-line:no-unused-expression
      new ExplosionSprite(this.center);
      super.destroy();








      share|improve this question













      I recently started learning TypeScript and created a small space shooter. I would like to get some feedback on the structure of my code.
      I have some classes with both static and instance methods with references back and forth and wonder if I could have done this cleaner somehow.



      Link to full code: Github

      Game: Firefly (has sound on by default)



      RenderObject.ts



      import IRenderObect from "../interfaces/IRenderObject";
      import Point from "./Point";

      const images: [imagePath: string]: HTMLImageElement = ;

      let renderContext: CanvasRenderingContext2D;
      const objectList: RenderObject = ;
      export class RenderObject
      public static setRenderContext(ctx: CanvasRenderingContext2D)
      renderContext = ctx;

      public static getObjectList()
      return objectList as ReadonlyArray<RenderObject>;

      public static setDimensionsForImage(imageSrc: string)
      objectList
      .filter(o => o.image.src === imageSrc)
      .forEach(o => o.setDimensions());

      public static displaceAll(pointA: Point, pointB: Point)
      const displaceX = pointA.x - pointB.x;
      const displaceY = pointA.y - pointB.y;
      objectList.forEach(o =>
      o.center = new Point(o.center.x + displaceX, o.center.y + displaceY);
      );

      public center: Point = new Point(0, 0);
      public angle: number;
      public width: number = 1;
      public height: number = 1;
      public image: HTMLImageElement;
      protected maxLifeSpan: number;
      private frameIndex: number = 0;
      private lifeSpan: number = 0;
      private ticksCurrentFrame: number = 0;

      constructor(private options: IRenderObect) Infinity;

      if (!images[options.imageSrc])
      this.image = new Image();
      this.image.src = options.imageSrc;
      this.image.onload = this.onImageLoad.bind(this);
      images[options.imageSrc] = this.image;
      else
      this.image = images[options.imageSrc];
      this.setDimensions();


      if (this.options.sprite && !this.options.sprite.loop)
      this.maxLifeSpan =
      this.options.sprite.frames * this.options.sprite.ticksPerFrame;


      this.center = options.initialPosition;


      public update()
      if (this.lifeSpan > this.maxLifeSpan)
      return this.destroy();


      this.lifeSpan++;
      this.spriteActions();
      this.draw();


      public destroy()
      const index = objectList.indexOf(this);
      objectList.splice(index, 1);


      protected draw()
      const x = this.center.x;
      const y = this.center.y;
      const degrees = this.angle + 90;
      const angleInRadians = degrees * Math.PI / 180;

      renderContext.translate(x, y);
      renderContext.rotate(angleInRadians);

      if (this.options.sprite)
      renderContext.drawImage(
      this.image,
      this.frameIndex * this.width,
      0,
      this.width,
      this.height,
      -this.width / 2,
      -this.height / 2,
      this.width,
      this.height
      );
      else
      renderContext.drawImage(this.image, -this.width / 2, -this.height / 2);


      renderContext.rotate(-angleInRadians);
      renderContext.translate(-x, -y);


      protected onImageLoad()
      RenderObject.setDimensionsForImage(this.image.src);


      private setDimensions()
      this.width = this.options.sprite
      ? this.image.width / this.options.sprite.frames
      : this.image.width;
      this.height = this.image.height;


      private spriteActions()
      if (!this.options.sprite)
      return;


      this.ticksCurrentFrame++;

      if (this.ticksCurrentFrame > this.options.sprite.ticksPerFrame)
      this.ticksCurrentFrame = 0;
      if (this.frameIndex < this.options.sprite.frames - 1)
      this.frameIndex++;
      else
      this.frameIndex = 0;






      Entity.ts



      import * as Calculations from "../Calculations";
      import IDirection from "../interfaces/IDirection";
      import IStatus from "../interfaces/IStatus";
      import Point from "./Point";
      import RenderObject from "./RenderObject";

      const entityList: Entity = ;
      export class Entity extends RenderObject
      public static getEntityList()
      return entityList as ReadonlyArray<Entity>;


      public static testCollision(entity1: Entity, entity2: Entity)
      return entity1.isCollidingWith(entity2);


      public health = Infinity;
      public maxHealth = Infinity;
      public speedX = 0;
      public speedY = 0;
      public impactDamage = 1;
      public status: IStatus = ;
      public faction?: string;
      public owner?: Entity;
      public acceleration = 0;
      protected turnSpeed = 0;
      protected initialized = false;

      constructor(imageSrc: string, initialPosition: Point)
      super( imageSrc, initialPosition );
      entityList.push(this);


      public update()
      if (!this.initialized)
      this.init();


      if (this.health <= 0)
      return this.destroy();


      this.processStatus();

      super.update();


      public destroy()
      const index = entityList.indexOf(this);
      entityList.splice(index, 1);
      super.destroy();


      public isCollidingWith(entity: Entity)

      public init()
      if (!this.initialized)
      this.initialized = true;
      this.maxHealth = this.health;



      protected move(directions: IDirection)
      // Angle 0 is X-axis, direction is in radians.
      const angle = this.angle * (Math.PI / 180);

      const forward = directions.forward ? 1 : 0;
      const back = directions.back ? -0.4 : 0;
      const left = directions.left ? 0.5 : 0;
      const right = directions.right ? -0.5 : 0;

      // Forward and backward.
      this.speedX =
      this.speedX + (forward + back) * this.acceleration * Math.cos(angle);
      this.speedY =
      this.speedY + (forward + back) * this.acceleration * Math.sin(angle);

      // Left and right.
      this.speedX =
      this.speedX +
      (left + right) * this.acceleration * Math.cos(angle - Math.PI / 2);
      this.speedY =
      this.speedY +
      (left + right) * this.acceleration * Math.sin(angle - Math.PI / 2);

      // Friction.
      this.speedX *= 0.97;
      this.speedY *= 0.97;

      this.center = new Point(
      this.center.x + this.speedX,
      this.center.y + this.speedY
      );


      protected turn(point: Point)
      const targetAngle = Calculations.getAngle(this.center, point);
      const turnDegrees =
      Calculations.mod(targetAngle - this.angle + 180, 360) - 180;

      if (turnDegrees > -4 && turnDegrees < 4)
      this.angle = targetAngle;
      else if (turnDegrees < 0)
      this.angle -= this.turnSpeed;
      else
      this.angle += this.turnSpeed;



      private processStatus()
      if (this.status.firing)
      this.status.firing--;
      // tslint:disable-next-line:no-unused-expression
      new RenderObject(
      angle: this.angle,
      imageSrc: "images/objects/GunFlare.png",
      initialPosition: this.center,
      lifeSpan: 1
      );


      if (this.status.takingFire)
      this.status.takingFire--;
      // tslint:disable-next-line:no-unused-expression
      new RenderObject(
      angle: this.angle,
      imageSrc: "images/objects/BulletImpact.png",
      initialPosition: this.center,
      lifeSpan: 1
      );





      Ship.ts



      import * as Calculations from "../Calculations";
      import IDirection from "../interfaces/IDirection";
      import Entity from "./Entity";
      import ExplosionSprite from "./Explosion";
      import doubleLaserShot from "./Laser";
      import Point from "./Point";
      import Projectile from "./Projectile";
      import SoundPool from "./SoundPool";

      const laserSound = new SoundPool("sound/effects/laser.wav", 0.02, 200);

      export class Ship extends Entity
      public health = 50;
      public shield = 0;
      public cooldown = 0;
      public cooldownTime = 20;
      public acceleration = 0.3;
      public inaccuracy = 100;
      public turnSpeed = 3;

      constructor(imageSrc: string, initialPosition: Point)
      super(imageSrc, initialPosition);


      public canFire()
      return this.cooldown === 0;


      public update()
      if (this.cooldown > 0)
      this.cooldown--;

      super.update();


      public destroy()
      // tslint:disable-next-line:no-unused-expression
      new ExplosionSprite(this.center);
      super.destroy();


      protected firePrimary()
      if (!this.canFire())
      return;

      this.status.firing = 1;
      doubleLaserShot(this);


      protected fireSecondary()
      if (!this.canFire())
      return;

      this.status.firing = 1;

      // const offset = this.width / 2.4;
      // tslint:disable-next-line:no-unused-expression
      new Projectile(this, "dumbFire");

      this.cooldown = this.cooldownTime;
      laserSound.play();




      PlayerShip.ts



      import MouseButtonMap from "../enums/MouseButtonMap";
      import IDirection from "../interfaces/IDirection";
      import InputController from "./InputController";
      import Point from "./Point";
      import RenderObject from "./RenderObject";
      import Ship from "./Ship";

      export class PlayerShip extends Ship
      public health = 500;
      public shield = 0;
      public acceleration = 0.8;
      public cooldownTime = 5;
      public inaccuracy = 0;
      public turnSpeed = 6;
      public alive = true;

      constructor()
      super(
      "images/objects/Firefly.png",
      new Point(window.innerWidth / 2 - 40, window.innerHeight / 2 - 40)
      );


      public update()
      this.turn(InputController.getMousePosition());

      const keysDown = InputController.getKeysDown();
      const directions: IDirection = keysDown.w,
      left: keysDown.ArrowLeft ;
      const currentPosition = this.center;
      this.move(directions);
      const newPosition = this.center;
      RenderObject.displaceAll(currentPosition, newPosition);

      if (InputController.getMouseDownButtons()[MouseButtonMap.LEFT])
      this.firePrimary();


      if (keysDown[" "])
      this.fireSecondary();


      super.update();


      public destroy()
      this.alive = false;
      super.destroy();




      EnemyShip.ts



      import * as Calculations from "../Calculations";
      import IDirection from "../interfaces/IDirection";
      import Entity from "./Entity";
      import PlayerShip from "./PlayerShip";
      import Point from './Point';
      import Ship from "./Ship";

      export class EnemyShip extends Ship
      public health = 50;
      public shield = 0;

      constructor(imageSrc = "images/objects/enemy1.png", initialPosition: Point)
      super(imageSrc, initialPosition);


      public update()
      this.actions();
      super.update();


      private findTarget()
      return Entity.getEntityList().find(p => p instanceof PlayerShip);


      private actions()
      if (this.findTarget())
      return this.attack(this.findTarget() as PlayerShip);



      private attack(target: Entity)
      this.turn(target.center);

      const playerFacing = Calculations.isFacing(target, this);
      let directions: IDirection = ;
      if (playerFacing)
      directions =
      forward: true,
      left: playerFacing > 0,
      right: playerFacing < 0
      ;
      else if (Calculations.lineDistance(this.center, target.center) < 300)
      directions =
      back: true
      ;

      this.move(directions);

      if (Calculations.chance(0.5))
      this.firePrimary();


      if (Calculations.chance(0.5))
      this.fireSecondary();





      Projectile.ts



      import getRandomInt from "../Calculations";
      import Entity from "./Entity";
      import ExplosionSprite from "./Explosion";
      import Point from "./Point";

      interface IProjectileType
      impactDamage: number;
      acceleration: number;
      maxLifeSpan: number;
      turnSpeed?: number;


      const projectileTypes: [key: string]: IProjectileType =
      dumbFire:
      acceleration: 0.1,
      impactDamage: 10,
      maxLifeSpan: 100
      ,
      homing:
      acceleration: 0.1,
      impactDamage: 10,
      maxLifeSpan: 100,
      turnSpeed: 0.8

      ;

      const image = "images/objects/bolt1.png";
      export class Projectile extends Entity
      public target?: Entity;
      constructor(owner: Entity, type: string, target?: Entity)

      public update()
      if (this.target)
      super.turn(this.target.center);

      super.move( forward: true );
      super.update();


      public destroy()
      // tslint:disable-next-line:no-unused-expression
      new ExplosionSprite(this.center);
      super.destroy();










      share|improve this question












      share|improve this question




      share|improve this question








      edited Mar 25 at 16:55









      200_success

      123k14142399




      123k14142399









      asked Mar 25 at 15:30









      Jonathan

      287113




      287113

























          active

          oldest

          votes











          Your Answer




          StackExchange.ifUsing("editor", function ()
          return StackExchange.using("mathjaxEditing", function ()
          StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix)
          StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
          );
          );
          , "mathjax-editing");

          StackExchange.ifUsing("editor", function ()
          StackExchange.using("externalEditor", function ()
          StackExchange.using("snippets", function ()
          StackExchange.snippets.init();
          );
          );
          , "code-snippets");

          StackExchange.ready(function()
          var channelOptions =
          tags: "".split(" "),
          id: "196"
          ;
          initTagRenderer("".split(" "), "".split(" "), channelOptions);

          StackExchange.using("externalEditor", function()
          // Have to fire editor after snippets, if snippets enabled
          if (StackExchange.settings.snippets.snippetsEnabled)
          StackExchange.using("snippets", function()
          createEditor();
          );

          else
          createEditor();

          );

          function createEditor()
          StackExchange.prepareEditor(
          heartbeatType: 'answer',
          convertImagesToLinks: false,
          noModals: false,
          showLowRepImageUploadWarning: true,
          reputationToPostImages: null,
          bindNavPrevention: true,
          postfix: "",
          onDemand: true,
          discardSelector: ".discard-answer"
          ,immediatelyShowMarkdownHelp:true
          );



          );








           

          draft saved


          draft discarded


















          StackExchange.ready(
          function ()
          StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f190442%2fspace-shooter-game-in-typescript%23new-answer', 'question_page');

          );

          Post as a guest



































          active

          oldest

          votes













          active

          oldest

          votes









          active

          oldest

          votes






          active

          oldest

          votes










           

          draft saved


          draft discarded


























           


          draft saved


          draft discarded














          StackExchange.ready(
          function ()
          StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f190442%2fspace-shooter-game-in-typescript%23new-answer', 'question_page');

          );

          Post as a guest













































































          Popular posts from this blog

          Python Lists

          Aion

          JavaScript Array Iteration Methods