@rtsdk/lance-topia
Version:
A Node.js based real-time multiplayer game server
1,635 lines (1,496 loc) • 184 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var fs = require('fs');
var path = require('path');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var fs__default = /*#__PURE__*/_interopDefaultLegacy(fs);
var path__default = /*#__PURE__*/_interopDefaultLegacy(path);
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 pruned strings (if necessary)
}, {
key: "prunedStringsClone",
value: function prunedStringsClone(serializer, prevObject) {
var _this = this;
if (!prevObject) return this;
prevObject = serializer.deserialize(prevObject).obj;
// get list of string properties which changed
var netScheme = this.constructor.netScheme;
var isString = function isString(p) {
return netScheme[p].type === BaseTypes.TYPES.STRING;
};
var hasChanged = function hasChanged(p) {
return prevObject[p] !== _this[p];
};
var changedStrings = Object.keys(netScheme).filter(isString).filter(hasChanged);
if (changedStrings.length == 0) return this;
// build a clone with pruned strings
var prunedCopy = new this.constructor(null, {
id: null
});
for (var _i = 0, _Object$keys = Object.keys(netScheme); _i < _Object$keys.length; _i++) {
var p = _Object$keys[_i];
prunedCopy[p] = changedStrings.indexOf(p) < 0 ? this[p] : null;
}
return prunedCopy;
}
}, {
key: "syncTo",
value: function syncTo(other) {
var netScheme = this.constructor.netScheme;
for (var _i2 = 0, _Object$keys2 = Object.keys(netScheme); _i2 < _Object$keys2.length; _i2++) {
var p = _Object$keys2[_i2];
// ignore classes and lists
if (netScheme[p].type === BaseTypes.TYPES.LIST || netScheme[p].type === BaseTypes.TYPES.CLASSINSTANCE) continue;
// strings might be pruned
if (netScheme[p].type === BaseTypes.TYPES.STRING) {
if (typeof other[p] === 'string') this[p] = other[p];
continue;
}
// all other values are copied
this[p] = other[p];
}
}
}]);
return Serializable;
}();
/**
* A TwoVector is a geometric object which is completely described
* by two values.
*/
var TwoVector = /*#__PURE__*/function (_Serializable) {
_inherits(TwoVector, _Serializable);
var _super = _createSuper(TwoVector);
/**
* Creates an instance of a TwoVector.
* @param {Number} x - first value
* @param {Number} y - second value
* @return {TwoVector} v - the new TwoVector
*/
function TwoVector(x, y) {
var _this;
_classCallCheck(this, TwoVector);
_this = _super.call(this);
_this.x = x;
_this.y = y;
return _possibleConstructorReturn(_this, _assertThisInitialized(_this));
}
/**
* Formatted textual description of the TwoVector.
* @return {String} description
*/
_createClass(TwoVector, [{
key: "toString",
value: function toString() {
function round3(x) {
return Math.round(x * 1000) / 1000;
}
return "[".concat(round3(this.x), ", ").concat(round3(this.y), "]");
}
/**
* Set TwoVector values