/** * Generic constructor for both instantiating an existing game * character (from JSON data), as well as creating a new one. * * @param {object} [data] initialization values (properties are * shallow-copied into the instance) * @constructor * @augments Bag * @mixes PlayerApi */ function Player(data) { Player.super_.call(this, data); utils.addNonEnumerable(this, 'session'); utils.addNonEnumerable(this, 'active', false); utils.addNonEnumerable(this, 'changes', []); utils.addNonEnumerable(this, 'anncs', []); utils.addNonEnumerable(this, 'msgCache', []); // convert selected properties to "Property" instances (works with simple // int values as well as serialized Property instances) for (var group in PROPS) { if (!this[group]) this[group] = {}; for (var key in PROPS[group]) { if (PROPS[group][key] === 'property') { this[group][key] = new Prop(key, this[group][key]); } else if (PROPS[group][key] === 'object') { if (!this[group][key]) { this[group][key] = {}; } for (var subkey in this[group][key]) { var propGroup = this[group][key]; propGroup[subkey] = new Prop(subkey, propGroup[subkey]); } } } } }
/** * Generic constructor for both instantiating an existing location * (from JSON data), and creating a new location. * * @param {object} [data] initialization values (properties are * shallow-copied into the location object) * @param {Geo} [geo] geometry data (optional, for testing); if * `undefined`, the respective `Geo` object is loaded from * persistence * @constructor * @augments GameObject * @mixes LocationApi */ function Location(data, geo) { data = data || {}; if (!data.tsid) { if (geo) { data.tsid = geo.getLocTsid(); } else { data.tsid = rpc.makeLocalTsid(this.TSID_INITIAL_LOCATION); } } Location.super_.call(this, data); // initialize items and players, convert to IdObjRefMap this.players = new IdObjRefMap(this.players); this.items = new IdObjRefMap(this.items); // convert neighbor list to OrderedHash if (this.neighbors) { this.neighbors = new OrderedHash(this.neighbors); } // initialize geometry utils.addNonEnumerable(this, 'geometry'); utils.addNonEnumerable(this, 'clientGeometry'); utils.addNonEnumerable(this, 'geo'); var geoData = geo || pers.get(this.getGeoTsid(), true); assert(_.isObject(geoData), 'no geometry data for ' + this); if (rpc.isLocal(this)) { this.updateGeo(geoData); } }
/** * Generic constructor for both instantiating an existing location * (from JSON data), and creating a new location. * * @param {object} [data] initialization values (properties are * shallow-copied into the location object) * @param {Geo} [geo] geometry data (optional, for testing); if * `undefined`, the respective `Geo` object is loaded from * persistence * @constructor * @augments GameObject * @mixes LocationApi */ function Location(data, geo) { data = data || {}; if (!data.tsid) { if (geo) { data.tsid = geo.getLocTsid(); } else { data.tsid = rpc.makeLocalTsid(Location.prototype.TSID_INITIAL); } } Location.super_.call(this, data); // initialize items and players, convert to IdObjRefMap if (!this.players || this.players instanceof Array) { this.players = utils.arrayToHash(this.players); } this.players = new IdObjRefMap(this.players); if (!this.items || this.items instanceof Array) { this.items = utils.arrayToHash(this.items); } this.items = new IdObjRefMap(this.items); // convert neighbor list to OrderedHash if (this.neighbors) { this.neighbors = new OrderedHash(this.neighbors); } // initialize geometry utils.addNonEnumerable(this, 'geometry'); utils.addNonEnumerable(this, 'clientGeometry'); utils.addNonEnumerable(this, 'geo'); var geoData = geo || pers.get(this.getGeoTsid(), true); assert(typeof geoData === 'object', 'no geometry data for ' + this); this.updateGeo(geoData); }
/** * Generic constructor for both instantiating an existing game object * (from JSON data), and creating a new object. * * @param {object} [data] initialization values (properties are * shallow-copied into the game object) * @constructor * @mixes GameObjectApi */ function GameObject(data) { if (!data) data = {}; // initialize TSID/class ID (use deprecated properties if necessary, and // keep them as non-enumerable so they are available, but not persisted) this.tsid = data.tsid || data.id || utils.makeTsid(this.TSID_INITIAL, config.getGsid()); utils.addNonEnumerable(this, 'id', this.tsid); // deprecated if (data.class_tsid || data.class_id) { this.class_tsid = data.class_tsid || data.class_id; utils.addNonEnumerable(this, 'class_id', this.class_tsid); // deprecated } // add non-enumerable internal properties utils.addNonEnumerable(this, '__isGO', true); utils.addNonEnumerable(this, 'deleted', false); utils.addNonEnumerable(this, 'stale', false); // copy supplied data orProxy.copyOwnProps(data, this); this.ts = this.ts || Date.now(); if (!this.gsTimers) this.gsTimers = {}; utils.makeNonEnumerable(this, 'gsTimers'); }
/** * If Slack integration is configured for the given group, this * modifies its function reponsible for generating the list of chat * participants (i.e. the output of `/who`) to include Slack users. * @private */ function patchGroup(group) { if (!groupToChannel[group.tsid]) return; // monkey patch the function generating /who output var orig = group.chat_get_roster_msg; utils.addNonEnumerable(group, 'chat_get_roster_msg', function chat_get_roster_msg() { // eslint-disable-line camelcase var channel = groupToChannel[group.tsid]; var roster = utils.shallowCopy(group.chat_roster); for (var i = 0; i < channel.members.length; i++) { var user = slack.dataStore.getUserById(channel.members[i]); if (user.presence === 'active' && user.id !== slack.activeUserId) { roster[user.id] = {label: user.name + ' (Slack)'}; } } return orig.call({chat_roster: roster}); } ); }
/** * A class for integer properties of game objects that provides atomic * manipulations on their values. * * @param {string} label name of the property * @param {number|object} data either just a numeric initial value, or * an object containing extended configuration like: * ```{val: 3, bottom: -3, top: 8000}``` * @constructor * @mixes PropertyApi */ function Property(label, data) { this.label = label; if (data === undefined) data = {}; if (_.isNumber(data)) { this.value = Math.round(data); } else if (_.isObject(data)) { this.value = _.isNumber(data.value) ? Math.round(data.value) : 0; } else { this.value = 0; } this.setLimits( _.isNumber(data.bottom) ? data.bottom : this.value, _.isNumber(data.top) ? data.top : this.value); // add a flag that indicates whether an update for this property's value // needs to be sent to the client utils.addNonEnumerable(this, 'changed', false); }
/** * Handles incoming messages from Slack. Figures out if the message is * something we're interested in (regular message sent to one of the * connected groups), and forwards it to the respective group if so. * @private */ function onSlackMessage(msg) { log.trace({data: msg}, 'incoming Slack message'); if (msg.type !== 'message') return; // ignore meta stuff // is this a connected group managed by this GS instance? var groupTsid = channelToGroup[msg.channel]; if (!groupTsid) return; // special handling for edits/removals of earlier messages if (msg.subtype === 'message_deleted') return; if (msg.subtype === 'message_changed' && msg.message) { msg = msg.message; // for edits, message prop seems to contain the actual payload msg.text = '[EDIT] ' + msg.text; } // prepare message payload var user = slack.dataStore.getUserById(msg.user); if (!user) { log.error({data: msg}, 'could not retrieve Slack user'); return; } if (user.id === slack.activeUserId) { // don't echo! return; } var out = { type: 'pc_groups_chat', tsid: groupTsid, pc: { // unique pseudo TSID so client assigns different colors: tsid: 'PSLACK' + user.id, label: user.name, }, txt: processMsgText(msg.text), }; utils.addNonEnumerable(out, 'fromSlack', true); dispatchToGroup(out, function cb(err) { if (err) { log.error({err: err, data: msg}, 'error dispatching group chat message from Slack'); } }); }