From ddc99e34982e64b65c6c703b8c8b53c47e49f909 Mon Sep 17 00:00:00 2001
From: Paolo Brasolin <paolo.brasolin@eurac.edu>
Date: Mon, 14 Mar 2022 21:21:19 +0100
Subject: [PATCH] feat: #fe refactor spear into its own class

---
 frontend/src/js/fight_scene.ts | 124 ++++++++++-----------------------
 frontend/src/js/foe.ts         |   7 ++
 frontend/src/js/main.ts        |   4 +-
 frontend/src/js/spear.ts       |  84 ++++++++++++++++++++++
 4 files changed, 129 insertions(+), 90 deletions(-)
 create mode 100644 frontend/src/js/spear.ts

diff --git a/frontend/src/js/fight_scene.ts b/frontend/src/js/fight_scene.ts
index cc5cc1f..786eb9f 100644
--- a/frontend/src/js/fight_scene.ts
+++ b/frontend/src/js/fight_scene.ts
@@ -1,22 +1,20 @@
 import "phaser";
 
 import Foe from "./foe";
+import Spear from "./spear";
 import backend from "./backend";
 
 // TODO: write interfaces
-import nr from "newton-raphson-method";
 import levenshtein from "damerau-levenshtein";
 
 export default class FightScene extends Phaser.Scene {
   foes: Array<Foe>;
-  spears: Array<Phaser.GameObjects.Sprite>;
   ground: Phaser.Types.Physics.Arcade.ImageWithStaticBody;
   player: Phaser.Types.Physics.Arcade.SpriteWithDynamicBody;
 
   constructor() {
     super("fight");
     this.foes = [];
-    this.spears = [];
   }
 
   preload() {
@@ -164,11 +162,39 @@ export default class FightScene extends Phaser.Scene {
     initAndBindGuessPreview(this);
   }
 
-  update() {
-    // TODO: re-enable
-    // this.spears.forEach((spear) => {
-    //   spear.setRotation(spear.body.velocity.angle());
-    // });
+  showMissMessage() {
+    const message = this.add
+      .sprite(this.cameras.main.width / 2, this.cameras.main.height / 2, "miss")
+      .setScale(1);
+    message.play({ key: "missing", repeat: 1 });
+    message.on("animationcomplete", () => {
+      message.anims.remove("miss");
+      message.destroy();
+    });
+  }
+
+  showHitMessage() {
+    const message = this.add
+      .sprite(this.cameras.main.width / 2, this.cameras.main.height / 2, "hit")
+      .setScale(1);
+    message.play({ key: "hit", repeat: 1 });
+    message.on("animationcomplete", () => {
+      message.anims.remove("hit");
+      message.destroy();
+    });
+  }
+
+  shootSpear(enemy: Phaser.GameObjects.Sprite, hit: boolean) {
+    const scene = this;
+    if (!hit) {
+      this.showMissMessage();
+    } else {
+      this.showHitMessage();
+      // TODO: ew.
+      scene.foes.splice(scene.foes.indexOf(enemy), 1); // FIXME
+    }
+
+    new Spear(this, this.player, enemy);
   }
 }
 
@@ -250,86 +276,6 @@ function initAndBindGuessPreview(scene: FightScene) {
   );
 }
 
-function shootSpear(
-  enemy: Phaser.GameObjects.Sprite,
-  hit: boolean,
-  scene: FightScene,
-) {
-  if (!hit) {
-    const message = scene.add
-      .sprite(
-        scene.cameras.main.width / 2,
-        scene.cameras.main.height / 2,
-        "miss",
-      )
-      .setScale(1);
-    message.play({ key: "missing", repeat: 1 });
-    message.on("animationcomplete", () => {
-      message.anims.remove("miss");
-      message.destroy();
-    });
-  } else {
-    const message = scene.add
-      .sprite(
-        scene.cameras.main.width / 2,
-        scene.cameras.main.height / 2,
-        "hit",
-      )
-      .setScale(1);
-    message.play({ key: "hit", repeat: 1 });
-    message.on("animationcomplete", () => {
-      message.anims.remove("hit");
-      message.destroy();
-    });
-    // TODO: ew.
-    scene.foes.splice(scene.foes.indexOf(enemy), 1); // FIXME
-  }
-
-  const spear = scene.add.sprite(scene.player.x, scene.player.y, "spear");
-  scene.spears.push(spear);
-  scene.physics.world.enable(spear);
-  scene.physics.add.collider(spear, scene.ground);
-  spear.body.setBounce(0.2);
-
-  const dx = scene.player.x - enemy.x;
-  const dy = 0;
-  const v = 450; // MAGIC NUMBER
-  const w = 100;
-  const g = 200;
-  // TODO: maybe introduce damping
-  // TODO: expand and use analytic derivative
-  const f = (theta) =>
-    2 * dy * Math.pow(w - v * Math.cos(theta), 2) +
-    2 * v * Math.sin(theta) * (w - v * Math.cos(theta)) * dx +
-    g * Math.pow(dx, 2);
-
-  const theta = nr(f, Math.PI, {
-    verbose: true,
-    maxIterations: 100,
-  });
-
-  const t = dx / (w - v * Math.cos(theta));
-
-  if (theta) {
-    spear.body.setVelocity(v * Math.cos(theta), v * Math.sin(theta));
-  }
-
-  scene.physics.add.overlap(spear, enemy, (player, nemico) => {
-    scene.physics.world.removeCollider(this);
-    // TODO: fancy bounce
-    scene.spears.splice(scene.spears.indexOf(spear), 1);
-    spear.destroy();
-    // TODO: refactor into flee method
-    nemico.play(nemico.species + "_run");
-    nemico.flipX = false;
-    nemico.body.setVelocity(-200, 0);
-    setTimeout(() => nemico.destroy(), 2000);
-  });
-
-  spear.scale = 2;
-  spear.anims.play("spearAni");
-}
-
 function submitTranscription(transcription: string, scene: FightScene) {
   if (scene.foes.length < 1) return;
 
@@ -354,7 +300,7 @@ function submitTranscription(transcription: string, scene: FightScene) {
   const hit = similarity >= 0.9;
   const enemy = match;
 
-  shootSpear(enemy.animalSprite, hit, scene);
+  scene.shootSpear(enemy.animalSprite, hit);
 }
 
 function gameStart(scene: any) {
diff --git a/frontend/src/js/foe.ts b/frontend/src/js/foe.ts
index 14c62fb..ccf208c 100644
--- a/frontend/src/js/foe.ts
+++ b/frontend/src/js/foe.ts
@@ -53,6 +53,13 @@ class Foe {
       .setInteractive();
     this.animalSprite.flipX = true;
 
+    this.animalSprite.flee = function () {
+      this.play(this.species + "_run");
+      this.flipX = false;
+      this.body.setVelocity(-200, 0);
+      setTimeout(() => this.destroy(), 2000); // TODO: disappear offscreen
+    };
+
     this.scene.physics.add.collider(this.animalSprite, this.scene.ground);
 
     setAnimation(this.animalSprite, this.species + "_walk");
diff --git a/frontend/src/js/main.ts b/frontend/src/js/main.ts
index 4687c97..cadc460 100644
--- a/frontend/src/js/main.ts
+++ b/frontend/src/js/main.ts
@@ -2,6 +2,8 @@ import * as Phaser from "phaser";
 
 import FightScene from "./fight_scene";
 
+export const GRAVITY_Y = 200;
+
 const config = {
   type: Phaser.AUTO,
   width: 1200,
@@ -12,7 +14,7 @@ const config = {
   physics: {
     default: "arcade",
     arcade: {
-      gravity: { y: 200 },
+      gravity: { y: GRAVITY_Y },
       debug: true,
     },
   },
diff --git a/frontend/src/js/spear.ts b/frontend/src/js/spear.ts
new file mode 100644
index 0000000..c017b72
--- /dev/null
+++ b/frontend/src/js/spear.ts
@@ -0,0 +1,84 @@
+import "phaser";
+import FightScene from "./fight_scene";
+
+import { GRAVITY_Y } from "./main";
+import newtonRaphson from "newton-raphson-method"; // TODO: TS signatures
+
+const SPEED = 450;
+
+class Spear extends Phaser.Physics.Arcade.Sprite {
+  source: Phaser.GameObjects.Sprite;
+  target: Phaser.GameObjects.Sprite;
+  body: Phaser.Physics.Arcade.Body;
+
+  constructor(
+    scene: FightScene,
+    source: Phaser.GameObjects.Sprite,
+    target: Phaser.GameObjects.Sprite,
+  ) {
+    super(scene, scene.player.x, scene.player.y, "spear");
+    scene.add.existing(this);
+
+    this.setScale(3);
+
+    this.source = source;
+    this.target = target;
+
+    //scene.physics.world.enableBody(this, Phaser.Physics.Arcade.DYNAMIC_BODY);
+    this.body = new Phaser.Physics.Arcade.Body(scene.physics.world, this);
+    scene.physics.world.add(this.body);
+    scene.physics.add.collider(this, scene.ground);
+    this.body.setBounce(0, 0.2); // TODO: bounce only at small angles
+
+    const theta = this.calculateSuccessfulLaunchAngle(source, target);
+
+    if (theta) {
+      this.body.setVelocity(SPEED * Math.cos(theta), SPEED * Math.sin(theta));
+    } else {
+      console.error("Cannot hit foe.");
+    }
+
+    scene.physics.add.overlap(this, this.target, (_, hitTarget) => {
+      scene.physics.world.removeCollider(this);
+      this.destroy();
+      hitTarget.flee();
+    });
+
+    // this.anims.play("spearAni");
+  }
+
+  preUpdate(): void {
+    const velocity = this.body.velocity as Phaser.Math.Vector2;
+    this.setRotation(velocity.angle());
+  }
+
+  calculateSuccessfulLaunchAngle(
+    source: Phaser.GameObjects.Sprite,
+    target: Phaser.GameObjects.Sprite,
+  ): number | undefined {
+    const dx = source.x - target.x;
+    const dy = source.y - target.y;
+    const v = SPEED; // NOTE: this is a MAGIC NUMBER
+    const w = target.body.velocity.x;
+    const g = GRAVITY_Y;
+
+    // TODO: air drag
+    // TODO: damp x velocity on impact
+
+    // NOTE: this is an implicit function to solve numerically for finding launch angle
+    const f = (theta: number) =>
+      2 * dy * Math.pow(w - v * Math.cos(theta), 2) +
+      2 * v * Math.sin(theta) * (w - v * Math.cos(theta)) * dx +
+      g * Math.pow(dx, 2);
+
+    // TODO: expand and use analytic derivative for better precision
+    const theta = newtonRaphson(f, Math.PI, {
+      verbose: true,
+      maxIterations: 100,
+    });
+
+    return typeof theta == "number" ? theta : undefined;
+  }
+}
+
+export default Spear;
-- 
GitLab