Browse Source

init vuex store ; heroes displayer ; server dev to connect two players

jojo 4 years ago
parent
commit
e7ce80c3e9

+ 2 - 1
package.json

@@ -10,7 +10,8 @@
     "dev": "vue-cli-service serve --open",
     "server": "cd server && npm start",
     "server-dev": "cd server && npm run server-dev",
-    "tu": "vue-cli-service test:unit --watch"
+    "tu": "vue-cli-service test:unit --watch",
+    "show":"ssh jojo@149.91.81.94 -p 1988 -nNT -o ServerAliveInterval=30 -R 8085:localhost:8080"
   },
   "dependencies": {
     "bootstrap": "^4.4.1",

+ 14 - 1
server/db/mariadb-connector.js

@@ -178,7 +178,7 @@ export default class MariadbConnector {
     try {
       conn = await this.pool.getConnection();
       const res = await conn.query(
-        "SELECT * FROM`games` WHERE (status = 'CREATED' OR status = 'PAUSED')  AND(player1 = ? OR player2 = '' OR player2 = ?)",
+        "SELECT * FROM games WHERE (status = 'CREATED' OR status = 'PAUSED')  AND(player1 = ? OR player2 = '' OR player2 = ?)",
         [username, username]
       );
       for (const { id, player1, player2, deck, adv_rules } of res) {
@@ -192,6 +192,19 @@ export default class MariadbConnector {
     }
     return games;
   }
+  async getGameById(gameId) {
+    let conn;
+    let game = {};
+    try {
+      conn = await this.pool.getConnection();
+      game = await conn.query('SELECT * FROM games WHERE id=?', gameId);
+    } finally {
+      if (conn && conn !== null) {
+        conn.end();
+      }
+    }
+    return game;
+  }
 }
 let convertAdvRulesToString = function(advRulesArray) {
   let advRulestStr = '';

+ 0 - 7
server/game-server/duel-controller.js

@@ -1,7 +0,0 @@
-'use strict';
-
-export default class DuelController {
-  constructor(playerOne, playerTwo) {
-    console.log('DuelCtrl constructor : ' + playerOne.playerName + ' and : ' + playerTwo.playerName);
-  }
-}

+ 47 - 0
server/game-server/games-manager.js

@@ -0,0 +1,47 @@
+import OnlineDuelSync from './online-duel-sync';
+
+export default function GamesManager(ioServer, mariadbConn) {
+  let currentGames = new Map();
+
+  // Method to add player into a game, create or add to game syncer
+  let addPlayerInGame = function(player, gameId, joinCreatedGame) {
+    if (currentGames.has(gameId)) {
+      player.isPlayingGameId = gameId;
+      return currentGames.get(gameId).addPlayer(player);
+    } else if (joinCreatedGame === false) {
+      player.isPlayingGameId = gameId;
+      currentGames.set(
+        gameId,
+        new OnlineDuelSync(ioServer, mariadbConn, gameId, player)
+      );
+      return true;
+
+      // We are supposed to join a new game, but not there, must have been deleted in the meantime
+    } else {
+      console.log(
+        'error : ' +
+          player.playerName +
+          ' tries to join a created game that does not exist anymore'
+      );
+      return false;
+    }
+  };
+
+  let playerLeft = function(player, disconnected) {
+    let id = player.isPlayingGameId;
+
+    if (currentGames.has(id)) {
+      currentGames.get(id).playerLeft(player, disconnected);
+      if (!currentGames.get(id).hasPlayers()) {
+        currentGames.delete(id);
+      }
+    }
+
+    player.isPlayingGameId = -1;
+  };
+
+  return {
+    addPlayerInGame,
+    playerLeft
+  };
+}

+ 97 - 0
server/game-server/online-duel-sync.js

@@ -0,0 +1,97 @@
+const fs = require('fs');
+
+export default function OnlineDuelSync(
+  ioServer,
+  mariadbConnector,
+  gameId,
+  firstPlayer
+) {
+  let io = ioServer;
+  let mdb = mariadbConnector;
+  let players = new Map();
+  players.set(firstPlayer.playerName, firstPlayer);
+
+  let loadGameData = function() {
+    let gameDetails = {};
+    try {
+      gameDetails = mdb.getGameById(gameId);
+    } catch (err) {
+      console.log('Error retreiving game : ', err.message);
+    }
+    return gameDetails;
+  };
+
+  let chat = function(name, message) {
+    io.to(gameId).emit('message', {
+      from: name,
+      text: message
+    });
+  };
+
+  firstPlayer
+    .getSocket()
+    .join(gameId, () => {
+      chat(
+        firstPlayer.playerName,
+        firstPlayer.playerName +
+          ' has entered the game, waiting for second player...'
+      );
+    })
+    .emit('update-game-data', loadGameData().game_data)
+    .on('chat', message => {
+      chat(firstPlayer.playerName, message);
+    });
+
+  let addPlayer = function(player) {
+    let res = true;
+    if (players.size === 1) {
+      if (players.has(player.playerName)) {
+        console.log(
+          'ERROR :  this player is already there ! ',
+          player.playerName
+        );
+        return false;
+      }
+      players.set(player.playerName, player);
+      player
+        .getSocket()
+        .join(gameId, () => {
+          chat(player.playerName, player.playerName + ' has entered the game');
+        })
+        .emit('update-game-data', loadGameData().game_data)
+        .on('chat', message => {
+          chat(player.playerName, message);
+        });
+    } else {
+      console.log(
+        'Already 2 players, weird :' +
+          player.playerName +
+          ' was trying to join : ',
+        players
+      );
+      res = false;
+    }
+    return res;
+  };
+
+  let playerLeft = function(player, disconnected) {
+    let playerLeaving = players.get(player.playerName);
+    if (playerLeaving) {
+      if (!disconnected) {
+        playerLeaving.getSocket().leave(gameId);
+      }
+
+      chat(player.playerName, player.playerName + ' has left the game');
+      players.delete(player.playerName);
+    }
+  };
+
+  let hasPlayers = function() {
+    return players.size > 0;
+  };
+  return {
+    addPlayer,
+    playerLeft,
+    hasPlayers
+  };
+}

+ 3 - 3
server/players/player-id.js

@@ -1,11 +1,11 @@
 'use strict';
 
 export default class PlayerId {
-  constructor(playerName = '', playerColor = 'unk') {
+  constructor(playerName = '') {
     this.playerSocket = null;
     this.playerName = playerName;
-    this.playerColor = playerColor;
-    this.connected=false;
+    this.connected = false;
+    this.isPlayingGameId = -1;
   }
 
   getSocket() {

+ 56 - 34
server/server.js

@@ -1,6 +1,6 @@
 'use strict';
 import PlayerId from './players/player-id';
-import DuelController from './game-server/duel-controller';
+import GamesManager from './game-server/games-manager';
 import MariadbConnector from './db/mariadb-connector';
 import ServerToolListner from './tools/server-tool-listener';
 
@@ -11,49 +11,44 @@ function Server() {
   const server = io.listen(2610);
 
   let mariadbConn = new MariadbConnector();
+  let gamesManager = new GamesManager(server, mariadbConn);
 
   let authorizedPlayers = new Map();
   let connectedPlayers = new Map();
   let authorizedPlayersNames = new Set();
 
-  // let players = [];
-
-  // let addNewPlayer = function (playerSocket, playerName) {
-  //   console.log('players length : ' + players.length + ' : ' + players);
-  //   if (players.length < 2) {
-  //     let newPlayer = new PlayerId(playerSocket, playerName);
-  //     console.log('push player : ' + newPlayer);
-  //     players.push(newPlayer);
-  //   }
-  //   if (players.length === 2) {
-  //     let duelController = new DuelController(players[0], players[1]);
-  //   }
-  // };
-
   server.on('connection', function(socket) {
     console.log('A player connected with id : ' + socket.id);
 
     socket.on('disconnect', reason => {
-      try {
-        removeAllGamesCreatedByPlayer(connectedPlayers.get(socket.id));
-      } catch (err) {
-        console.log('Error removing games : ' + err.message);
-      }
-      forceClientsReloadGames();
+      console.log('A player disconnected, reason : ' + reason);
       if (
         reason === 'client namespace disconnect' ||
-        reason === 'server namespace disconnect'
+        reason === 'server namespace disconnect' ||
+        reason === 'transport close'
       ) {
-        console.log('A player disconnected with id : ' + socket.id);
-        let playerName = connectedPlayers.get(socket.id);
-        if (playerName) {
+        let username = connectedPlayers.get(socket.id);
+        if (username) {
+          let player = authorizedPlayers.get(username);
+
+          // First, tell game manager a player left, is he was playing
+          if (player.isPlayingGameId >= 0) {
+            gamesManager.playerLeft(player, true);
+          }
+          // If player had created a game, remove it
+          removeAllGamesCreatedByPlayer(username)
+            .then(res => {
+              if (res.affectedRows > 0) {
+                // Tell other clients to reload their games if there were
+                forceClientsReloadGames();
+              }
+            })
+            .catch(err => console.log('Error removing game :>> ', err.message));
           connectedPlayers.delete(socket.id);
-          authorizedPlayers.get(playerName).setConnected(false);
-          authorizedPlayers.get(playerName).setSocket(null);
-          console.log(playerName + ' disconnected');
+          player.setConnected(false);
+          player.setSocket(null);
+          console.log(username + ' disconnected');
         }
-      } else {
-        console.log('disconnected unexpectidly with reason : ' + reason);
       }
     });
     socket.on('connect', () => {
@@ -92,7 +87,7 @@ function Server() {
         } else {
           // In case server did not detect disconnection and did not clean the pending created games
           try {
-            removeAllGamesCreatedByPlayer(playerName);
+            await removeAllGamesCreatedByPlayer(playerName);
           } catch (err) {
             console.log('Error removing games : ' + err.message);
           }
@@ -140,9 +135,15 @@ function Server() {
           res: 'ok',
           message: id
         };
+        gamesManager.addPlayerInGame(
+          authorizedPlayers.get(connectedPlayers.get(socket.id)),
+          id,
+          false
+        );
         console.log('Force all clients to reload their games');
         forceClientsReloadGames();
       } catch (error) {
+        console.log('error in create-game:>> ', error);
         response = {
           res: 'ko',
           message: 'Error from server'
@@ -151,14 +152,35 @@ function Server() {
       callback(response);
     });
 
-    socket.on('remove-created-game', async (username, callback) => {
+    socket.on('join-game', async (gameDetails, callback) => {
+      let result = gamesManager.addPlayerInGame(
+        authorizedPlayers.get(connectedPlayers.get(socket.id)),
+        gameDetails.id,
+        gameDetails.joinCreatedGame
+      );
+      let response = {};
+      if (result === true) {
+        response.res = 'ok';
+        response.message = 'Game joined';
+      } else {
+        response.res = 'ko';
+        response.message = 'Unable to join game';
+      }
+      callback(response);
+    });
+
+    socket.on('leave-game', async (username, callback) => {
+      // Remove player from game he is playing
+      gamesManager.playerLeft(authorizedPlayers.get(username), false);
       try {
-        removeAllGamesCreatedByPlayer(username);
+        let res = removeAllGamesCreatedByPlayer(username);
+        if (res.affectedRows > 0) {
+          forceClientsReloadGames();
+        }
       } catch (err) {
         console.log('Error removing games : ' + err.message);
       }
       callback(true);
-      forceClientsReloadGames();
     });
   });
 

+ 17 - 0
src/App.vue

@@ -46,6 +46,7 @@
 import AppMenu from './menu/AppMenu';
 import AppGame from './game/AppGame';
 import { globalEventsBus } from './main';
+
 export default {
   name: 'App',
   data() {
@@ -74,6 +75,22 @@ export default {
       this.onlineGameId = -1;
       this.display = 'app-menu';
     });
+    this.$store.watch(
+      () => this.$store.getters['messages'],
+      messages => {
+        if (messages.length > 0) {
+          let message = messages.pop();
+          this.$bvToast.toast(message.text, {
+            title: message.from,
+            toaster: 'b-toaster-top-right',
+            solid: true,
+            variant: 'info',
+            autoHideDelay: 10000,
+            appendToast: true
+          });
+        }
+      }
+    );
   }
 };
 </script>

+ 84 - 0
src/common/heroes-display/HeroesDisplay.vue

@@ -0,0 +1,84 @@
+<template>
+  <div>
+    <button class="btn btn-primary" @click="showFilter = !showFilter">
+      Show/Hide filter
+    </button>
+    <button class="btn btn-primary" @click="resetFilter">
+      Reset filter
+    </button>
+    <heroes-filter
+      v-if="showFilter"
+      :filter-options="filterOptions"
+      v-model="filter"
+    ></heroes-filter>
+    <hr />
+    <heroes-selector v-if="selectHeroes.enable"></heroes-selector>
+    <div
+      class="row row-cols-2 row-cols-sm-2 row-cols-md-3 row-cols-lg-4 row-cols-xl-6 mt-3"
+    >
+      <hero
+        v-for="(hero, index) in heroesBy(filter)"
+        :key="index"
+        :hero="hero"
+        :selectable="['draft', 'tournament'].includes(selectHeroes.mode)"
+      ></hero>
+      <!-- v-model="selected" -->
+    </div>
+  </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex';
+import Hero from './components/Hero';
+import HeroesFilter from './components/HeroesFilter';
+import HeroesSelector from './components/HeroesSelector';
+
+let initialFilter = function(filterOptions) {
+  return {
+    minPower: 0,
+    maxPower: 6,
+    minCost: 0,
+    maxCost: 7,
+    faction: filterOptions.faction[0],
+    popularity: filterOptions.popularity[0],
+    draft: filterOptions.draft[0],
+    name: '',
+    byName: false
+  };
+};
+
+export default {
+  props: ['filterOptions', 'selectHeroes'],
+  data() {
+    return {
+      showFilter: false,
+      filter: initialFilter(this.filterOptions)
+    };
+  },
+  components: {
+    Hero,
+    HeroesFilter,
+    HeroesSelector
+  },
+  computed: {
+    ...mapGetters({
+      heroesBy: 'heroes/by',
+      allHeroes: 'heroes/all'
+    })
+  },
+  methods: {
+    resetFilter() {
+      Object.assign(this.filter, initialFilter(this.filterOptions));
+    }
+  },
+  watch: {
+    'filter.byName'(newValue) {
+      if (newValue === false) {
+        this.filter.name = '';
+      }
+    }
+  }
+};
+</script>
+
+<style></style>

+ 44 - 0
src/common/heroes-display/components/Hero.vue

@@ -0,0 +1,44 @@
+<template>
+  <div class="col mb-4">
+    <div class="card" style="width: 15rem;">
+      <div class="card-body">
+        <h5 class="card-title">{{ hero.name }}</h5>
+      </div>
+      <ul class="list-group list-group-flush">
+        <li class="list-group-item">Power : {{ hero.power }}</li>
+        <li class="list-group-item">Cost : {{ hero.cost }}</li>
+        <li class="list-group-item">
+          {{ hero.faction }} {{ hero.isDraftable | draftText }}
+        </li>
+      </ul>
+      <div class="card-body">
+        <p class="card-text text-justify">
+          {{ hero.ability.desc }}
+        </p>
+      </div>
+      <div class="text-center mb-2" v-if="selectable">
+        <button class="btn btn-primary" style="width:50%;">
+          Select
+        </button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  props: ['hero', 'selectable'],
+  computed: {},
+  filters: {
+    draftText(value) {
+      if (value === true) {
+        return '(draft)';
+      } else {
+        return '';
+      }
+    }
+  }
+};
+</script>
+
+<style></style>

+ 110 - 0
src/common/heroes-display/components/HeroesFilter.vue

@@ -0,0 +1,110 @@
+<template>
+  <div class="row">
+    <div class="col" v-if="filterOptions.faction.length > 1">
+      <label>Faction</label>
+      <select
+        class="form-control pull-left"
+        v-model="value.faction"
+        style="width:180px"
+      >
+        <option
+          v-for="(f, index) in filterOptions.faction"
+          :key="f"
+          :selected="index === 0"
+          >{{ f }}
+        </option>
+      </select>
+    </div>
+    <div class="col" v-if="filterOptions.power === true">
+      <label>Power</label>
+      <input
+        type="number"
+        class="form-control"
+        placeholder="Min power"
+        v-model.number="value.minPower"
+        style="width:180px"
+      />
+      <input
+        type="number"
+        class="form-control"
+        placeholder="Max power"
+        v-model.number="value.maxPower"
+        style="width:180px"
+      />
+    </div>
+    <div class="col" v-if="filterOptions.cost === true">
+      <label>Cost</label>
+      <input
+        type="number"
+        class="form-control"
+        placeholder="Min cost"
+        v-model.number="value.minCost"
+        style="width:180px"
+      />
+      <input
+        type="number"
+        class="form-control"
+        placeholder="Max cost"
+        v-model.number="value.maxCost"
+        style="width:180px"
+      />
+    </div>
+    <div class="col" v-if="filterOptions.draft.length > 1">
+      <label>Draft mode</label>
+      <select
+        class="form-control pull-left"
+        v-model="value.draft"
+        style="width:180px"
+      >
+        <option
+          v-for="(d, index) in filterOptions.draft"
+          :key="d"
+          :selected="index === 0"
+          >{{ d }}
+        </option>
+      </select>
+    </div>
+    <div class="col" v-if="filterOptions.popularity.length > 1">
+      <label>Popularity rule</label>
+      <select
+        class="form-control pull-left"
+        v-model="value.popularity"
+        style="width:180px"
+      >
+        <option
+          v-for="(p, index) in filterOptions.popularity"
+          :key="p"
+          :selected="index === 0"
+          >{{ p }}
+        </option>
+      </select>
+    </div>
+    <div class="col" v-if="filterOptions.byName === true">
+      <label>Name</label>
+
+      <div class="form-check">
+        <input
+          type="checkbox"
+          class="form-check-input"
+          v-model="value.byName"
+        />
+        <label class="form-check-label">Filter by name</label>
+      </div>
+      <input
+        type="text"
+        class="form-control pull-left"
+        v-model="value.name"
+        style="width:180px"
+        :disabled="!value.byName"
+      />
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  props: ['filterOptions', 'value']
+};
+</script>
+
+<style></style>

+ 11 - 0
src/common/heroes-display/components/HeroesSelector.vue

@@ -0,0 +1,11 @@
+<template>
+  <div>
+    <hr />
+  </div>
+</template>
+
+<script>
+export default {};
+</script>
+
+<style></style>

+ 33 - 5
src/common/socket-service.js

@@ -4,8 +4,9 @@ import io from 'socket.io-client';
 const SERVER_URL = 'http://' + process.env.VUE_APP_SERVER_HOST;
 const SERVER_PORT = process.env.VUE_APP_SERVER_PORT;
 
-export default function SocketService(socketEventBus) {
+export default function SocketService(socketEventBus, vuexStore) {
   let eventBus = socketEventBus;
+  let store = vuexStore;
   let ioClient = null;
   let connect = function(name) {
     let promise = new Promise((resolve, reject) => {
@@ -47,6 +48,9 @@ export default function SocketService(socketEventBus) {
         console.log('force reload games list !');
         eventBus.$emit('reload-games');
       });
+      ioClient.on('message', message => {
+        store.dispatch('addMessageToQueue', message);
+      });
     });
     return promise;
   };
@@ -96,12 +100,30 @@ export default function SocketService(socketEventBus) {
       error => Promise.reject(error)
     );
   };
-  let removeCreatedGames = function(username) {
+  let joinGame = function(username, gameId, joinCreatedGame) {
+    console.log('gameId :>> ', gameId);
+    return checkConnection(username).then(
+      () => {
+        return new Promise((resolve, reject) => {
+          ioClient.emit('join-game', { id: gameId, joinCreatedGame }, function(
+            response
+          ) {
+            if (response.res === 'ok') {
+              resolve(response.message);
+            } else {
+              reject(response.message);
+            }
+          });
+        });
+      },
+      error => Promise.reject(error)
+    );
+  };
+  let leaveGame = function(username) {
     return checkConnection(username).then(
       () => {
         return new Promise(resolve => {
-          console.log('emit : remove-created-game');
-          ioClient.emit('remove-created-game', username, function(response) {
+          ioClient.emit('leave-game', username, function(response) {
             resolve(response);
           });
         });
@@ -109,11 +131,17 @@ export default function SocketService(socketEventBus) {
       error => Promise.reject(error)
     );
   };
+
+  let chat = function(message) {
+    ioClient.emit('chat', message);
+  };
   return {
     connect,
     disconnect,
     getGamesList,
     createGame,
-    removeCreatedGames
+    leaveGame,
+    joinGame,
+    chat
   };
 }

+ 78 - 20
src/game/AppGame.vue

@@ -1,33 +1,78 @@
 <template>
-  <div class="row">
-    <div class="col-12 col-md-8 offset-sm-2 col-lg-6 offset-md-3">
-      <h2>Launch game id {{ gameId }} for {{ username }}</h2>
-      <button
-        class="btn btn-danger"
-        v-if="isGameRunning"
-        @click="isGameRunning = !isGameRunning"
-      >
-        Stop game
-      </button>
-      <button
-        class="btn btn-success"
-        v-else
-        @click="isGameRunning = !isGameRunning"
-      >
-        Start game
-      </button>
+  <div>
+    <div class="row">
+      <div class="col-12 col-md-8 offset-sm-2 col-lg-6 offset-md-3">
+        <h2>Launch game id {{ gameId }} for {{ username }}</h2>
+        <button
+          class="btn btn-danger"
+          v-if="isGameRunning"
+          @click="isGameRunning = !isGameRunning"
+        >
+          Stop game
+        </button>
+        <button
+          class="btn btn-success"
+          v-else
+          @click="isGameRunning = !isGameRunning"
+        >
+          Start game
+        </button>
+        <input type="text" v-model="chatMessage" />
+        <button
+          class="btn btn-primary"
+          @click="sendChat"
+          :disabled="chatMessage === ''"
+        >
+          Chat
+        </button>
+      </div>
     </div>
+    <hr />
+    <label>Deck mode to select heroes :</label>
+    <select
+      class="form-control pull-left"
+      v-model="deckSelectionMode"
+      style="width:180px"
+    >
+      <option selected>faction</option>
+      <option>draft</option>
+      <option>tournament</option>
+      <option value="display">just display</option>
+    </select>
+    <hr />
+    <heroes-display
+      :filter-options="filterOptions"
+      :select-heroes="{ enable: true, mode: deckSelectionMode }"
+    ></heroes-display>
   </div>
 </template>
 
 <script>
 import { globalEventsBus } from '../main';
 import { socketService } from '../main';
+import HeroesDisplay from '../common/heroes-display/HeroesDisplay';
+import { mapActions } from 'vuex';
 export default {
+  components: {
+    HeroesDisplay
+  },
   props: ['game-id', 'username'],
   data() {
     return {
-      isGameRunning: false
+      isGameRunning: false,
+      filterOptions: {
+        //  - faction (orcs humans elves meca none all)
+        //  - popularity (with without)
+        //  - draftMode (yes no all)
+        faction: ['all', 'orcs', 'humans', 'elves', 'meca', 'none'],
+        popularity: ['without', 'with'],
+        draft: ['all', 'yes', 'no'],
+        power: true,
+        cost: true,
+        byName: true
+      },
+      deckSelectionMode: 'faction',
+      chatMessage: ''
     };
   },
   watch: {
@@ -35,14 +80,27 @@ export default {
       if (this.isGameRunning) {
         globalEventsBus.$emit('game-running');
       } else {
-        socketService.removeCreatedGames(this.username).catch(err => {
+        socketService.leaveGame(this.username).catch(err => {
           console.log('Error communicating to server to remove games : ' + err);
         });
         globalEventsBus.$emit('game-stopped');
       }
     }
   },
-  methods: {}
+  computed: {
+    ...mapActions({
+      setHeroesFromJson: 'heroes/setFromLocalJson'
+    })
+  },
+  methods: {
+    sendChat() {
+      socketService.chat(this.chatMessage);
+      this.chatMessage = '';
+    }
+  },
+  created() {
+    this.setHeroesFromJson.then();
+  }
 };
 </script>
 

+ 4 - 1
src/main.js

@@ -10,13 +10,16 @@ Vue.use(IconsPlugin);
 
 import SocketService from './common/socket-service';
 
+import { store } from './store/store';
+
 export const socketEventBus = new Vue({});
-export const socketService = new SocketService(socketEventBus);
+export const socketService = new SocketService(socketEventBus, store);
 
 export const globalEventsBus = new Vue();
 
 /* eslint-disable no-new */
 new Vue({
   el: '#app',
+  store,
   render: h => h(App)
 });

+ 4 - 1
src/menu/login/MenuLogin.vue

@@ -30,7 +30,7 @@
         </button>
         <button
           class="btn btn-primary form-control"
-          @click.prevent="$emit('enter-local')"
+          @click.prevent="test"
           :disabled="isOneGameRunning"
         >
           Local Game
@@ -91,6 +91,9 @@ export default {
         socketService.disconnect();
         Object.assign(this.$data, initialState());
       }
+    },
+    test() {
+      socketService.test(this.username);
     }
   },
   computed: {},

+ 14 - 5
src/menu/online-room/MenuOnlineRoom.vue

@@ -71,11 +71,20 @@ export default {
       this.selectedIndex = -1;
     },
     joinGame() {
-      this.$emit('back');
-      this.$emit('join-game-id', {
-        id: this.gamesList[this.selectedIndex].id,
-        username: this.username
-      });
+      let index = this.selectedIndex;
+      let id = this.gamesList[index].id;
+      socketService
+        .joinGame(this.username, id, this.gamesList[index].player2 === '')
+        .then(() => {
+          this.$emit('back');
+          this.$emit('join-game-id', {
+            id,
+            username: this.username
+          });
+        })
+        .catch(err => {
+          console.log('error joining : ', err);
+        });
     },
     updateGamesList() {
       socketService

+ 482 - 0
src/server-shared/all-heroes.json

@@ -0,0 +1,482 @@
+{
+    "version": "1.0.0",
+    "heroes": [
+      {
+        "name" : "Paysan",
+        "cost" : 0,
+        "power" : 1,
+        "faction": "humans",
+        "draftMode" : true,
+        "popularity": "any",
+        "ability": "PaysanAbility",
+        "nbInDeck": 2
+      },
+      {
+        "name" : "Dragon",
+        "cost" : 7,
+        "power" : 6,
+        "faction": "humans",
+        "draftMode" : true,
+        "popularity": "any",
+        "ability": "DragonAbility",
+        "nbInDeck": 1
+      },
+      {
+        "name" : "Archange",
+        "cost" : 4,
+        "power" : 3,
+        "faction": "humans",
+        "draftMode" : true,
+        "popularity": "any",
+        "ability": "ArchangeAbility",
+        "nbInDeck": 1
+      },
+      {
+        "name" : "Chevalier",
+        "cost" : 3,
+        "power" : 4,
+        "faction": "humans",
+        "draftMode" : true,
+        "popularity": "any",
+        "ability": "ChevalierAbility",
+        "nbInDeck": 2
+      },
+      {
+        "name" : "Faucon Geant",
+        "cost" : 1,
+        "power" : 2,
+        "faction": "humans",
+        "draftMode" : false,
+        "popularity": "any",
+        "ability": "FauconAbility",
+        "nbInDeck": 1
+      },
+      {
+        "name" : "Intendant",
+        "cost" : 2,
+        "power" : 2,
+        "faction": "humans",
+        "draftMode" : false,
+        "popularity": "any",
+        "ability": "IntendantAbility",
+        "nbInDeck": 1
+      },
+      {
+        "name" : "Canon",
+        "cost" : 1,
+        "power" : 4,
+        "faction": "humans",
+        "draftMode" : false,
+        "popularity": "any",
+        "ability": "CanonAbility",
+        "nbInDeck": 1
+      },
+      {
+        "name" : "Char",
+        "cost" : 3,
+        "power" : 3,
+        "faction": "humans",
+        "draftMode" : false,
+        "popularity": "any",
+        "ability": "CharAbility",
+        "nbInDeck": 1
+      },
+      {
+        "name" : "Chevre",
+        "cost" : 1,
+        "power" : 2,
+        "faction": "humans",
+        "draftMode" : false,
+        "popularity": "any",
+        "ability": "ChevreAbility",
+        "nbInDeck": 1
+      },
+      {
+        "name" : "Stratege",
+        "cost" : 1,
+        "power" : 2,
+        "faction": "humans",
+        "draftMode" : false,
+        "popularity": "any",
+        "ability": "StrategeAbility",
+        "nbInDeck": 1
+      },
+      {
+        "name" : "Homme-Arbre",
+        "cost" : 4,
+        "power" : 4,
+        "faction": "elves",
+        "draftMode" : true,
+        "popularity": "any",
+        "ability": "HommeArbreAbility",
+        "nbInDeck": 2
+      },
+      {
+        "name" : "Elf",
+        "cost" : 2,
+        "power" : 2,
+        "faction": "elves",
+        "draftMode" : true,
+        "popularity": "any",
+        "ability": "ElfAbility",
+        "nbInDeck": 2
+      },
+      {
+        "name" : "Stratege",
+        "cost" : 1,
+        "power" : 2,
+        "faction": "elves",
+        "draftMode" : true,
+        "popularity": "any",
+        "ability": "StrategeAbility",
+        "nbInDeck": 1
+      },
+      {
+        "name" : "Intendant",
+        "cost" : 2,
+        "power" : 2,
+        "faction": "elves",
+        "draftMode" : true,
+        "popularity": "any",
+        "ability": "IntendantAbility",
+        "nbInDeck": 1
+      },
+      {
+        "name" : "Faucon Geant",
+        "cost" : 1,
+        "power" : 2,
+        "faction": "elves",
+        "draftMode" : true,
+        "popularity": "any",
+        "ability": "FauconAbility",
+        "nbInDeck": 2
+      },
+      {
+        "name" : "Paysan",
+        "cost" : 0,
+        "power" : 1,
+        "faction": "elves",
+        "draftMode" : false,
+        "popularity": "any",
+        "ability": "PaysanAbility",
+        "nbInDeck": 1
+      },
+      {
+        "name" : "Archange",
+        "cost" : 4,
+        "power" : 3,
+        "faction": "elves",
+        "draftMode" : true,
+        "popularity": "any",
+        "ability": "ArchangeAbility",
+        "nbInDeck": 1
+      },
+      {
+        "name" : "Chevre",
+        "cost" : 1,
+        "power" : 2,
+        "faction": "elves",
+        "draftMode" : false,
+        "popularity": "any",
+        "ability": "ChevreAbility",
+        "nbInDeck": 1
+      },
+      {
+        "name" : "Voilier Celeste",
+        "cost" : 1,
+        "power" : 2,
+        "faction": "elves",
+        "draftMode" : false,
+        "popularity": "any",
+        "ability": "VoilierAbility",
+        "nbInDeck": 1
+      },
+      {
+        "name" : "Orc",
+        "cost" : 3,
+        "power" : 3,
+        "faction": "orcs",
+        "draftMode" : true,
+        "popularity": "any",
+        "ability": "orcsAbility",
+        "nbInDeck": 2
+      },
+      {
+        "name" : "Behemoth",
+        "cost" : 5,
+        "power" : 5,
+        "faction": "orcs",
+        "draftMode" : true,
+        "popularity": "any",
+        "ability": "BehemothAbility",
+        "nbInDeck": 2
+      },
+      {
+        "name" : "Gobelin",
+        "cost" : 0,
+        "power" : 2,
+        "faction": "orcs",
+        "draftMode" : true,
+        "popularity": "any",
+        "ability": "GobelinAbility",
+        "nbInDeck": 2
+      },
+      {
+        "name" : "Char",
+        "cost" : 3,
+        "power" : 3,
+        "faction": "orcs",
+        "draftMode" : true,
+        "popularity": "any",
+        "ability": "CharAbility",
+        "nbInDeck": 2
+      },
+      {
+        "name" : "Chevre",
+        "cost" : 1,
+        "power" : 2,
+        "faction": "orcs",
+        "draftMode" : true,
+        "popularity": "any",
+        "ability": "ChevreAbility",
+        "nbInDeck": 2
+      },
+      {
+        "name" : "Paysan",
+        "cost" : 0,
+        "power" : 1,
+        "faction": "orcs",
+        "draftMode" : false,
+        "popularity": "any",
+        "ability": "PaysanAbility",
+        "nbInDeck": 1
+      },
+      {
+        "name" : "Intendant",
+        "cost" : 2,
+        "power" : 2,
+        "faction": "orcs",
+        "draftMode" : true,
+        "popularity": "any",
+        "ability": "IntendantAbility",
+        "nbInDeck": 1
+      },
+      {
+        "name" : "Geant de Fer",
+        "cost" : 2,
+        "power" : 3,
+        "faction": "meca",
+        "draftMode" : true,
+        "popularity": "any",
+        "ability": "GeantAbility",
+        "nbInDeck": 2
+      },
+      {
+        "name" : "Golem",
+        "cost" : 4,
+        "power" : 4,
+        "faction": "meca",
+        "draftMode" : true,
+        "popularity": "with",
+        "ability": "GolemAbilityWithPop",
+        "nbInDeck": 2
+      },
+      {
+        "name" : "Golem",
+        "cost" : 4,
+        "power" : 4,
+        "faction": "meca",
+        "draftMode" : true,
+        "popularity": "without",
+        "ability": "GolemAbilityWithoutPop",
+        "nbInDeck": 2
+      },
+      {
+        "name" : "Canon",
+        "cost" : 1,
+        "power" : 4,
+        "faction": "meca",
+        "draftMode" : true,
+        "popularity": "any",
+        "ability": "CanonAbility",
+        "nbInDeck": 2
+      },
+      {
+        "name" : "Char",
+        "cost" : 3,
+        "power" : 3,
+        "faction": "meca",
+        "draftMode" : false,
+        "popularity": "any",
+        "ability": "CharAbility",
+        "nbInDeck": 1
+      },
+      {
+        "name" : "Voilier Celeste",
+        "cost" : 1,
+        "power" : 2,
+        "faction": "meca",
+        "draftMode" : true,
+        "popularity": "any",
+        "ability": "VoilierAbility",
+        "nbInDeck": 2
+      },
+      {
+        "name" : "Intendant",
+        "cost" : 2,
+        "power" : 2,
+        "faction": "meca",
+        "draftMode" : false,
+        "popularity": "any",
+        "ability": "IntendantAbility",
+        "nbInDeck": 1
+      },
+      {
+        "name" : "Gobelin",
+        "cost" : 0,
+        "power" : 2,
+        "faction": "meca",
+        "draftMode" : false,
+        "popularity": "any",
+        "ability": "GobelinAbility",
+        "nbInDeck": 1
+      },
+      {
+        "name" : "Stratege",
+        "cost" : 1,
+        "power" : 2,
+        "faction": "meca",
+        "draftMode" : true,
+        "popularity": "any",
+        "ability": "StrategeAbility",
+        "nbInDeck": 1
+      },
+      {
+        "name" : "Dragon",
+        "cost" : 7,
+        "power" : 6,
+        "faction": "none",
+        "draftMode" : true,
+        "popularity": "any",
+        "ability": "DragonAbility",
+        "nbInDeck": 1
+      }
+    ],
+    "abilities": [
+      {
+        "abilityName" : "PaysanAbility",
+        "abilityHook": "AfterRecruit",
+        "optionnal": "false",
+        "abilityDesc-FR" : "Quand le Paysan est recrute, prenez 2 Nourritures."
+      },
+      {
+        "abilityName" : "DragonAbility",
+        "abilityHook": "AfterRecruit",
+        "optionnal": "false",
+        "abilityDesc-FR" : "Quand le Dragon est recrute, chaque joueur doit defausser 1 Nourriture dans chaque region ou il le peut."
+      },
+      {
+        "abilityName" : "ArchangeAbility",
+        "abilityHook": "AfterRecruit",
+        "optionnal": "false",
+        "abilityDesc-FR" : "Quand l'Archange est recrute, gagnez 4 Nourritures dans une region ou vous avez au moins un heros."
+      },
+      {
+        "abilityName" : "ChevalierAbility",
+        "abilityHook": "AfterRecruit",
+        "optionnal": "false",
+        "abilityDesc-FR" : "Quand le Chevalier est recrute, vous devez reprendre dans votre main un de vos Heros en jeu (le Chevalier si besoin)."
+      },
+      {
+        "abilityName" : "FauconAbility",
+        "abilityHook": "BeforeMilitary",
+        "optionnal": "true",
+        "abilityDesc-FR" : "Le Faucon Geant peut profiter gratuitement d'une action Deplacer au debut de votre phase Militaire."
+      },
+      {
+        "abilityName" : "IntendantAbility",
+        "abilityHook": "AfterRecruit",
+        "optionnal": "false",
+        "abilityDesc-FR" : "Quand l'Intendant est recrute, gagnez 1 Nourriture dans chaque region ou vous avez au moins un heros."
+      },
+      {
+        "abilityName" : "CanonAbility",
+        "abilityHook": "BeforeMaintenance",
+        "optionnal": "false",
+        "abilityDesc-FR" : "Au debut de votre Maintenance, vous devez defausser le canon."
+      },
+      {
+        "abilityName" : "CharAbility",
+        "abilityHook": "AfterDeploy",
+        "optionnal": "true",
+        "abilityDesc-FR" : "Quand le Char est deploye, vous pouvez renvoyer (sans Nourriture) un heros adverse de cout 3 ou moins de cette region vers son Campement."
+      },
+      {
+        "abilityName" : "ChevreAbility",
+        "abilityHook": "BeforeMaintenance",
+        "optionnal": "true",
+        "abilityDesc-FR" : "Au debut de votre Maintenance, si la chevre est dans une region, vous pouvez la defausser pour gagner une nourriture dans cette region."
+      },
+      {
+        "abilityName" : "StrategeAbility",
+        "abilityHook": "AfterRecruit",
+        "optionnal": "true",
+        "abilityDesc-FR" : "Quand le Stratege est recrute, vous pouvez renvoyer (sans Nourriture) n'importe quel nombre de vos Heros des regions vers votre Campement."
+      },
+      {
+        "abilityName" : "HommeArbreAbility",
+        "abilityHook": ["AfterDeploy","AfterMove"],
+        "optionnal": "false",
+        "abilityDesc-FR" : "Quand l'Homme-Arbre est deploye ou deplace, gagnez 1 Nourriture dans cette region."
+      },
+      {
+        "abilityName" : "ElfAbility",
+        "abilityHook": "AfterDeploy",
+        "optionnal": "false",
+        "abilityDesc-FR" : "Quand l'Elfe est deploye ou deplace, gagnez 2 Nourritures dans cette region."
+      },
+      {
+        "abilityName" : "VoilierAbility",
+        "abilityHook": "AfterDeploy",
+        "optionnal": "true",
+        "abilityDesc-FR" : "Quand le Voilier Celeste est deploye, vous pouvez ajouter gratuitement avec lui un Heros de cout 0 ou 1 de votre main. Si ce Heros a un effet quand il est recrute, appliquez-le."
+      },
+      {
+        "abilityName" : "orcsAbility",
+        "abilityHook": "AfterDeploy",
+        "optionnal": "false",
+        "abilityDesc-FR" : "Quand l'orcs est deploye, chaque joueur doit defausser 1 Nourriture dans cette region, s'il le peut."
+      },
+      {
+        "abilityName" : "BehemothAbility",
+        "abilityHook": "AfterRecruit",
+        "optionnal": "false",
+        "abilityDesc-FR" : "Quand le Behemoth est recrute, l'adversaire doit defausser un Heros dans une region, s'il le peut."
+      },
+      {
+        "abilityName" : "GobelinAbility",
+        "abilityHook": "AfterRecruit",
+        "optionnal": "false",
+        "abilityDesc-FR" : "Quand le Gobelin est recrute, deployez-le gratuitement dans une region, sans pouvoir emmener de Nourriture."
+      },
+      {
+        "abilityName" : "GeantAbility",
+        "abilityHook": "AfterDiscard",
+        "optionnal": "false",
+        "abilityDesc-FR" : "Quand le Geant de Fer est defausse depuis une region ou votre campement, piochez une carte."
+      },
+      {
+        "abilityName" : "GolemAbilityWithoutPop",
+        "abilityHook": "BeforeControl",
+        "optionnal": "false",
+        "abilityDesc-FR" : "Vous controlez la region ou se trouve le Golem meme en cas d'egalite avec votre adversaire."
+      },
+      {
+        "abilityName" : "GolemAbilityWithPop",
+        "abilityHook": "AfterRecruit",
+        "optionnal": "false",
+        "abilityDesc-FR" : "Piochez 1 carte pour chaque jeton Popularité en votre possession."
+      }
+    ]
+  }
+  

+ 28 - 0
src/server-shared/heroesHelper.js

@@ -0,0 +1,28 @@
+export const initHeroesFromJson = function(allHeroesJson) {
+  let abilitiesMap = new Map();
+  allHeroesJson.abilities.forEach(ability => {
+    abilitiesMap.set(ability.abilityName, {
+      name: ability.abilityName,
+      hook: ability.abilityHook,
+      isOptionnal: ability.optionnal,
+      desc: ability['abilityDesc-FR']
+    });
+  });
+  let heroesSet = new Set();
+  allHeroesJson.heroes.forEach(hero => {
+    let i = 0;
+    while (i < hero.nbInDeck) {
+      heroesSet.add({
+        name: hero.name,
+        cost: hero.cost,
+        power: hero.power,
+        faction: hero.faction,
+        ability: abilitiesMap.get(hero.ability),
+        isDraftable: hero.draftMode,
+        popularity: hero.popularity
+      });
+      i++;
+    }
+  });
+  return heroesSet;
+};

+ 85 - 0
src/store/heroes/heroes.js

@@ -0,0 +1,85 @@
+import { initHeroesFromJson } from '../../server-shared/heroesHelper';
+import allHeroesJson from '../../server-shared/all-heroes.json';
+const state = {
+  heroes: new Set()
+};
+
+const getters = {
+  all(state) {
+    return state.heroes;
+  },
+  by: state => filter => {
+    // filter has
+    //  - faction (orcs humans elves meca none all)
+    //  - popularity (with without)
+    //  - draft (yes no all)
+    //  -  minPower <= power <= maxPower
+    //  -  minCost <= cost <= maxCost
+    //  - byName : if true filter name with filter.name
+    if (!filter.faction) filter.faction = 'all';
+    if (!filter.popularity) filter.popularity = 'without';
+    if (!filter.minPower) filter.minPower = 0;
+    if (!filter.maxPower) filter.maxPower = 0;
+    if (!filter.minCost) filter.minCost = 0;
+    if (!filter.maxCost) filter.minCost = 0;
+
+    let draftFilter = 'all';
+    switch (filter.draft) {
+      case 'yes':
+        draftFilter = true;
+        break;
+      case 'no':
+        draftFilter = false;
+        break;
+
+      default:
+        draftFilter = 'all';
+        break;
+    }
+
+    return Array.from(state.heroes).filter(hero => {
+      if (filter.faction !== 'all' && hero.faction !== filter.faction)
+        return false;
+      if (hero.popularity !== 'any' && hero.popularity !== filter.popularity)
+        return false;
+
+      if (draftFilter !== 'all' && hero.isDraftable !== draftFilter)
+        return false;
+
+      if (hero.power < filter.minPower || hero.power > filter.maxPower)
+        return false;
+      if (hero.cost < filter.minCost || hero.cost > filter.maxCost)
+        return false;
+
+      if (
+        filter.byName === true &&
+        !hero.name.toLowerCase().includes(filter.name.toLowerCase())
+      )
+        return false;
+
+      return true;
+    });
+  }
+};
+
+const mutations = {
+  SET_HEROES: (state, payload) => {
+    state.heroes = payload;
+  }
+};
+
+const actions = {
+  setFromLocalJson: ({ commit }) => {
+    commit('SET_HEROES', initHeroesFromJson(allHeroesJson));
+  },
+  setAvailableHeroes: ({ commit }, payload) => {
+    commit('SET_HEROES', payload);
+  }
+};
+export default {
+  namespaced: true,
+  state,
+  getters,
+  mutations,
+  actions
+};

+ 40 - 0
src/store/store.js

@@ -0,0 +1,40 @@
+'use strict';
+import Vue from 'vue';
+import Vuex from 'vuex';
+
+Vue.use(Vuex);
+
+import heroes from './heroes/heroes';
+
+const state = {
+  username: '',
+  gameId: -1,
+  messages: []
+};
+
+const getters = {
+  messages(state) {
+    return state.messages;
+  }
+};
+
+const mutations = {
+  ADD_MESSAGE: (state, payload) => {
+    state.messages.push(payload);
+  }
+};
+const actions = {
+  addMessageToQueue: ({ commit }, payload) => {
+    commit('ADD_MESSAGE', payload);
+  }
+};
+export const store = new Vuex.Store({
+  state,
+  getters,
+  mutations,
+  actions,
+
+  modules: {
+    heroes
+  }
+});