Source: games/game-tag.js

import { VRSPACE } from "../client/vrspace.js";
import { VRSPACEUI } from '../ui/vrspace-ui.js';
import { BasicGame } from './basic-game.js';
import { CountdownForm } from './countdown-form.js'
import { Form } from '../ui/widget/form.js';

class Scoreboard extends Form {
  constructor(players, callback) {
    super();
    this.players = players;
    this.callback = callback;
  }
  
  init() {
    this.createPanel();
    this.grid = new BABYLON.GUI.Grid();
    this.horizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT;
    this.grid.horizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT;
    
    this.grid.addColumnDefinition(0.7);
    this.grid.addColumnDefinition(0.3);

    this.players.forEach((player,index)=>{
      let score = typeof player.tagScore == "undefined"?0:player.tagScore;
      this.grid.addRowDefinition(this.heightInPixels, true);
      this.grid.addControl(this.textBlock(this.playerName(player)), index, 0);
      this.grid.addControl(this.textBlock(score), index, 1);
    });

    this.addControl(this.grid);

    this.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_BOTTOM;
    //if ( this.game.playing ) {
      let closeButton = this.textButton("OK", () => this.callback(false), VRSPACEUI.contentBase+"/content/icons/tick.png", "green");
      this.addControl(closeButton);
    //}
    let quitButton = this.textButton("Quit", () => this.callback(true), VRSPACEUI.contentBase+"/content/icons/close.png", "red");
    this.addControl(quitButton);
    
    VRSPACEUI.hud.addForm(this,512,this.heightInPixels*(this.grid.rowCount+1));
  }

  playerName(vrObject) {
    if ( vrObject.name ) {
      return vrObject.name;
    }
    return "Player "+vrObject.id;
  }
}

/**
 * Tag has a lot of meanings and uses, class name contains Game to avoid confusion.
 * 
 * In the game of tag, players try to catch each other. Player caught becomes the hunter.
 */
export class GameTag extends BasicGame {
  static instance = null;
  
  constructor( world, vrObject ) {
    super(world,vrObject);
    this.fps = 5;
    this.catchRadius = 1;
    this.delay = 3;
    this.minDelay = 1;
    this.maxDelay = 5;
    this.sounds = {
      soundAlarm: VRSPACEUI.contentBase + "/content/sound/bowesy__alarm.wav",
      soundClock: VRSPACEUI.contentBase + "/content/sound/deadrobotmusic__sprinkler-timer-loop.wav",
      soundTick: VRSPACEUI.contentBase + "/content/sound/fupicat__videogame-menu-highlight.wav",
      soundStart: VRSPACEUI.contentBase + "/content/sound/ricardus__zildjian-4ft-gong.wav",
      soundVictory: VRSPACEUI.contentBase + "/content/sound/colorscrimsontears__fanfare-3-rpg.wav"
    }
    this.chaseIcon = VRSPACEUI.contentBase + "/content/icons/man-run.png";
    this.targetIcon = VRSPACEUI.contentBase + "/content/icons/target-aim.png";
    this.camera = this.scene.activeCamera;
    this.speed = this.camera.speed;
    this.gameStateCheck = null;
    this.counting = false;
    this.scoreboard = null;
    this.invitePlayers();
    if ( GameTag.instance ) {
      throw "There can be only one";
    } else {
      GameTag.instance = this;
    }
  }

  dispose() {
    super.dispose();
    this.camera.speed = this.speed;
    this.players.filter(player=>typeof player.tagScore != "undefined").forEach(player=>delete player.tagScore);
    GameTag.instance = null;
    if ( this.callback ) {
      this.callback(false);
    }
  }
  
  static createOrJoinInstance(callback) {
    if ( GameTag.instance ) {
      // already exists
      if ( ! GameTag.instance.callback ) {
        GameTag.instance.callback = callback;
      }
      GameTag.instance.startRequested();
    } else if (VRSPACE.me) {
      VRSPACE.createScriptedObject({
        name: "Game of Tag",
        properties: { clientId: VRSPACE.me.id },
        active: true,
        script: '/babylon/js/games/game-tag.js'
      }, "Game").then( obj => {
        obj.addLoadListener((obj, loaded)=>{
          //console.log("Game script loaded: ", obj);
          obj.attachedScript.callback=callback;
        });
        console.log("Created new script ", obj);
      });
    } else {
      console.error("Attemting to start the game before entering a world");
    }
  }
 
  startCountdown(delay) {
    this.counting = true;
    let countForm = new CountdownForm(delay);
    countForm.init();
    let timerSound = new BABYLON.Sound(
      "clock",
      this.sounds.soundClock,
      this.scene,
      null,
      {loop: true, autoplay: true}
    );
    timerSound.play();
    let tickSound = new BABYLON.Sound(
      "clock",
      this.sounds.soundTick,
      this.scene,
      null,
      {loop: false, autoplay: true }
    );
    let startSound = new BABYLON.Sound(
      "gong",
      this.sounds.soundStart,
      this.scene,
      null,
      {loop: false, autoplay: false }
    );

    if ( !this.hunter && this.isMine() || VRSPACE.me == this.hunter ) {
      this.enableMovement(false);
      this.camera.speed = this.speed * 1.2;
    } else {
      this.camera.speed = this.speed;
    }
    
    let countDown = setInterval( () => {
      if ( delay-- <= 0 ) {
        this.counting = false;
        this.enableMovement(true);
        clearInterval(countDown);
        countForm.dispose();
        timerSound.dispose();
        tickSound.dispose();
        startSound.play();
        this.gameStarted = true;
        if ( this.isMine() && ! this.gameStateCheck) {
          VRSPACE.sendCommand("Game", {id: this.vrObject.id, action:"start" });
          this.gameStateCheck = setInterval( () => this.checkGameState(), 1000/this.fps);
        }
      } else {
        tickSound.play();
        countForm.update(delay);
      }
    }, 1000);
  }

  inRange(pos,target,range) {
    let dx = pos.x - target.x;
    let dz = pos.z - target.z;
    let radius = Math.sqrt( dx*dx + dz*dz );
    let ret = radius <= range && Math.abs(pos.y - target.y) <= range;
    //console.log("pos: "+pos+" target: "+target+" range: "+range+" radius: "+radius+" "+ret);
    return ret;
  }

  playerPosition(player) {
    if ( player == VRSPACE.me ) {
      // does not have avatar, VRObject position may not be updated, may be in 3rd person view
      return this.avatarPosition();
    }
    return player.position;
  }
  
  checkGameState() {
    if ( this.counting ) {
      return;
    }
    let caught = this.players.find(player=>{
      return player!=this.hunter && this.inRange(this.playerPosition(this.hunter), this.playerPosition(player), this.catchRadius)
    });
    if ( caught ) {
      this.counting = true;
      VRSPACE.sendEvent(this.vrObject, {caught: {className: caught.className, id: caught.id} });
    }
  }
  
  attachSounds(baseMesh) {
    let options = {
      loop: true,
      autoplay: false,
      streaming: false,
      panningModel: "linear",
      maxDistance: 100,
      spatialSound: true
    }
    let alarm = new BABYLON.Sound(
      "soundAlarm",
      this.sounds.soundAlarm,
      this.scene, 
      null, // callback 
      options
    );
    alarm.attachToMesh(baseMesh);
    baseMesh.soundAlarm = alarm;
  }
 
  showGameStatus() {
    this.closeGameStatus();
    if ( ! this.gameStarted ) {
      super.showGameStatus();
      return;
    }
    VRSPACEUI.hud.showButtons(false);
    VRSPACEUI.hud.newRow();
    this.scoreboard = new Scoreboard(this.players, (quit)=>{
      this.closeGameStatus();
      if ( quit ) {
        this.quitGame();
      }
    });
    this.scoreboard.init();
  }

  closeGameStatus() {
    super.closeGameStatus();
    if ( this.scoreboard ) {
      VRSPACEUI.hud.clearRow();
      VRSPACEUI.hud.showButtons(true);
      this.scoreboard.dispose();
      this.scoreboard = null;
    }
  }
 
  increaseScore() {
    if ( typeof this.hunter.tagScore == "undefined" ) {
      this.hunter.tagScore = 0;
    }
    this.hunter.tagScore++;
  }
  
  remoteChange(vrObject, changes) {
    console.log("Remote changes for "+vrObject.id, changes);
    if ( changes.joined ) {
      this.totalPlayers++;
      this.updateStatus();
      this.playerJoins(changes.joined);
    } else if ( changes.quit ) {
      this.totalPlayers--;
      this.updateStatus();
      this.playerQuits(changes.quit);
    } else if ( changes.starting ) {
      if ( this.playing ) {
        this.closeGameStatus();
        this.delay = changes.starting;
        // also add all players that joined the game before this instance was created
        this.vrObject.players.forEach(player=>this.playerJoins(player));
        this.startCountdown(this.delay);
      } else if ( this.joinDlg ) {
        this.joinDlg.close();
        this.joinDlg = null;
      }
    } else if ( changes.start && this.playing ) {
      this.gameStarted = true;
      this.hunter = this.changePlayerStatus(changes.start, "soundAlarm", this.chaseIcon);
      this.players.filter(player => player != this.hunter).forEach((player)=>{
        this.changePlayerStatus(player, null, this.targetIcon);
      });
    } else if ( changes.caught && this.playing ) {
      this.increaseScore();
      this.changePlayerStatus(this.hunter, null, this.targetIcon);
      // hunter needs to be known before countdown (disables movement)
      this.hunter = this.changePlayerStatus(changes.caught, "soundAlarm", this.chaseIcon);
      this.startCountdown(this.delay);
    } else {
      console.log("Unknown/ignored notification: ", changes);
    }
  }

}