<commit_before>/*
 * main.js is the entry point for the application.
 */
define(function(require) {

	// context is the application context
	// dashboard is the bridge between the application context and the apps
	var context = require('context');
	var dashboard = require('dashboard');

	// Vendor libraries
	var $ = require('jquery');
	var Backbone = require('backbone');
	var underscore = require('underscore');

	// Our libraries
	var FabMoAPI = require('fabmo');
	var FabMoUI = require('fabmo-ui');

	var WheelControl = require('handwheel');
	var Keyboard = require('keyboard');
	var Keypad = require('keypad');

	var wheel, keypad, keyboard;

	// API object defines our connection to the tool.
	var engine = new FabMoAPI();

	var modalIsShown = false;

	// Detect touch screen

	var supportsTouch = 'ontouchstart' in window || navigator.msMaxTouchPoints;

	// Initial read of engine configuration
	engine.getConfig();

	context.apps = new context.models.Apps();

	// Load the apps from the server
	context.apps.fetch({
		success: function() {
			// Create the menu based on the apps thus retrieved
			context.appMenuView = new context.views.AppMenuView({collection : context.apps, el : '#app_menu_container'});

			// Create a FabMo object for the dashboard
			dashboard.setEngine(engine);
			dashboard.ui=new FabMoUI(dashboard.engine);

			keyboard = setupKeyboard();
			keypad = setupKeypad();

			// Start the application
			router = new context.Router();
			router.setContext(context);

			// Sort of a hack, but works OK.
			$('#spinner').hide();

			// Start backbone routing
			Backbone.history.start();

			// Request a status update from the tool
			engine.getStatus();
		}
	});

function getManualMoveSpeed(move) {
	var speed_ips = null;
	try {
		switch(move.axis) {
			case 'x':
			case 'y':
				speed_ips = engine.config.machine.manual.xy_speed;
				break;
			case 'z':
				speed_ips = engine.config.machine.manual.z_speed;
				break;
		}
	} catch(e) {
		console.error(e);
	}
	return speed_ips;
}

function getManualNudgeIncrement(move) {
	var increment_inches = null;
	try {
		switch(move.axis) {
			case 'x':
			case 'y':
				increment_inches = engine.config.machine.manual.xy_increment;
				break;
			case 'z':
				increment_inches = engine.config.machine.manual.z_increment;
				break;
		}
	} catch(e) {
		console.error(e);
	}
	return increment_inches;
}

function setupKeyboard() {
	var keyboard = new Keyboard('#keyboard');
	keyboard.on('go', function(move) {

		if(move) {
			dashboard.engine.manualStart(move.axis, move.dir*60.0*(getManualMoveSpeed(move) || 0.1));
		}
	});

	keyboard.on('stop', function(evt) {
		dashboard.engine.manualStop();
	})
	return keyboard;
}

function setupKeypad() {

	var keypad = new Keypad('#keypad');
	keypad.on('go', function(move) {
		if(move) {
			dashboard.engine.manualStart(move.axis, move.dir*60.0*(getManualMoveSpeed(move) || 0.1));
		}
	});

	keypad.on('stop', function(evt) {
		dashboard.engine.manualStop();
	});

	keypad.on('nudge', function(nudge) {
		dashboard.engine.manualMoveFixed(nudge.axis, 60*getManualMoveSpeed(nudge), nudge.dir*getManualNudgeIncrement(nudge))
	});
	return keypad;
}

function showModal(options) {
	if(modalIsShown) {
		return;
	}
	modalIsShown = true;

	if(options['title']) {
		$('#modalDialogTitle').html(options.title).show();
	} else {
		$('#modalDialogTitle').hide();
	}

	if(options['lead']) {
		$('#modalDialogLead').html(options.lead).show();
	} else {
		$('#modalDialogLead').hide();
	}

	if(options['message']) {
		$('#modalDialogMessage').html(options.message).show();
	} else {
		$('#modalDialogMessage').hide();
	}

	if(options['detail']) {
		$('#modalDialogDetail').html(options.detail).show();
	} else {
		$('#modalDialogDetail').hide();
	}

	$('#modalDialogButtons').hide();
	if(options['okText']) {
		$('#modalDialogButtons').show();
		$('#modalDialogOKButton').text(options.okText).show().one('click', function() {
			$('#modalDialogCancelButton').off('click');
			(options['ok'] || function() {})();
		});
	} else {
		$('#modalDialogOKButton').hide();
	}

	if(options['cancelText']) {
		$('#modalDialogButtons').show();
		$('#modalDialogCancelButton').text(options.cancelText).show().one('click', function() {
			$('#modalDialogOKButton').off('click');
			(options['cancel'] || function() {})();
		});
	} else {
		$('#modalDialogCancelButton').hide();
	}

	if(options['cancel']) {
		$('#modalDialogClose').show().one('click', function() {
			options.cancel();
		});
	} else {
		$('#modalDialogClose').hide();
	}

	$('#modalDialog').foundation('reveal', 'open');
}

function hideModal() {
	if(!modalIsShown) { return; }
	modalIsShown = false;
	$('#modalDialog').foundation('reveal', 'close');
}

// Kill the currently running job when the modal error dialog is dismissed
/*$(document).on('close.fndtn.reveal', '[data-reveal]', function (evt) {
  var modal = $(this);
  if(engine.status.state === "stopped") {
	  dashboard.engine.quit();
	  console.info("Quitting the tool on dismiss")
  } else if(engine.status.state === "paused") {
  	dashboard.engine.resume();
  } else {
	  console.warn("Not quitting the tool because it's not stopped.")
  }
});*/

// listen for escape key press to quit the engine
$(document).on('keyup', function(e) {
    if(e.keyCode == 27) {
        console.log("ESC key pressed - quitting engine.");
        dashboard.engine.quit();
    }
});

//goto this location

$('html').on('click', function (e) {
		if (e.target.id === "go-here") {
		var x = $('.posx').attr('value','')[1].value;
		var y = $('.posy').attr('value','')[1].value;
		var z = $('.posz').attr('value','')[1].value;
		var gcode = "G0 X" + x + " Y" + y + " Z" + z;
		dashboard.engine.gcode(gcode);
		$('.go-here').hide();
		} else if (e.target.id === "axis"){
			 $("input:text").focus(function() { $(this).select(); } );
			$('.go-here').show();
		} else {
			$('.go-here').hide();
		}
});
$('.posx, .posy, .posz').keyup(function(e){
    if(e.keyCode == 13){
    	e.preventDefault();
        e.stopPropagation();
        var x = $('.posx').attr('value','')[1].value;
		var y = $('.posy').attr('value','')[1].value;
		var z = $('.posz').attr('value','')[1].value;
		var gcode = "G0 X" + x + " Y" + y + " Z" + z;
		dashboard.engine.gcode(gcode);
		$('.go-here').hide();

    }

});

// Handlers for the home/probe buttons
$('.button-zerox').click(function(e) {dashboard.engine.sbp('ZX'); });
$('.button-zeroy').click(function(e) {dashboard.engine.sbp('ZY'); });
$('.button-zeroz').click(function(e) {dashboard.engine.sbp('ZZ'); });
$('.button-zeroa').click(function(e) {dashboard.engine.sbp('ZA'); });
$('.button-zerob').click(function(e) {dashboard.engine.sbp('ZB'); });

$('.play').on('click', function(e){
	$("#main").addClass("offcanvas-overlap-left");
	dashboard.engine.job_run(function (){
		dashboard.engine.getJobQueue(function (err, data){
			if (data.name == 'undefined' || data.length === 0) {
				$('.nextJob').text('No Job Pending');
				$('.play').hide();
				$('.gotoJobManager').show();
				$('.nextJob').css('top', '2px');
				$('.startnextLabel').css('top', '2px');
			} else {
				$('.nextJob').text(data[0].name);
			}
		});
	});
});

engine.on('status', function(status) {
	try {
		if(status.unit) {
			wheel.setUnits(status.unit);
		}
		dashboard.engine.getJobQueue(function (err, data){
			if (data.name == 'undefined' || data.length === 0) {
				$('.nextJob').text('No Job Pending');
				$('.play').hide();
				$('.gotoJobManager').show();
				$('.nextJob').css('top', '2px');
				$('.startnextLabel').css('top', '2px');
			} else {
				$('.nextJob').text(data[0].name);
				$('.play').show();
				$('.gotoJobManager').hide();
				$('.nextJob').css('top', '-9.5px');
				$('.startnextLabel').css('top', '-9.5px');
			}
		});
	} catch(e) {

	}
});

var disconnected = false;

engine.on('disconnect', function() {
	if(!disconnected) {
		disconnected = true;
		$('#disconnectDialog').foundation('reveal', 'open');
	}
});

engine.on('connect', function() {
	if(disconnected) {
		disconnected = false;
		$('#disconnectDialog').foundation('reveal', 'close');
	}
});

engine.on('status', function(status) {
	if(status['info']) {
		if(status.info['message']) {
			showModal({
				title : 'Message',
				lead : status.info.message,
				okText : 'Continue',
				cancelText : 'Quit',
				ok : function() {
					dashboard.engine.resume();
				},
				cancel : function() {
					dashboard.engine.quit();
				}

 			})
		} else if(status.info['error']) {

			if(dashboard.engine.status.job) {
				var detailHTML = '
' +
					  'Job Name:  ' + dashboard.engine.status.job.name + '
' +
					  'Job Description:  ' + dashboard.engine.status.job.description +
					'
'
			} else {
				var detailHTML = '
Additional information for this error is unavailable.
';
			}

			showModal({
				title : 'Message',
				lead : '
An error has occurred!
',
				message: status.info.error,
				detail : detailHTML,
				cancelText : 'Quit',
				cancel : function() {
					dashboard.engine.quit();
				}

 			})

		}
	} else {
		hideModal();
	}
});
setInterval(function() {
	engine.ping(function(err, time) {
		if(err) {
			console.error(err);
		} else {
			console.info("PING Response time: " + time + "ms");
		}
	});
}, 3000);

(function () {
if ($(window).width() < 620) {
    function start_marquee() {
        function go() {
            i = i < width ? i + step : 1;
            m.style.marginLeft = -i + 'px';
        }
        var i = 0,
            step = 3,
            space = '     ';
        var m = document.getElementById('marquee');
        var t = m.innerHTML;
        m.innerHTML = t + space;
        m.style.position = 'absolute';
        var width = (m.clientWidth + 1);
        m.style.position = '';
        m.innerHTML = t + space + t + space + t + space + t + space + t + space + t + space + t + space;
        if (m.addEventListener) {
            m.addEventListener('mouseenter', function () {
                step = 0;
            }, false);
            m.addEventListener('mouseleave', function () {
                step = 3;
            }, false);
        }
        var x = setInterval(go, 50);
    }
    if (window.addEventListener) {
        window.addEventListener('load', start_marquee, false);
    } else if (window.attachEvent) { //IE7-8
        window.attachEvent('onload', start_marquee);
    }

	$('.currentContainer').css('width', '100px');
	$('.currentJobTitle').css('width', '50%');
	$('.currentJobTitle').css('padding-left', '100px');
}
})();
$.post('/time', {
	'utc' : new Date().toUTCString()
});
function touchScreen () {
	if (supportsTouch) {
		$('#app-client-container').css({'-webkit-overflow-scrolling':'touch','overflow-y':'scroll'});
	}
}
touchScreen();
});

<commit_msg>can only use go here feature when tool is idle<commit_after>/*
 * main.js is the entry point for the application.
 */
define(function(require) {

	// context is the application context
	// dashboard is the bridge between the application context and the apps
	var context = require('context');
	var dashboard = require('dashboard');

	// Vendor libraries
	var $ = require('jquery');
	var Backbone = require('backbone');
	var underscore = require('underscore');

	// Our libraries
	var FabMoAPI = require('fabmo');
	var FabMoUI = require('fabmo-ui');

	var WheelControl = require('handwheel');
	var Keyboard = require('keyboard');
	var Keypad = require('keypad');

	var wheel, keypad, keyboard;

	// API object defines our connection to the tool.
	var engine = new FabMoAPI();

	var modalIsShown = false;

	// Detect touch screen

	var supportsTouch = 'ontouchstart' in window || navigator.msMaxTouchPoints;

	// Initial read of engine configuration
	engine.getConfig();

	context.apps = new context.models.Apps();

	// Load the apps from the server
	context.apps.fetch({
		success: function() {
			// Create the menu based on the apps thus retrieved
			context.appMenuView = new context.views.AppMenuView({collection : context.apps, el : '#app_menu_container'});

			// Create a FabMo object for the dashboard
			dashboard.setEngine(engine);
			dashboard.ui=new FabMoUI(dashboard.engine);

			keyboard = setupKeyboard();
			keypad = setupKeypad();

			// Start the application
			router = new context.Router();
			router.setContext(context);

			// Sort of a hack, but works OK.
			$('#spinner').hide();

			// Start backbone routing
			Backbone.history.start();

			// Request a status update from the tool
			engine.getStatus();
		}
	});

function getManualMoveSpeed(move) {
	var speed_ips = null;
	try {
		switch(move.axis) {
			case 'x':
			case 'y':
				speed_ips = engine.config.machine.manual.xy_speed;
				break;
			case 'z':
				speed_ips = engine.config.machine.manual.z_speed;
				break;
		}
	} catch(e) {
		console.error(e);
	}
	return speed_ips;
}

function getManualNudgeIncrement(move) {
	var increment_inches = null;
	try {
		switch(move.axis) {
			case 'x':
			case 'y':
				increment_inches = engine.config.machine.manual.xy_increment;
				break;
			case 'z':
				increment_inches = engine.config.machine.manual.z_increment;
				break;
		}
	} catch(e) {
		console.error(e);
	}
	return increment_inches;
}

function setupKeyboard() {
	var keyboard = new Keyboard('#keyboard');
	keyboard.on('go', function(move) {

		if(move) {
			dashboard.engine.manualStart(move.axis, move.dir*60.0*(getManualMoveSpeed(move) || 0.1));
		}
	});

	keyboard.on('stop', function(evt) {
		dashboard.engine.manualStop();
	})
	return keyboard;
}

function setupKeypad() {

	var keypad = new Keypad('#keypad');
	keypad.on('go', function(move) {
		if(move) {
			dashboard.engine.manualStart(move.axis, move.dir*60.0*(getManualMoveSpeed(move) || 0.1));
		}
	});

	keypad.on('stop', function(evt) {
		dashboard.engine.manualStop();
	});

	keypad.on('nudge', function(nudge) {
		dashboard.engine.manualMoveFixed(nudge.axis, 60*getManualMoveSpeed(nudge), nudge.dir*getManualNudgeIncrement(nudge))
	});
	return keypad;
}

function showModal(options) {
	if(modalIsShown) {
		return;
	}
	modalIsShown = true;

	if(options['title']) {
		$('#modalDialogTitle').html(options.title).show();
	} else {
		$('#modalDialogTitle').hide();
	}

	if(options['lead']) {
		$('#modalDialogLead').html(options.lead).show();
	} else {
		$('#modalDialogLead').hide();
	}

	if(options['message']) {
		$('#modalDialogMessage').html(options.message).show();
	} else {
		$('#modalDialogMessage').hide();
	}

	if(options['detail']) {
		$('#modalDialogDetail').html(options.detail).show();
	} else {
		$('#modalDialogDetail').hide();
	}

	$('#modalDialogButtons').hide();
	if(options['okText']) {
		$('#modalDialogButtons').show();
		$('#modalDialogOKButton').text(options.okText).show().one('click', function() {
			$('#modalDialogCancelButton').off('click');
			(options['ok'] || function() {})();
		});
	} else {
		$('#modalDialogOKButton').hide();
	}

	if(options['cancelText']) {
		$('#modalDialogButtons').show();
		$('#modalDialogCancelButton').text(options.cancelText).show().one('click', function() {
			$('#modalDialogOKButton').off('click');
			(options['cancel'] || function() {})();
		});
	} else {
		$('#modalDialogCancelButton').hide();
	}

	if(options['cancel']) {
		$('#modalDialogClose').show().one('click', function() {
			options.cancel();
		});
	} else {
		$('#modalDialogClose').hide();
	}

	$('#modalDialog').foundation('reveal', 'open');
}

function hideModal() {
	if(!modalIsShown) { return; }
	modalIsShown = false;
	$('#modalDialog').foundation('reveal', 'close');
}

// Kill the currently running job when the modal error dialog is dismissed
/*$(document).on('close.fndtn.reveal', '[data-reveal]', function (evt) {
  var modal = $(this);
  if(engine.status.state === "stopped") {
	  dashboard.engine.quit();
	  console.info("Quitting the tool on dismiss")
  } else if(engine.status.state === "paused") {
  	dashboard.engine.resume();
  } else {
	  console.warn("Not quitting the tool because it's not stopped.")
  }
});*/

// listen for escape key press to quit the engine
$(document).on('keyup', function(e) {
    if(e.keyCode == 27) {
        console.log("ESC key pressed - quitting engine.");
        dashboard.engine.quit();
    }
});

//goto this location

$('html').on('click', function (e) {
		if (e.target.id === "go-here") {
		var x = $('.posx').attr('value','')[1].value;
		var y = $('.posy').attr('value','')[1].value;
		var z = $('.posz').attr('value','')[1].value;
		var gcode = "G0 X" + x + " Y" + y + " Z" + z;
		dashboard.engine.gcode(gcode);
		$('.go-here').hide();
		} else if (e.target.id === "axis"){
			 $("input:text").focus(function() {
             $(this).select();
             $('.go-here').show();
             } );

		} else {
			$('.go-here').hide();
		}
});
$('.posx, .posy, .posz').keyup(function(e){
    if(e.keyCode == 13){
    	e.preventDefault();
        e.stopPropagation();
        var x = $('.posx').attr('value','')[1].value;
		var y = $('.posy').attr('value','')[1].value;
		var z = $('.posz').attr('value','')[1].value;
		var gcode = "G0 X" + x + " Y" + y + " Z" + z;
		dashboard.engine.gcode(gcode);
		$('.go-here').hide();

    }

});

// Handlers for the home/probe buttons
$('.button-zerox').click(function(e) {dashboard.engine.sbp('ZX'); });
$('.button-zeroy').click(function(e) {dashboard.engine.sbp('ZY'); });
$('.button-zeroz').click(function(e) {dashboard.engine.sbp('ZZ'); });
$('.button-zeroa').click(function(e) {dashboard.engine.sbp('ZA'); });
$('.button-zerob').click(function(e) {dashboard.engine.sbp('ZB'); });

$('.play').on('click', function(e){
	$("#main").addClass("offcanvas-overlap-left");
	dashboard.engine.job_run(function (){
		dashboard.engine.getJobQueue(function (err, data){
			if (data.name == 'undefined' || data.length === 0) {
				$('.nextJob').text('No Job Pending');
				$('.play').hide();
				$('.gotoJobManager').show();
				$('.nextJob').css('top', '2px');
				$('.startnextLabel').css('top', '2px');
			} else {
				$('.nextJob').text(data[0].name);
			}
		});
	});
});

var disconnected = false;

engine.on('disconnect', function() {
	if(!disconnected) {
		disconnected = true;
		$('#disconnectDialog').foundation('reveal', 'open');
	}
});

engine.on('connect', function() {
	if(disconnected) {
		disconnected = false;
		$('#disconnectDialog').foundation('reveal', 'close');
	}
});

engine.on('status', function(status) {
   try {
		if(status.unit) {
			wheel.setUnits(status.unit);
		}
	} catch(e) {

	}
    if (status.state != 'idle'){
        $('#position input').attr('disabled', true);
    } else {
        $('#position input').attr('disabled', false);
    }
    if(status['info']) {
		if(status.info['message']) {
			showModal({
				title : 'Message',
				lead : status.info.message,
				okText : 'Continue',
				cancelText : 'Quit',
				ok : function() {
					dashboard.engine.resume();
				},
				cancel : function() {
					dashboard.engine.quit();
				}

 			})
		} else if(status.info['error']) {

			if(dashboard.engine.status.job) {
				var detailHTML = '
' +
					  'Job Name:  ' + dashboard.engine.status.job.name + '
' +
					  'Job Description:  ' + dashboard.engine.status.job.description +
					'
'
			} else {
				var detailHTML = '
Additional information for this error is unavailable.
';
			}

			showModal({
				title : 'Message',
				lead : '
An error has occurred!
',
				message: status.info.error,
				detail : detailHTML,
				cancelText : 'Quit',
				cancel : function() {
					dashboard.engine.quit();
				}

 			})

		}
	} else {
		hideModal();
	}
});
setInterval(function() {
	engine.ping(function(err, time) {
		if(err) {
			console.error(err);
		} else {
			console.info("PING Response time: " + time + "ms");
		}
	});
}, 3000);

(function () {
if ($(window).width() < 620) {
    function start_marquee() {
        function go() {
            i = i < width ? i + step : 1;
            m.style.marginLeft = -i + 'px';
        }
        var i = 0,
            step = 3,
            space = '     ';
        var m = document.getElementById('marquee');
        var t = m.innerHTML;
        m.innerHTML = t + space;
        m.style.position = 'absolute';
        var width = (m.clientWidth + 1);
        m.style.position = '';
        m.innerHTML = t + space + t + space + t + space + t + space + t + space + t + space + t + space;
        if (m.addEventListener) {
            m.addEventListener('mouseenter', function () {
                step = 0;
            }, false);
            m.addEventListener('mouseleave', function () {
                step = 3;
            }, false);
        }
        var x = setInterval(go, 50);
    }
    if (window.addEventListener) {
        window.addEventListener('load', start_marquee, false);
    } else if (window.attachEvent) { //IE7-8
        window.attachEvent('onload', start_marquee);
    }

	$('.currentContainer').css('width', '100px');
	$('.currentJobTitle').css('width', '50%');
	$('.currentJobTitle').css('padding-left', '100px');
}
})();
$.post('/time', {
	'utc' : new Date().toUTCString()
});
function touchScreen () {
	if (supportsTouch) {
		$('#app-client-container').css({'-webkit-overflow-scrolling':'touch','overflow-y':'scroll'});
	}
}
touchScreen();
});

<|endoftext|>


<commit_before>var bar; // Intended global

require([
    "lib/chai/chai",
    "text!plugins/c9.ide.layout.classic/skins.xml",
    "events",
    "text!/static/standalone/skin/default/dark.css"
], function (chai, skin, events, theme) {
    "use strict";
    var expect = chai.expect;
    var EventEmitter = events.EventEmitter;

    function html(path, message) {
        var htmlNode;
        if (typeof path != "object" || !(path instanceof HTMLElement))
            htmlNode = html.constr(path);
        else
            htmlNode = path;
        var not = false;

        function testOk(value) {
            if (not)
                expect(value, message || path).not.ok;
            else
                expect(value, message || path).ok;
            not = false;
        }

        var obj = {
            get is(){ return obj; },
            get has(){ return obj; },
            get have(){ return obj; },
            get and(){ return obj; },
            get to(){ return obj; },
            get not(){ not = true; return obj; },

            get ok(){ testOk(htmlNode); return obj; },
            get exists(){ testOk(htmlNode); return obj; },
            get exist(){ testOk(htmlNode); return obj; },
            get node(){ return htmlNode },

            get visible() {
                testOk(htmlNode && (htmlNode.offsetWidth || htmlNode.offsetHeight));
                return obj;
            },

            text: function(text) {
                testOk(htmlNode.textContent.match(text));
                return obj;
            },

            icon: function(icon) {
                testOk(htmlNode.innerHTML.indexOf(icon) > -1);
                return obj;
            },

            className: function(name) {
                testOk(htmlNode.className.indexOf(name) > -1);
                return obj;
            },
            child: function(query) {
                if (typeof query == "number") {
                    if (query < 0)
                        query = htmlNode.children.length + query;
                    htmlNode = htmlNode.children[query];
                } else {
                    htmlNode = htmlNode.querySelector(query);
                }
                return obj;
            }
        };
        return obj;
    }
    expect.html = html;
    expect.html.setConstructor = function(fn) {
        html.constr = fn;
    };

    expect.html.mocked = function(options, imports, register) {
        register(null, {
            c9 : (function(){
                var x = new EventEmitter();
                x.location = "";
                x.has = function(){ return false; };
                x.toInternalPath = x.toExternalPath = function(p) {
                    return p;
                };
                x.connected = true;
                return x;
            })(),
            vfs: (function(){
                var x = new EventEmitter();
                return x;
            })(),
            anims: (function(){
                var x = new EventEmitter();
                x.animateSplitBoxNode = function(node, opt) {
                    node.setAttribute("height", parseInt(opt.height, 10));
                };
                return x;
            })(),
            watcher: (function(){
                var x = new EventEmitter();
                x.watch = function(){};
                x.unwatch = function(){};
                x.check = function(){};
                return x;
            })(),
            save: (function(){
                var x = new EventEmitter();
                x.saveAll = function(c){ c(); };
                x.getSavingState = function(tab) { return "saved"; };
                return x;
            })(),
            findreplace: {

            },
            ace: (function() {
                var x = new EventEmitter();
                x.getElement = function(){};
                return x;
            })(),
            css: {
                get packed() { return true; },
                get packedThemes() { return true; },
                defineLessLibrary: function(){},
                insert: function() {},
                insertLess: function() {}
            },
            settings: (function(){
                var obj = new EventEmitter();
                obj.save = function(){};
                obj.set = function(){};
                obj.get = function(){};
                obj.getNumber = function() {};
                obj.emit("read", {});
                return obj;
            })(),
            fs: (function(){
                var obj = new EventEmitter();
                obj.writeFile = function(){};
                obj.watch = function(){};
                return obj;
            })(),
            tooltip: {
                add: function(){}
            },
            clipboard: (function(){
                var cb = new EventEmitter();
                cb.registerHandler = function(){};
                return cb;
            })(),
            preferences: (function(){
                var prefs = new EventEmitter();
                prefs.add = function(){};
                return prefs;
            })(),
            commands: (function(){
                var commands = {};

                if (typeof apf != "undefined") {
                    apf.button.prototype.$propHandlers["command"] =
                    apf.item.prototype.$propHandlers["command"] = function(value) {
                        this.onclick = function(){
                            commands[value].exec(
                                apf.getPlugin("tabManager").focussedPage.editor
                            );
                        };
                    };
                }

                var c = new EventEmitter();

                c.commands = commands;
                c.addCommands = function(a, b, c) {
                    a.forEach(function(o) {
                        commands[o.name] = o;
                    });
                };
                c.addCommand = function(o) {
                    commands[o.name] = o;
                };
                c.removeCommand = function(o) {
                    delete commands[o.name];
                };
                c.exec = function(name) {
                    commands[name].exec();
                };
                c.getExceptionList = function(){ return []; };

                return c;
            })(),
            log: {},
            http: {},
            api: {
                stats: {
                    post: function(type, message, cb) {
                        cb && cb();
                    }
                }
            },
            layout: (function(){
                // Load the skin
                if (imports.ui) {
                    var plugin = new imports.Plugin();

                    imports.ui.insertCss(theme, false, plugin);

                    imports.ui.insertSkin({
                        "data"       : skin,
                        "media-path" : "plugins/c9.ide.layout.classic/images/",
                        "icon-path"  : "plugins/c9.ide.layout.classic/icons/"
                    }, {addElement: function(){}});

                    document.documentElement.style.background = "white";
                }

                var layout = plugin || new EventEmitter();

                layout.initMenus = function() {};
                layout.findParent = function(){
                    if (!bar || bar.$amlDestroyed) {
                        bar = apf.document.documentElement.appendChild(
                            new imports.ui.bar());
                        bar.$ext.style.position = "fixed";
                        bar.$ext.style.left = "20px";
                        bar.$ext.style.right = "320px";
                        bar.$ext.style.bottom = "20px";
                        bar.$ext.style.height = "200px";
                        bar.$ext.style.minHeight = "200px";
                        plugin.addElement(bar);
                    }
                    bar.setAttribute("resizable", true);

                    return bar;
                };
                layout.getElement = function(){
                    return new apf.bar();
                };
                layout.setFindArea = function(active, callback, isDefault) {
                    // callback();
                };
                layout.proposeLayoutChange = function(){};
                return layout;
            })(),
            panels: (function(){
                var panel, column;

                var api = {
                    register: function(p, o) {
                        if (!column) {
                            column = apf.document.documentElement.appendChild(
                                new imports.ui.bar({style:"background : #303130;"}));
                            column.$ext.style.position = "fixed";
                            column.$ext.style.top = "75px";
                            column.$ext.style.right = "20px";
                            column.$ext.style.left = "";
                            column.$ext.style.bottom = "20px";
                            column.$ext.style.width = "300px";
                            column.$ext.style.height = "";
                        }

                        panel = p;
                        p.draw({container: column.$ext, aml: column});
                    },
                    isActive: function() { return false; },
                    unregister: function(){},
                    activate: function(){
                        // panel.panel.show()
                    },
                    deactivate: function(){
                        // panel.panel.hide()
                    },
                    on: function(){

                    }
                };
                if (imports.Plugin) {
                    var plugin = new imports.Plugin();
                    plugin.freezePublicAPI(api);
                    return plugin;
                } else {
                    return api;
                }
            })(),
            Panel: function(developer, deps, options) {
                var plugin = new imports.Plugin(developer, deps);
                var emit = plugin.getEmitter();
                var drawn = false;
                var where;
                plugin.on("load", function(){
                    where = options.where || "left";
                    var panels = apf.getPlugin("panels");
                    panels.register(plugin);
                });
                plugin.freezePublicAPI.baseclass();
                plugin.freezePublicAPI({
                    get autohide(){ return false; },
                    get width(){ return options.width; },
                    get minWidth(){ return options.minWidth; },
                    get aml(){ return plugin.getElement(options.elementName); },
                    get area(){ return ""; },
                    get where(){ return where; },
                    setCommand: function(){},
                    attachTo: function(){},
                    detach: function(){},
                    emit: emit,
                    show: function(){ emit("show"); },
                    hide: function(){ emit("hide"); },
                    draw: function draw(area) {
                        if (drawn) return false;
                        drawn = true;

                        emit.sticky("draw", {
                            html: area.container,
                            aml: area.aml
                        });

                        return true;
                    }
                });

                return plugin;
            },
            tree: (function(){
                var tree = new EventEmitter();
                tree.createFolder = function(){};
                tree.getElement = function(){};
                tree.getAllExpanded = function(){ return []; };
                return tree;
            })(),
            "tree.favorites" : (function(){
                var tree = new EventEmitter();
                tree.addFavorites = function(){};
                tree.getFavoritePaths = function(){ return [] };
                return tree;
            })(),
            tabManager: (function(){
                var tabManager = new EventEmitter();
                tabManager.open = function(){ tabManager.emit("open") };
                tabManager.openFile = function(){ tabManager.emit("open") };
                tabManager.findPage = function(){};
                tabManager.getPanes = function(){ return [] };
                tabManager.checkAllTabs = function(){};
                return tabManager;
            })(),
            tabbehavior: (function(){
                var x = new EventEmitter();
                x.getElement = function(){};
                return x;
            })(),
            menus: (function(){
                var menus = new EventEmitter();
                menus.addItemByPath = function(x, aml, y, plugin) {
                    aml && (plugin || y).addElement(aml);
                    return aml;
                };
                menus.addItemToMenu = menus.addItemByPath;
                menus.get = function(){return {}};
                menus.remove = function(){};
                menus.enableItem = function(){};
                menus.disableItem = function(){};
                return menus;
            })(),
            util: {
                alert: function() {},
                escapeXml: function(s) { return s; },
                stableStringify: function(s) { return JSON.stringify(s); },
            },
            gotoline: {
                toggle: function(){ }
            },
            "auth.bootstrap": {
                login: function(callback) { callback(); }
            },
            "auth": {},
            "info": {
                getUser: function(callback) {
                    var user = { id: 1 };
                    return callback ? callback(null, user) : user;
                },
                getWorkspace: function(callback) {
                    var ws = { id: 2 };
                    return callback ? callback(null, ws) : ws;
                }
            },
            "ace.gotoline": {},
            "ace.stripws": {
                disable: function(){},
                enable: function(){}
            },
            "dialog.alert": {show: function() { console.log(console, Array.prototype.slice.apply(arguments)); }},
            "dialog.confirm": {show: function() {}},
            "dialog.question": {show: function() {}},
            "dialog.file": {show: function() {}},
            "dialog.fileremove": {show: function() {}},
            "dialog.fileoverwrite": {show: function() {}},
            "dialog.error": {
                showError: function(msg) {
                    console.warn(msg);
                }
            },
            "run.gui": { getElement : function(){} },
            "debugger": {debug: function() {}, stop: function(){}},
            "focusManager": {
                focus: function() {

                }
            },
            "metrics": {
                getLastPing: function() { throw Error("Not implemented"); },
                getLastest: function() { throw Error("Not implemented"); },
            },
            error_handler: {reportError: function(){}},
            installer: {
                show: function(){}
            },
            proc: {
                execFile: function() {},
                spawn: function() {}
            },
            threewaymerge: {
                patchAce: function(oldVal, newVal, doc) {
                    if (doc == undefined) {
                        doc = newVal;
                        newVal = oldVal;
                    }
                    doc.setValue(newVal);
                }
            },
            metadata: (function(){
                var x = new EventEmitter();
                return x;
            })(),
            editors: (function(){
                var x = new EventEmitter();
                return x;
            })(),
            timeslider: (function(){
                var x = new EventEmitter();
                return x;
            })(),
            OTDocument: (function(){
                var x = new EventEmitter();
                return x;
            })(),
            "notification.bubble": (function(){
                var x = new EventEmitter();
                return x;
            })(),
            "collab": (function(){
                var x = new EventEmitter();
                return x;
            })(),
            "collab.connect": (function(){
                var x = new EventEmitter();
                return x;
            })(),
            "collab.workspace": (function(){
                var x = new EventEmitter();
                x.users = [];
                return x;
            })(),
            "collab.util": (function(){
                var x = new EventEmitter();
                return x;
            })(),
        });
    };

    expect.setupArchitectTest = function(config, architect, options) {
        if (options && options.mockPlugins) {
            config.push({
                consumes: [],
                provides: options.mockPlugins,
                setup: expect.html.mocked
            });
        }
        architect.resolveConfig(config, function(err, config) {
            /*global describe it before after = */
            if (err) throw err;
            var app = window.app = architect.createApp(config, function(err, app) {
                if (err && err.unresolved && !config.unresolved) {
                    console.warn("Adding mock services for " + err.unresolved);
                    config.unresolved = err.unresolved;
                    expect.setupArchitectTest(config, architect, {
                        mockPlugins: config.unresolved
                    });
                    return;
                }

                describe('app', function() {
                    it('should load test app', function(done) {
                        expect(err).not.ok;
                        done();
                    });
                });

                onload && onload();

            });
            if (app) {
                app.on("service", function(name, plugin) {
                    if (!plugin.name && typeof plugin != "function")
                        plugin.name = name;
                });
                app.rerun = function() {
                    expect.setupArchitectTest(config, architect);
                };
            }
        });
    };

});<commit_msg>Fix tests<commit_after>var bar; // Intended global

require([
    "lib/chai/chai",
    "text!plugins/c9.ide.layout.classic/skins.xml",
    "events",
    "text!/static/standalone/skin/default/dark.css"
], function (chai, skin, events, theme) {
    "use strict";
    var expect = chai.expect;
    var EventEmitter = events.EventEmitter;

    function html(path, message) {
        var htmlNode;
        if (typeof path != "object" || !(path instanceof HTMLElement))
            htmlNode = html.constr(path);
        else
            htmlNode = path;
        var not = false;

        function testOk(value) {
            if (not)
                expect(value, message || path).not.ok;
            else
                expect(value, message || path).ok;
            not = false;
        }

        var obj = {
            get is(){ return obj; },
            get has(){ return obj; },
            get have(){ return obj; },
            get and(){ return obj; },
            get to(){ return obj; },
            get not(){ not = true; return obj; },

            get ok(){ testOk(htmlNode); return obj; },
            get exists(){ testOk(htmlNode); return obj; },
            get exist(){ testOk(htmlNode); return obj; },
            get node(){ return htmlNode },

            get visible() {
                testOk(htmlNode && (htmlNode.offsetWidth || htmlNode.offsetHeight));
                return obj;
            },

            text: function(text) {
                testOk(htmlNode.textContent.match(text));
                return obj;
            },

            icon: function(icon) {
                testOk(htmlNode.innerHTML.indexOf(icon) > -1);
                return obj;
            },

            className: function(name) {
                testOk(htmlNode.className.indexOf(name) > -1);
                return obj;
            },
            child: function(query) {
                if (typeof query == "number") {
                    if (query < 0)
                        query = htmlNode.children.length + query;
                    htmlNode = htmlNode.children[query];
                } else {
                    htmlNode = htmlNode.querySelector(query);
                }
                return obj;
            }
        };
        return obj;
    }
    expect.html = html;
    expect.html.setConstructor = function(fn) {
        html.constr = fn;
    };

    expect.html.mocked = function(options, imports, register) {
        register(null, {
            c9 : (function(){
                var x = new EventEmitter();
                x.location = "";
                x.has = function(){ return false; };
                x.toInternalPath = x.toExternalPath = function(p) {
                    return p;
                };
                x.connected = true;
                return x;
            })(),
            vfs: (function(){
                var x = new EventEmitter();
                return x;
            })(),
            anims: (function(){
                var x = new EventEmitter();
                x.animateSplitBoxNode = function(node, opt) {
                    node.setAttribute("height", parseInt(opt.height, 10));
                };
                return x;
            })(),
            watcher: (function(){
                var x = new EventEmitter();
                x.watch = function(){};
                x.unwatch = function(){};
                x.check = function(){};
                return x;
            })(),
            save: (function(){
                var x = new EventEmitter();
                x.saveAll = function(c){ c(); };
                x.getSavingState = function(tab) { return "saved"; };
                return x;
            })(),
            findreplace: {

            },
            ace: (function() {
                var x = new EventEmitter();
                x.getElement = function(){};
                return x;
            })(),
            css: {
                get packed() { return true; },
                get packedThemes() { return true; },
                defineLessLibrary: function(){},
                insert: function() {},
                insertLess: function() {}
            },
            settings: (function(){
                var obj = new EventEmitter();
                obj.save = function(){};
                obj.set = function(){};
                obj.get = function(){};
                obj.getNumber = function() {};
                obj.emit("read", {});
                return obj;
            })(),
            fs: (function(){
                var obj = new EventEmitter();
                obj.writeFile = function(){};
                obj.watch = function(){};
                return obj;
            })(),
            tooltip: {
                add: function(){}
            },
            clipboard: (function(){
                var cb = new EventEmitter();
                cb.registerHandler = function(){};
                return cb;
            })(),
            preferences: (function(){
                var prefs = new EventEmitter();
                prefs.add = function(){};
                return prefs;
            })(),
            commands: (function(){
                var commands = {};

                if (typeof apf != "undefined") {
                    apf.button.prototype.$propHandlers["command"] =
                    apf.item.prototype.$propHandlers["command"] = function(value) {
                        this.onclick = function(){
                            commands[value].exec(
                                apf.getPlugin("tabManager").focussedPage.editor
                            );
                        };
                    };
                }

                var c = new EventEmitter();

                c.commands = commands;
                c.addCommands = function(a, b, c) {
                    a.forEach(function(o) {
                        commands[o.name] = o;
                    });
                };
                c.addCommand = function(o) {
                    commands[o.name] = o;
                };
                c.removeCommand = function(o) {
                    delete commands[o.name];
                };
                c.exec = function(name) {
                    commands[name].exec();
                };
                c.getExceptionList = function(){ return []; };

                return c;
            })(),
            log: {},
            http: {},
            api: {
                stats: {
                    post: function(type, message, cb) {
                        cb && cb();
                    }
                }
            },
            layout: (function(){
                // Load the skin
                if (imports.ui) {
                    var plugin = new imports.Plugin();

                    imports.ui.insertCss(theme, false, plugin);

                    imports.ui.insertSkin({
                        "data"       : skin,
                        "media-path" : "plugins/c9.ide.layout.classic/images/",
                        "icon-path"  : "plugins/c9.ide.layout.classic/icons/"
                    }, {addElement: function(){}});

                    document.documentElement.style.background = "white";
                }

                var layout = plugin || new EventEmitter();

                layout.initMenus = function() {};
                layout.findParent = function(){
                    if (!bar || bar.$amlDestroyed) {
                        bar = apf.document.documentElement.appendChild(
                            new imports.ui.bar());
                        bar.$ext.style.position = "fixed";
                        bar.$ext.style.left = "20px";
                        bar.$ext.style.right = "320px";
                        bar.$ext.style.bottom = "20px";
                        bar.$ext.style.height = "200px";
                        bar.$ext.style.minHeight = "200px";
                        plugin.addElement(bar);
                    }
                    bar.setAttribute("resizable", true);

                    return bar;
                };
                layout.getElement = function(){
                    return new apf.bar();
                };
                layout.setFindArea = function(active, callback, isDefault) {
                    // callback();
                };
                layout.proposeLayoutChange = function(){};
                return layout;
            })(),
            panels: (function(){
                var panel, column;

                var api = {
                    register: function(p, o) {
                        if (!column) {
                            column = apf.document.documentElement.appendChild(
                                new imports.ui.bar({style:"background : #303130;"}));
                            column.$ext.style.position = "fixed";
                            column.$ext.style.top = "75px";
                            column.$ext.style.right = "20px";
                            column.$ext.style.left = "";
                            column.$ext.style.bottom = "20px";
                            column.$ext.style.width = "300px";
                            column.$ext.style.height = "";
                        }

                        panel = p;
                        p.draw({container: column.$ext, aml: column});
                    },
                    isActive: function() { return false; },
                    unregister: function(){},
                    activate: function(){
                        // panel.panel.show()
                    },
                    deactivate: function(){
                        // panel.panel.hide()
                    },
                    on: function(){

                    }
                };
                if (imports.Plugin) {
                    var plugin = new imports.Plugin();
                    plugin.freezePublicAPI(api);
                    return plugin;
                } else {
                    return api;
                }
            })(),
            Panel: function(developer, deps, options) {
                var plugin = new imports.Plugin(developer, deps);
                var emit = plugin.getEmitter();
                var drawn = false;
                var where;
                plugin.on("load", function(){
                    where = options.where || "left";
                    var panels = apf.getPlugin("panels");
                    panels.register(plugin);
                });
                plugin.freezePublicAPI.baseclass();
                plugin.freezePublicAPI({
                    get autohide(){ return false; },
                    get width(){ return options.width; },
                    get minWidth(){ return options.minWidth; },
                    get aml(){ return plugin.getElement(options.elementName); },
                    get area(){ return ""; },
                    get where(){ return where; },
                    setCommand: function(){},
                    attachTo: function(){},
                    detach: function(){},
                    emit: emit,
                    show: function(){ emit("show"); },
                    hide: function(){ emit("hide"); },
                    draw: function draw(area) {
                        if (drawn) return false;
                        drawn = true;

                        emit.sticky("draw", {
                            html: area.container,
                            aml: area.aml
                        });

                        return true;
                    }
                });

                return plugin;
            },
            tree: (function(){
                var tree = new EventEmitter();
                tree.createFolder = function(){};
                tree.getElement = function(){};
                tree.getAllExpanded = function(){ return []; };
                return tree;
            })(),
            "tree.favorites" : (function(){
                var tree = new EventEmitter();
                tree.addFavorites = function(){};
                tree.getFavoritePaths = function(){ return [] };
                return tree;
            })(),
            tabManager: (function(){
                var tabManager = new EventEmitter();
                tabManager.open = function(){ tabManager.emit("open") };
                tabManager.openFile = function(){ tabManager.emit("open") };
                tabManager.findPage = function(){};
                tabManager.getPanes = function(){ return [] };
                tabManager.checkAllTabs = function(){};
                return tabManager;
            })(),
            tabbehavior: (function(){
                var x = new EventEmitter();
                x.getElement = function(){};
                return x;
            })(),
            menus: (function(){
                var menus = new EventEmitter();
                menus.addItemByPath = function(x, aml, y, plugin) {
                    aml && (plugin || y).addElement(aml);
                    return aml;
                };
                menus.addItemToMenu = menus.addItemByPath;
                menus.get = function(){return {}};
                menus.remove = function(){};
                menus.enableItem = function(){};
                menus.disableItem = function(){};
                return menus;
            })(),
            util: {
                alert: function() {},
                escapeXml: function(s) { return s; },
                stableStringify: function(s) { return JSON.stringify(s); },
            },
            gotoline: {
                toggle: function(){ }
            },
            "auth.bootstrap": {
                login: function(callback) { callback(); }
            },
            "auth": {},
            "info": {
                getUser: function(callback) {
                    var user = { id: 1 };
                    return callback ? callback(null, user) : user;
                },
                getWorkspace: function(callback) {
                    var ws = { id: 2 };
                    return callback ? callback(null, ws) : ws;
                }
            },
            "ace.gotoline": {},
            "ace.stripws": {
                disable: function(){},
                enable: function(){}
            },
            "dialog.alert": {show: function() { console.log(console, Array.prototype.slice.apply(arguments)); }},
            "dialog.confirm": {show: function() {}},
            "dialog.question": {show: function() {}},
            "dialog.file": {show: function() {}},
            "dialog.fileremove": {show: function() {}},
            "dialog.fileoverwrite": {show: function() {}},
            "dialog.error": {
                showError: function(msg) {
                    console.warn(msg);
                }
            },
            "installer": { createSession : function(){} },
            "run.gui": { getElement : function(){} },
            "debugger": {debug: function() {}, stop: function(){}},
            "focusManager": {
                focus: function() {

                }
            },
            "metrics": {
                getLastPing: function() { throw Error("Not implemented"); },
                getLastest: function() { throw Error("Not implemented"); },
            },
            error_handler: {reportError: function(){}},
            proc: {
                execFile: function() {},
                spawn: function() {}
            },
            threewaymerge: {
                patchAce: function(oldVal, newVal, doc) {
                    if (doc == undefined) {
                        doc = newVal;
                        newVal = oldVal;
                    }
                    doc.setValue(newVal);
                }
            },
            metadata: (function(){
                var x = new EventEmitter();
                return x;
            })(),
            editors: (function(){
                var x = new EventEmitter();
                return x;
            })(),
            timeslider: (function(){
                var x = new EventEmitter();
                return x;
            })(),
            OTDocument: (function(){
                var x = new EventEmitter();
                return x;
            })(),
            "notification.bubble": (function(){
                var x = new EventEmitter();
                return x;
            })(),
            "collab": (function(){
                var x = new EventEmitter();
                return x;
            })(),
            "collab.connect": (function(){
                var x = new EventEmitter();
                return x;
            })(),
            "collab.workspace": (function(){
                var x = new EventEmitter();
                x.users = [];
                return x;
            })(),
            "collab.util": (function(){
                var x = new EventEmitter();
                return x;
            })(),
        });
    };

    expect.setupArchitectTest = function(config, architect, options) {
        if (options && options.mockPlugins) {
            config.push({
                consumes: [],
                provides: options.mockPlugins,
                setup: expect.html.mocked
            });
        }
        architect.resolveConfig(config, function(err, config) {
            /*global describe it before after = */
            if (err) throw err;
            var app = window.app = architect.createApp(config, function(err, app) {
                if (err && err.unresolved && !config.unresolved) {
                    console.warn("Adding mock services for " + err.unresolved);
                    config.unresolved = err.unresolved;
                    expect.setupArchitectTest(config, architect, {
                        mockPlugins: config.unresolved
                    });
                    return;
                }

                describe('app', function() {
                    it('should load test app', function(done) {
                        expect(err).not.ok;
                        done();
                    });
                });

                onload && onload();

            });
            if (app) {
                app.on("service", function(name, plugin) {
                    if (!plugin.name && typeof plugin != "function")
                        plugin.name = name;
                });
                app.rerun = function() {
                    expect.setupArchitectTest(config, architect);
                };
            }
        });
    };

});<|endoftext|>


<commit_before>import { PolymerElement } from '@polymer/polymer/polymer-element.js';
import './boardgame-ajax.js';
import { html } from '@polymer/polymer/lib/utils/html-tag.js';

/*

  StateManager keeps track of fetching state bundles from the server and
  figuring out when it makes sense to have the game-view install them.

  When the game-view becomes active, the info bundle is feteched. This
  includes information about who the current viewing player is and also
  includes the initial state bundle.

  Once the first state bundle has been installed, a socket is connected to
  receive updates about when the state increases. When the state version
  increases, that increments TargetVersion, which changes the URL to fetch.

*/

class BoardgameGameStateManager extends PolymerElement {
  static get template() {
    return html`
    <boardgame-ajax id="version" game-path="[[effectiveGameVersionPath]]" game-route="[[gameRoute]]" handle-as="json" last-response="{{versionData}}"></boardgame-ajax>
    <boardgame-ajax id="info" game-path="[[gameViewPath]]" game-route="[[gameRoute]]" handle-as="json" last-response="{{infoData}}"></boardgame-ajax>
`;
  }

  static get is() {
    return "boardgame-game-state-manager"
  }

  static get properties() {
    return {
      gameRoute: Object,
      gameFinished: {
        type: Boolean,
        value: false,
      },
      chest: Object,
      admin: Boolean,
      autoCurrentPlayer: Boolean,
      active: {
        type: Boolean,
        observer: "_activeChanged"
      },
      loggedIn: {
        type: Boolean,
        observer: "_loggedInChanged",
      },
      targetVersion: {
        type: Number,
        value: -1
      },
      gameVersionPath: {
        type: String,
        computed: "_computeGameVersionPath(active, requestedPlayer, admin, targetVersion, autoCurrentPlayer)",
        observer: "_gameVersionPathChanged"
      },
      gameViewPath : {
        type: String,
        computed: "_computeGameViewPath(requestedPlayer, admin, lastFetchedVersion)"
      },
      gameBasePath : String,
      //This is split out because lastFetchedVersion should be current
      //when it's sent, but when its value is changed it shouldn't be
      //considered a meaningful change that needs a refetch.
      effectiveGameVersionPath: {
        type: String,
        computed: "_computeEffectiveGameVersionPath(gameVersionPath, lastFetchedVersion, version)",
      },
      viewingAsPlayer: Number,
      requestedPlayer: {
        type: Number,
        value: 0
      },
      gameVersion: Number,
      //lastFetchedVersion is the last version we've fetched from the server.
      lastFetchedVersion: {
        type: Number,
        value: 0,
      },
      infoData: {
        type: Object,
        observer: '_infoDataChanged',
      },
      versionData: {
        type: Object,
        observer: "_versionDataChanged"
      },
      socketActive: {
        type: Boolean,
        notify: true,
        value: false,
      },
      _socketUrl: {
        type: String,
        computed: "_computeSocketUrl(active, _infoInstalled)",
        observer: "_socketUrlChanged",
      },
      _infoInstalled: {
        type: Boolean,
        value: false,
      },
      _socket: Object,
    }
  }

  ready() {
    super.ready();
    this.updateData();
  }

  _computeEffectiveGameVersionPath(gameVersionPath, lastFetchedVersion, version) {
    if (!gameVersionPath) return "";
    //version is already part of gameVersionPath. However, often on first
    //load, version and lastFetchedVersion are the same, and we should skip
    //fetching because we already have that info. However in some cases the
    //info bundle will not have all of the most up to date stuff, and we still
    //do need to fetch.
    if (lastFetchedVersion == version) return "";
    return gameVersionPath + "&from=" + lastFetchedVersion
  }

  _computeGameVersionPath(active, requestedPlayer, admin, version, autoCurrentPlayer) {
    if (!active) return "";
    if (version < 0) return "";
    //TODO: factor this out with computeGameViewUrl a bit
    return "version/" + version + "?player=" + requestedPlayer+"&admin=" + (admin ? 1 : 0) + "&current=" + (autoCurrentPlayer ? 1 : 0);
  }

  _computeGameViewPath(requestedPlayer, admin, lastFetchedVersion){
    return "info?player=" + requestedPlayer+"&admin=" + (admin ? 1 : 0) + "&from=" + lastFetchedVersion;
  }

  _computeSocketUrl(active, infoInstalled) {
    if (!active) return "";
    if (!infoInstalled) return "";
    let result = this.$.version.gameBasePath + "socket";
    result = result.split("http:").join("ws:");
    result = result.split("https:").join("wss:");
    return result;
  }

  _loggedInChanged(newValue) {
    this.softReset();
  }

  _activeChanged(newValue) {
    if (newValue) {
      this.reset();
    } else {
      //If we don't clear this out when we deactivate then when we become
      //re-active there might be a brief period where our gameRoute is the
      //old one.
      this.gameRoute = null;
    }
  }

  _gameVersionPathChanged(newValue, oldValue) {
    if (!newValue) return;

    if (this.autoCurrentPlayer && this.requestedPlayer == this.viewingAsPlayer && this.targetVersion == this.gameVersion) {
      return
    }

    //TODO: the autoCurrent player stuff has to be done here...
    requestAnimationFrame(() => this.$.version.generateRequest());
  }

  _socketUrlChanged(newValue) {
    if (this._socket) {
      this._socket.close();
      this._socket = "";
    }

    this._connectSocket();

  }

  _connectSocket() {

    var theUrl = this._socketUrl;

    //Ifthere's no URL, don't establish a socket.
    if (!theUrl) return;

    this._socket = new WebSocket(theUrl);

    this._socket.onclose = e => this._socketClosed(e);
    this._socket.onerror = e => this._socketError(e);
    this._socket.onmessage = e => this._socketMessage(e);
    this._socket.onopen = e => this._socketOpened(e);
  }

  _socketMessage(e) {
    let version = parseInt(e.data);
    if (isNaN(version)) {
      return;
    }
    this.targetVersion = version;
  }

  _socketError(e) {
    //TOOD: do something more substantive
    console.warn("Socket error", e)
  }

  _socketOpened(e) {
    this.socketActive = true;
  }

  _socketClosed(e) {
    console.warn("Socket closed", e);
    this.socketActive = false;
    //We alawyas want a socket, so connect. Wait a bit so we don't just
    //busy spin if the server is down.

    //If we closed because we no longer have a valid URL, then
    //_connectSocket will just exit, and this loop won't be called.

    //TOOD: exponentional backoff on server connect.
    setTimeout(() => this._connectSocket(), 250);
  }

  updateData() {
    this.fetchInfo();
  }

  //When we should do a soft reset; that is, when we haven't flipped out and
  //back; it's still the same game we're viewing as before.
  softReset() {
    this.infoData = null;
    this._infoInstalled = false;
    window.requestAnimationFrame(() => this.updateData());
  }

  //When evertyhing should be reset
  reset() {
    this.lastFetchedVersion = 0;
    this.softReset();
  }

  fetchInfo() {
    if (this.$.info.loading) {
      return
    }

    if (!this.active) {
      return
    }

    if (!this.gameRoute) {
      //The URL will be junk
      return
    }
    this.$.info.generateRequest();
  }

  _prepareStateBundle(game, moveForms, viewingAsPlayer) {

    var bundle = {};

    bundle.gameState = JSON.stringify(game.CurrentState, null, 2);
    //Have to use this.set to get databinding system to notice the change.

    bundle.originalWallClockStartTime = Date.now();

    bundle.pathsToTick = this._expandState(game.CurrentState, game.ActiveTimers);

    bundle.game = game;
    bundle.moveForms = this._expandMoveForms(moveForms);
    bundle.viewingAsPlayer = viewingAsPlayer;

    return bundle;
  }

  _expandMoveForms(moveForms) {
    if (!moveForms) return null;
    for (let i = 0; i < moveForms.length; i++){
      let form = moveForms[i];
      //Some forms don't have fields and that's OK.
      if (!form.Fields) continue;
      for (let j = 0; j < form.Fields.length; j++) {
        let field = form.Fields[j];
        if (field.EnumName) {
          field.Enum = this.chest.Enums[field.EnumName];
        }
      }
    }
    return moveForms;
  }

  _expandState(currentState, timerInfos) {
    //Takes the currentState and returns an object where all of the Stacks are replaced by actual references to the component they reference.

    var pathsToTick = [];

    this._expandLeafState(currentState, currentState.Game, ["Game"], pathsToTick, timerInfos)
    for (var i = 0; i < currentState.Players.length; i++) {
      this._expandLeafState(currentState, currentState.Players[i], ["Players", i], pathsToTick, timerInfos)
    }

    return pathsToTick;

  }

  _expandLeafState(wholeState, leafState, pathToLeaf, pathsToTick, timerInfos) {
    //Returns an expanded version of leafState. leafState should have keys that are either bools, floats, strings, or Stacks.

    var entries = Object.entries(leafState);
    for (var i = 0; i < entries.length; i++) {
      let item = entries[i];
      let key = item[0];
      let val = item[1];
      //Note: null is typeof "object"
      if (val && typeof val == "object") {
        if (val.Deck) {
          this._expandStack(val, wholeState);
        } else if (val.IsTimer) {
          this._expandTimer(val, pathToLeaf.concat([key]), pathsToTick, timerInfos);
        }
      }
    }

    //Copy in Player computed state if it exists, for convenience. Do it after expanding properties
    if (pathToLeaf && pathToLeaf.length == 2 && pathToLeaf[0] == "Players") {
      if (wholeState.Computed && wholeState.Computed.Players && wholeState.Computed.Players.length) {
        leafState.Computed = wholeState.Computed.Players[pathToLeaf[1]];
      }
    }
  }

  _expandStack(stack, wholeState) {
    if (!stack.Deck) {
      //Meh, I guess it's not a stack
      return;
    }

    var deck = this.chest.Decks[stack.Deck];

    var gameName = (this.gameRoute) ? this.gameRoute.name : "";

    var components = [];

    for (var i = 0; i < stack.Indexes.length; i++) {
      let index = stack.Indexes[i];
      if (index == -1) {
        components[i] = null;
        continue;
      }

      if(index == -2) {
        //TODO: to handle this appropriately we'd need to know how to
        //produce a GenericComponent for each Deck clientside.
        components[i] = {};
      } else {
        components[i] = this._componentForDeckAndIndex(stack.Deck, index, wholeState);
      }

      if (stack.Ids) {
        components[i].Id = stack.Ids[i];
      }
      components[i].Deck = stack.Deck;
      components[i].GameName = gameName;
    }

    stack.GameName = gameName;

    stack.Components = components;

  }

  _expandTimer(timer, pathToLeaf, pathsToTick, timerInfo) {

    //Always make sure these default to a number so databinding can use them.
    timer.TimeLeft = 0;
    timer.originalTimeLeft = 0;

    if (!timerInfo) return;

    let info = timerInfo[timer.Id];

    if (!info) return;
    timer.TimeLeft = info.TimeLeft;
    timer.originalTimeLeft = timer.TimeLeft;
    pathsToTick.push(pathToLeaf);
  }

  _componentForDeckAndIndex(deckName, index, wholeState) {
    let deck = this.chest.Decks[deckName];

    if (!deck) return null;

    let result = this._copyObj(deck[index]);

    if (wholeState && wholeState.Components) {
      if (wholeState.Components[deckName]) {
        result.DynamicValues = wholeState.Components[deckName][index];
      }
    }

    return result

  }

  _copyObj(obj) {
    let copy = {}
    for (let attr in obj) {
      if (obj.hasOwnProperty(attr)) copy[attr] = obj[attr]
    }
    return copy
  }

  _enqueueStateBundle(bundle) {
    this.dispatchEvent(new CustomEvent('install-state-bundle', {composed: true, detail: bundle}));
  }

  _infoDataChanged(newValue, oldValue) {
    if (!newValue) {
      //Sometimes we set null, like when we select the view.
      return
    }

    this.chest = newValue.Chest;

    var gameInfo = {
      chest: newValue.Chest,
      playersInfo: newValue.Players,
      hasEmptySlots: newValue.HasEmptySlots,
      gameOpen: newValue.GameOpen,
      gameVisible: newValue.GameVisible,
      isOwner: newValue.IsOwner,
    }

    this.dispatchEvent(new CustomEvent("install-game-static-info", {composed: true, detail: gameInfo}))

    var bundle = this._prepareStateBundle(newValue.Game, newValue.Forms, newValue.ViewingAsPlayer);
    this._enqueueStateBundle(bundle);

    this._infoInstalled = true;

    //We don't use newValue.Game.Version, because in some cases the current
    //state we're returning is not actually current state, but an old one to
    //force us to play animations for moves that are made before a player move
    //is.
    this.lastFetchedVersion = newValue.Game.CurrentState.Version;
    this.targetVersion = newValue.Game.Version;
  }

  _versionDataChanged(newValue) {
    if (!newValue) return;
    if (newValue.Error) {
      console.log("Version getter returned error: " + newValue.Error)
      return
    }

    var serverBundle = newValue.Bundles[newValue.Bundles.length - 1];

    var bundle = this._prepareStateBundle(serverBundle.Game, serverBundle.Forms, serverBundle.ViewingAsPlayer);
    this._enqueueStateBundle(bundle);

    this.lastFetchedVersion = serverBundle.Game.Version;
  }
}

customElements.define(BoardgameGameStateManager.is, BoardgameGameStateManager);
<commit_msg>Create a queue client-side of state bundles to apply, and pump the queue based on timing information. Bundles are slightly wrong (missing last one) but otherwise behavior roughly works. Part of #396.<commit_after>import { PolymerElement } from '@polymer/polymer/polymer-element.js';
import './boardgame-ajax.js';
import { html } from '@polymer/polymer/lib/utils/html-tag.js';

/*

  StateManager keeps track of fetching state bundles from the server and
  figuring out when it makes sense to have the game-view install them.

  When the game-view becomes active, the info bundle is feteched. This
  includes information about who the current viewing player is and also
  includes the initial state bundle.

  Once the first state bundle has been installed, a socket is connected to
  receive updates about when the state increases. When the state version
  increases, that increments TargetVersion, which changes the URL to fetch.

*/

class BoardgameGameStateManager extends PolymerElement {
  static get template() {
    return html`
    <boardgame-ajax id="version" game-path="[[effectiveGameVersionPath]]" game-route="[[gameRoute]]" handle-as="json" last-response="{{versionData}}"></boardgame-ajax>
    <boardgame-ajax id="info" game-path="[[gameViewPath]]" game-route="[[gameRoute]]" handle-as="json" last-response="{{infoData}}"></boardgame-ajax>
`;
  }

  static get is() {
    return "boardgame-game-state-manager"
  }

  static get properties() {
    return {
      gameRoute: Object,
      gameFinished: {
        type: Boolean,
        value: false,
      },
      chest: Object,
      admin: Boolean,
      autoCurrentPlayer: Boolean,
      active: {
        type: Boolean,
        observer: "_activeChanged"
      },
      loggedIn: {
        type: Boolean,
        observer: "_loggedInChanged",
      },
      targetVersion: {
        type: Number,
        value: -1
      },
      gameVersionPath: {
        type: String,
        computed: "_computeGameVersionPath(active, requestedPlayer, admin, targetVersion, autoCurrentPlayer)",
        observer: "_gameVersionPathChanged"
      },
      gameViewPath : {
        type: String,
        computed: "_computeGameViewPath(requestedPlayer, admin, lastFetchedVersion)"
      },
      gameBasePath : String,
      //This is split out because lastFetchedVersion should be current
      //when it's sent, but when its value is changed it shouldn't be
      //considered a meaningful change that needs a refetch.
      effectiveGameVersionPath: {
        type: String,
        computed: "_computeEffectiveGameVersionPath(gameVersionPath, lastFetchedVersion, version)",
      },
      viewingAsPlayer: Number,
      requestedPlayer: {
        type: Number,
        value: 0
      },
      gameVersion: Number,
      //lastFetchedVersion is the last version we've fetched from the server.
      lastFetchedVersion: {
        type: Number,
        value: 0,
      },
      infoData: {
        type: Object,
        observer: '_infoDataChanged',
      },
      versionData: {
        type: Object,
        observer: "_versionDataChanged"
      },
      socketActive: {
        type: Boolean,
        notify: true,
        value: false,
      },
      _socketUrl: {
        type: String,
        computed: "_computeSocketUrl(active, _infoInstalled)",
        observer: "_socketUrlChanged",
      },
      _infoInstalled: {
        type: Boolean,
        value: false,
      },
      _socket: Object,
      _pendingStateBundles: Object,
      _stateBundleTimeout: Number,
    }
  }

  ready() {
    super.ready();

    this._pendingStateBundles = [];

    this.updateData();
  }

  _computeEffectiveGameVersionPath(gameVersionPath, lastFetchedVersion, version) {
    if (!gameVersionPath) return "";
    //version is already part of gameVersionPath. However, often on first
    //load, version and lastFetchedVersion are the same, and we should skip
    //fetching because we already have that info. However in some cases the
    //info bundle will not have all of the most up to date stuff, and we still
    //do need to fetch.
    if (lastFetchedVersion == version) return "";
    return gameVersionPath + "&from=" + lastFetchedVersion
  }

  _computeGameVersionPath(active, requestedPlayer, admin, version, autoCurrentPlayer) {
    if (!active) return "";
    if (version < 0) return "";
    //TODO: factor this out with computeGameViewUrl a bit
    return "version/" + version + "?player=" + requestedPlayer+"&admin=" + (admin ? 1 : 0) + "&current=" + (autoCurrentPlayer ? 1 : 0);
  }

  _computeGameViewPath(requestedPlayer, admin, lastFetchedVersion){
    return "info?player=" + requestedPlayer+"&admin=" + (admin ? 1 : 0) + "&from=" + lastFetchedVersion;
  }

  _computeSocketUrl(active, infoInstalled) {
    if (!active) return "";
    if (!infoInstalled) return "";
    let result = this.$.version.gameBasePath + "socket";
    result = result.split("http:").join("ws:");
    result = result.split("https:").join("wss:");
    return result;
  }

  _loggedInChanged(newValue) {
    this.softReset();
  }

  _activeChanged(newValue) {
    if (newValue) {
      this.reset();
    } else {
      //If we don't clear this out when we deactivate then when we become
      //re-active there might be a brief period where our gameRoute is the
      //old one.
      this.gameRoute = null;
    }
  }

  _gameVersionPathChanged(newValue, oldValue) {
    if (!newValue) return;

    if (this.autoCurrentPlayer && this.requestedPlayer == this.viewingAsPlayer && this.targetVersion == this.gameVersion) {
      return
    }

    //TODO: the autoCurrent player stuff has to be done here...
    requestAnimationFrame(() => this.$.version.generateRequest());
  }

  _socketUrlChanged(newValue) {
    if (this._socket) {
      this._socket.close();
      this._socket = "";
    }

    this._connectSocket();

  }

  _connectSocket() {

    var theUrl = this._socketUrl;

    //Ifthere's no URL, don't establish a socket.
    if (!theUrl) return;

    this._socket = new WebSocket(theUrl);

    this._socket.onclose = e => this._socketClosed(e);
    this._socket.onerror = e => this._socketError(e);
    this._socket.onmessage = e => this._socketMessage(e);
    this._socket.onopen = e => this._socketOpened(e);
  }

  _socketMessage(e) {
    let version = parseInt(e.data);
    if (isNaN(version)) {
      return;
    }
    this.targetVersion = version;
  }

  _socketError(e) {
    //TOOD: do something more substantive
    console.warn("Socket error", e)
  }

  _socketOpened(e) {
    this.socketActive = true;
  }

  _socketClosed(e) {
    console.warn("Socket closed", e);
    this.socketActive = false;
    //We alawyas want a socket, so connect. Wait a bit so we don't just
    //busy spin if the server is down.

    //If we closed because we no longer have a valid URL, then
    //_connectSocket will just exit, and this loop won't be called.

    //TOOD: exponentional backoff on server connect.
    setTimeout(() => this._connectSocket(), 250);
  }

  updateData() {
    this.fetchInfo();
  }

  //When we should do a soft reset; that is, when we haven't flipped out and
  //back; it's still the same game we're viewing as before.
  softReset() {
    this.infoData = null;
    this._infoInstalled = false;
    window.requestAnimationFrame(() => this.updateData());
  }

  //When evertyhing should be reset
  reset() {
    this.lastFetchedVersion = 0;
    this._resetPendingStateBundles();
    this.softReset();
  }

  fetchInfo() {
    if (this.$.info.loading) {
      return
    }

    if (!this.active) {
      return
    }

    if (!this.gameRoute) {
      //The URL will be junk
      return
    }
    this.$.info.generateRequest();
  }

  _prepareStateBundle(game, moveForms, viewingAsPlayer, delay) {

    var bundle = {};

    bundle.gameState = JSON.stringify(game.CurrentState, null, 2);
    //Have to use this.set to get databinding system to notice the change.

    bundle.originalWallClockStartTime = Date.now();

    bundle.pathsToTick = this._expandState(game.CurrentState, game.ActiveTimers);

    bundle.game = game;
    bundle.moveForms = this._expandMoveForms(moveForms);
    bundle.viewingAsPlayer = viewingAsPlayer;
    bundle.delay = delay;

    return bundle;
  }

  _expandMoveForms(moveForms) {
    if (!moveForms) return null;
    for (let i = 0; i < moveForms.length; i++){
      let form = moveForms[i];
      //Some forms don't have fields and that's OK.
      if (!form.Fields) continue;
      for (let j = 0; j < form.Fields.length; j++) {
        let field = form.Fields[j];
        if (field.EnumName) {
          field.Enum = this.chest.Enums[field.EnumName];
        }
      }
    }
    return moveForms;
  }

  _expandState(currentState, timerInfos) {
    //Takes the currentState and returns an object where all of the Stacks are replaced by actual references to the component they reference.

    var pathsToTick = [];

    this._expandLeafState(currentState, currentState.Game, ["Game"], pathsToTick, timerInfos)
    for (var i = 0; i < currentState.Players.length; i++) {
      this._expandLeafState(currentState, currentState.Players[i], ["Players", i], pathsToTick, timerInfos)
    }

    return pathsToTick;

  }

  _expandLeafState(wholeState, leafState, pathToLeaf, pathsToTick, timerInfos) {
    //Returns an expanded version of leafState. leafState should have keys that are either bools, floats, strings, or Stacks.

    var entries = Object.entries(leafState);
    for (var i = 0; i < entries.length; i++) {
      let item = entries[i];
      let key = item[0];
      let val = item[1];
      //Note: null is typeof "object"
      if (val && typeof val == "object") {
        if (val.Deck) {
          this._expandStack(val, wholeState);
        } else if (val.IsTimer) {
          this._expandTimer(val, pathToLeaf.concat([key]), pathsToTick, timerInfos);
        }
      }
    }

    //Copy in Player computed state if it exists, for convenience. Do it after expanding properties
    if (pathToLeaf && pathToLeaf.length == 2 && pathToLeaf[0] == "Players") {
      if (wholeState.Computed && wholeState.Computed.Players && wholeState.Computed.Players.length) {
        leafState.Computed = wholeState.Computed.Players[pathToLeaf[1]];
      }
    }
  }

  _expandStack(stack, wholeState) {
    if (!stack.Deck) {
      //Meh, I guess it's not a stack
      return;
    }

    var deck = this.chest.Decks[stack.Deck];

    var gameName = (this.gameRoute) ? this.gameRoute.name : "";

    var components = [];

    for (var i = 0; i < stack.Indexes.length; i++) {
      let index = stack.Indexes[i];
      if (index == -1) {
        components[i] = null;
        continue;
      }

      if(index == -2) {
        //TODO: to handle this appropriately we'd need to know how to
        //produce a GenericComponent for each Deck clientside.
        components[i] = {};
      } else {
        components[i] = this._componentForDeckAndIndex(stack.Deck, index, wholeState);
      }

      if (stack.Ids) {
        components[i].Id = stack.Ids[i];
      }
      components[i].Deck = stack.Deck;
      components[i].GameName = gameName;
    }

    stack.GameName = gameName;

    stack.Components = components;

  }

  _expandTimer(timer, pathToLeaf, pathsToTick, timerInfo) {

    //Always make sure these default to a number so databinding can use them.
    timer.TimeLeft = 0;
    timer.originalTimeLeft = 0;

    if (!timerInfo) return;

    let info = timerInfo[timer.Id];

    if (!info) return;
    timer.TimeLeft = info.TimeLeft;
    timer.originalTimeLeft = timer.TimeLeft;
    pathsToTick.push(pathToLeaf);
  }

  _componentForDeckAndIndex(deckName, index, wholeState) {
    let deck = this.chest.Decks[deckName];

    if (!deck) return null;

    let result = this._copyObj(deck[index]);

    if (wholeState && wholeState.Components) {
      if (wholeState.Components[deckName]) {
        result.DynamicValues = wholeState.Components[deckName][index];
      }
    }

    return result

  }

  _copyObj(obj) {
    let copy = {}
    for (let attr in obj) {
      if (obj.hasOwnProperty(attr)) copy[attr] = obj[attr]
    }
    return copy
  }

  //A new state bundle has been enqueued. Ensure that we're working ot fire a state bundle.
  _scheduleNextStateBundle() {

    if (this._stateBundleTimeout) return;
    if (!this._pendingStateBundles.length) return;

    let nextBundle = this._pendingStateBundles[0];

    if (!nextBundle.delay) {
      this._fireNextStateBundle();
      return;
    }

    this._stateBundleTimeout = setTimeout(() => this._fireNextStateBundle(), nextBundle.delay);

  }

  _resetPendingStateBundles() {
    if (this._stateBundleTimeout) clearTimeout(this._stateBundleTimeout);
    this._stateBundleTimeout = 0;
    this._pendingStateBundles = [];
  }

  _fireNextStateBundle() {
    //Called when the next state bundle should be installed NOW.
    this._stateBundleTimeout = 0;
    let bundle = this._pendingStateBundles.shift();
    if (bundle) {
      this.dispatchEvent(new CustomEvent('install-state-bundle', {composed: true, detail: bundle}));
    }
    this._scheduleNextStateBundle();
  }

  //Add the next state bundle to the end
  _enqueueStateBundle(bundle) {
    this._pendingStateBundles.push(bundle);
    this._scheduleNextStateBundle();
  }

  _infoDataChanged(newValue, oldValue) {
    if (!newValue) {
      //Sometimes we set null, like when we select the view.
      return
    }

    this.chest = newValue.Chest;

    var gameInfo = {
      chest: newValue.Chest,
      playersInfo: newValue.Players,
      hasEmptySlots: newValue.HasEmptySlots,
      gameOpen: newValue.GameOpen,
      gameVisible: newValue.GameVisible,
      isOwner: newValue.IsOwner,
    }

    this.dispatchEvent(new CustomEvent("install-game-static-info", {composed: true, detail: gameInfo}))

    var bundle = this._prepareStateBundle(newValue.Game, newValue.Forms, newValue.ViewingAsPlayer, 0);
    this._enqueueStateBundle(bundle);

    this._infoInstalled = true;

    //We don't use newValue.Game.Version, because in some cases the current
    //state we're returning is not actually current state, but an old one to
    //force us to play animations for moves that are made before a player move
    //is.
    this.lastFetchedVersion = newValue.Game.CurrentState.Version;
    this.targetVersion = newValue.Game.Version;
  }

  _versionDataChanged(newValue) {
    if (!newValue) return;
    if (newValue.Error) {
      console.log("Version getter returned error: " + newValue.Error)
      return
    }

    let lastServerBundle = {};

    for (let i = 0; i < newValue.Bundles.length; i++) {
      let serverBundle = newValue.Bundles[i];
      let bundle = this._prepareStateBundle(serverBundle.Game, serverBundle.Forms, serverBundle.ViewingAsPlayer, serverBundle.Delay);
      this._enqueueStateBundle(bundle);
      lastServerBundle = serverBundle;
    }

    this.lastFetchedVersion = lastServerBundle.Game.Version;
  }
}

customElements.define(BoardgameGameStateManager.is, BoardgameGameStateManager);
<|endoftext|>


<commit_before>/*
 * iceball : threads/simple/note.js
 * copyright (c) 2015 Susisu
 */

"use strict";

function end_module() {
    module.exports = Object.freeze({
        "Note"      : Note,
        "normalize" : normalize,
        "NoteThread": NoteThread
    });
}

var thread = require("../thread.js");

var SpeedChange = require("./speedchange.js").SpeedChange;

function normalize(notes, spdChanges, stdDuration, spdMultiplier) {
    // clone arrays
    notes      = notes.slice();
    spdChanges = spdChanges.slice();
    // sort by frame
    notes.sort(function (x, y) { return x.hitFrame - y.hitFrame; });
    spdChanges.sort(function (x, y) { return x.frame - y.frame; });

    var numN  = notes.length,      // number of notes
        numSC = spdChanges.length; // number of speed changes

    var ni,      // note index
        sci = 0; // speed change index

    for (ni = 0; ni < numN; ni++) {
        var note = notes[ni];
        // get the index of the former speed change
        while (sci < 0 || (sci < numSC && spdChanges[sci].frame < note.frame)) {
            sci++;
        }
        sci--;

        var currSpeed    = sci < 0 ? 1.0 : spdChanges[sci].speed;
        var currFrame    = note.frame;
        var currPosition = 0.0;

        while (currPosition > -1.0) {
            currFrame--;

            while (sci >= 0 && currFrame < spdChanges[sci].frame) {
                sci--;

                if (sci < 0) {
                    currSpeed = 1.0
                }
                else {
                    currSpeed = spdChanges[sci].speed;
                }
            }

            currPosition -= currSpeed * note.relSpeed * spdMultiplier / stdDuration;
        }

        note.spawnFrame    = currFrame;
        note.posCorrection = currentPos + 1.0;
    }

    notes.sort(function (x, y) { return x.spawnFrame - y.spawnFrame; });

    return ntoes;
}

function Note(frame, relSpeed) {
    this.frame         = frame;
    this.relSpeed      = relSpeed;
    this.spawnFrame    = 0;
    this.posCorrection = 0.0;
}

Note.prototype = Object.create(Object.prototype, {
    "constructor": {
        "writable"    : true,
        "configurable": true,
        "value": Note
    }
});

function NoteThread() {
    thread.Thread.call(this);
}

NoteThread.prototype = Object.create(thread.Thread.prototype, {
    "constructor": {
        "writable"    : true,
        "configurable": true,
        "value": NoteThread
    }
});

end_module();
<commit_msg>Fix property name<commit_after>/*
 * iceball : threads/simple/note.js
 * copyright (c) 2015 Susisu
 */

"use strict";

function end_module() {
    module.exports = Object.freeze({
        "Note"      : Note,
        "normalize" : normalize,
        "NoteThread": NoteThread
    });
}

var thread = require("../thread.js");

var SpeedChange = require("./speedchange.js").SpeedChange;

function Note(frame, relSpeed) {
    this.frame         = frame;
    this.relSpeed      = relSpeed;
    this.spawnFrame    = 0;
    this.posCorrection = 0.0;
}

Note.prototype = Object.create(Object.prototype, {
    "constructor": {
        "writable"    : true,
        "configurable": true,
        "value": Note
    }
});

function normalize(notes, spdChanges, stdDuration, spdMultiplier) {
    // clone arrays
    notes      = notes.slice();
    spdChanges = spdChanges.slice();
    // sort by frame
    notes.sort(function (x, y) { return x.frame - y.frame; });
    spdChanges.sort(function (x, y) { return x.frame - y.frame; });

    var numN  = notes.length,      // number of notes
        numSC = spdChanges.length; // number of speed changes

    var ni,      // note index
        sci = 0; // speed change index

    for (ni = 0; ni < numN; ni++) {
        var note = notes[ni];
        // get the index of the former speed change
        while (sci < 0 || (sci < numSC && spdChanges[sci].frame < note.frame)) {
            sci++;
        }
        sci--;

        var currSpeed    = sci < 0 ? 1.0 : spdChanges[sci].speed;
        var currFrame    = note.frame;
        var currPosition = 0.0;

        while (currPosition > -1.0) {
            currFrame--;

            while (sci >= 0 && currFrame < spdChanges[sci].frame) {
                sci--;

                if (sci < 0) {
                    currSpeed = 1.0
                }
                else {
                    currSpeed = spdChanges[sci].speed;
                }
            }

            currPosition -= currSpeed * note.relSpeed * spdMultiplier / stdDuration;
        }

        note.spawnFrame    = currFrame;
        note.posCorrection = currentPos + 1.0;
    }

    notes.sort(function (x, y) { return x.spawnFrame - y.spawnFrame; });

    return ntoes;
}

function NoteThread() {
    thread.Thread.call(this);
}

NoteThread.prototype = Object.create(thread.Thread.prototype, {
    "constructor": {
        "writable"    : true,
        "configurable": true,
        "value": NoteThread
    }
});

end_module();
<|endoftext|>