+ },
+ beforeDestroy: function() {
+ this.cleanBeforeDestroy();
+ },
+ methods: {
+ cleanBeforeDestroy: function() {
+ clearInterval(this.socketCloseListener);
+ document.removeEventListener('visibilitychange', this.visibilityChange);
+ window.removeEventListener('focus', this.onFocus);
+ window.removeEventListener('blur', this.onBlur);
+ if (!!this.askLastate) clearInterval(this.askLastate);
+ if (!!this.retrySendmove) clearInterval(this.retrySendmove);
+ if (!!this.clockUpdate) clearInterval(this.clockUpdate);
+ this.conn.removeEventListener("message", this.socketMessageListener);
+ this.send("disconnect");
+ this.conn = null;
+ },
+ visibilityChange: function() {
+ // TODO: Use document.hidden? https://webplatform.news/issues/2019-03-27
+ this.focus = (document.visibilityState == "visible");
+ if (!this.focus && !!this.rematchOffer) {
+ this.rematchOffer = "";
+ this.send("rematchoffer", { data: false });
+ // Do not remove rematch offer from (local) storage
+ }
+ this.send(this.focus ? "getfocus" : "losefocus");
+ },
+ onFocus: function() {
+ this.focus = true;
+ this.send("getfocus");
+ },
+ onBlur: function() {
+ this.focus = false;
+ if (!!this.rematchOffer) {
+ this.rematchOffer = "";
+ this.send("rematchoffer", { data: false });
+ }
+ this.send("losefocus");
+ },
+ isLargeScreen: function() {
+ return window.innerWidth >= 500;
+ },
+ gotoRules: function() {
+ this.$router.push("/variants/" + this.game.vname);
+ },
+ participateInChat: function(p) {
+ return Object.keys(p.tmpIds).some(x => p.tmpIds[x].focus) && !!p.name;
+ },
+ someAnonymousPresent: function() {
+ return (
+ Object.values(this.people).some(p =>
+ !p.name && Object.keys(p.tmpIds).some(x => p.tmpIds[x].focus)
+ )
+ );
+ },
+ atCreation: function() {
+ document.addEventListener('visibilitychange', this.visibilityChange);
+ window.addEventListener('focus', this.onFocus);
+ window.addEventListener('blur', this.onBlur);
+ // 0] (Re)Set variables
+ this.gameRef = this.$route.params["id"];
+ // next = next corr games IDs to navigate faster (if applicable)
+ this.nextIds = JSON.parse(this.$route.query["next"] || "[]");
+ // Always add myself to players' list
+ const my = this.st.user;
+ const tmpId = getRandString();
+ this.$set(
+ this.people,
+ my.sid,
+ {
+ id: my.id,
+ name: my.name,
+ tmpIds: {
+ tmpId: { focus: true }
+ }
+ }
+ );
+ this.game = {
+ players: [{ name: "" }, { name: "" }],
+ chats: [],
+ rendered: false
+ };
+ let chatComp = this.$refs["chatcomp"];
+ if (!!chatComp) chatComp.chats = [];
+ this.virtualClocks = [[0,0], [0,0]];
+ this.vr = null;
+ this.rulesContent = "";
+ this.drawOffer = "";
+ this.lastateAsked = false;
+ this.rematchOffer = "";
+ this.lastate = undefined;
+ this.roomInitialized = false;
+ this.askGameTime = 0;
+ this.gameIsLoading = false;
+ this.gotLastate = false;
+ this.gotMoveIdx = -1;
+ this.opponentGotMove = false;
+ this.askLastate = null;
+ this.retrySendmove = null;
+ this.clockUpdate = null;
+ this.newConnect = {};
+ // 1] Initialize connection
+ this.connexionString =
+ params.socketUrl +
+ "/?sid=" + this.st.user.sid +
+ "&id=" + this.st.user.id +
+ "&tmpId=" + tmpId +
+ "&page=" +
+ // Discard potential "/?next=[...]" for page indication:
+ encodeURIComponent(this.$route.path.match(/\/game\/[a-zA-Z0-9]+/)[0]);
+ this.conn = new WebSocket(this.connexionString);
+ this.conn.addEventListener("message", this.socketMessageListener);
+ this.socketCloseListener = setInterval(
+ () => {
+ if (this.conn.readyState == 3) {
+ this.conn.removeEventListener(
+ "message", this.socketMessageListener);
+ this.conn = new WebSocket(this.connexionString);
+ this.conn.addEventListener("message", this.socketMessageListener);
+ }
+ },
+ 1000
+ );
+ // Socket init required before loading remote game:
+ const socketInit = callback => {
+ if (this.conn.readyState == 1)
+ // 1 == OPEN state
+ callback();
+ else
+ // Socket not ready yet (initial loading)
+ // NOTE: first arg is Websocket object, unused here:
+ this.conn.onopen = () => callback();
+ };
+ this.fetchGame((game) => {
+ if (!!game)
+ this.loadVariantThenGame(game, () => socketInit(this.roomInit));
+ else
+ // Live game stored remotely: need socket to retrieve it
+ // NOTE: the callback "roomInit" will be lost, so it's not provided.
+ // --> It will be given when receiving "fullgame" socket event.
+ socketInit(() => { this.send("askfullgame"); });
+ });
+ },
+ roomInit: function() {
+ if (!this.roomInitialized) {
+ // Notify the room only now that I connected, because
+ // messages might be lost otherwise (if game loading is slow)
+ this.send("connect");
+ this.send("pollclients");
+ // We may ask fullgame several times if some moves are lost,
+ // but room should be init only once:
+ this.roomInitialized = true;
+ }
+ },
+ send: function(code, obj) {
+ if (!!this.conn && this.conn.readyState == 1)
+ this.conn.send(JSON.stringify(Object.assign({ code: code }, obj)));
+ },
+ isConnected: function(index) {
+ const player = this.game.players[index];
+ // Is it me ? In this case no need to bother with focus
+ if (this.st.user.sid == player.sid || this.st.user.id == player.id)
+ // Still have to check for name (because of potential multi-accounts
+ // on same browser, although this should be rare...)
+ return (!this.st.user.name || this.st.user.name == player.name);
+ // Try to find a match in people:
+ return (
+ (
+ !!player.sid &&
+ Object.keys(this.people).some(sid => {
+ return (
+ sid == player.sid &&
+ Object.values(this.people[sid].tmpIds).some(v => v.focus)
+ );
+ })
+ )
+ ||
+ (
+ !!player.id &&
+ Object.values(this.people).some(p => {
+ return (
+ p.id == player.id &&
+ Object.values(p.tmpIds).some(v => v.focus)
+ );
+ })
+ )
+ );
+ },
+ getOppsid: function() {
+ let oppsid = this.game.oppsid;
+ if (!oppsid) {
+ oppsid = Object.keys(this.people).find(
+ sid => this.people[sid].id == this.game.oppid
+ );
+ }
+ // oppsid is useful only if opponent is online:
+ if (!!oppsid && !!this.people[oppsid]) return oppsid;
+ return null;
+ },
+ // NOTE: action if provided is always a closing action
+ toggleChat: function(action) {
+ if (!action && document.getElementById("modalChat").checked)
+ // Entering chat
+ document.getElementById("inputChat").focus();
+ else {
+ document.getElementById("chatBtn").classList.remove("somethingnew");
+ if (!!this.game.mycolor) {
+ // Update "chatRead" variable either on server or locally
+ if (this.game.type == "corr")
+ this.updateCorrGame({ chatRead: this.game.mycolor });
+ else if (this.game.type == "live")
+ GameStorage.update(this.gameRef, { chatRead: true });
+ }
+ }
+ },
+ processChat: function(chat) {
+ this.send("newchat", { data: chat });
+ // NOTE: anonymous chats in corr games are not stored on server (TODO?)
+ if (!!this.game.mycolor) {
+ if (this.game.type == "corr")
+ this.updateCorrGame({ chat: chat });
+ else {
+ // Live game
+ chat.added = Date.now();
+ GameStorage.update(this.gameRef, { chat: chat });
+ }
+ }
+ },
+ clearChat: function() {
+ if (!!this.game.mycolor) {
+ if (this.game.type == "corr") {
+ ajax(
+ "/chats",
+ "DELETE",
+ { data: { gid: this.game.id } }
+ );
+ } else {
+ // Live game
+ GameStorage.update(this.gameRef, { delchat: true });
+ }
+ this.$set(this.game, "chats", []);
+ }
+ },
+ getGameType: function(game) {
+ if (!!game.id.toString().match(/^i/)) return "import";
+ return game.cadence.indexOf("d") >= 0 ? "corr" : "live";
+ },
+ // Notify something after a new move (to opponent and me on MyGames page)
+ notifyMyGames: function(thing, data) {
+ this.send(
+ "notify" + thing,
+ {
+ data: data,
+ targets: this.game.players.map(p => {
+ return { sid: p.sid, id: p.id };
+ })
+ }
+ );
+ },
+ showNextGame: function() {
+ // Did I play in current game? If not, add it to nextIds list
+ if (this.game.score == "*" && this.vr.turn == this.game.mycolor)
+ this.nextIds.unshift(this.game.id);
+ const nextGid = this.nextIds.pop();
+ this.$router.push(
+ "/game/" + nextGid + "/?next=" + JSON.stringify(this.nextIds));
+ },
+ askGameAgain: function() {
+ this.gameIsLoading = true;
+ const currentUrl = document.location.href;
+ const doAskGame = () => {
+ if (document.location.href != currentUrl) return; //page change
+ this.fetchGame((game) => {
+ if (!!game)
+ // This is my game: just reload.
+ this.loadGame(game);
+ else
+ // Just ask fullgame again (once!), this is much simpler.
+ // If this fails, the user could just reload page :/
+ this.send("askfullgame");
+ });
+ };
+ // Delay of at least 2s between two game requests
+ const now = Date.now();
+ const delay = Math.max(2000 - (now - this.askGameTime), 0);
+ this.askGameTime = now;
+ setTimeout(doAskGame, delay);
+ },
+ socketMessageListener: function(msg) {
+ if (!this.conn) return;