Achievement & Event improvements

* User stat set vs user stat increment system events
* Proper addMultipleEventListener() and removeMultipleEventListener() Events APIs
* userStatSet vs userStatInc user stat achievement types. userStatInc for example can be used for door minutes used
This commit is contained in:
Bryan Ashby 2019-01-06 21:56:12 -07:00
parent 925ca134c6
commit 34c9178099
6 changed files with 107 additions and 35 deletions

View File

@ -31,7 +31,7 @@
- Applying customizations via the achievements section in theme.hjson
Some tips:
- For 'userStat' types, see user_property.js
- For 'userStatSet' types, see user_property.js
Don"t forget to RTFM ...er uh... see the documentation for more information and
don"t be shy to ask for help:
@ -53,7 +53,7 @@
achievements: {
user_login_count: {
type: userStat
type: userStatSet
statName: login_count
match: {
2: {
@ -90,7 +90,7 @@
}
user_post_count: {
type: userStat
type: userStatSet
statName: post_count
match: {
5: {
@ -121,7 +121,7 @@
}
user_upload_count: {
type: userStat
type: userStatSet
statName: ul_total_count
match: {
1: {
@ -159,7 +159,7 @@
}
user_download_count: {
type: userStat
type: userStatSet
statName: dl_total_count
match: {
1: {
@ -196,7 +196,7 @@
}
user_door_runs: {
type: userStat
type: userStatSet
statName: door_run_total_count
match: {
1: {
@ -233,7 +233,7 @@
}
user_door_total_minutes: {
type: userStat
type: userStatInc
statName: door_run_total_minutes
match: {
1: {

View File

@ -45,7 +45,11 @@ class Achievement {
static factory(data) {
let achievement;
switch(data.type) {
case Achievement.Types.UserStat : achievement = new UserStatAchievement(data); break;
case Achievement.Types.UserStatSet :
case Achievement.Types.UserStatInc :
achievement = new UserStatAchievement(data);
break;
default : return;
}
@ -56,13 +60,15 @@ class Achievement {
static get Types() {
return {
UserStat : 'userStat',
UserStatSet : 'userStatSet',
UserStatInc : 'userStatInc',
};
}
isValid() {
switch(this.data.type) {
case Achievement.Types.UserStat :
case Achievement.Types.UserStatSet :
case Achievement.Types.UserStatInc :
if(!_.isString(this.data.statName)) {
return false;
}
@ -129,12 +135,12 @@ class Achievements {
const configLoaded = (achievementConfig) => {
if(true !== achievementConfig.enabled) {
Log.info('Achievements are not enabled');
this.stopMonitoringUserStatUpdateEvents();
this.stopMonitoringUserStatEvents();
delete this.achievementConfig;
} else {
Log.info('Achievements are enabled');
this.achievementConfig = achievementConfig;
this.monitorUserStatUpdateEvents();
this.monitorUserStatEvents();
}
};
@ -240,18 +246,22 @@ class Achievements {
);
}
monitorUserStatUpdateEvents() {
if(this.userStatEventListener) {
monitorUserStatEvents() {
if(this.userStatEventListeners) {
return; // already listening
}
this.userStatEventListener = this.events.on(Events.getSystemEvents().UserStatUpdate, userStatEvent => {
const listenEvents = [
Events.getSystemEvents().UserStatSet,
Events.getSystemEvents().UserStatIncrement
];
this.userStatEventListeners = this.events.addMultipleEventListener(listenEvents, userStatEvent => {
if([ UserProps.AchievementTotalCount, UserProps.AchievementTotalPoints ].includes(userStatEvent.statName)) {
return;
}
const statValue = parseInt(userStatEvent.statValue, 10);
if(isNaN(statValue)) {
if(!_.isNumber(userStatEvent.statValue) && !_.isNumber(userStatEvent.statIncrementBy)) {
return;
}
@ -262,7 +272,7 @@ class Achievements {
if(false === achievement.enabled) {
return false;
}
return Achievement.Types.UserStat === achievement.type &&
return [ Achievement.Types.UserStatSet, Achievement.Types.UserStatInc ].includes(achievement.type) &&
achievement.statName === userStatEvent.statName;
}
);
@ -276,6 +286,14 @@ class Achievements {
return;
}
const statValue = parseInt(
Achievement.Types.UserStatSet === achievement.data.type ? userStatEvent.statValue : userStatEvent.statIncrementBy,
10
);
if(isNaN(statValue)) {
return;
}
const [ details, matchField, matchValue ] = achievement.getMatchDetails(statValue);
if(!details || _.isUndefined(matchField) || _.isUndefined(matchValue)) {
return;
@ -345,10 +363,10 @@ class Achievements {
});
}
stopMonitoringUserStatUpdateEvents() {
if(this.userStatEventListener) {
this.events.removeListener(Events.getSystemEvents().UserStatUpdate, this.userStatEventListener);
delete this.userStatEventListener;
stopMonitoringUserStatEvents() {
if(this.userStatEventListeners) {
this.events.removeMultipleEventListener(this.userStatEventListeners);
delete this.userStatEventListeners;
}
}

View File

@ -5,10 +5,14 @@
const { MenuModule } = require('./menu_module.js');
const { resetScreen } = require('./ansi_term.js');
const { Errors } = require('./enig_error.js');
const Events = require('./events.js');
const StatLog = require('./stat_log.js');
const UserProps = require('./user_property.js');
// deps
const async = require('async');
const SSHClient = require('ssh2').Client;
const moment = require('moment');
exports.moduleInfo = {
name : 'DoorParty',
@ -54,10 +58,18 @@ exports.getModule = class DoorPartyModule extends MenuModule {
let pipeRestored = false;
let pipedStream;
const startTime = moment();
const restorePipe = function() {
if(pipedStream && !pipeRestored && !clientTerminated) {
self.client.term.output.unpipe(pipedStream);
self.client.term.output.resume();
const endTime = moment();
const runTimeMinutes = Math.floor(moment.duration(endTime.diff(startTime)).asMinutes());
if(runTimeMinutes > 0) {
StatLog.incrementUserStat(self.client.user, UserProps.DoorRunTotalMinutes, runTimeMinutes);
}
}
};
@ -83,6 +95,9 @@ exports.getModule = class DoorPartyModule extends MenuModule {
const rlogin = `\x00${self.client.user.username}\x00[${self.config.bbsTag}]${self.client.user.username}\x00${self.client.term.termType}\x00`;
stream.write(rlogin);
StatLog.incrementUserStat(self.client.user, UserProps.DoorRunTotalCount, 1);
Events.emit(Events.getSystemEvents().UserRunDoor, { user : self.client.user } );
pipedStream = stream; // :TODO: this is hacky...
self.client.term.output.pipe(stream);

View File

@ -5,6 +5,9 @@ const events = require('events');
const Log = require('./logger.js').log;
const SystemEvents = require('./system_events.js');
// deps
const _ = require('lodash');
module.exports = new class Events extends events.EventEmitter {
constructor() {
super();
@ -35,12 +38,30 @@ module.exports = new class Events extends events.EventEmitter {
return super.once(event, listener);
}
addListenerMultipleEvents(events, listener) {
Log.trace( { events }, 'Registring event listeners');
//
// Listen to multiple events for a single listener.
// Called with: listener(event, eventName)
//
// The returned object must be used with removeMultipleEventListener()
//
addMultipleEventListener(events, listener) {
Log.trace( { events }, 'Registering event listeners');
const listeners = [];
events.forEach(eventName => {
this.on(eventName, event => {
listener(eventName, event);
});
const listenWrapper = _.partial(listener, _, eventName);
this.on(eventName, listenWrapper);
listeners.push( { eventName, listenWrapper } );
});
return listeners;
}
removeMultipleEventListener(listeners) {
Log.trace( { events }, 'Removing listeners');
listeners.forEach(listener => {
this.removeListener(listener.eventName, listener.listenWrapper);
});
}

View File

@ -122,12 +122,18 @@ class StatLog {
// User specific stats
// These are simply convenience methods to the user's properties
//
setUserStat(user, statName, statValue, cb) {
setUserStatWithOptions(user, statName, statValue, options, cb) {
// note: cb is optional in PersistUserProperty
user.persistProperty(statName, statValue, cb);
const Events = require('./events.js'); // we need to late load currently
return Events.emit(Events.getSystemEvents().UserStatUpdate, { user, statName, statValue } );
if(!options.noEvent) {
const Events = require('./events.js'); // we need to late load currently
Events.emit(Events.getSystemEvents().UserStatSet, { user, statName, statValue } );
}
}
setUserStat(user, statName, statValue, cb) {
return this.setUserStatWithOptions(user, statName, statValue, {}, cb);
}
getUserStat(user, statName) {
@ -143,16 +149,27 @@ class StatLog {
let newValue = parseInt(user.properties[statName]);
if(newValue) {
if(!_.isNumber(newValue)) {
if(!_.isNumber(newValue) && cb) {
return cb(new Error(`Value for ${statName} is not a number!`));
}
newValue += incrementBy;
} else {
newValue = incrementBy;
}
return this.setUserStat(user, statName, newValue, cb);
this.setUserStatWithOptions(user, statName, newValue, { noEvent : true }, err => {
if(!err) {
const Events = require('./events.js'); // we need to late load currently
Events.emit(
Events.getSystemEvents().UserStatIncrement,
{ user, statName, statIncrementBy: incrementBy, statValue : newValue }
);
}
if(cb) {
return cb(err);
}
});
}
// the time "now" in the ISO format we use and love :)
@ -362,7 +379,7 @@ class StatLog {
systemEvents.UserAchievementEarned,
];
Events.addListenerMultipleEvents(interestedEvents, (eventName, event) => {
Events.addMultipleEventListener(interestedEvents, (event, eventName) => {
this.appendUserLogEntry(
event.user,
'system_event',

View File

@ -21,6 +21,7 @@ module.exports = {
UserSendMail : 'codes.l33t.enigma.system.user_send_mail',
UserRunDoor : 'codes.l33t.enigma.system.user_run_door',
UserSendNodeMsg : 'codes.l33t.enigma.system.user_send_node_msg',
UserStatUpdate : 'codes.l33t.enigma.system.user_stat_set', // { ..., statName, statValue }
UserStatSet : 'codes.l33t.enigma.system.user_stat_set', // { ..., statName, statValue }
UserStatIncrement : 'codes.l33t.enigma.system.user_stat_increment', // {..., statName, statIncrementBy, statValue }
UserAchievementEarned : 'codes.l33t.enigma.system.user_achievement_earned', // {..., achievementTag, points }
};