<file_sep>import Ship from './Ship';

export default class MainScene extends Phaser.Scene {
  private phaserSprite: Phaser.GameObjects.Sprite;
  private ship: Ship;
  private cursors: CursorKeys;

  constructor() {
    super({
      key: "MainScene"
    });
  }

  preload(): void {
    this.load.image("ship", "../assets/games/endlessRunner/head.png");
    this.load.image("projectile", "../assets/games/endlessRunner/head.png");
  }

  create(): void {
    this.ship = new Ship(this, 50, 50);
    this.cursors = this.input.keyboard.createCursorKeys();

    this.input.on('pointerdown', pointer => this.ship.startFire(pointer));
    this.input.on('pointerup', () => this.ship.stopFire());
  }

  update(): void {
    const movement = new Phaser.Math.Vector2(0, 0);

    if (this.cursors.down.isDown) {
      movement.y += 4;
    } else if (this.cursors.up.isDown) {
      movement.y -= 4;
    }

    if (this.cursors.left.isDown) {
      movement.x -= 4;
    } else if (this.cursors.right.isDown) {
      movement.x += 4;
    }

    this.ship.move(movement);
  }
}
<file_sep>const { cos, sin } = Math;

export const rotateVector2 = (a: number, v: Vec2): Phaser.Math.Vector2 => {
  const ca = cos(a);
  const sa = sin(a);

  return new Phaser.Math.Vector2(
    ca * v.x - sa * v.y,
    sa * v.x + ca * v.y,
  );
};

<file_sep>import Projectile from './Projectile';
import { rotateVector2 } from './utils';

const speed = 12;

export default class ProjectileEmitter {
  private projectiles: Phaser.GameObjects.Group;
  private firing: boolean = false;

  private getFocus: () => Vec2;
  private getOrigin: () => Vec2;

  private fireTicks: number = 5;
  private ticksSinceLastFire: number = 0;

  constructor(
    public scene: Phaser.Scene,
  ) {
    this.projectiles = scene.add.group([]);
  }

  private getNextProjectileCount(): number {
    return 7;
  }

  private getProjAngleSpacing(
    focusDistance: number,
    projectileCount: number,
  ): number {
    const low = 50;
    const high = 300;
    const t = 1 - (Phaser.Math.Clamp(focusDistance, low, high) - low) / (high - low);

    const lowArc = Math.PI / 12;
    const highArc = Math.PI / 3;
    const a = t * t * (highArc - lowArc) + lowArc;

    return a / projectileCount;
  }

  private fire() {
    const origin = this.getOrigin();
    const focus = this.getFocus();
    const projCount = this.getNextProjectileCount();

    const vFocus = new Phaser.Math.Vector2(
      focus.x - origin.x,
      focus.y - origin.y,
    );
    const focusDistance = vFocus.length();

    // don't fire if the focus point is too close to the origin
    if (focusDistance < .1) return;

    const projAngleSpacing = this.getProjAngleSpacing(focusDistance, projCount);
    vFocus.scale(speed / focusDistance);

    for (let i = 0; i < projCount; i++) {
      const v = rotateVector2(
        projAngleSpacing * (i - (projCount - 1) / 2),
        vFocus,
      );

      const proj = new Projectile(this.scene, {
        x: origin.x,
        y: origin.y,
        vx: v.x,
        vy: v.y,
      });

      this.projectiles.add(proj);
    }
  }

  public startFire(
    getOrigin: () => Vec2,
    getFocus: () => Vec2,
  ) {
    this.getOrigin = getOrigin;
    this.getFocus = getFocus;
    this.firing = true;
  }

  public stopFire() {
    this.firing = false;
  }

  update() {
    this.ticksSinceLastFire++;
    if (this.ticksSinceLastFire >= this.fireTicks) {
      if (this.firing) {
        this.fire();
        this.ticksSinceLastFire = 0;
      }
    }
  }
}
<file_sep>import ProjectileEmitter from './ProjectileEmitter';

export default class Ship extends Phaser.GameObjects.Sprite {
  private emitter: ProjectileEmitter;

  constructor(scene: Phaser.Scene, x: number, y: number) {
    super(scene, x, y, 'ship');
    scene.add.existing(this);
    scene.events.on('update', this.update.bind(this));

    this.emitter = new ProjectileEmitter(scene);
  }

  move(v: Phaser.Math.Vector2): void {
    this.x += v.x;
    this.y += v.y;
  }

  public startFire(pointer: Phaser.Input.Pointer): void {
    this.emitter.startFire(
      () => this,
      () => pointer,
    );
  }

  public stopFire() {
    this.emitter.stopFire();
  }

  update() {
    this.emitter.update();
  }
}
<file_sep>interface ProjectileConfig {
  x: number,
  y: number,
  vx: number,
  vy: number,
}

export default class Projectile extends Phaser.GameObjects.Sprite {
  constructor(
    public scene: Phaser.Scene,
    public config: ProjectileConfig,
  ) {
    super(scene, config.x, config.y, 'projectile');
    scene.add.existing(this);

    this.scaleX = 0.2;
    this.scaleY = 0.2;

    scene.events.on('update', this.update.bind(this));
  }

  update() {
    const { vx, vy } = this.config;
    this.x += vx;
    this.y += vy;
  }
}
