UNPKG

@rtsdk/lance-topia

Version:

A Node.js based real-time multiplayer game server

1,540 lines (1,413 loc) 354 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Client = {})); })(this, (function (exports) { 'use strict'; function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } } function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); Object.defineProperty(subClass, "prototype", { writable: false }); if (superClass) _setPrototypeOf(subClass, superClass); } function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } } function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } function _possibleConstructorReturn(self, call) { if (call && (typeof call === "object" || typeof call === "function")) { return call; } else if (call !== void 0) { throw new TypeError("Derived constructors may only return object or undefined"); } return _assertThisInitialized(self); } function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; } function _superPropBase(object, property) { while (!Object.prototype.hasOwnProperty.call(object, property)) { object = _getPrototypeOf(object); if (object === null) break; } return object; } function _get() { if (typeof Reflect !== "undefined" && Reflect.get) { _get = Reflect.get.bind(); } else { _get = function _get(target, property, receiver) { var base = _superPropBase(target, property); if (!base) return; var desc = Object.getOwnPropertyDescriptor(base, property); if (desc.get) { return desc.get.call(arguments.length < 3 ? target : receiver); } return desc.value; }; } return _get.apply(this, arguments); } function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function () {}; return { s: F, n: function () { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function (e) { throw e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function () { it = it.call(o); }, n: function () { var step = it.next(); normalCompletion = step.done; return step; }, e: function (e) { didErr = true; err = e; }, f: function () { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; } function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } /** * This class implements a singleton game world instance, created by Lance. * It represents an instance of the game world, and includes all the game objects. * It is the state of the game. */ var GameWorld = /*#__PURE__*/function () { /** * Constructor of the World instance. Invoked by Lance on startup. * * @hideconstructor */ function GameWorld() { _classCallCheck(this, GameWorld); this.stepCount = 0; this.objects = {}; this.playerCount = 0; this.idCount = 0; } /** * Gets a new, fresh and unused id that can be used for a new object * @private * @return {Number} the new id */ _createClass(GameWorld, [{ key: "getNewId", value: function getNewId() { var possibleId = this.idCount; // find a free id while (possibleId in this.objects) possibleId++; this.idCount = possibleId + 1; return possibleId; } /** * Returns all the game world objects which match a criteria * @param {Object} query The query object * @param {Object} [query.id] object id * @param {Object} [query.playerId] player id * @param {Object} [query.roomName] roomName * @param {Object} [query.AI] Is AI. {AI: true} or {AI: false} * @param {Class} [query.instanceType] matches whether `object instanceof instanceType` * @param {Array} [query.components] An array of component names * @param {Boolean} [query.returnSingle] Return the first object matched * @return {Array | Object} All game objects which match all the query parameters, or the first match if returnSingle was specified */ }, { key: "queryObjects", value: function queryObjects(query) { var queriedObjects = []; // todo this is currently a somewhat inefficient implementation for API testing purposes. // It should be implemented with cached dictionaries like in nano-ecs this.forEachObject(function (id, object) { var conditions = []; // object id condition conditions.push(!("id" in query) || query.id !== null && object.id === query.id); // player id condition conditions.push(!("playerId" in query) || query.playerId !== null && object.playerId === query.playerId); // roomName condition conditions.push(!("roomName" in query) || query.roomName !== null && object.roomName === query.roomName); // roomName condition conditions.push(!("AI" in query) || query.AI !== null && !!object.AI === !!query.AI); // instance type conditio conditions.push(!("instanceType" in query) || query.instanceType !== null && object instanceof query.instanceType); // components conditions if ("components" in query) { query.components.forEach(function (componentClass) { conditions.push(object.hasComponent(componentClass)); }); } // all conditions are true, object is qualified for the query if (conditions.every(function (value) { return value; })) { queriedObjects.push(object); if (query.returnSingle) return false; } }); // return a single object or null if (query.returnSingle) { return queriedObjects.length > 0 ? queriedObjects[0] : null; } return queriedObjects; } /** * Returns The first game object encountered which matches a criteria. * Syntactic sugar for {@link queryObjects} with `returnSingle: true` * @param {Object} query See queryObjects * @return {Object} The game object, if found */ }, { key: "queryObject", value: function queryObject(query) { return this.queryObjects(Object.assign(query, { returnSingle: true })); } /** * Add an object to the game world * @private * @param {Object} object object to add */ }, { key: "addObject", value: function addObject(object) { this.objects[object.id] = object; } /** * Remove an object from the game world * @private * @param {number} id id of the object to remove */ }, { key: "removeObject", value: function removeObject(id) { delete this.objects[id]; } /** * World object iterator. * Invoke callback(objId, obj) for each object * * @param {function} callback function receives id and object. If callback returns false, the iteration will cease */ }, { key: "forEachObject", value: function forEachObject(callback) { for (var _i = 0, _Object$keys = Object.keys(this.objects); _i < _Object$keys.length; _i++) { var id = _Object$keys[_i]; var returnValue = callback(id, this.objects[id]); // TODO: the key should be Number(id) if (returnValue === false) break; } } }]); return GameWorld; }(); function createCommonjsModule(fn) { var module = { exports: {} }; return fn(module, module.exports), module.exports; } // ES3 safe var _undefined$1 = void 0; var is$4 = function (value) { return value !== _undefined$1 && value !== null; }; // prettier-ignore var possibleTypes = { "object": true, "function": true, "undefined": true /* document.all */ }; var is$3 = function (value) { if (!is$4(value)) return false; return hasOwnProperty.call(possibleTypes, typeof value); }; var is$2 = function (value) { if (!is$3(value)) return false; try { if (!value.constructor) return false; return value.constructor.prototype === value; } catch (error) { return false; } }; var is$1 = function (value) { if (typeof value !== "function") return false; if (!hasOwnProperty.call(value, "length")) return false; try { if (typeof value.length !== "number") return false; if (typeof value.call !== "function") return false; if (typeof value.apply !== "function") return false; } catch (error) { return false; } return !is$2(value); }; var classRe = /^\s*class[\s{/}]/, functionToString = Function.prototype.toString; var is = function (value) { if (!is$1(value)) return false; if (classRe.test(functionToString.call(value))) return false; return true; }; var isImplemented$2 = function () { var assign = Object.assign, obj; if (typeof assign !== "function") return false; obj = { foo: "raz" }; assign(obj, { bar: "dwa" }, { trzy: "trzy" }); return obj.foo + obj.bar + obj.trzy === "razdwatrzy"; }; var isImplemented$1 = function () { try { Object.keys("primitive"); return true; } catch (e) { return false; } }; // eslint-disable-next-line no-empty-function var noop = function () {}; var _undefined = noop(); // Support ES3 engines var isValue = function (val) { return val !== _undefined && val !== null; }; var keys$1 = Object.keys; var shim$2 = function (object) { return keys$1(isValue(object) ? Object(object) : object); }; var keys = isImplemented$1() ? Object.keys : shim$2; var validValue = function (value) { if (!isValue(value)) throw new TypeError("Cannot use null or undefined"); return value; }; var max = Math.max; var shim$1 = function (dest, src /*, …srcn*/) { var error, i, length = max(arguments.length, 2), assign; dest = Object(validValue(dest)); assign = function (key) { try { dest[key] = src[key]; } catch (e) { if (!error) error = e; } }; for (i = 1; i < length; ++i) { src = arguments[i]; keys(src).forEach(assign); } if (error !== undefined) throw error; return dest; }; var assign = isImplemented$2() ? Object.assign : shim$1; var forEach = Array.prototype.forEach, create = Object.create; var process = function (src, obj) { var key; for (key in src) obj[key] = src[key]; }; // eslint-disable-next-line no-unused-vars var normalizeOptions = function (opts1 /*, …options*/) { var result = create(null); forEach.call(arguments, function (options) { if (!isValue(options)) return; process(Object(options), result); }); return result; }; var str = "razdwatrzy"; var isImplemented = function () { if (typeof str.contains !== "function") return false; return str.contains("dwa") === true && str.contains("foo") === false; }; var indexOf = String.prototype.indexOf; var shim = function (searchString /*, position*/) { return indexOf.call(this, searchString, arguments[1]) > -1; }; var contains = isImplemented() ? String.prototype.contains : shim; var d_1 = createCommonjsModule(function (module) { var d = (module.exports = function (dscr, value/*, options*/) { var c, e, w, options, desc; if (arguments.length < 2 || typeof dscr !== "string") { options = value; value = dscr; dscr = null; } else { options = arguments[2]; } if (is$4(dscr)) { c = contains.call(dscr, "c"); e = contains.call(dscr, "e"); w = contains.call(dscr, "w"); } else { c = w = true; e = false; } desc = { value: value, configurable: c, enumerable: e, writable: w }; return !options ? desc : assign(normalizeOptions(options), desc); }); d.gs = function (dscr, get, set/*, options*/) { var c, e, options, desc; if (typeof dscr !== "string") { options = set; set = get; get = dscr; dscr = null; } else { options = arguments[3]; } if (!is$4(get)) { get = undefined; } else if (!is(get)) { options = get; get = set = undefined; } else if (!is$4(set)) { set = undefined; } else if (!is(set)) { options = set; set = undefined; } if (is$4(dscr)) { c = contains.call(dscr, "c"); e = contains.call(dscr, "e"); } else { c = true; e = false; } desc = { get: get, set: set, configurable: c, enumerable: e }; return !options ? desc : assign(normalizeOptions(options), desc); }; }); var validCallable = function (fn) { if (typeof fn !== "function") throw new TypeError(fn + " is not a function"); return fn; }; var eventEmitter = createCommonjsModule(function (module, exports) { var apply = Function.prototype.apply, call = Function.prototype.call , create = Object.create, defineProperty = Object.defineProperty , defineProperties = Object.defineProperties , hasOwnProperty = Object.prototype.hasOwnProperty , descriptor = { configurable: true, enumerable: false, writable: true } , on, once, off, emit, methods, descriptors, base; on = function (type, listener) { var data; validCallable(listener); if (!hasOwnProperty.call(this, '__ee__')) { data = descriptor.value = create(null); defineProperty(this, '__ee__', descriptor); descriptor.value = null; } else { data = this.__ee__; } if (!data[type]) data[type] = listener; else if (typeof data[type] === 'object') data[type].push(listener); else data[type] = [data[type], listener]; return this; }; once = function (type, listener) { var once, self; validCallable(listener); self = this; on.call(this, type, once = function () { off.call(self, type, once); apply.call(listener, this, arguments); }); once.__eeOnceListener__ = listener; return this; }; off = function (type, listener) { var data, listeners, candidate, i; validCallable(listener); if (!hasOwnProperty.call(this, '__ee__')) return this; data = this.__ee__; if (!data[type]) return this; listeners = data[type]; if (typeof listeners === 'object') { for (i = 0; (candidate = listeners[i]); ++i) { if ((candidate === listener) || (candidate.__eeOnceListener__ === listener)) { if (listeners.length === 2) data[type] = listeners[i ? 0 : 1]; else listeners.splice(i, 1); } } } else { if ((listeners === listener) || (listeners.__eeOnceListener__ === listener)) { delete data[type]; } } return this; }; emit = function (type) { var i, l, listener, listeners, args; if (!hasOwnProperty.call(this, '__ee__')) return; listeners = this.__ee__[type]; if (!listeners) return; if (typeof listeners === 'object') { l = arguments.length; args = new Array(l - 1); for (i = 1; i < l; ++i) args[i - 1] = arguments[i]; listeners = listeners.slice(); for (i = 0; (listener = listeners[i]); ++i) { apply.call(listener, this, args); } } else { switch (arguments.length) { case 1: call.call(listeners, this); break; case 2: call.call(listeners, this, arguments[1]); break; case 3: call.call(listeners, this, arguments[1], arguments[2]); break; default: l = arguments.length; args = new Array(l - 1); for (i = 1; i < l; ++i) { args[i - 1] = arguments[i]; } apply.call(listeners, this, args); } } }; methods = { on: on, once: once, off: off, emit: emit }; descriptors = { on: d_1(on), once: d_1(once), off: d_1(off), emit: d_1(emit) }; base = defineProperties({}, descriptors); module.exports = exports = function (o) { return (o == null) ? create(base) : defineProperties(Object(o), descriptors); }; exports.methods = methods; }); // TODO: needs documentation // I think the API could be simpler // - Timer.run(waitSteps, cb) // - Timer.repeat(waitSteps, count, cb) // count=null=>forever // - Timer.cancel(cb) var Timer = /*#__PURE__*/function () { function Timer() { _classCallCheck(this, Timer); this.currentTime = 0; this.isActive = false; this.idCounter = 0; this.events = {}; } _createClass(Timer, [{ key: "play", value: function play() { this.isActive = true; } }, { key: "tick", value: function tick() { var event; var eventId; if (this.isActive) { this.currentTime++; for (eventId in this.events) { event = this.events[eventId]; if (event) { if (event.type == 'repeat') { if ((this.currentTime - event.startOffset) % event.time == 0) { event.callback.apply(event.thisContext, event.args); } } if (event.type == 'single') { if ((this.currentTime - event.startOffset) % event.time == 0) { event.callback.apply(event.thisContext, event.args); event.destroy(); } } } } } } }, { key: "destroyEvent", value: function destroyEvent(eventId) { delete this.events[eventId]; } }, { key: "loop", value: function loop(time, callback) { var timerEvent = new TimerEvent(this, TimerEvent.TYPES.repeat, time, callback); this.events[timerEvent.id] = timerEvent; return timerEvent; } }, { key: "add", value: function add(time, callback, thisContext, args) { var timerEvent = new TimerEvent(this, TimerEvent.TYPES.single, time, callback, thisContext, args); this.events[timerEvent.id] = timerEvent; return timerEvent; } // todo implement timer delete all events }, { key: "destroy", value: function destroy(id) { delete this.events[id]; } }]); return Timer; }(); // timer event var TimerEvent = /*#__PURE__*/_createClass(function TimerEvent(timer, type, time, callback, thisContext, args) { _classCallCheck(this, TimerEvent); this.id = ++timer.idCounter; this.timer = timer; this.type = type; this.time = time; this.callback = callback; this.startOffset = timer.currentTime; this.thisContext = thisContext; this.args = args; this.destroy = function () { this.timer.destroy(this.id); }; }); TimerEvent.TYPES = { repeat: 'repeat', single: 'single' }; /** * Tracing Services. * Use the trace functions to trace game state. Turn on tracing by * specifying the minimum trace level which should be recorded. For * example, setting traceLevel to Trace.TRACE_INFO will cause info, * warn, and error traces to be recorded. */ var Trace = /*#__PURE__*/function () { function Trace(options) { _classCallCheck(this, Trace); this.options = Object.assign({ traceLevel: this.TRACE_DEBUG }, options); this.traceBuffer = []; this.step = 'initializing'; // syntactic sugar functions this.error = this.trace.bind(this, Trace.TRACE_ERROR); this.warn = this.trace.bind(this, Trace.TRACE_WARN); this.info = this.trace.bind(this, Trace.TRACE_INFO); this.debug = this.trace.bind(this, Trace.TRACE_DEBUG); this.trace = this.trace.bind(this, Trace.TRACE_ALL); } /** * Include all trace levels. * @memberof Trace * @member {Number} TRACE_ALL */ _createClass(Trace, [{ key: "trace", value: function trace(level, dataCB) { // all traces must be functions which return strings if (typeof dataCB !== 'function') { throw new Error("Lance trace was called but instead of passing a function, it received a [".concat(_typeof(dataCB), "]")); } if (level < this.options.traceLevel) return; this.traceBuffer.push({ data: dataCB(), level: level, step: this.step, time: new Date() }); } }, { key: "rotate", value: function rotate() { var buffer = this.traceBuffer; this.traceBuffer = []; return buffer; } }, { key: "length", get: function get() { return this.traceBuffer.length; } }, { key: "setStep", value: function setStep(s) { this.step = s; } }], [{ key: "TRACE_ALL", get: function get() { return 0; } /** * Include debug traces and higher. * @memberof Trace * @member {Number} TRACE_DEBUG */ }, { key: "TRACE_DEBUG", get: function get() { return 1; } /** * Include info traces and higher. * @memberof Trace * @member {Number} TRACE_INFO */ }, { key: "TRACE_INFO", get: function get() { return 2; } /** * Include warn traces and higher. * @memberof Trace * @member {Number} TRACE_WARN */ }, { key: "TRACE_WARN", get: function get() { return 3; } /** * Include error traces and higher. * @memberof Trace * @member {Number} TRACE_ERROR */ }, { key: "TRACE_ERROR", get: function get() { return 4; } /** * Disable all tracing. * @memberof Trace * @member {Number} TRACE_NONE */ }, { key: "TRACE_NONE", get: function get() { return 1000; } }]); return Trace; }(); /** * The GameEngine contains the game logic. Extend this class * to implement game mechanics. The GameEngine derived * instance runs once on the server, where the final decisions * are always taken, and one instance will run on each client as well, * where the client emulates what it expects to be happening * on the server. * * The game engine's logic must listen to user inputs and * act on these inputs to change the game state. For example, * the game engine listens to controller/keyboard inputs to infer * movement for the player/ship/first-person. The game engine listens * to clicks, button-presses to infer firing, etc.. * * Note that the game engine runs on both the server and on the * clients - but the server decisions always have the final say, * and therefore clients must resolve server updates which conflict * with client-side predictions. */ var GameEngine = /*#__PURE__*/function () { /** * Create a game engine instance. This needs to happen * once on the server, and once on each client. * * @param {Object} options - options object * @param {Number} options.traceLevel - the trace level. */ function GameEngine(options) { _classCallCheck(this, GameEngine); // place the game engine in the LANCE globals var isServerSide = typeof window === 'undefined'; var glob = isServerSide ? global : window; glob.LANCE = { gameEngine: this }; // set options var defaultOpts = { traceLevel: Trace.TRACE_NONE }; if (!isServerSide) defaultOpts.clientIDSpace = 1000000; this.options = Object.assign(defaultOpts, options); /** * client's player ID, as a string. If running on the client, this is set at runtime by the clientEngine * @member {String} */ this.playerId = NaN; // set up event emitting and interface var eventEmitter$1 = this.options.eventEmitter; if (typeof eventEmitter$1 === 'undefined') eventEmitter$1 = new eventEmitter(); /** * Register a handler for an event * * @method on * @memberof GameEngine * @instance * @param {String} eventName - name of the event * @param {Function} eventHandler - handler function */ this.on = eventEmitter$1.on; /** * Register a handler for an event, called just once (if at all) * * @method once * @memberof GameEngine * @instance * @param {String} eventName - name of the event * @param {Function} eventHandler - handler function */ this.once = eventEmitter$1.once; /** * Remove a handler * * @method removeListener * @memberof GameEngine * @instance * @param {String} eventName - name of the event * @param {Function} eventHandler - handler function */ this.removeListener = eventEmitter$1.off; this.off = eventEmitter$1.off; this.emit = eventEmitter$1.emit; // set up trace this.trace = new Trace({ traceLevel: this.options.traceLevel }); } _createClass(GameEngine, [{ key: "findLocalShadow", value: function findLocalShadow(serverObj) { for (var _i = 0, _Object$keys = Object.keys(this.world.objects); _i < _Object$keys.length; _i++) { var localId = _Object$keys[_i]; if (Number(localId) < this.options.clientIDSpace) continue; var localObj = this.world.objects[localId]; if (localObj.hasOwnProperty('inputId') && localObj.inputId === serverObj.inputId) return localObj; } return null; } }, { key: "initWorld", value: function initWorld(worldSettings) { this.world = new GameWorld(); // on the client we have a different ID space if (this.options.clientIDSpace) { this.world.idCount = this.options.clientIDSpace; } /** * The worldSettings defines the game world constants, such * as width, height, depth, etc. such that all other classes * can reference these values. * @member {Object} worldSettings * @memberof GameEngine */ this.worldSettings = Object.assign({}, worldSettings); } /** * Start the game. This method runs on both server * and client. Extending the start method is useful * for setting up the game's worldSettings attribute, * and registering methods on the event handler. */ }, { key: "start", value: function start() { var _this = this; this.trace.info(function () { return '========== game engine started =========='; }); this.initWorld(); // create the default timer this.timer = new Timer(); this.timer.play(); this.on('postStep', function (step, isReenact) { if (!isReenact) _this.timer.tick(); }); this.emit('start', { timestamp: new Date().getTime() }); } /** * Single game step. * * @param {Boolean} isReenact - is this step a re-enactment of the past. * @param {Number} t - the current time (optional) * @param {Number} dt - elapsed time since last step was called. (optional) * @param {Boolean} physicsOnly - do a physics step only, no game logic */ }, { key: "step", value: function step(isReenact, t, dt, physicsOnly) { var _this2 = this; // physics-only step if (physicsOnly) { if (dt) dt /= 1000; // physics engines work in seconds this.physicsEngine.step(dt, objectFilter); return; } // emit preStep event if (isReenact === undefined) throw new Error('game engine does not forward argument isReenact to super class'); isReenact = Boolean(isReenact); var step = ++this.world.stepCount; var clientIDSpace = this.options.clientIDSpace; this.emit('preStep', { step: step, isReenact: isReenact, dt: dt }); // skip physics for shadow objects during re-enactment function objectFilter(o) { return !isReenact || o.id < clientIDSpace; } // physics step if (this.physicsEngine && !this.ignorePhysics) { if (dt) dt /= 1000; // physics engines work in seconds this.physicsEngine.step(dt, objectFilter); } // for each object // - apply incremental bending // - refresh object positions after physics this.world.forEachObject(function (id, o) { if (typeof o.refreshFromPhysics === 'function') o.refreshFromPhysics(); _this2.trace.trace(function () { return "object[".concat(id, "] after ").concat(isReenact ? 'reenact' : 'step', " : ").concat(o.toString()); }); }); // emit postStep event this.emit('postStep', { step: step, isReenact: isReenact }); } /** * Add object to the game world. * On the client side, the object may not be created, if the server copy * of this object is already in the game world. This could happen when the client * is using delayed-input, and the RTT is very low. * * @param {Object} object - the object. * @return {Object} the final object. */ }, { key: "addObjectToWorld", value: function addObjectToWorld(object) { // if we are asked to create a local shadow object // the server copy may already have arrived. if (Number(object.id) >= this.options.clientIDSpace) { var serverCopyArrived = false; this.world.forEachObject(function (id, o) { if (o.hasOwnProperty('inputId') && o.inputId === object.inputId) { serverCopyArrived = true; return false; } }); if (serverCopyArrived) { this.trace.info(function () { return "========== shadow object NOT added ".concat(object.toString(), " =========="); }); return null; } } this.world.addObject(object); // tell the object to join the game, by creating // its corresponding physical entities and renderer entities. if (typeof object.onAddToWorld === 'function') object.onAddToWorld(this); this.emit('objectAdded', object); this.trace.info(function () { return "========== object added ".concat(object.toString(), " =========="); }); return object; } /** * Override this function to implement input handling. * This method will be called on the specific client where the * input was received, and will also be called on the server * when the input reaches the server. The client does not call this * method directly, rather the client calls {@link ClientEngine#sendInput} * so that the input is sent to both server and client, and so that * the input is delayed artificially if so configured. * * The input is described by a short string, and is given an index. * The index is used internally to keep track of inputs which have already been applied * on the client during synchronization. The input is also associated with * the ID of a player. * * @param {Object} inputDesc - input descriptor object * @param {String} inputDesc.input - describe the input (e.g. "up", "down", "fire") * @param {Number} inputDesc.messageIndex - input identifier * @param {Number} inputDesc.step - the step on which this input occurred * @param {Number} playerId - the player ID * @param {Boolean} isServer - indicate if this function is being called on the server side */ }, { key: "processInput", value: function processInput(inputDesc, playerId, isServer) { this.trace.info(function () { return "game engine processing input[".concat(inputDesc.messageIndex, "] <").concat(inputDesc.input, "> from playerId ").concat(playerId); }); } /** * Remove an object from the game world. * * @param {Object|String} objectId - the object or object ID */ }, { key: "removeObjectFromWorld", value: function removeObjectFromWorld(objectId) { if (_typeof(objectId) === 'object') objectId = objectId.id; var object = this.world.objects[objectId]; if (!object) { throw new Error("Game attempted to remove a game object which doesn't (or never did) exist, id=".concat(objectId)); } this.trace.info(function () { return "========== destroying object ".concat(object.toString(), " =========="); }); if (typeof object.onRemoveFromWorld === 'function') object.onRemoveFromWorld(this); this.emit('objectDestroyed', object); this.world.removeObject(objectId); } /** * Check if a given object is owned by the player on this client * * @param {Object} object the game object to check * @return {Boolean} true if the game object is owned by the player on this client */ }, { key: "isOwnedByPlayer", value: function isOwnedByPlayer(object) { return object.playerId == this.playerId; } /** * Register Game Object Classes * * @example * registerClasses(serializer) { * serializer.registerClass(Paddle); * serializer.registerClass(Ball); * } * * @param {Serializer} serializer - the serializer */ }, { key: "registerClasses", value: function registerClasses(serializer) {} /** * Decide whether the player game is over by returning an Object, need to be implemented * * @return {Object} truthful if the game is over for the player and the object is returned as GameOver data */ }, { key: "getPlayerGameOverResult", value: function getPlayerGameOverResult() { return null; } }]); return GameEngine; }(); // The base Physics Engine class defines the expected interface // for all physics engines var PhysicsEngine = /*#__PURE__*/function () { function PhysicsEngine(options) { _classCallCheck(this, PhysicsEngine); this.options = options; this.gameEngine = options.gameEngine; if (!options.gameEngine) { console.warn('Physics engine initialized without gameEngine!'); } } /** * A single Physics step. * * @param {Number} dt - time elapsed since last step * @param {Function} objectFilter - a test function which filters which objects should move */ _createClass(PhysicsEngine, [{ key: "step", value: function step(dt, objectFilter) {} }]); return PhysicsEngine; }(); var Utils = /*#__PURE__*/function () { function Utils() { _classCallCheck(this, Utils); } _createClass(Utils, null, [{ key: "hashStr", value: function hashStr(str, bits) { var hash = 5381; var i = str.length; bits = bits ? bits : 8; while (i) { hash = hash * 33 ^ str.charCodeAt(--i); } hash = hash >>> 0; hash = hash % (Math.pow(2, bits) - 1); // JavaScript does bitwise operations (like XOR, above) on 32-bit signed // integers. Since we want the results to be always positive, convert the // signed int to an unsigned by doing an unsigned bitshift. */ return hash; } }, { key: "arrayBuffersEqual", value: function arrayBuffersEqual(buf1, buf2) { if (buf1.byteLength !== buf2.byteLength) return false; var dv1 = new Int8Array(buf1); var dv2 = new Int8Array(buf2); for (var i = 0; i !== buf1.byteLength; i++) { if (dv1[i] !== dv2[i]) return false; } return true; } }, { key: "httpGetPromise", value: function httpGetPromise(url) { return new Promise(function (resolve, reject) { var req = new XMLHttpRequest(); req.open('GET', url, true); req.onload = function () { if (req.status >= 200 && req.status < 400) resolve(JSON.parse(req.responseText));else reject(); }; req.onerror = function () {}; req.send(); }); } }]); return Utils; }(); /** * The BaseTypes class defines the base types used in Lance. * These are the types which can be used to define an object's netscheme attributes, * which can be serialized by lance. * @example * static get netScheme() { * return { * strength: { type: BaseTypes.TYPES.FLOAT32 }, * shield: { type: BaseTypes.TYPES.INT8 }, * name: { type: BaseTypes.TYPES.STRING }, * backpack: { type: BaseTypes.TYPES.CLASSINSTANCE }, * coins: { * type: BaseTypes.TYPES.LIST, * itemType: BaseTypes.TYPES.UINT8 * } * }; * } */ var BaseTypes = /*#__PURE__*/_createClass(function BaseTypes() { _classCallCheck(this, BaseTypes); }); /** * @type {object} * @property {string} FLOAT32 Seriablizable float * @property {string} INT32 Seriablizable 32-bit integer * @property {string} INT16 Seriablizable 16-bit integer * @property {string} INT8 Seriablizable 8-bit integer * @property {string} UINT8 Seriablizable unsigned 8-bit integer * @property {string} STRING Seriablizable string * @property {string} CLASSINSTANCE Seriablizable class. Make sure you register all the classes included in this way. * @property {string} LIST Seriablizable list. In the netScheme definition, if an attribute is defined as a list, the itemType should also be defined. */ BaseTypes.TYPES = { /** * Seriablizable float * @alias TYPES.FLOAT32 * @memberof! BaseTypes# */ FLOAT32: 'FLOAT32', /** * Seriablizable 32-bit int * @alias TYPES.INT32 * @memberof! BaseTypes# */ INT32: 'INT32', /** * Seriablizable 16-bit int * @alias TYPES.INT16 * @memberof! BaseTypes# */ INT16: 'INT16', /** * Seriablizable 8-bit int * @alias TYPES.INT8 * @memberof! BaseTypes# */ INT8: 'INT8', /** * Seriablizable unsigned 8-bit int * @alias TYPES.UINT8 * @memberof! BaseTypes# */ UINT8: 'UINT8', /** * Seriablizable string * @alias TYPES.STRING * @memberof! BaseTypes# */ STRING: 'STRING', /** * Seriablizable class. Make sure you registered the classes included in this way. * @alias TYPES.CLASSINSTANCE * @memberof! BaseTypes# */ CLASSINSTANCE: 'CLASSINSTANCE', /** * Seriablizable list. * @alias TYPES.LIST * @memberof! BaseTypes# */ LIST: 'LIST' }; var Serializable = /*#__PURE__*/function () { function Serializable() { _classCallCheck(this, Serializable); } _createClass(Serializable, [{ key: "serialize", value: /** * Class can be serialized using either: * - a class based netScheme * - an instance based netScheme * - completely dynamically (not implemented yet) * * @param {Object} serializer - Serializer instance * @param {Object} [options] - Options object * @param {Object} options.dataBuffer [optional] - Data buffer to write to. If null a new data buffer will be created * @param {Number} options.bufferOffset [optional] - The buffer data offset to start writing at. Default: 0 * @param {String} options.dry [optional] - Does not actually write to the buffer (useful to gather serializeable size) * @return {Object} the serialized object. Contains attributes: dataBuffer - buffer which contains the serialized data; bufferOffset - offset where the serialized data starts. */ function serialize(serializer, options) { options = Object.assign({ bufferOffset: 0 }, options); var netScheme; var dataBuffer; var dataView; var classId = 0; var bufferOffset = options.bufferOffset; var localBufferOffset = 0; // used for counting the bufferOffset // instance classId if (this.classId) { classId = this.classId; } else { classId = Utils.hashStr(this.constructor.name); } // instance netScheme if (this.netScheme) { netScheme = this.netScheme; } else if (this.constructor.netScheme) { netScheme = this.constructor.netScheme; } else { // todo define behaviour when a netScheme is undefined console.warn('no netScheme defined! This will result in awful performance'); } // TODO: currently we serialize every node twice, once to calculate the size // of the buffers and once to write them out. This can be reduced to // a single pass by starting with a large (and static) ArrayBuffer and // recursively building it up. // buffer has one Uint8Array for class id, then payload if (options.dataBuffer == null && options.dry != true) { var bufferSize = this.serialize(serializer, { dry: true }).bufferOffset; dataBuffer = new ArrayBuffer(bufferSize); } else { dataBuffer = options.dataBuffer; } if (options.dry != true) { dataView = new DataView(dataBuffer); // first set the id of the class, so that the deserializer can fetch information about it dataView.setUint8(bufferOffset + localBufferOffset, classId); } // advance the offset counter localBufferOffset += Uint8Array.BYTES_PER_ELEMENT; if (netScheme) { var _iterator = _createForOfIteratorHelper(Object.keys(netScheme).sort()), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var property = _step.value; // write the property to buffer if (options.dry != true) { serializer.writeDataView(dataView, this[property], bufferOffset + localBufferOffset, netScheme[property]); } if (netScheme[property].type === BaseTypes.TYPES.STRING) { // derive the size of the string localBufferOffset += Uint16Array.BYTES_PER_ELEMENT; if (this[property] !== null && this[property] !== undefined) localBufferOffset += this[property].length * Uint16Array.BYTES_PER_ELEMENT; } else if (netScheme[property].type === BaseTypes.TYPES.CLASSINSTANCE) { // derive the size of the included class var objectInstanceBufferOffset = this[property].serialize(serializer, { dry: true }).bufferOffset; localBufferOffset += objectInstanceBufferOffset; } else if (netScheme[property].type === BaseTypes.TYPES.LIST) { // derive the size of the list // list starts with number of elements localBufferOffset += Uint16Array.BYTES_PER_ELEMENT; var _iterator2 = _createForOfIteratorHelper(this[property]), _step2; try { for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { var item = _step2.value; // todo inelegant, currently doesn't support list of lists if (netScheme[property].itemType === BaseTypes.TYPES.CLASSINSTANCE) { var listBufferOffset = item.serialize(serializer, { dry: true }).bufferOffset; localBufferOffset += listBufferOffset; } else if (netScheme[property].itemType === BaseTypes.TYPES.STRING) { // size includes string length plus double-byte characters localBufferOffset += Uint16Array.BYTES_PER_ELEMENT * (1 + item.length); } else { localBufferOffset += serializer.getTypeByteSize(netScheme[property].itemType); } } } catch (err) { _iterator2.e(err); } finally { _iterator2.f(); } } else { // advance offset localBufferOffset += serializer.getTypeByteSize(netScheme[property].type); } } } catch (err) { _iterator.e(err); } finally { _iterator.f(); } } return { dataBuffer: dataBuffer, bufferOffset: localBufferOffset }; } // build a clone of this object with