+ ACS: AC for achievement count check
+ ACS: AP for achievement point check + User minutes used on the system are now tracked + MCI: TO for total time spent online system (friendly format) * Fix up a couple ACS bugs with |value| * Fix formatting of achievement text + Add more achievements * Fix achievement duration formatting
This commit is contained in:
parent
091a9ae2c7
commit
2788c37492
|
@ -26,6 +26,7 @@ This document attempts to track **major** changes and additions in ENiGMA½. For
|
||||||
* `oputil.js user rm` and `oputil.js user info` are in! See [oputil CLI](/docs/admin/oputil.md).
|
* `oputil.js user rm` and `oputil.js user info` are in! See [oputil CLI](/docs/admin/oputil.md).
|
||||||
* Performing a file scan/import using `oputil.js fb scan` now recognizes various `FILES.BBS` formats.
|
* Performing a file scan/import using `oputil.js fb scan` now recognizes various `FILES.BBS` formats.
|
||||||
* Usernames found in the `config.users.badUserNames` are now not only disallowed from applying, but disconnected at any login attempt.
|
* Usernames found in the `config.users.badUserNames` are now not only disallowed from applying, but disconnected at any login attempt.
|
||||||
|
* Total minutes online is now tracked for users. Of course, it only starts after you get the update :)
|
||||||
|
|
||||||
|
|
||||||
## 0.0.8-alpha
|
## 0.0.8-alpha
|
||||||
|
|
|
@ -989,9 +989,9 @@
|
||||||
pointsSGR: "|12"
|
pointsSGR: "|12"
|
||||||
textSGR: "|00|03"
|
textSGR: "|00|03"
|
||||||
globalTextSGR: "|03"
|
globalTextSGR: "|03"
|
||||||
boardName: "|10"
|
boardNameSGR: "|10"
|
||||||
userName: "|11"
|
userNameSGR: "|11"
|
||||||
achievedValue: "|15"
|
achievedValueSGR: "|15"
|
||||||
}
|
}
|
||||||
|
|
||||||
overrides: {
|
overrides: {
|
||||||
|
|
|
@ -307,29 +307,60 @@
|
||||||
match: {
|
match: {
|
||||||
1: {
|
1: {
|
||||||
title: "Nevermind!"
|
title: "Nevermind!"
|
||||||
globalText: "{userName} ran a door for {achievedValue!durationSeconds}. Guess it's not their thing!"
|
globalText: "{userName} ran a door for {achievedValue!durationMinutes}. Guess it's not their thing!"
|
||||||
text: "You ran a door for only {achievedValue!durationSeconds}. Not your thing?"
|
text: "You ran a door for only {achievedValue!durationMinutes}. Not your thing?"
|
||||||
points: 5
|
points: 5
|
||||||
}
|
}
|
||||||
10: {
|
10: {
|
||||||
title: "It's OK I Guess"
|
title: "It's OK I Guess"
|
||||||
globalText: "{userName} ran a door for {achievedValue!durationSeconds}!"
|
globalText: "{userName} ran a door for {achievedValue!durationMinutes}!"
|
||||||
text: "You ran a door for {achievedValue!durationSeconds}!"
|
text: "You ran a door for {achievedValue!durationMinutes}!"
|
||||||
points: 10
|
points: 10
|
||||||
}
|
}
|
||||||
30: {
|
30: {
|
||||||
title: "Good Game"
|
title: "Good Game"
|
||||||
globalText: "{userName} ran a door for {achievedValue!durationSeconds}!"
|
globalText: "{userName} ran a door for {achievedValue!durationMinutes}!"
|
||||||
text: "You ran a door for {achievedValue!durationSeconds}!"
|
text: "You ran a door for {achievedValue!durationMinutes}!"
|
||||||
points: 20
|
points: 20
|
||||||
}
|
}
|
||||||
60: {
|
60: {
|
||||||
title: "Textmode Dragon Slayer"
|
title: "Textmode Dragon Slayer"
|
||||||
globalText: "{userName} has spent {achievedValue!durationSeconds} in a door!"
|
globalText: "{userName} has spent {achievedValue!durationMinutes} in a door!"
|
||||||
text: "You've spent {achievedValue!durationSeconds} in a door!"
|
text: "You've spent {achievedValue!durationMinutes} in a door!"
|
||||||
points: 25
|
points: 25
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
user_total_system_online_minutes: {
|
||||||
|
type: userStatSet
|
||||||
|
statName: minutes_online_total_count
|
||||||
|
match: {
|
||||||
|
30: {
|
||||||
|
title: "Just Poking Around"
|
||||||
|
globalText: "{userName} has spent {achievedValue!durationMinutes} on {boardName}!"
|
||||||
|
text: "You've been on {boardName} for a total of {achievedValue!durationMinutes}!"
|
||||||
|
points: 5
|
||||||
|
}
|
||||||
|
60: {
|
||||||
|
title: "Mildly Interesting"
|
||||||
|
globalText: "{userName} has spent {achievedValue!durationMinutes} on {boardName}!"
|
||||||
|
text: "You've been on {boardName} for a total of {achievedValue!durationMinutes}!"
|
||||||
|
points: 15
|
||||||
|
}
|
||||||
|
120: {
|
||||||
|
title: "Nothing Better to Do"
|
||||||
|
globalText: "{userName} has spent {achievedValue!durationMinutes} on {boardName}!"
|
||||||
|
text: "You've been on {boardName} for a total of {achievedValue!durationMinutes}!"
|
||||||
|
points: 25
|
||||||
|
}
|
||||||
|
1440: {
|
||||||
|
title: "Idle Bot"
|
||||||
|
globalText: "{userName} is probably a bot. They've spent {achievedValue!durationMinutes} on {boardName}!"
|
||||||
|
text: "You're a bot, aren't you? You've been on {boardName} for a total of {achievedValue!durationMinutes}!"
|
||||||
|
points: 50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -406,19 +406,23 @@ class Achievements {
|
||||||
|
|
||||||
getFormattedTextFor(info, textType, defaultSgr = '|07') {
|
getFormattedTextFor(info, textType, defaultSgr = '|07') {
|
||||||
const themeDefaults = _.get(info.client.currentTheme, 'achievements.defaults', {});
|
const themeDefaults = _.get(info.client.currentTheme, 'achievements.defaults', {});
|
||||||
const defSgr = themeDefaults[`${textType}SGR`] || defaultSgr;
|
const textTypeSgr = themeDefaults[`${textType}SGR`] || defaultSgr;
|
||||||
|
|
||||||
const wrap = (fieldName, value) => {
|
const formatObj = this.getFormatObject(info);
|
||||||
return `${themeDefaults[fieldName] || defSgr}${value}${defSgr}`;
|
|
||||||
|
const wrap = (input) => {
|
||||||
|
const re = new RegExp(`{(${Object.keys(formatObj).join('|')})([^}]*)}`, 'g');
|
||||||
|
return input.replace(re, (m, formatVar, formatOpts) => {
|
||||||
|
const varSgr = themeDefaults[`${formatVar}SGR`] || textTypeSgr;
|
||||||
|
let r = `${varSgr}{${formatVar}`;
|
||||||
|
if(formatOpts) {
|
||||||
|
r += formatOpts;
|
||||||
|
}
|
||||||
|
return `${r}}${textTypeSgr}`;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
let formatObj = this.getFormatObject(info);
|
return stringFormat(`${textTypeSgr}${wrap(info.details[textType])}`, formatObj);
|
||||||
formatObj = _.reduce(formatObj, (out, v, k) => {
|
|
||||||
out[k] = wrap(k, v);
|
|
||||||
return out;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
return stringFormat(`${defSgr}${info.details[textType]}`, formatObj);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createAchievementInterruptItems(info, cb) {
|
createAchievementInterruptItems(info, cb) {
|
||||||
|
|
|
@ -1004,7 +1004,7 @@ function peg$parse(input, options) {
|
||||||
TW : function termWidth() {
|
TW : function termWidth() {
|
||||||
return !isNaN(value) && _.get(client, 'term.termWidth', 0) >= value;
|
return !isNaN(value) && _.get(client, 'term.termWidth', 0) >= value;
|
||||||
},
|
},
|
||||||
ID : function isUserId(value) {
|
ID : function isUserId() {
|
||||||
if(!user) {
|
if(!user) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -1024,6 +1024,20 @@ function peg$parse(input, options) {
|
||||||
const midnight = now.clone().startOf('day')
|
const midnight = now.clone().startOf('day')
|
||||||
const minutesPastMidnight = now.diff(midnight, 'minutes');
|
const minutesPastMidnight = now.diff(midnight, 'minutes');
|
||||||
return !isNaN(value) && minutesPastMidnight >= value;
|
return !isNaN(value) && minutesPastMidnight >= value;
|
||||||
|
},
|
||||||
|
AC : function achievementCount() {
|
||||||
|
if(!user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const count = user.getPropertyAsNumber(UserProps.AchievementTotalCount) || 0;
|
||||||
|
return !isNan(value) && points >= value;
|
||||||
|
},
|
||||||
|
AP : function achievementPoints() {
|
||||||
|
if(!user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const points = user.getPropertyAsNumber(UserProps.AchievementTotalPoints) || 0;
|
||||||
|
return !isNan(value) && points >= value;
|
||||||
}
|
}
|
||||||
}[acsCode](value);
|
}[acsCode](value);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -23,6 +23,8 @@
|
||||||
// General
|
// General
|
||||||
// * http://en.wikipedia.org/wiki/ANSI_escape_code
|
// * http://en.wikipedia.org/wiki/ANSI_escape_code
|
||||||
// * http://www.inwap.com/pdp10/ansicode.txt
|
// * http://www.inwap.com/pdp10/ansicode.txt
|
||||||
|
// * Excellent information with many standards covered (for hterm):
|
||||||
|
// https://chromium.googlesource.com/apps/libapps/+/master/hterm/doc/ControlSequences.md
|
||||||
//
|
//
|
||||||
// Other Implementations
|
// Other Implementations
|
||||||
// * https://github.com/chjj/term.js/blob/master/src/term.js
|
// * https://github.com/chjj/term.js/blob/master/src/term.js
|
||||||
|
|
|
@ -40,6 +40,7 @@ const MenuStack = require('./menu_stack.js');
|
||||||
const ACS = require('./acs.js');
|
const ACS = require('./acs.js');
|
||||||
const Events = require('./events.js');
|
const Events = require('./events.js');
|
||||||
const UserInterruptQueue = require('./user_interrupt_queue.js');
|
const UserInterruptQueue = require('./user_interrupt_queue.js');
|
||||||
|
const UserProps = require('./user_property.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const stream = require('stream');
|
const stream = require('stream');
|
||||||
|
@ -442,13 +443,36 @@ Client.prototype.startIdleMonitor = function() {
|
||||||
|
|
||||||
//
|
//
|
||||||
// Every 1m, check for idle.
|
// Every 1m, check for idle.
|
||||||
|
// We also update minutes spent online the system here,
|
||||||
|
// if we have a authenticated user.
|
||||||
//
|
//
|
||||||
this.idleCheck = setInterval( () => {
|
this.idleCheck = setInterval( () => {
|
||||||
const nowMs = Date.now();
|
const nowMs = Date.now();
|
||||||
|
|
||||||
const idleLogoutSeconds = this.user.isAuthenticated() ?
|
let idleLogoutSeconds;
|
||||||
Config().users.idleLogoutSeconds :
|
if(this.user.isAuthenticated()) {
|
||||||
Config().users.preAuthIdleLogoutSeconds;
|
idleLogoutSeconds = Config().users.idleLogoutSeconds;
|
||||||
|
|
||||||
|
//
|
||||||
|
// We don't really want to be firing off an event every 1m for
|
||||||
|
// every user, but want at least some updates for various things
|
||||||
|
// such as achievements. Send off every 5m.
|
||||||
|
//
|
||||||
|
const minOnline = this.user.incrementProperty(UserProps.MinutesOnlineTotalCount, 1);
|
||||||
|
if(0 === (minOnline % 5)) {
|
||||||
|
Events.emit(
|
||||||
|
Events.getSystemEvents().UserStatIncrement,
|
||||||
|
{
|
||||||
|
user : this.user,
|
||||||
|
statName : UserProps.MinutesOnlineTotalCount,
|
||||||
|
statIncrementBy : 1,
|
||||||
|
statValue : minOnline
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
idleLogoutSeconds = Config().users.preAuthIdleLogoutSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
if(nowMs - this.lastKeyPressMs >= (idleLogoutSeconds * 1000)) {
|
if(nowMs - this.lastKeyPressMs >= (idleLogoutSeconds * 1000)) {
|
||||||
this.emit('idle timeout');
|
this.emit('idle timeout');
|
||||||
|
@ -473,6 +497,14 @@ Client.prototype.end = function () {
|
||||||
currentModule.leave();
|
currentModule.leave();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// persist time online for authenticated users
|
||||||
|
if(this.user.isAuthenticated()) {
|
||||||
|
this.user.persistProperty(
|
||||||
|
UserProps.MinutesOnlineTotalCount,
|
||||||
|
this.user.getProperty(UserProps.MinutesOnlineTotalCount)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
this.stopIdleMonitor();
|
this.stopIdleMonitor();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -160,6 +160,10 @@ const PREDEFINED_MCI_GENERATORS = {
|
||||||
const minutes = client.user.properties[UserProps.DoorRunTotalMinutes] || 0;
|
const minutes = client.user.properties[UserProps.DoorRunTotalMinutes] || 0;
|
||||||
return moment.duration(minutes, 'minutes').humanize();
|
return moment.duration(minutes, 'minutes').humanize();
|
||||||
},
|
},
|
||||||
|
TO : function friendlyTotalTimeOnSystem(client) {
|
||||||
|
const minutes = client.user.properties[UserProps.MinutesOnlineTotalCount] || 0;
|
||||||
|
return moment.duration(minutes, 'minutes').humanize();
|
||||||
|
},
|
||||||
|
|
||||||
//
|
//
|
||||||
// Date/Time
|
// Date/Time
|
||||||
|
|
16
core/user.js
16
core/user.js
|
@ -443,6 +443,22 @@ module.exports = class User {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setProperty(propName, propValue) {
|
||||||
|
this.properties[propName] = propValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
incrementProperty(propName, incrementBy) {
|
||||||
|
incrementBy = incrementBy || 1;
|
||||||
|
let newValue = parseInt(this.getProperty(propName));
|
||||||
|
if(newValue) {
|
||||||
|
newValue += incrementBy;
|
||||||
|
} else {
|
||||||
|
newValue = incrementBy;
|
||||||
|
}
|
||||||
|
this.setProperty(propName, newValue);
|
||||||
|
return newValue;
|
||||||
|
}
|
||||||
|
|
||||||
getProperty(propName) {
|
getProperty(propName) {
|
||||||
return this.properties[propName];
|
return this.properties[propName];
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,5 +55,7 @@ module.exports = {
|
||||||
|
|
||||||
AchievementTotalCount : 'achievement_total_count',
|
AchievementTotalCount : 'achievement_total_count',
|
||||||
AchievementTotalPoints : 'achievement_total_points',
|
AchievementTotalPoints : 'achievement_total_points',
|
||||||
|
|
||||||
|
MinutesOnlineTotalCount : 'minutes_online_total_count',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,9 @@ The following are ACS codes available as of this writing:
|
||||||
| NR<i>ratio</i> | User has upload/download count ratio >= _ratio_ |
|
| NR<i>ratio</i> | User has upload/download count ratio >= _ratio_ |
|
||||||
| KR<i>ratio</i> | User has a upload/download byte ratio >= _ratio_ |
|
| KR<i>ratio</i> | User has a upload/download byte ratio >= _ratio_ |
|
||||||
| PC<i>ratio</i> | User has a post/call ratio >= _ratio_ |
|
| PC<i>ratio</i> | User has a post/call ratio >= _ratio_ |
|
||||||
| MM<i>minutes</i> | It is currently >= _minutes_ past midnight (system time)
|
| MM<i>minutes</i> | It is currently >= _minutes_ past midnight (system time) |
|
||||||
|
| AC<i>achievementCount</i> | User has >= _achievementCount_ achievements |
|
||||||
|
| AP<i>achievementPoints</i> | User has >= _achievementPoints_ achievement points |
|
||||||
|
|
||||||
\* Many more ACS codes are planned for the near future.
|
\* Many more ACS codes are planned for the near future.
|
||||||
|
|
||||||
|
|
|
@ -160,7 +160,7 @@
|
||||||
TW : function termWidth() {
|
TW : function termWidth() {
|
||||||
return !isNaN(value) && _.get(client, 'term.termWidth', 0) >= value;
|
return !isNaN(value) && _.get(client, 'term.termWidth', 0) >= value;
|
||||||
},
|
},
|
||||||
ID : function isUserId(value) {
|
ID : function isUserId() {
|
||||||
if(!user) {
|
if(!user) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -180,6 +180,20 @@
|
||||||
const midnight = now.clone().startOf('day')
|
const midnight = now.clone().startOf('day')
|
||||||
const minutesPastMidnight = now.diff(midnight, 'minutes');
|
const minutesPastMidnight = now.diff(midnight, 'minutes');
|
||||||
return !isNaN(value) && minutesPastMidnight >= value;
|
return !isNaN(value) && minutesPastMidnight >= value;
|
||||||
|
},
|
||||||
|
AC : function achievementCount() {
|
||||||
|
if(!user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const count = user.getPropertyAsNumber(UserProps.AchievementTotalCount) || 0;
|
||||||
|
return !isNan(value) && points >= value;
|
||||||
|
},
|
||||||
|
AP : function achievementPoints() {
|
||||||
|
if(!user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const points = user.getPropertyAsNumber(UserProps.AchievementTotalPoints) || 0;
|
||||||
|
return !isNan(value) && points >= value;
|
||||||
}
|
}
|
||||||
}[acsCode](value);
|
}[acsCode](value);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
Loading…
Reference in New Issue