Source: games/hide-and-seek.js

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

class ScoreBoard extends Form {
  constructor(game, callback) {
    super();
    this.game = game;
    this.seeker = game.seeker;
    this.seen = game.seen;
    this.winners = game.winners;
    this.losers = game.losers;
    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.2);
    this.grid.addColumnDefinition(0.5);
    this.grid.addColumnDefinition(0.3);

    this.grid.addRowDefinition(this.heightInPixels, true);

    let seekerScore = Object.keys(this.losers).length;
    let seekerIcon = this.makeIcon("seekerIcon", this.game.searchIcon);
    this.grid.addControl(seekerIcon, 0, 0);
    this.grid.addControl(this.textBlock(this.playerName(this.seeker)), 0, 1);
    this.grid.addControl(this.textBlock(seekerScore), 0, 2);

    let winnerIcon = this.makeIcon("winnerIcon", this.game.wonIcon);
    let loserIcon = this.makeIcon("loserIcon", this.game.lostIcon);
    let seenIcon = this.makeIcon("seenIcon", this.game.foundIcon);
    
    this.showPlayers(this.winners, 1, winnerIcon);
    this.showPlayers(this.losers, 0, loserIcon);
    this.showPlayers(this.seen , 0, seenIcon);
    let otherPlayers = {};
    this.game.players.forEach(player=>{
      let id = player.getID().toString();
      if ( !this.winners[id] && !this.losers[id] && !this.seen[id] && player != this.seeker) {
        // not found yet
        otherPlayers[id] = player;
      }
    });
    this.showPlayers(otherPlayers, 0);
        
    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));
  }

  showPlayers(obj, score, icon) {
    let keys = Object.keys(obj);
    let rowCount = this.grid.rowCount;
    for ( let i = 0; i < keys.length; i++ ) {
      this.grid.addRowDefinition(this.heightInPixels, true);
      if ( icon ) {
        this.grid.addControl(icon, i+rowCount, 0);
      }
      this.grid.addControl(this.textBlock(this.playerName(obj[keys[i]])), i+rowCount, 1);
      this.grid.addControl(this.textBlock(score), i+rowCount, 2);
    }
  }
  
  makeIcon(name, url) {
    let ret = new BABYLON.GUI.Image(name, url);
    ret.stretch = BABYLON.GUI.Image.STRETCH_UNIFORM;
    return ret;
  }
  
  playerName(vrObject) {
    if ( vrObject.name ) {
      return vrObject.name;
    }
    return "Player "+vrObject.id;
  }
}

/**
 * Super original hide and seek game!
 * 
 * Player that started the game needs to search for other players.
 * If seen, both players rush to the place where the game started, first one to arrive, scores.
 */
export class HideAndSeek extends BasicGame {
  static instance = null;
  
  constructor( world, vrObject ) {
    super(world,vrObject);
    this.camera = this.scene.activeCamera;
    this.visibilityHelper = new VisibilityHelper();
    this.fps = 5;
    this.goalRadius = 1.5;
    this.gameIcon = VRSPACEUI.contentBase + "/content/emoji/cool.png";
    this.searchIcon = VRSPACEUI.contentBase + "/content/icons/search.png";
    this.foundIcon = VRSPACEUI.contentBase + "/content/icons/eye.png";
    this.wonIcon = VRSPACEUI.contentBase + "/content/icons/tick.png";
    this.lostIcon = VRSPACEUI.contentBase + "/content/icons/close.png";
    this.sounds = {
      soundSeek: VRSPACEUI.contentBase + "/content/sound/sergeyionov__cr-water-sonar.wav",
      soundFail: VRSPACEUI.contentBase + "/content/sound/kevinvg207__wrong-buzzer.wav",
      soundVictory: VRSPACEUI.contentBase + "/content/sound/colorscrimsontears__fanfare-3-rpg.wav",
      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",
      soundEnd: VRSPACEUI.contentBase + "/content/sound/ricardus__zildjian-4ft-gong.wav"
    }
    this.gameStarted = false;
    this.invitePlayers();
    this.markStartingPosition();
    this.seeker = null;
    this.seen = {};
    this.winners = {};
    this.losers = {};
    this.materials = [];
    console.log("Players already in the game:", this.vrObject.players );
    if ( HideAndSeek.instance ) {
      throw "There can be only one";
    } else {
      HideAndSeek.instance = this;
    }
  }
  
  dispose() {
    super.dispose();
    this.visibilityHelper.dispose();
    this.visibilityHelper = null;
    if ( this.visibilityCheck ) {
      clearInterval(this.visibilityCheck);
    }
    if ( this.particleSystem ) {
      this.particleSystem.dispose();
      this.particleSystem = null;
      this.particleSource.dispose();
      this.goal.dispose();
      this.goalMaterial.dispose();
    }
    this.materials.forEach( m => m.dispose());
    HideAndSeek.instance = null;
    if ( this.callback ) {
      this.callback(false);
    }
  }
  
  markStartingPosition() {
    //if ( BABYLON.GPUParticleSystem.IsSupported ) {
      // hardware particles work differently on different devices 
      //this.particleSystem = new BABYLON.GPUParticleSystem("GoalParticles", {capacity: 100}, this.scene);
    //} else {
      this.particleSystem = new BABYLON.ParticleSystem("GoalParticles", 100, this.scene);
    //}
    this.particleSystem.disposeOnStop = true;
    this.particleSystem.particleTexture = new BABYLON.Texture(this.gameIcon, this.scene);
    this.particleSource = BABYLON.MeshBuilder.CreateSphere("particlePositon",{diameter: 0.1},this.scene);
    this.particleSource.isVisible = false;

    let pos = this.vrObject.position;
    this.particleSource.position = new BABYLON.Vector3(pos.x, pos.y, pos.z);

    this.particleSystem.emitter = this.particleSource;
    this.particleSystem.addColorGradient(0, new BABYLON.Color4(.1, .1, .1, .5), new BABYLON.Color4(.2, .2, .2, .5));
    this.particleSystem.addColorGradient(0.1, new BABYLON.Color4(1, 1, 1, 1), new BABYLON.Color4(0.9, 0.9, 0.9, 1));
    this.particleSystem.addColorGradient(0.9, new BABYLON.Color4(1, 1, 1, 1), new BABYLON.Color4(0.9, 0.9, 0.9, 1));
    this.particleSystem.addColorGradient(1, new BABYLON.Color4(.1, .1, .1, .5), new BABYLON.Color4(.2, .2, .2, .5));
    this.particleSystem.blendMode = BABYLON.ParticleSystem.BLENDMODE_STANDARD;
    
    this.particleSystem.addSizeGradient(0, 0.05); //size at start of particle lifetime
    this.particleSystem.addSizeGradient(.5, .3); //size at half lifetime
    this.particleSystem.addSizeGradient(1, .1); //size at end of particle lifetime

    this.particleSystem.addVelocityGradient(0, 1);
    this.particleSystem.addVelocityGradient(1, 0);
    this.particleSystem.maxInitialRotation=Math.PI;
    this.particleSystem.maxAngularSpeed=5*Math.PI;

    this.particleSystem.minLifeTime = 3;
    this.particleSystem.maxLifeTime = 4;

    this.particleSystem.emitRate = 10;
    
    this.particleSystem.createSphereEmitter(0.5,1);
    
    this.particleSystem.minEmitPower = 0.1;
    this.particleSystem.maxEmitPower = 1;
    this.particleSystem.updateSpeed = 0.005;
    this.particleSystem.gravity = new BABYLON.Vector3(0,-10,0);
    
    this.particleSystem.start();
    
    let animation = VRSPACEUI.createAnimation(this.particleSource, "position", 0.05);
    VRSPACEUI.updateAnimation(animation,new BABYLON.Vector3(pos.x, pos.y, pos.z),new BABYLON.Vector3(pos.x, pos.y+20, pos.z));
    
    this.goal = BABYLON.MeshBuilder.CreateCylinder("Goal", {height:.2,diameter:this.goalRadius*2}, this.scene);
    this.goal.position = new BABYLON.Vector3(pos.x, pos.y, pos.z);
    this.goalMaterial = new BABYLON.StandardMaterial("GoalMaterial",this.scene);
    this.goalMaterial.emissiveTexture = new BABYLON.Texture(this.gameIcon, this.scene);
    this.goalMaterial.disableLighting = true;
    this.goal.material = this.goalMaterial;
  }
  
  showGameStatus() {
    this.closeGameStatus();
    if ( ! this.gameStarted ) {
      super.showGameStatus();
      return;
    }
    VRSPACEUI.hud.showButtons(false);
    VRSPACEUI.hud.newRow();
    this.scoreBoard = new ScoreBoard(this, (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;
    }
  }
  
  attachSounds(baseMesh) {
    let options = {
      loop: false,
      autoplay: false,
      streaming: false,
      panningModel: "linear",
      maxDistance: 100,
      spatialSound: true
    }
    let fail = new BABYLON.Sound(
      "fail",
      this.sounds.soundFail,
      this.scene, 
      null, // callback 
      options
    );
    fail.attachToMesh(baseMesh);
    baseMesh.soundFail = fail;

    let victory = new BABYLON.Sound(
      "victory",
      this.sounds.soundVictory,
      this.scene, 
      null, // callback 
      options
    );
    victory.attachToMesh(baseMesh);
    baseMesh.soundVictory = victory;

    options.loop = true;

    let alarm = new BABYLON.Sound(
      "alarm",
      this.sounds.soundAlarm,
      this.scene, 
      null, // callback 
      options
    );
    alarm.attachToMesh(baseMesh);
    baseMesh.soundAlarm = alarm;
    
    let seek = new BABYLON.Sound(
      "alarm",
      this.sounds.soundSeek,
      this.scene, 
      null, // callback 
      options
    );
    seek.attachToMesh(baseMesh);
    baseMesh.soundSeek = seek;
  }
  
  startCountdown(delay, chatLog) {
    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.isMine() ) {
      if (chatLog) {
        chatLog.group.setEnabled(false);
      }
      this.enableMovement(false);
      this.curtain = BABYLON.MeshBuilder.CreatePlane("Curtain", {size:10}, this.scene);
      this.curtain.position = new BABYLON.Vector3(0,0,1);
      this.curtain.parent = this.camera;
      this.curtain.material = new BABYLON.StandardMaterial("CurtainMaterial",this.scene);
      this.curtain.material.diffuseColor=new BABYLON.Color4(0,0,0,0.9);
    }
    
    let countDown = setInterval( () => {
      if ( delay-- <= 0 ) {
        if (chatLog) {
          chatLog.group.setEnabled(true);
        }
        this.enableMovement(true);
        clearInterval(countDown);
        countForm.dispose();
        timerSound.dispose();
        tickSound.dispose();
        startSound.play();
        this.gameStarted = true;
        if ( this.curtain ) {
          this.curtain.dispose();
          delete this.curtain;
        }
        if ( this.isMine() ) {
          VRSPACE.sendCommand("Game", {id: this.vrObject.id, action:"start" });
          this.visibilityCheck = setInterval( () => this.checkVisibility(), 1000/this.fps);
        }
      } else {
        tickSound.play();
        countForm.update(delay);
      }
    }, 1000);
  }


  updateGameStatus(id, playerEvent, stateObject, sound, icon, color) {
    if ( ! stateObject.hasOwnProperty(id) ) {
      let player = this.changePlayerStatus(playerEvent, sound, icon, color);
      stateObject[id] = player;
    }
  }
  
  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.seen && this.playing) {
      this.updateStatus(changes.seen.className+" "+changes.seen.id, changes.seen, this.seen, "soundAlarm", this.foundIcon);
    } else if ( changes.starting ) {
      if ( this.playing ) {
        this.closeGameStatus();
        this.delay = changes.starting;
        this.startCountdown(this.delay, this.world.chatLog);
        // also add all players that joined the game before this instance was created
        this.vrObject.players.forEach(player=>this.playerJoins(player));
      } else if ( this.joinDlg ) {
        this.joinDlg.close();
        this.joinDlg = null;
      }
    } else if ( changes.start && this.playing) {
      this.gameStarted = true;
      this.seeker = this.changePlayerStatus(changes.start, "soundSeek", this.searchIcon);
    } else if (changes.won && this.playing) {
      let id = changes.won.className+" "+changes.won.id;
      this.updateGameStatus(id, changes.won, this.winners, "soundVictory", this.wonIcon, new BABYLON.Color4(0,1,0,1));
      delete this.seen[id];
      this.showGameStatus();
      this.checkEnd()
    } else if (changes.lost && this.playing) {
      let id = changes.lost.className+" "+changes.lost.id;
      this.updateGameStatus(id, changes.lost, this.losers, "soundFail", this.lostIcon, new BABYLON.Color4(1,0,0,1));
      delete this.seen[id];
      this.showGameStatus();
      this.checkEnd()
    } else if ( changes.end && this.playing) {
      this.playing = false;
      this.showGameStatus();
      let endSound = new BABYLON.Sound(
        "gong",
        this.sounds.soundEnd,
        this.scene,
        null,
        {loop: false, autoplay: true }
      );
      endSound.play();
    } else {
      console.log("Unknown/ignored notification: ", changes);
    }
  }
 
  inGoalRange(pos) {
    let dx = pos.x - this.goal.position.x;
    let dz = pos.z - this.goal.position.z;
    let radius = Math.sqrt( dx*dx + dz*dz );
    return radius <= this.goalRadius;
  }
  
  checkEnd() {
    if ( this.isMine() && Object.keys(this.winners).length + Object.keys(this.losers).length + 1 == this.players.length ) {
      VRSPACE.sendEvent(this.vrObject, {end: Date.now() - this.startTime });
    }
  }
  
  checkVisibility() {
    let visible = this.visibilityHelper.getVisibleUsers(this.players);
    if ( visible.length > 0 ) {
      // anyone not seen before?
      visible.forEach( (user) => {
        let id = user.className+" "+user.id;
        if ( ! this.seen.hasOwnProperty(id) && !this.winners.hasOwnProperty(id) && !this.losers.hasOwnProperty(id)) {
          this.updateGameStatus(id, user, this.seen, "soundAlarm", this.foundIcon);
          VRSPACE.sendEvent(this.vrObject, {seen: {className: user.className, id: user.id} });
        }
      });
      // anyone at the goal area?
      for ( let id in this.seen ) {
        let vrObject = this.seen[id];
        if ( !this.winners.hasOwnProperty(id) && !this.losers.hasOwnProperty(id) && this.inGoalRange(vrObject.avatar.baseMesh().position) ) {
          VRSPACE.sendEvent(this.vrObject, {won: {className: vrObject.className, id: vrObject.id} });
        }
      }
    }
    // am I at the goal area?
    if ( this.inGoalRange(this.avatarPosition()) ) {
      for ( let id in this.seen ) {
        if ( !this.winners.hasOwnProperty(id) && !this.losers.hasOwnProperty(id) ) {
          let vrObject = this.seen[id];
          this.updateGameStatus(id, vrObject, this.losers, "soundFail", this.lostIcon, new BABYLON.Color4(1,0,0,1));
          VRSPACE.sendEvent(this.vrObject, {lost: {className: vrObject.className, id: vrObject.id} });
       }
      }
    }
    
  }
 
  static createOrJoinInstance(callback) {
    if ( HideAndSeek.instance ) {
      // already exists
      if ( ! HideAndSeek.instance.callback ) {
        HideAndSeek.instance.callback = callback;
      }
      HideAndSeek.instance.startRequested();
    } else if (VRSPACE.me) {
      VRSPACE.createScriptedObject({
        name: "Hide and Seek",
        properties: { clientId: VRSPACE.me.id },
        active: true,
        script: '/babylon/js/games/hide-and-seek.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");
    }
  }
  
}