From a34dab6a73326c9241dbb702654a50c7497a0862 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 2 Jan 2019 22:13:42 -0700 Subject: [PATCH] WIP on user achievements * Hook up events for testing * Start to plug in experimental interrupt --- core/achievement.js | 103 ++++++++++++++++++++++++++++++++++++++++++ core/config.js | 35 ++++++++++++++ core/database.js | 12 +++++ core/stat_log.js | 7 ++- core/system_events.js | 1 + 5 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 core/achievement.js diff --git a/core/achievement.js b/core/achievement.js new file mode 100644 index 00000000..bb11e3c4 --- /dev/null +++ b/core/achievement.js @@ -0,0 +1,103 @@ +/* jslint node: true */ +'use strict'; + +// ENiGMA½ +const Events = require('./events.js'); +const Config = require('./config.js').get; +const UserDb = require('./database.js').dbs.user; +const UserInterruptQueue = require('./user_interrupt_queue.js'); +const { + getConnectionByUserId +} = require('./client_connections.js'); + +// deps +const _ = require('lodash'); + +class Achievements { + constructor(events) { + this.events = events; + } + + init(cb) { + this.monitorUserStatUpdateEvents(); + return cb(null); + } + + loadAchievementHitCount(user, achievementTag, field, value, cb) { + UserDb.get( + `SELECT COUNT() AS count + FROM user_achievement + WHERE user_id = ? AND achievement_tag = ? AND match_field = ? AND match_value >= ?;`, + [ user.userId, achievementTag, field, value ], + (err, row) => { + return cb(err, row && row.count || 0); + } + ); + } + + monitorUserStatUpdateEvents() { + this.events.on(Events.getSystemEvents().UserStatUpdate, userStatEvent => { + const statValue = parseInt(userStatEvent.statValue, 10); + if(isNaN(statValue)) { + return; + } + + const config = Config(); + const achievementTag = _.findKey( + _.get(config, 'userAchievements.achievements', {}), + achievement => { + if(false === achievement.enabled) { + return false; + } + return 'userStat' === achievement.type && + achievement.statName === userStatEvent.statName; + } + ); + + if(!achievementTag) { + return; + } + + const achievement = config.userAchievements.achievements[achievementTag]; + let matchValue = Object.keys(achievement.match || {}).sort( (a, b) => b - a).find(v => statValue >= v); + if(matchValue) { + const match = achievement.match[matchValue]; + + // + // Check if we've triggered this event before + // + this.loadAchievementHitCount(userStatEvent.user, achievementTag, null, matchValue, (err, count) => { + if(count > 0) { + return; + } + + const conn = getConnectionByUserId(userStatEvent.user.userId); + if(!conn) { + return; + } + + const interruptItem = { + text : match.text, + pause : true, + }; + + UserInterruptQueue.queue(interruptItem, { omit : conn} ); + }); + } + }); + } +} + +let achievements; + +exports.moduleInitialize = (initInfo, cb) => { + + if(false === _.get(Config(), 'userAchievements.enabled')) { + // :TODO: Log disabled + return cb(null); + } + + achievements = new Achievements(initInfo.events); + return achievements.init(cb); +}; + diff --git a/core/config.js b/core/config.js index 9c9c4cd4..f854e2f5 100644 --- a/core/config.js +++ b/core/config.js @@ -1003,6 +1003,41 @@ function getDefaultConfig() { systemEvents : { loginHistoryMax: -1, // set to -1 for forever } + }, + + userAchievements : { + enabled : true, + + artHeader : 'achievement_header', + artFooter : 'achievement_footer', + + achievements : { + user_login_count : { + type : 'userStat', + statName : 'login_count', + retroactive : true, + match : { + 10 : { + title : 'Return Caller', + globalText : '{userName} has logged in {statValue} times!', + text : 'You\'ve logged in {statValue} times!', + points : 5, + }, + 25 : { + title : 'Seems To Like It!', + globalText : '{userName} has logged in {statValue} times!', + text : 'You\'ve logged in {statValue} times!', + points : 10, + }, + 100 : { + title : '{boardName} Addict', + globalText : '{userName} the BBS {boardName} addict has logged in {statValue} times!', + text : 'You\'re a {boardName} addict! You\'ve logged in {statValue} times!', + points : 10, + } + } + } + } } }; } diff --git a/core/database.js b/core/database.js index 040cc1de..4cf2513c 100644 --- a/core/database.js +++ b/core/database.js @@ -189,6 +189,18 @@ const DB_INIT_TABLE = { );` ); + dbs.user.run( + `CREATE TABLE IF NOT EXISTS user_achievement ( + user_id INTEGER NOT NULL, + achievement_tag VARCHAR NOT NULL, + timestamp DATETIME NOT NULL, + match_field VARCHAR NOT NULL, + match_value VARCHAR NOT NULL, + UNIQUE(user_id, achievement_tag, match_field, match_value), + FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE + );` + ); + return cb(null); }, diff --git a/core/stat_log.js b/core/stat_log.js index 6cf6198b..ffe099ae 100644 --- a/core/stat_log.js +++ b/core/stat_log.js @@ -120,11 +120,14 @@ class StatLog { // // User specific stats - // These are simply convience methods to the user's properties + // These are simply convenience methods to the user's properties // setUserStat(user, statName, statValue, cb) { // note: cb is optional in PersistUserProperty - return user.persistProperty(statName, statValue, cb); + 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 } ); } getUserStat(user, statName) { diff --git a/core/system_events.js b/core/system_events.js index 0f8118a2..80612195 100644 --- a/core/system_events.js +++ b/core/system_events.js @@ -21,4 +21,5 @@ 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 } };