Moved folders to website directory
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
var mongoose = require("mongoose");
|
||||
var Schema = mongoose.Schema;
|
||||
var shared = require('../../common');
|
||||
var _ = require('lodash');
|
||||
var TaskSchemas = require('./task');
|
||||
|
||||
var ChallengeSchema = new Schema({
|
||||
_id: {type: String, 'default': shared.uuid},
|
||||
name: String,
|
||||
shortName: String,
|
||||
description: String,
|
||||
official: {type: Boolean,'default':false},
|
||||
habits: [TaskSchemas.HabitSchema],
|
||||
dailys: [TaskSchemas.DailySchema],
|
||||
todos: [TaskSchemas.TodoSchema],
|
||||
rewards: [TaskSchemas.RewardSchema],
|
||||
leader: {type: String, ref: 'User'},
|
||||
group: {type: String, ref: 'Group'},
|
||||
timestamp: {type: Date, 'default': Date.now},
|
||||
members: [{type: String, ref: 'User'}],
|
||||
memberCount: {type: Number, 'default': 0},
|
||||
prize: {type: Number, 'default': 0}
|
||||
});
|
||||
|
||||
ChallengeSchema.virtual('tasks').get(function () {
|
||||
var tasks = this.habits.concat(this.dailys).concat(this.todos).concat(this.rewards);
|
||||
var tasks = _.object(_.pluck(tasks,'id'), tasks);
|
||||
return tasks;
|
||||
});
|
||||
|
||||
ChallengeSchema.methods.toJSON = function(){
|
||||
var doc = this.toObject();
|
||||
doc._isMember = this._isMember;
|
||||
return doc;
|
||||
}
|
||||
|
||||
// --------------
|
||||
// Syncing logic
|
||||
// --------------
|
||||
|
||||
function syncableAttrs(task) {
|
||||
var t = (task.toObject) ? task.toObject() : task; // lodash doesn't seem to like _.omit on EmbeddedDocument
|
||||
// only sync/compare important attrs
|
||||
var omitAttrs = 'challenge history tags completed streak notes'.split(' ');
|
||||
if (t.type != 'reward') omitAttrs.push('value');
|
||||
return _.omit(t, omitAttrs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare whether any changes have been made to tasks. If so, we'll want to sync those changes to subscribers
|
||||
*/
|
||||
function comparableData(obj) {
|
||||
return JSON.stringify(
|
||||
_(obj.habits.concat(obj.dailys).concat(obj.todos).concat(obj.rewards))
|
||||
.sortBy('id') // we don't want to update if they're sort-order is different
|
||||
.transform(function(result, task){
|
||||
result.push(syncableAttrs(task));
|
||||
})
|
||||
.value())
|
||||
}
|
||||
|
||||
ChallengeSchema.methods.isOutdated = function(newData) {
|
||||
return comparableData(this) !== comparableData(newData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncs all new tasks, deleted tasks, etc to the user object
|
||||
* @param user
|
||||
* @return nothing, user is modified directly. REMEMBER to save the user!
|
||||
*/
|
||||
ChallengeSchema.methods.syncToUser = function(user, cb) {
|
||||
if (!user) return;
|
||||
var self = this;
|
||||
self.shortName = self.shortName || self.name;
|
||||
|
||||
// Add challenge to user.challenges
|
||||
if (!_.contains(user.challenges, self._id)) {
|
||||
user.challenges.push(self._id);
|
||||
}
|
||||
|
||||
// Sync tags
|
||||
var tags = user.tags || [];
|
||||
var i = _.findIndex(tags, {id: self._id})
|
||||
if (~i) {
|
||||
if (tags[i].name !== self.shortName) {
|
||||
// update the name - it's been changed since
|
||||
user.tags[i].name = self.shortName;
|
||||
}
|
||||
} else {
|
||||
user.tags.push({
|
||||
id: self._id,
|
||||
name: self.shortName,
|
||||
challenge: true
|
||||
});
|
||||
}
|
||||
|
||||
// Sync new tasks and updated tasks
|
||||
_.each(self.tasks, function(task){
|
||||
var list = user[task.type+'s'];
|
||||
var userTask = user.tasks[task.id] || (list.push(syncableAttrs(task)), list[list.length-1]);
|
||||
if (!userTask.notes) userTask.notes = task.notes; // don't override the notes, but provide it if not provided
|
||||
userTask.challenge = {id:self._id};
|
||||
userTask.tags = userTask.tags || {};
|
||||
userTask.tags[self._id] = true;
|
||||
_.merge(userTask, syncableAttrs(task));
|
||||
})
|
||||
|
||||
// Flag deleted tasks as "broken"
|
||||
_.each(user.tasks, function(task){
|
||||
if (task.challenge && task.challenge.id==self._id && !self.tasks[task.id]) {
|
||||
task.challenge.broken = 'TASK_DELETED';
|
||||
}
|
||||
})
|
||||
|
||||
user.save(cb);
|
||||
};
|
||||
|
||||
|
||||
module.exports.schema = ChallengeSchema;
|
||||
module.exports.model = mongoose.model("Challenge", ChallengeSchema);
|
||||
@@ -0,0 +1,59 @@
|
||||
var mongoose = require("mongoose");
|
||||
var shared = require('../../common');
|
||||
var _ = require('lodash');
|
||||
var async = require('async');
|
||||
var cc = require('coupon-code');
|
||||
var autoinc = require('mongoose-id-autoinc');
|
||||
|
||||
var CouponSchema = new mongoose.Schema({
|
||||
_id: {type: String, 'default': cc.generate},
|
||||
event: {type:String, enum:['wondercon','google_6mo']},
|
||||
user: {type: 'String', ref: 'User'}
|
||||
});
|
||||
|
||||
CouponSchema.statics.generate = function(event, count, callback) {
|
||||
async.times(count, function(n,cb){
|
||||
mongoose.model('Coupon').create({event: event}, cb);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
CouponSchema.statics.apply = function(user, code, next){
|
||||
async.auto({
|
||||
get_coupon: function (cb) {
|
||||
mongoose.model('Coupon').findById(cc.validate(code), cb);
|
||||
},
|
||||
apply_coupon: ['get_coupon', function (cb, results) {
|
||||
if (!results.get_coupon) return cb("Invalid coupon code");
|
||||
if (results.get_coupon.user) return cb("Coupon already used");
|
||||
switch (results.get_coupon.event) {
|
||||
case 'wondercon':
|
||||
user.items.gear.owned.eyewear_special_wondercon_red = true;
|
||||
user.items.gear.owned.eyewear_special_wondercon_black = true;
|
||||
user.items.gear.owned.back_special_wondercon_black = true;
|
||||
user.items.gear.owned.back_special_wondercon_red = true;
|
||||
user.items.gear.owned.body_special_wondercon_red = true;
|
||||
user.items.gear.owned.body_special_wondercon_black = true;
|
||||
user.items.gear.owned.body_special_wondercon_gold = true;
|
||||
user.extra = {signupEvent: 'wondercon'};
|
||||
user.save(cb);
|
||||
break;
|
||||
}
|
||||
}],
|
||||
expire_coupon: ['apply_coupon', function (cb, results) {
|
||||
results.get_coupon.user = user._id;
|
||||
results.get_coupon.save(cb);
|
||||
}]
|
||||
}, function(err, results){
|
||||
if (err) return next(err);
|
||||
next(null,results.apply_coupon[0]);
|
||||
})
|
||||
}
|
||||
|
||||
CouponSchema.plugin(autoinc.plugin, {
|
||||
model: 'Coupon',
|
||||
field: 'seq'
|
||||
});
|
||||
|
||||
module.exports.schema = CouponSchema;
|
||||
module.exports.model = mongoose.model("Coupon", CouponSchema);
|
||||
|
||||
@@ -0,0 +1,370 @@
|
||||
var mongoose = require("mongoose");
|
||||
var Schema = mongoose.Schema;
|
||||
var shared = require('../../common');
|
||||
var _ = require('lodash');
|
||||
var async = require('async');
|
||||
var logging = require('../logging');
|
||||
|
||||
var GroupSchema = new Schema({
|
||||
_id: {type: String, 'default': shared.uuid},
|
||||
name: String,
|
||||
description: String,
|
||||
leader: {type: String, ref: 'User'},
|
||||
members: [{type: String, ref: 'User'}],
|
||||
invites: [{type: String, ref: 'User'}],
|
||||
type: {type: String, "enum": ['guild', 'party']},
|
||||
privacy: {type: String, "enum": ['private', 'public'], 'default':'private'},
|
||||
//_v: {type: Number,'default': 0},
|
||||
chat: Array,
|
||||
/*
|
||||
# [{
|
||||
# timestamp: Date
|
||||
# user: String
|
||||
# text: String
|
||||
# contributor: String
|
||||
# uuid: String
|
||||
# id: String
|
||||
# }]
|
||||
*/
|
||||
leaderOnly: { // restrict group actions to leader (members can't do them)
|
||||
challenges: {type:Boolean, 'default':false},
|
||||
//invites: {type:Boolean, 'default':false}
|
||||
},
|
||||
memberCount: {type: Number, 'default': 0},
|
||||
challengeCount: {type: Number, 'default': 0},
|
||||
balance: Number,
|
||||
logo: String,
|
||||
leaderMessage: String,
|
||||
challenges: [{type:'String', ref:'Challenge'}], // do we need this? could depend on back-ref instead (Challenge.find({group:GID}))
|
||||
quest: {
|
||||
key: String,
|
||||
active: {type:Boolean, 'default':false},
|
||||
leader: {type:String, ref:'User'},
|
||||
progress:{
|
||||
hp: Number,
|
||||
collect: {type:Schema.Types.Mixed, 'default':{}}, // {feather: 5, ingot: 3}
|
||||
rage: Number, // limit break / "energy stored in shell", for explosion-attacks
|
||||
},
|
||||
|
||||
//Shows boolean for each party-member who has accepted the quest. Eg {UUID: true, UUID: false}. Once all users click
|
||||
//'Accept', the quest begins. If a false user waits too long, probably a good sign to prod them or boot them.
|
||||
//TODO when booting user, remove from .joined and check again if we can now start the quest
|
||||
members: Schema.Types.Mixed,
|
||||
extra: Schema.Types.Mixed
|
||||
}
|
||||
}, {
|
||||
strict: 'throw',
|
||||
minimize: false // So empty objects are returned
|
||||
});
|
||||
|
||||
/**
|
||||
* Derby duplicated stuff. This is a temporary solution, once we're completely off derby we'll run an mongo migration
|
||||
* to remove duplicates, then take these fucntions out
|
||||
*/
|
||||
function removeDuplicates(doc){
|
||||
// Remove duplicate members
|
||||
if (doc.members) {
|
||||
var uniqMembers = _.uniq(doc.members);
|
||||
if (uniqMembers.length != doc.members.length) {
|
||||
doc.members = uniqMembers;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME this isn't always triggered, since we sometimes use update() or findByIdAndUpdate()
|
||||
// @see https://github.com/LearnBoost/mongoose/issues/964
|
||||
GroupSchema.pre('save', function(next){
|
||||
removeDuplicates(this);
|
||||
this.memberCount = _.size(this.members);
|
||||
this.challengeCount = _.size(this.challenges);
|
||||
next();
|
||||
})
|
||||
|
||||
GroupSchema.methods.toJSON = function(){
|
||||
var doc = this.toObject();
|
||||
removeDuplicates(doc);
|
||||
doc._isMember = this._isMember;
|
||||
|
||||
//fix(groups): temp fix to remove chat entries stored as strings (not sure why that's happening..).
|
||||
// Required as angular 1.3 is strict on dupes, and no message.id to `track by`
|
||||
_.remove(doc.chat,function(msg){return !msg.id});
|
||||
|
||||
// @see pre('save') comment above
|
||||
this.memberCount = _.size(this.members);
|
||||
this.challengeCount = _.size(this.challenges);
|
||||
|
||||
return doc;
|
||||
}
|
||||
|
||||
var chatDefaults = module.exports.chatDefaults = function(msg,user){
|
||||
var message = {
|
||||
id: shared.uuid(),
|
||||
text: msg,
|
||||
timestamp: +new Date,
|
||||
likes: {},
|
||||
flags: {},
|
||||
flagCount: 0
|
||||
};
|
||||
if (user) {
|
||||
_.defaults(message, {
|
||||
uuid: user._id,
|
||||
contributor: user.contributor && user.contributor.toObject(),
|
||||
backer: user.backer && user.backer.toObject(),
|
||||
user: user.profile.name
|
||||
});
|
||||
} else {
|
||||
message.uuid = 'system';
|
||||
}
|
||||
return message;
|
||||
}
|
||||
GroupSchema.methods.sendChat = function(message, user){
|
||||
var group = this;
|
||||
group.chat.unshift(chatDefaults(message,user));
|
||||
group.chat.splice(200);
|
||||
// Kick off chat notifications in the background.
|
||||
var lastSeenUpdate = {$set:{}, $inc:{_v:1}};
|
||||
lastSeenUpdate['$set']['newMessages.'+group._id] = {name:group.name,value:true};
|
||||
if (group._id == 'habitrpg') {
|
||||
// TODO For Tavern, only notify them if their name was mentioned
|
||||
// var profileNames = [] // get usernames from regex of @xyz. how to handle space-delimited profile names?
|
||||
// User.update({'profile.name':{$in:profileNames}},lastSeenUpdate,{multi:true}).exec();
|
||||
} else {
|
||||
mongoose.model('User').update({_id:{$in:group.members, $ne: user ? user._id : ''}},lastSeenUpdate,{multi:true}).exec();
|
||||
}
|
||||
}
|
||||
|
||||
var cleanQuestProgress = function(merge){
|
||||
var clean = {
|
||||
key: null,
|
||||
progress: {
|
||||
up: 0,
|
||||
down: 0,
|
||||
collect: {}
|
||||
},
|
||||
completed: null
|
||||
};
|
||||
merge = merge || {progress:{}};
|
||||
_.merge(clean, _.omit(merge,'progress'));
|
||||
_.merge(clean.progress, merge.progress);
|
||||
return clean;
|
||||
}
|
||||
GroupSchema.statics.cleanQuestProgress = cleanQuestProgress;
|
||||
|
||||
// Participants: Grant rewards & achievements, finish quest
|
||||
GroupSchema.methods.finishQuest = function(quest, cb) {
|
||||
var group = this;
|
||||
var questK = quest.key;
|
||||
var updates = {$inc:{},$set:{}};
|
||||
|
||||
updates['$inc']['achievements.quests.' + questK] = 1;
|
||||
updates['$inc']['stats.gp'] = +quest.drop.gp;
|
||||
updates['$inc']['stats.exp'] = +quest.drop.exp;
|
||||
updates['$inc']['_v'] = 1;
|
||||
if (group._id == 'habitrpg') {
|
||||
updates['$set']['party.quest.completed'] = questK; // Just show the notif
|
||||
} else {
|
||||
updates['$set']['party.quest'] = cleanQuestProgress({completed: questK}); // clear quest progress
|
||||
}
|
||||
|
||||
_.each(quest.drop.items, function(item){
|
||||
var dropK = item.key;
|
||||
switch (item.type) {
|
||||
case 'gear':
|
||||
// TODO This means they can lose their new gear on death, is that what we want?
|
||||
updates['$set']['items.gear.owned.'+dropK] = true;
|
||||
break;
|
||||
case 'eggs':
|
||||
case 'food':
|
||||
case 'hatchingPotions':
|
||||
case 'quests':
|
||||
updates['$inc']['items.'+item.type+'.'+dropK] = _.where(quest.drop.items,{type:item.type,key:item.key}).length;
|
||||
break;
|
||||
case 'pets':
|
||||
updates['$set']['items.pets.'+dropK] = 5;
|
||||
break;
|
||||
case 'mounts':
|
||||
updates['$set']['items.mounts.'+dropK] = true;
|
||||
break;
|
||||
}
|
||||
})
|
||||
var q = group._id === 'habitrpg' ? {} : {_id:{$in:_.keys(group.quest.members)}};
|
||||
group.quest = {};group.markModified('quest');
|
||||
mongoose.model('User').update(q, updates, {multi:true}, cb);
|
||||
}
|
||||
|
||||
// FIXME this is a temporary measure, we need to remove quests from users when they traverse parties
|
||||
function isOnQuest(user,progress,group){
|
||||
return group && progress && user.party.quest.key && user.party.quest.key == group.quest.key;
|
||||
}
|
||||
|
||||
GroupSchema.statics.collectQuest = function(user, progress, cb) {
|
||||
this.findOne({type: 'party', members: {'$in': [user._id]}},function(err, group){
|
||||
if (!isOnQuest(user,progress,group)) return cb(null);
|
||||
var quest = shared.content.quests[group.quest.key];
|
||||
|
||||
_.each(progress.collect,function(v,k){
|
||||
group.quest.progress.collect[k] += v;
|
||||
});
|
||||
|
||||
var foundText = _.reduce(progress.collect, function(m,v,k){
|
||||
m.push(v + ' ' + quest.collect[k].text('en'));
|
||||
return m;
|
||||
}, []);
|
||||
foundText = foundText ? foundText.join(', ') : 'nothing';
|
||||
group.sendChat("`" + user.profile.name + " found "+foundText+".`");
|
||||
group.markModified('quest.progress.collect');
|
||||
|
||||
// Still needs completing
|
||||
if (_.find(shared.content.quests[group.quest.key].collect, function(v,k){
|
||||
return group.quest.progress.collect[k] < v.count;
|
||||
})) return group.save(cb);
|
||||
|
||||
async.series([
|
||||
function(cb2){
|
||||
group.finishQuest(quest,cb2);
|
||||
},
|
||||
function(cb2){
|
||||
group.sendChat('`All items found! Party has received their rewards.`');
|
||||
group.save(cb2);
|
||||
}
|
||||
],cb);
|
||||
})
|
||||
}
|
||||
|
||||
// to set a boss: `db.groups.update({_id:'habitrpg'},{$set:{quest:{key:'dilatory',active:true,progress:{hp:1000,rage:1500}}}})`
|
||||
module.exports.tavern = {};
|
||||
var tavernQ = {_id:'habitrpg','quest.key':{$ne:null}};
|
||||
process.nextTick(function(){
|
||||
mongoose.model('Group').findOne(tavernQ,function(err,tavern){
|
||||
module.exports.tavern = tavern;
|
||||
});
|
||||
})
|
||||
GroupSchema.statics.tavernBoss = function(user,progress) {
|
||||
if (!progress) return;
|
||||
|
||||
// hack: prevent crazy damage to world boss
|
||||
var dmg = Math.min(900, Math.abs(progress.up||0)),
|
||||
rage = -Math.min(900, Math.abs(progress.down||0));
|
||||
|
||||
async.waterfall([
|
||||
function(cb){
|
||||
mongoose.model('Group').findOne(tavernQ,cb);
|
||||
},
|
||||
function(tavern,cb){
|
||||
if (!(tavern && tavern.quest && tavern.quest.key)) return cb(true);
|
||||
module.exports.tavern = tavern;
|
||||
|
||||
var quest = shared.content.quests[tavern.quest.key];
|
||||
if (tavern.quest.progress.hp <= 0) {
|
||||
tavern.sendChat(quest.completionChat('en'));
|
||||
tavern.finishQuest(quest, function(){});
|
||||
tavern.save(cb);
|
||||
module.exports.tavern = undefined;
|
||||
} else {
|
||||
// Deal damage. Note a couple things here, str & def are calculated. If str/def are defined in the database,
|
||||
// use those first - which allows us to update the boss on the go if things are too easy/hard.
|
||||
if (!tavern.quest.extra) tavern.quest.extra = {};
|
||||
tavern.quest.progress.hp -= dmg / (tavern.quest.extra.def || quest.boss.def);
|
||||
tavern.quest.progress.rage -= rage * (tavern.quest.extra.str || quest.boss.str);
|
||||
if (tavern.quest.progress.rage >= quest.boss.rage.value) {
|
||||
if (!tavern.quest.extra.worldDmg) tavern.quest.extra.worldDmg = {};
|
||||
var wd = tavern.quest.extra.worldDmg;
|
||||
// var scene = wd.tavern ? wd.stables ? wd.market ? false : 'market' : 'stables' : 'tavern'; // Dilatory attacks tavern, stables, market
|
||||
var scene = wd.stables ? wd.bailey ? wd.guide ? false : 'guide' : 'bailey' : 'stables'; // Stressbeast attacks stables, Bailey, Justin
|
||||
if (!scene) {
|
||||
tavern.sendChat('`'+quest.boss.name('en')+' tries to unleash '+quest.boss.rage.title('en')+', but is too tired.`');
|
||||
tavern.quest.progress.rage = 0 //quest.boss.rage.value;
|
||||
} else {
|
||||
tavern.sendChat(quest.boss.rage[scene]('en'));
|
||||
tavern.quest.extra.worldDmg[scene] = true;
|
||||
tavern.quest.extra.worldDmg.recent = scene;
|
||||
tavern.markModified('quest.extra.worldDmg');
|
||||
tavern.quest.progress.rage = 0;
|
||||
tavern.quest.progress.hp += (quest.boss.rage.healing * tavern.quest.progress.hp);
|
||||
}
|
||||
}
|
||||
if ((tavern.quest.progress.hp < quest.boss.desperation.threshold) && !tavern.quest.extra.desperate) {
|
||||
tavern.sendChat(quest.boss.desperation.text('en'));
|
||||
tavern.quest.extra.desperate = true;
|
||||
tavern.quest.extra.def = quest.boss.desperation.def;
|
||||
tavern.quest.extra.str = quest.boss.desperation.str;
|
||||
tavern.markModified('quest.extra');
|
||||
}
|
||||
tavern.save(cb);
|
||||
}
|
||||
}
|
||||
],function(err,res){
|
||||
if (err === true) return; // no current quest
|
||||
if (err) return logging.error(err);
|
||||
dmg = rage = null;
|
||||
})
|
||||
}
|
||||
|
||||
GroupSchema.statics.bossQuest = function(user, progress, cb) {
|
||||
this.findOne({type: 'party', members: {'$in': [user._id]}},function(err, group){
|
||||
if (!isOnQuest(user,progress,group)) return cb(null);
|
||||
var quest = shared.content.quests[group.quest.key];
|
||||
if (!progress || !quest) return cb(null); // FIXME why is this ever happening, progress should be defined at this point
|
||||
var down = progress.down * quest.boss.str; // multiply by boss strength
|
||||
|
||||
group.quest.progress.hp -= progress.up;
|
||||
group.sendChat("`" + user.profile.name + " attacks " + quest.boss.name('en') + " for " + (progress.up.toFixed(1)) + " damage, " + quest.boss.name('en') + " attacks party for " + Math.abs(down).toFixed(1) + " damage.`"); //TODO Create a party preferred language option so emits like this can be localized
|
||||
|
||||
// If boss has Rage, increment Rage as well
|
||||
if (quest.boss.rage) {
|
||||
group.quest.progress.rage += Math.abs(down);
|
||||
if (group.quest.progress.rage >= quest.boss.rage.value) {
|
||||
group.sendChat(quest.boss.rage.effect('en'));
|
||||
group.quest.progress.rage = 0;
|
||||
if (quest.boss.rage.healing) group.quest.progress.hp += (group.quest.progress.hp * quest.boss.rage.healing); //TODO To make Rage effects more expandable, let's turn these into functions in quest.boss.rage
|
||||
if (group.quest.progress.hp > quest.boss.hp) group.quest.progress.hp = quest.boss.hp;
|
||||
}
|
||||
}
|
||||
// Everyone takes damage
|
||||
var series = [
|
||||
function(cb2){
|
||||
mongoose.models.User.update({_id:{$in: _.keys(group.quest.members)}}, {$inc:{'stats.hp':down, _v:1}}, {multi:true}, cb2);
|
||||
}
|
||||
]
|
||||
|
||||
// Boss slain, finish quest
|
||||
if (group.quest.progress.hp <= 0) {
|
||||
group.sendChat('`You defeated ' + quest.boss.name('en') + '! Questing party members receive the rewards of victory.`');
|
||||
// Participants: Grant rewards & achievements, finish quest
|
||||
series.push(function(cb2){
|
||||
group.finishQuest(quest,cb2);
|
||||
});
|
||||
}
|
||||
|
||||
series.push(function(cb2){group.save(cb2)});
|
||||
async.series(series,cb);
|
||||
})
|
||||
}
|
||||
|
||||
GroupSchema.methods.toJSON = function() {
|
||||
var doc = this.toObject();
|
||||
if(doc.chat){
|
||||
doc.chat.forEach(function(msg){
|
||||
msg.flags = {};
|
||||
});
|
||||
}
|
||||
|
||||
return doc;
|
||||
};
|
||||
|
||||
|
||||
module.exports.schema = GroupSchema;
|
||||
var Group = module.exports.model = mongoose.model("Group", GroupSchema);
|
||||
|
||||
// initialize tavern if !exists (fresh installs)
|
||||
Group.count({_id:'habitrpg'},function(err,ct){
|
||||
if (ct > 0) return;
|
||||
new Group({
|
||||
_id: 'habitrpg',
|
||||
chat: [],
|
||||
leader: '9',
|
||||
name: 'HabitRPG',
|
||||
type: 'guild',
|
||||
privacy:'public'
|
||||
}).save();
|
||||
})
|
||||
@@ -0,0 +1,104 @@
|
||||
// User.js
|
||||
// =======
|
||||
// Defines the user data model (schema) for use via the API.
|
||||
|
||||
// Dependencies
|
||||
// ------------
|
||||
var mongoose = require("mongoose");
|
||||
var Schema = mongoose.Schema;
|
||||
var shared = require('../../common');
|
||||
var _ = require('lodash');
|
||||
|
||||
// Task Schema
|
||||
// -----------
|
||||
|
||||
var TaskSchema = {
|
||||
//_id:{type: String,'default': helpers.uuid},
|
||||
id: {type: String,'default': shared.uuid},
|
||||
dateCreated: {type:Date, 'default':Date.now},
|
||||
text: String,
|
||||
notes: {type: String, 'default': ''},
|
||||
tags: {type: Schema.Types.Mixed, 'default': {}}, //{ "4ddf03d9-54bd-41a3-b011-ca1f1d2e9371" : true },
|
||||
value: {type: Number, 'default': 0}, // redness
|
||||
priority: {type: Number, 'default': '1'},
|
||||
attribute: {type: String, 'default': "str", enum: ['str','con','int','per']},
|
||||
challenge: {
|
||||
id: {type: 'String', ref:'Challenge'},
|
||||
broken: String, // CHALLENGE_DELETED, TASK_DELETED, UNSUBSCRIBED, CHALLENGE_CLOSED
|
||||
winner: String // user.profile.name
|
||||
// group: {type: 'Strign', ref: 'Group'} // if we restore this, rename `id` above to `challenge`
|
||||
}
|
||||
};
|
||||
|
||||
var HabitSchema = new Schema(
|
||||
_.defaults({
|
||||
type: {type:String, 'default': 'habit'},
|
||||
history: Array, // [{date:Date, value:Number}], // this causes major performance problems
|
||||
up: {type: Boolean, 'default': true},
|
||||
down: {type: Boolean, 'default': true}
|
||||
}, TaskSchema)
|
||||
, { _id: false, minimize:false }
|
||||
);
|
||||
|
||||
var collapseChecklist = {type:Boolean, 'default':false};
|
||||
var checklist = [{
|
||||
completed:{type:Boolean,'default':false},
|
||||
text: String,
|
||||
_id:false,
|
||||
id: {type:String,'default':shared.uuid}
|
||||
}];
|
||||
|
||||
var DailySchema = new Schema(
|
||||
_.defaults({
|
||||
type: {type:String, 'default': 'daily'},
|
||||
history: Array,
|
||||
completed: {type: Boolean, 'default': false},
|
||||
repeat: {
|
||||
m: {type: Boolean, 'default': true},
|
||||
t: {type: Boolean, 'default': true},
|
||||
w: {type: Boolean, 'default': true},
|
||||
th: {type: Boolean, 'default': true},
|
||||
f: {type: Boolean, 'default': true},
|
||||
s: {type: Boolean, 'default': true},
|
||||
su: {type: Boolean, 'default': true}
|
||||
},
|
||||
collapseChecklist:collapseChecklist,
|
||||
checklist:checklist,
|
||||
streak: {type: Number, 'default': 0}
|
||||
}, TaskSchema)
|
||||
, { _id: false, minimize:false }
|
||||
)
|
||||
|
||||
var TodoSchema = new Schema(
|
||||
_.defaults({
|
||||
type: {type:String, 'default': 'todo'},
|
||||
completed: {type: Boolean, 'default': false},
|
||||
dateCompleted: Date,
|
||||
date: String, // due date for todos // FIXME we're getting parse errors, people have stored as "today" and "3/13". Need to run a migration & put this back to type: Date
|
||||
collapseChecklist:collapseChecklist,
|
||||
checklist:checklist
|
||||
}, TaskSchema)
|
||||
, { _id: false, minimize:false }
|
||||
);
|
||||
|
||||
var RewardSchema = new Schema(
|
||||
_.defaults({
|
||||
type: {type:String, 'default': 'reward'}
|
||||
}, TaskSchema)
|
||||
, { _id: false, minimize:false }
|
||||
);
|
||||
|
||||
/**
|
||||
* Workaround for bug when _id & id were out of sync, we can remove this after challenges has been running for a while
|
||||
*/
|
||||
//_.each([HabitSchema, DailySchema, TodoSchema, RewardSchema], function(schema){
|
||||
// schema.post('init', function(doc){
|
||||
// if (!doc.id && doc._id) doc.id = doc._id;
|
||||
// })
|
||||
//})
|
||||
|
||||
module.exports.TaskSchema = TaskSchema;
|
||||
module.exports.HabitSchema = HabitSchema;
|
||||
module.exports.DailySchema = DailySchema;
|
||||
module.exports.TodoSchema = TodoSchema;
|
||||
module.exports.RewardSchema = RewardSchema;
|
||||
@@ -0,0 +1,518 @@
|
||||
// User.js
|
||||
// =======
|
||||
// Defines the user data model (schema) for use via the API.
|
||||
|
||||
// Dependencies
|
||||
// ------------
|
||||
var mongoose = require("mongoose");
|
||||
var Schema = mongoose.Schema;
|
||||
var shared = require('../../common');
|
||||
var _ = require('lodash');
|
||||
var TaskSchemas = require('./task');
|
||||
var Challenge = require('./challenge').model;
|
||||
var moment = require('moment');
|
||||
|
||||
// User Schema
|
||||
// -----------
|
||||
|
||||
var UserSchema = new Schema({
|
||||
// ### UUID and API Token
|
||||
_id: {
|
||||
type: String,
|
||||
'default': shared.uuid
|
||||
},
|
||||
apiToken: {
|
||||
type: String,
|
||||
'default': shared.uuid
|
||||
},
|
||||
|
||||
// ### Mongoose Update Object
|
||||
// We want to know *every* time an object updates. Mongoose uses __v to designate when an object contains arrays which
|
||||
// have been updated (http://goo.gl/gQLz41), but we want *every* update
|
||||
_v: { type: Number, 'default': 0 },
|
||||
achievements: {
|
||||
originalUser: Boolean,
|
||||
helpedHabit: Boolean,
|
||||
ultimateGear: Boolean,
|
||||
beastMaster: Boolean,
|
||||
beastMasterCount: Number,
|
||||
mountMaster: Boolean,
|
||||
mountMasterCount: Number,
|
||||
triadBingo: Boolean,
|
||||
triadBingoCount: Number,
|
||||
veteran: Boolean,
|
||||
snowball: Number,
|
||||
spookDust: Number,
|
||||
streak: Number,
|
||||
challenges: Array,
|
||||
quests: Schema.Types.Mixed,
|
||||
rebirths: Number,
|
||||
rebirthLevel: Number,
|
||||
perfect: Number,
|
||||
habitBirthday: Boolean, // TODO: Deprecate this. Superseded by habitBirthdays
|
||||
habitBirthdays: Number,
|
||||
valentine: Number,
|
||||
costumeContest: Boolean,
|
||||
nye: Number
|
||||
},
|
||||
auth: {
|
||||
blocked: Boolean,
|
||||
facebook: Schema.Types.Mixed,
|
||||
local: {
|
||||
email: String,
|
||||
hashed_password: String,
|
||||
salt: String,
|
||||
username: String
|
||||
},
|
||||
timestamps: {
|
||||
created: {type: Date,'default': Date.now},
|
||||
loggedin: {type: Date,'default': Date.now}
|
||||
}
|
||||
},
|
||||
|
||||
backer: {
|
||||
tier: Number,
|
||||
npc: String,
|
||||
tokensApplied: Boolean
|
||||
},
|
||||
|
||||
contributor: {
|
||||
level: Number, // 1-9, see https://trello.com/c/wkFzONhE/277-contributor-gear https://github.com/HabitRPG/habitrpg/issues/3801
|
||||
admin: Boolean,
|
||||
sudo: Boolean,
|
||||
text: String, // Artisan, Friend, Blacksmith, etc
|
||||
contributions: String, // a markdown textarea to list their contributions + links
|
||||
critical: String
|
||||
},
|
||||
|
||||
balance: {type: Number, 'default':0},
|
||||
filters: {type: Schema.Types.Mixed, 'default': {}},
|
||||
|
||||
purchased: {
|
||||
ads: {type: Boolean, 'default': false},
|
||||
skin: {type: Schema.Types.Mixed, 'default': {}}, // eg, {skeleton: true, pumpkin: true, eb052b: true}
|
||||
hair: {type: Schema.Types.Mixed, 'default': {}},
|
||||
shirt: {type: Schema.Types.Mixed, 'default': {}},
|
||||
background: {type: Schema.Types.Mixed, 'default': {}},
|
||||
txnCount: {type: Number, 'default':0},
|
||||
mobileChat: Boolean,
|
||||
plan: {
|
||||
planId: String,
|
||||
paymentMethod: String, //enum: ['Paypal','Stripe', 'Gift', '']}
|
||||
customerId: String,
|
||||
dateCreated: Date,
|
||||
dateTerminated: Date,
|
||||
dateUpdated: Date,
|
||||
extraMonths: {type:Number, 'default':0},
|
||||
gemsBought: {type: Number, 'default': 0},
|
||||
mysteryItems: {type: Array, 'default': []},
|
||||
consecutive: {
|
||||
count: {type:Number, 'default':0},
|
||||
offset: {type:Number, 'default':0}, // when gifted subs, offset++ for each month. offset-- each new-month (cron). count doesn't ++ until offset==0
|
||||
gemCapExtra: {type:Number, 'default':0},
|
||||
trinkets: {type:Number, 'default':0}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
flags: {
|
||||
customizationsNotification: {type: Boolean, 'default': false},
|
||||
showTour: {type: Boolean, 'default': true},
|
||||
dropsEnabled: {type: Boolean, 'default': false},
|
||||
itemsEnabled: {type: Boolean, 'default': false},
|
||||
newStuff: {type: Boolean, 'default': false},
|
||||
rewrite: {type: Boolean, 'default': true},
|
||||
partyEnabled: Boolean, // FIXME do we need this?
|
||||
contributor: Boolean,
|
||||
classSelected: {type: Boolean, 'default': false},
|
||||
mathUpdates: Boolean,
|
||||
rebirthEnabled: {type: Boolean, 'default': false},
|
||||
freeRebirth: {type: Boolean, 'default': false},
|
||||
levelDrops: {type:Schema.Types.Mixed, 'default':{}},
|
||||
chatRevoked: Boolean,
|
||||
// Used to track the status of recapture emails sent to each user,
|
||||
// can be 0 - no email sent - 1, 2, 3 or 4 - 4 means no more email will be sent to the user
|
||||
recaptureEmailsPhase: {type: Number, 'default': 0},
|
||||
communityGuidelinesAccepted: {type: Boolean, 'default': false}
|
||||
},
|
||||
history: {
|
||||
exp: Array, // [{date: Date, value: Number}], // big peformance issues if these are defined
|
||||
todos: Array //[{data: Date, value: Number}] // big peformance issues if these are defined
|
||||
},
|
||||
|
||||
// FIXME remove?
|
||||
invitations: {
|
||||
guilds: {type: Array, 'default': []},
|
||||
party: Schema.Types.Mixed
|
||||
},
|
||||
items: {
|
||||
gear: {
|
||||
owned: _.transform(shared.content.gear.flat, function(m,v,k){
|
||||
m[v.key] = {type: Boolean};
|
||||
if (v.key.match(/[weapon|armor|head|shield]_warrior_0/))
|
||||
m[v.key]['default'] = true;
|
||||
}),
|
||||
|
||||
equipped: {
|
||||
weapon: {type: String, 'default': 'weapon_warrior_0'},
|
||||
armor: {type: String, 'default': 'armor_base_0'},
|
||||
head: {type: String, 'default': 'head_base_0'},
|
||||
shield: {type: String, 'default': 'shield_base_0'},
|
||||
back: String,
|
||||
headAccessory: String,
|
||||
eyewear: String,
|
||||
body: String
|
||||
},
|
||||
costume: {
|
||||
weapon: {type: String, 'default': 'weapon_base_0'},
|
||||
armor: {type: String, 'default': 'armor_base_0'},
|
||||
head: {type: String, 'default': 'head_base_0'},
|
||||
shield: {type: String, 'default': 'shield_base_0'},
|
||||
back: String,
|
||||
headAccessory: String,
|
||||
eyewear: String,
|
||||
body: String
|
||||
},
|
||||
},
|
||||
|
||||
special:{
|
||||
snowball: {type: Number, 'default': 0},
|
||||
spookDust: {type: Number, 'default': 0},
|
||||
valentine: Number,
|
||||
valentineReceived: Array, // array of strings, by sender name
|
||||
nye: Number,
|
||||
nyeReceived: Array
|
||||
},
|
||||
|
||||
// -------------- Animals -------------------
|
||||
// Complex bit here. The result looks like:
|
||||
// pets: {
|
||||
// 'Wolf-Desert': 0, // 0 means does not own
|
||||
// 'PandaCub-Red': 10, // Number represents "Growth Points"
|
||||
// etc...
|
||||
// }
|
||||
pets:
|
||||
_.defaults(
|
||||
// First transform to a 1D eggs/potions mapping
|
||||
_.transform(shared.content.pets, function(m,v,k){ m[k] = Number; }),
|
||||
// Then add quest pets
|
||||
_.transform(shared.content.questPets, function(m,v,k){ m[k] = Number; }),
|
||||
// Then add additional pets (backer, contributor)
|
||||
_.transform(shared.content.specialPets, function(m,v,k){ m[k] = Number; })
|
||||
),
|
||||
currentPet: String, // Cactus-Desert
|
||||
|
||||
// eggs: {
|
||||
// 'PandaCub': 0, // 0 indicates "doesn't own"
|
||||
// 'Wolf': 5 // Number indicates "stacking"
|
||||
// }
|
||||
eggs: _.transform(shared.content.eggs, function(m,v,k){ m[k] = Number; }),
|
||||
|
||||
// hatchingPotions: {
|
||||
// 'Desert': 0, // 0 indicates "doesn't own"
|
||||
// 'CottonCandyBlue': 5 // Number indicates "stacking"
|
||||
// }
|
||||
hatchingPotions: _.transform(shared.content.hatchingPotions, function(m,v,k){ m[k] = Number; }),
|
||||
|
||||
// Food: {
|
||||
// 'Watermelon': 0, // 0 indicates "doesn't own"
|
||||
// 'RottenMeat': 5 // Number indicates "stacking"
|
||||
// }
|
||||
food: _.transform(shared.content.food, function(m,v,k){ m[k] = Number; }),
|
||||
|
||||
// mounts: {
|
||||
// 'Wolf-Desert': true,
|
||||
// 'PandaCub-Red': false,
|
||||
// etc...
|
||||
// }
|
||||
mounts: _.defaults(
|
||||
// First transform to a 1D eggs/potions mapping
|
||||
_.transform(shared.content.pets, function(m,v,k){ m[k] = Boolean; }),
|
||||
// Then add quest pets
|
||||
_.transform(shared.content.questPets, function(m,v,k){ m[k] = Boolean; }),
|
||||
// Then add additional pets (backer, contributor)
|
||||
_.transform(shared.content.specialMounts, function(m,v,k){ m[k] = Boolean; })
|
||||
),
|
||||
currentMount: String,
|
||||
|
||||
// Quests: {
|
||||
// 'boss_0': 0, // 0 indicates "doesn't own"
|
||||
// 'collection_honey': 5 // Number indicates "stacking"
|
||||
// }
|
||||
quests: _.transform(shared.content.quests, function(m,v,k){ m[k] = Number; }),
|
||||
|
||||
lastDrop: {
|
||||
date: {type: Date, 'default': Date.now},
|
||||
count: {type: Number, 'default': 0}
|
||||
}
|
||||
},
|
||||
|
||||
lastCron: {type: Date, 'default': Date.now},
|
||||
|
||||
// {GROUP_ID: Boolean}, represents whether they have unseen chat messages
|
||||
newMessages: {type: Schema.Types.Mixed, 'default': {}},
|
||||
|
||||
party: {
|
||||
// id // FIXME can we use a populated doc instead of fetching party separate from user?
|
||||
order: {type:String, 'default':'level'},
|
||||
orderAscending: {type:String, 'default':'ascending'},
|
||||
quest: {
|
||||
key: String,
|
||||
progress: {
|
||||
up: {type: Number, 'default': 0},
|
||||
down: {type: Number, 'default': 0},
|
||||
collect: {type: Schema.Types.Mixed, 'default': {}} // {feather:1, ingot:2}
|
||||
},
|
||||
completed: String // When quest is done, we move it from key => completed, and it's a one-time flag (for modal) that they unset by clicking "ok" in browser
|
||||
}
|
||||
},
|
||||
preferences: {
|
||||
armorSet: String,
|
||||
dayStart: {type:Number, 'default': 0, min: 0, max: 23},
|
||||
size: {type:String, enum: ['broad','slim'], 'default': 'slim'},
|
||||
hair: {
|
||||
color: {type: String, 'default': 'red'},
|
||||
base: {type: Number, 'default': 3},
|
||||
bangs: {type: Number, 'default': 1},
|
||||
beard: {type: Number, 'default': 0},
|
||||
mustache: {type: Number, 'default': 0},
|
||||
flower: {type: Number, 'default': 1}
|
||||
},
|
||||
hideHeader: {type:Boolean, 'default':false},
|
||||
skin: {type:String, 'default':'915533'},
|
||||
shirt: {type: String, 'default': 'blue'},
|
||||
timezoneOffset: Number,
|
||||
sound: {type:String, 'default':'off', enum: ['off','danielTheBard', 'wattsTheme']},
|
||||
language: String,
|
||||
automaticAllocation: Boolean,
|
||||
allocationMode: {type:String, enum: ['flat','classbased','taskbased'], 'default': 'flat'},
|
||||
costume: Boolean,
|
||||
dateFormat: {type: String, enum:['MM/dd/yyyy', 'dd/MM/yyyy', 'yyyy/MM/dd'], 'default': 'MM/dd/yyyy'},
|
||||
sleep: {type: Boolean, 'default': false},
|
||||
stickyHeader: {type: Boolean, 'default': true},
|
||||
disableClasses: {type: Boolean, 'default': false},
|
||||
newTaskEdit: {type: Boolean, 'default': false},
|
||||
dailyDueDefaultView: {type: Boolean, 'default': false},
|
||||
tagsCollapsed: {type: Boolean, 'default': false},
|
||||
advancedCollapsed: {type: Boolean, 'default': false},
|
||||
toolbarCollapsed: {type:Boolean, 'default':false},
|
||||
background: String,
|
||||
webhooks: {type: Schema.Types.Mixed, 'default': {}}
|
||||
},
|
||||
profile: {
|
||||
blurb: String,
|
||||
imageUrl: String,
|
||||
name: String,
|
||||
},
|
||||
stats: {
|
||||
hp: {type: Number, 'default': 50},
|
||||
mp: {type: Number, 'default': 10},
|
||||
exp: {type: Number, 'default': 0},
|
||||
gp: {type: Number, 'default': 0},
|
||||
lvl: {type: Number, 'default': 1},
|
||||
|
||||
// Class System
|
||||
'class': {type: String, enum: ['warrior','rogue','wizard','healer'], 'default': 'warrior'},
|
||||
points: {type: Number, 'default': 0},
|
||||
str: {type: Number, 'default': 0},
|
||||
con: {type: Number, 'default': 0},
|
||||
int: {type: Number, 'default': 0},
|
||||
per: {type: Number, 'default': 0},
|
||||
buffs: {
|
||||
str: {type: Number, 'default': 0},
|
||||
int: {type: Number, 'default': 0},
|
||||
per: {type: Number, 'default': 0},
|
||||
con: {type: Number, 'default': 0},
|
||||
stealth: {type: Number, 'default': 0},
|
||||
streaks: {type: Boolean, 'default': false},
|
||||
snowball: {type: Boolean, 'default': false},
|
||||
spookDust: {type: Boolean, 'default': false}
|
||||
},
|
||||
training: {
|
||||
int: {type: Number, 'default': 0},
|
||||
per: {type: Number, 'default': 0},
|
||||
str: {type: Number, 'default': 0},
|
||||
con: {type: Number, 'default': 0}
|
||||
}
|
||||
},
|
||||
|
||||
tags: {type: [{
|
||||
_id: false,
|
||||
id: { type: String, 'default': shared.uuid },
|
||||
name: String,
|
||||
challenge: String
|
||||
}]},
|
||||
|
||||
challenges: [{type: 'String', ref:'Challenge'}],
|
||||
|
||||
inbox: {
|
||||
newMessages: {type:Number, 'default':0},
|
||||
blocks: {type:Array, 'default':[]},
|
||||
messages: {type:Schema.Types.Mixed, 'default':{}}, //reflist
|
||||
optOut: {type:Boolean, 'default':false}
|
||||
},
|
||||
|
||||
habits: {type:[TaskSchemas.HabitSchema]},
|
||||
dailys: {type:[TaskSchemas.DailySchema]},
|
||||
todos: {type:[TaskSchemas.TodoSchema]},
|
||||
rewards: {type:[TaskSchemas.RewardSchema]},
|
||||
|
||||
extra: Schema.Types.Mixed
|
||||
|
||||
}, {
|
||||
strict: true,
|
||||
minimize: false // So empty objects are returned
|
||||
});
|
||||
|
||||
UserSchema.methods.deleteTask = function(tid) {
|
||||
this.ops.deleteTask({params:{id:tid}},function(){}); // TODO remove this whole method, since it just proxies, and change all references to this method
|
||||
}
|
||||
|
||||
UserSchema.methods.toJSON = function() {
|
||||
var doc = this.toObject();
|
||||
doc.id = doc._id;
|
||||
|
||||
// FIXME? Is this a reference to `doc.filters` or just disabled code? Remove?
|
||||
doc.filters = {};
|
||||
doc._tmp = this._tmp; // be sure to send down drop notifs
|
||||
|
||||
return doc;
|
||||
};
|
||||
|
||||
//UserSchema.virtual('tasks').get(function () {
|
||||
// var tasks = this.habits.concat(this.dailys).concat(this.todos).concat(this.rewards);
|
||||
// var tasks = _.object(_.pluck(tasks,'id'), tasks);
|
||||
// return tasks;
|
||||
//});
|
||||
|
||||
UserSchema.post('init', function(doc){
|
||||
shared.wrap(doc);
|
||||
})
|
||||
|
||||
UserSchema.pre('save', function(next) {
|
||||
|
||||
// Populate new users with default content
|
||||
if (this.isNew){
|
||||
//TODO for some reason this doesn't work here: `_.merge(this, shared.content.userDefaults);`
|
||||
var self = this;
|
||||
_.each(['habits', 'dailys', 'todos', 'rewards', 'tags'], function(taskType){
|
||||
self[taskType] = _.map(shared.content.userDefaults[taskType], function(task){
|
||||
var newTask = _.cloneDeep(task);
|
||||
|
||||
// Render task's text and notes in user's language
|
||||
if(taskType === 'tags'){
|
||||
// tasks automatically get id=helpers.uuid() from TaskSchema id.default, but tags are Schema.Types.Mixed - so we need to manually invoke here
|
||||
newTask.id = shared.uuid();
|
||||
newTask.name = newTask.name(self.preferences.language);
|
||||
}else{
|
||||
newTask.text = newTask.text(self.preferences.language);
|
||||
newTask.notes = newTask.notes(self.preferences.language);
|
||||
|
||||
if(newTask.checklist){
|
||||
newTask.checklist = _.map(newTask.checklist, function(checklistItem){
|
||||
checklistItem.text = checklistItem.text(self.preferences.language);
|
||||
return checklistItem;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return newTask;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
//this.markModified('tasks');
|
||||
if (_.isNaN(this.preferences.dayStart) || this.preferences.dayStart < 0 || this.preferences.dayStart > 23) {
|
||||
this.preferences.dayStart = 0;
|
||||
}
|
||||
|
||||
if (!this.profile.name) {
|
||||
var fb = this.auth.facebook;
|
||||
this.profile.name =
|
||||
(this.auth.local && this.auth.local.username) ||
|
||||
(fb && (fb.displayName || fb.name || fb.username || (fb.first_name && fb.first_name + ' ' + fb.last_name))) ||
|
||||
'Anonymous';
|
||||
}
|
||||
|
||||
// Determines if Beast Master should be awarded
|
||||
var petCount = shared.countPets(_.reduce(this.items.pets,function(m,v){
|
||||
//HOTFIX - Remove when solution is found, the first argument passed to reduce is a function
|
||||
if(_.isFunction(v)) return m;
|
||||
return m+(v?1:0)},0), this.items.pets);
|
||||
|
||||
if (petCount >= 90 || this.achievements.beastMasterCount > 0) {
|
||||
this.achievements.beastMaster = true
|
||||
}
|
||||
|
||||
// Determines if Mount Master should be awarded
|
||||
var mountCount = shared.countMounts(_.reduce(this.items.mounts,function(m,v){
|
||||
//HOTFIX - Remove when solution is found, the first argument passed to reduce is a function
|
||||
if(_.isFunction(v)) return m;
|
||||
return m+(v?1:0)},0), this.items.mounts);
|
||||
|
||||
if (mountCount >= 90 || this.achievements.mountMasterCount > 0) {
|
||||
this.achievements.mountMaster = true
|
||||
}
|
||||
|
||||
// Determines if Triad Bingo should be awarded
|
||||
|
||||
var triadCount = shared.countTriad(this.items.pets);
|
||||
|
||||
if ((mountCount >= 90 && triadCount >= 90) || this.achievements.triadBingoCount > 0) {
|
||||
this.achievements.triadBingo = true;
|
||||
}
|
||||
|
||||
// EXAMPLE CODE for allowing all existing and new players to be
|
||||
// automatically granted an item during a certain time period:
|
||||
// if (!this.items.pets['JackOLantern-Base'] && moment().isBefore('2014-11-01'))
|
||||
// this.items.pets['JackOLantern-Base'] = 5;
|
||||
|
||||
//our own version incrementer
|
||||
if (_.isNaN(this._v) || !_.isNumber(this._v)) this._v = 0;
|
||||
this._v++;
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
UserSchema.methods.unlink = function(options, cb) {
|
||||
var cid = options.cid, keep = options.keep, tid = options.tid;
|
||||
var self = this;
|
||||
switch (keep) {
|
||||
case 'keep':
|
||||
self.tasks[tid].challenge = {};
|
||||
break;
|
||||
case 'remove':
|
||||
self.deleteTask(tid);
|
||||
break;
|
||||
case 'keep-all':
|
||||
_.each(self.tasks, function(t){
|
||||
if (t.challenge && t.challenge.id == cid) {
|
||||
t.challenge = {};
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'remove-all':
|
||||
_.each(self.tasks, function(t){
|
||||
if (t.challenge && t.challenge.id == cid) {
|
||||
self.deleteTask(t.id);
|
||||
}
|
||||
})
|
||||
break;
|
||||
}
|
||||
self.markModified('habits');
|
||||
self.markModified('dailys');
|
||||
self.markModified('todos');
|
||||
self.markModified('rewards');
|
||||
self.save(cb);
|
||||
}
|
||||
|
||||
module.exports.schema = UserSchema;
|
||||
module.exports.model = mongoose.model("User", UserSchema);
|
||||
|
||||
mongoose.model("User")
|
||||
.find({'contributor.admin':true})
|
||||
.sort('-contributor.level -backer.npc profile.name')
|
||||
.select('profile contributor backer')
|
||||
.exec(function(err,mods){
|
||||
module.exports.mods = mods
|
||||
});
|
||||
Reference in New Issue
Block a user