2013-08-31 21:47:03 +00:00
var mongoose = require ( "mongoose" ) ;
var Schema = mongoose . Schema ;
2013-12-11 16:30:39 +00:00
var shared = require ( 'habitrpg-shared' ) ;
2013-08-31 21:47:03 +00:00
var _ = require ( 'lodash' ) ;
2013-12-20 22:43:10 +00:00
var async = require ( 'async' ) ;
2013-12-23 19:43:53 +00:00
var User = require ( './user' ) . model ;
2013-08-29 05:45:19 +00:00
2013-08-31 21:47:03 +00:00
var GroupSchema = new Schema ( {
2013-12-11 16:30:39 +00:00
_id : { type : String , 'default' : shared . uuid } ,
2013-08-29 05:45:19 +00:00
name : String ,
description : String ,
2013-10-28 05:27:07 +00:00
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' ] } ,
2013-11-15 04:29:48 +00:00
//_v: {type: Number,'default': 0},
2013-08-29 05:45:19 +00:00
chat : Array ,
/ *
# [ {
# timestamp : Date
# user : String
# text : String
# contributor : String
# uuid : String
# id : String
# } ]
* /
2013-10-29 00:31:27 +00:00
memberCount : { type : Number , 'default' : 0 } ,
challengeCount : { type : Number , 'default' : 0 } ,
2013-08-29 05:45:19 +00:00
balance : Number ,
logo : String ,
2013-10-27 00:24:45 +00:00
leaderMessage : String ,
2013-12-20 19:42:36 +00:00
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 } ,
2013-12-28 22:29:29 +00:00
leader : { type : String , ref : 'User' } ,
2013-12-20 22:43:10 +00:00
progress : {
hp : Number ,
2013-12-24 17:25:09 +00:00
collect : { type : Schema . Types . Mixed , 'default' : { } } // {feather: 5, ingot: 3}
2013-12-20 22:43:10 +00:00
} ,
2013-12-20 19:42:36 +00:00
2013-12-20 22:43:10 +00:00
//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
2013-12-20 19:42:36 +00:00
members : Schema . Types . Mixed
}
2013-10-27 09:55:05 +00:00
} , {
2013-10-27 00:24:45 +00:00
strict : 'throw' ,
2013-10-27 09:55:43 +00:00
minimize : false // So empty objects are returned
2013-10-27 09:55:05 +00:00
} ) ;
2013-08-29 05:45:19 +00:00
2013-08-31 21:47:03 +00:00
/ * *
* 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 ;
}
}
}
2013-10-30 03:37:22 +00:00
// FIXME this isn't always triggered, since we sometimes use update() or findByIdAndUpdate()
// @see https://github.com/LearnBoost/mongoose/issues/964
2013-08-31 21:47:03 +00:00
GroupSchema . pre ( 'save' , function ( next ) {
removeDuplicates ( this ) ;
2013-10-29 00:31:27 +00:00
this . memberCount = _ . size ( this . members ) ;
this . challengeCount = _ . size ( this . challenges ) ;
2013-08-31 21:47:03 +00:00
next ( ) ;
} )
GroupSchema . methods . toJSON = function ( ) {
var doc = this . toObject ( ) ;
removeDuplicates ( doc ) ;
2013-10-29 19:26:13 +00:00
doc . _isMember = this . _isMember ;
2013-10-30 03:37:22 +00:00
// @see pre('save') comment above
this . memberCount = _ . size ( this . members ) ;
this . challengeCount = _ . size ( this . challenges ) ;
2013-08-31 21:47:03 +00:00
return doc ;
}
2013-12-20 22:43:10 +00:00
GroupSchema . methods . sendChat = function ( message , user ) {
var group = this ;
var message = {
id : shared . uuid ( ) ,
text : message ,
2014-01-06 05:10:34 +00:00
timestamp : + ( new Date ) ,
likes : { }
2013-12-20 22:43:10 +00:00
} ;
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' ;
}
group . chat . unshift ( message ) ;
group . chat . splice ( 200 ) ;
}
2013-12-28 22:29:29 +00:00
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 ;
2013-12-24 17:25:09 +00:00
// Participants: Grant rewards & achievements, finish quest
GroupSchema . methods . finishQuest = function ( quest , cb ) {
2013-12-20 22:43:10 +00:00
var group = this ;
2013-12-24 17:25:09 +00:00
var questK = quest . key ;
var updates = { $inc : { } , $set : { } } ;
2013-12-25 06:30:48 +00:00
updates [ '$inc' ] [ 'achievements.quests.' + questK ] = 1 ;
2013-12-24 17:25:09 +00:00
updates [ '$inc' ] [ 'stats.gp' ] = + quest . drop . gp ;
updates [ '$inc' ] [ 'stats.exp' ] = + quest . drop . exp ;
updates [ '$inc' ] [ '_v' ] = 1 ;
2013-12-28 22:29:29 +00:00
updates [ '$set' ] [ 'party.quest' ] = cleanQuestProgress ( { completed : questK } ) ;
2013-12-24 17:25:09 +00:00
2014-01-21 21:13:15 +00:00
_ . 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' :
updates [ '$inc' ] [ 'items.' + quest . drop . type + '.' + dropK ] = 1 ;
break ;
case 'pets' :
updates [ '$set' ] [ 'items.pets.' + dropK ] = 5 ;
break ;
case 'mounts' :
updates [ '$set' ] [ 'items.mounts.' + dropK ] = true ;
break ;
}
} )
2013-12-24 17:25:09 +00:00
var members = _ . keys ( group . quest . members ) ;
group . quest = { } ; group . markModified ( 'quest' ) ;
2013-12-28 22:29:29 +00:00
// FIXME this is TERRIBLE practice. Looks like there are circular dependencies in the models, such that `var User` at
// this point is undefined. So we get around that by loading from mongoose only once we get to this point
2013-12-24 17:25:09 +00:00
mongoose . models . User . update ( { _id : { $in : members } } , updates , { multi : true } , cb ) ;
}
2013-12-26 22:39:09 +00:00
// FIXME this is a temporary measure, we need to remove quests from users when they traverse parties
function isOnQuest ( user , group ) {
return group && user . party . quest . key && user . party . quest . key == group . quest . key ;
2013-12-24 17:25:09 +00:00
}
2013-12-26 22:39:09 +00:00
GroupSchema . statics . collectQuest = function ( user , progress , cb ) {
this . findOne ( { type : 'party' , members : { '$in' : [ user . _id ] } } , function ( err , group ) {
if ( ! isOnQuest ( user , group ) ) return cb ( null ) ;
var quest = shared . content . quests [ group . quest . key ] ;
_ . each ( progress . collect , function ( v , k ) {
group . quest . progress . collect [ k ] += v ;
2013-12-24 17:25:09 +00:00
} ) ;
2013-12-23 19:43:53 +00:00
2013-12-26 22:39:09 +00:00
var foundText = _ . reduce ( progress . collect , function ( m , v , k ) {
m . push ( v + ' ' + quest . collect [ k ] . text ) ;
return m ;
} , [ ] ) ;
foundText = foundText ? foundText . join ( ', ' ) : 'nothing' ;
2013-12-30 14:46:40 +00:00
group . sendChat ( "`" + user . profile . name + " found " + foundText + ".`" ) ;
2013-12-26 22:39:09 +00:00
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 ) ;
} )
}
GroupSchema . statics . bossQuest = function ( user , progress , cb ) {
this . findOne ( { type : 'party' , members : { '$in' : [ user . _id ] } } , function ( err , group ) {
if ( ! isOnQuest ( user , 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 ;
2013-12-30 14:46:40 +00:00
group . sendChat ( "`" + user . profile . name + " attacks " + quest . boss . name + " for " + ( progress . up . toFixed ( 1 ) ) + " damage, " + quest . boss . name + " attacks party for " + Math . abs ( down ) . toFixed ( 1 ) + " damage.`" ) ;
2013-12-26 22:39:09 +00:00
// 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 ) {
2014-01-14 04:33:01 +00:00
group . sendChat ( '`You defeated ' + quest . boss . name + '! Questing party members receive the rewards of victory.`' ) ;
2013-12-26 22:39:09 +00:00
// 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 ) ;
} )
2013-12-20 22:43:10 +00:00
}
2013-08-31 21:47:03 +00:00
module . exports . schema = GroupSchema ;
2013-11-14 04:20:33 +00:00
module . exports . model = mongoose . model ( "Group" , GroupSchema ) ;