From 0e163034cf511f521c39dadd481422505889a02b Mon Sep 17 00:00:00 2001 From: Alice Harris Date: Thu, 4 Sep 2014 10:13:22 +1000 Subject: [PATCH 1/5] semi-random tentative crappy code for improving quest aborting, especially when the quest owner has left the party --- src/controllers/groups.js | 20 ++++++++++++++++++-- views/options/social/boss.jade | 19 ++++++++++++++----- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/controllers/groups.js b/src/controllers/groups.js index c126373484..6fd86bdd90 100644 --- a/src/controllers/groups.js +++ b/src/controllers/groups.js @@ -571,13 +571,13 @@ api.questAccept = function(req, res, next) { var user = res.locals.user; var key = req.query.key; - if (!group) return res.json(400, {err: "Must be in a party to start quests (this will change in the future)."}); + if (!group) return res.json(400, {err: "Must be in a party to start quests."}); // If ?key=xxx is provided, we're starting a new quest and inviting the party. Otherwise, we're a party member accepting the invitation if (key) { var quest = shared.content.quests[key]; if (!quest) return res.json(404,{err:'Quest ' + key + ' not found'}); - if (quest.lvl && user.stats.lvl < quest.lvl) return res.json(400, {err: "You must be level "+quest.lvl+" to being this quest."}); + if (quest.lvl && user.stats.lvl < quest.lvl) return res.json(400, {err: "You must be level "+quest.lvl+" to begin this quest."}); if (group.quest.key) return res.json(400, {err: 'Party already on a quest (and only have one quest at a time)'}); if (!user.items.quests[key]) return res.json(400, {err: "You don't own that quest scroll"}); group.quest.key = key; @@ -609,8 +609,23 @@ api.questReject = function(req, res, next) { questStart(req,res,next); } +api.questCancel = function(req, res, next){ + // Cancel a quest BEFORE it has begun (i.e., in the invitation stage) + var group = res.locals.group; + async.parallel([ + function(cb){ + group.quest = {key:null,progress:{},leader:null}; + group.markModified('quest'); + group.save(cb); + } + ], function(err){ + if (err) return next(err); + res.json(group); + }) +} api.questAbort = function(req, res, next){ + // Abort a quest AFTER it has begun (see questCancel for BEFORE) var group = res.locals.group; async.parallel([ function(cb){ @@ -639,3 +654,4 @@ api.questAbort = function(req, res, next){ res.json(group); }) } + diff --git a/views/options/social/boss.jade b/views/options/social/boss.jade index c051d9ee49..837deff14d 100644 --- a/views/options/social/boss.jade +++ b/views/options/social/boss.jade @@ -9,16 +9,23 @@ mixin boss(tavern, mobile) table.table.table-striped tr(ng-repeat='member in group.members') td - span(ng-if=':: group.quest.leader==member._id') + span(ng-if=':: group.quest.leader && group.quest.leader==member._id') |*  {{::member.profile.name}} td {{group.quest.members[member._id] === true ? env.t('accepted') : group.quest.members[member._id] === false ? env.t('rejected') : env.t('pending')}} + //span(ng-if=':: group.quest.leader && group.members.indexOf(group.quest.leader) > -1') |*  =env.t('questOwner') + br + span(ng-if=':: !group.quest.leader') no quest leader + br + span(ng-if=':: group.members.indexOf(group.quest.leader) == -1') not in group + // span(ng-if=':: !group.quest.leader || group.members.indexOf(group.quest.leader) == -1')=env.t('questOwnerMIA') hr .npc_ian.pull-left p=env.t('questStart') - button.btn.btn-sm.btn-warning(ng-if=':: !group.quest.leader || group.quest.leader==user._id', ng-click='party.$questAccept({"force":true})')=env.t('begin') + button.btn.btn-sm.btn-warning(ng-if=':: !group.quest.leader || group.members.indexOf(group.quest.leader) == -1 || group.quest.leader==user._id', ng-click='party.$questAccept({"force":true})')=env.t('begin') + //button.btn.btn-sm.btn-warning(ng-if=':: !group.quest.leader || group.members.indexOf(group.quest.leader) == -1 || group.quest.leader==user._id', ng-click='party.$questCancel()')=env.t('cancel') //-TODO Cancel button //-TODO Both force-start & cancel should only be available to quest-initiator @@ -65,8 +72,10 @@ mixin boss(tavern, mobile) span(ng-if=':: group.quest.leader==v._id') |*  {{::v.profile.name}} - |*  - =env.t('questOwner') + // span(ng-if=':: group.quest.leader && group.members.indexOf(group.quest.leader) > -1') + // |*  + // =env.t('questOwner') + // span(ng-if=':: !group.quest.leader || group.members.indexOf(group.quest.leader) == -1')=env.t('questOwnerMIA') quest-rewards(key='{{::group.quest.key}}') div(ng-if='::Content.quests[group.quest.key].boss') @@ -86,4 +95,4 @@ mixin boss(tavern, mobile) unless tavern - button.btn.btn-xs.btn-danger(ng-if='::!group.quest.leader || group.quest.leader==user._id', ng-click='questAbort()')=env.t('abort') + button.btn.btn-xs.btn-danger(ng-if='::!group.quest.leader || group.members.indexOf(group.quest.leader) == -1 || group.quest.leader==user._id', ng-click='questAbort()')=env.t('abort') From f520b8ae6625b77a042296727d1de2572e776b36 Mon Sep 17 00:00:00 2001 From: Alice Harris Date: Mon, 8 Sep 2014 18:45:04 +1000 Subject: [PATCH 2/5] add extra Debug tools useful for quests --- public/js/controllers/footerCtrl.js | 22 ++++++++++++++++++---- views/shared/footer.jade | 5 ++++- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/public/js/controllers/footerCtrl.js b/public/js/controllers/footerCtrl.js index 72a763df7b..30ec72c48e 100644 --- a/public/js/controllers/footerCtrl.js +++ b/public/js/controllers/footerCtrl.js @@ -54,6 +54,11 @@ * Debug functions. Note that the server route for gems is only available if process.env.DEBUG=true */ if (_.contains(['development','test'],window.env.NODE_ENV)) { + $scope.setHealthLow = function(){ + User.set({ + 'stats.hp': 1 + }); + } $scope.addMissedDay = function(){ if (!confirm("Are you sure you want to reset the day?")) return; var dayBefore = moment(User.user.lastCron).subtract('days', 1).toDate(); @@ -65,11 +70,16 @@ User.log({}); }) } + $scope.addGold = function(){ + User.set({ + 'stats.gp': User.user.stats.gp + 500, + }); + } $scope.addLevelsAndGold = function(){ User.set({ 'stats.exp': User.user.stats.exp + 10000, - 'stats.gp': User.user.stats.gp + 10000, - 'stats.mp': User.user.stats.mp + 10000 + 'stats.gp': User.user.stats.gp + 10000, + 'stats.mp': User.user.stats.mp + 10000 }); } $scope.addOneLevel = function(){ @@ -77,6 +87,10 @@ 'stats.exp': User.user.stats.exp + (Math.round(((Math.pow(User.user.stats.lvl, 2) * 0.25) + (10 * User.user.stats.lvl) + 139.75) / 10) * 10) }); } + $scope.addBossQuestProgressUp = function(){ + User.set({ + 'party.quest.progress.up': User.user.party.quest.progress.up + 1000 + }); + } } - - }]) \ No newline at end of file + }]) diff --git a/views/shared/footer.jade b/views/shared/footer.jade index 69e590cbec..56c32a694b 100644 --- a/views/shared/footer.jade +++ b/views/shared/footer.jade @@ -74,10 +74,13 @@ footer.footer(ng-controller='FooterCtrl') else if (env.NODE_ENV==='development' || env.NODE_ENV==='test') && !env.isStaticPage h4 Debug .btn-group-vertical + a.btn.btn-default(ng-click='setHealthLow()') Health = 1 a.btn.btn-default(ng-click='addMissedDay()') +1 Missed Day a.btn.btn-default(ng-click='addTenGems()') +10 Gems - a.btn.btn-default(ng-click='addLevelsAndGold()') +Exp +GP + a.btn.btn-default(ng-click='addGold()') +GP + a.btn.btn-default(ng-click='addLevelsAndGold()') +Exp +GP +MP a.btn.btn-default(ng-click='addOneLevel()') +1 Level + a.btn.btn-default(ng-click='addBossQuestProgressUp()') +1000 Boss Quest Progress Up div(ng-init='deferredScripts()') From 801753b37aa5dfed428d73ca3c4e01411a826391 Mon Sep 17 00:00:00 2001 From: Alice Harris Date: Mon, 8 Sep 2014 18:46:11 +1000 Subject: [PATCH 3/5] add Cancel option to API for quests that have not been started (invitation stage) --- public/js/services/groupServices.js | 1 + src/routes/apiv2.coffee | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/public/js/services/groupServices.js b/public/js/services/groupServices.js index 85214eba79..cabb68fa9f 100644 --- a/public/js/services/groupServices.js +++ b/public/js/services/groupServices.js @@ -19,6 +19,7 @@ angular.module('groupServices', ['ngResource']). removeMember: {method: "POST", url: API_URL + '/api/v2/groups/:gid/removeMember'}, questAccept: {method: "POST", url: API_URL + '/api/v2/groups/:gid/questAccept'}, questReject: {method: "POST", url: API_URL + '/api/v2/groups/:gid/questReject'}, + questCancel: {method: "POST", url: API_URL + '/api/v2/groups/:gid/questCancel'}, questAbort: {method: "POST", url: API_URL + '/api/v2/groups/:gid/questAbort'} }); diff --git a/src/routes/apiv2.coffee b/src/routes/apiv2.coffee index 805debb89f..edacbdc243 100644 --- a/src/routes/apiv2.coffee +++ b/src/routes/apiv2.coffee @@ -465,10 +465,18 @@ module.exports = (swagger, v2) -> middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup] action: groups.questReject + "/groups/{gid}/questCancel": + spec: + method: 'POST' + description: 'Cancel quest before it starts (in invitation stage)' + parameters: [path('gid','Group to cancel quest in','string')] + middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup] + action: groups.questCancel + "/groups/{gid}/questAbort": spec: method: 'POST' - description: 'Abort quest' + description: 'Abort quest after it has started (all progress will be lost)' parameters: [path('gid','Group to abort quest in','string')] middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup] action: groups.questAbort From b5ac0c635ae3ad38f58db1ac10aefdfbc32d9913 Mon Sep 17 00:00:00 2001 From: Alice Harris Date: Mon, 8 Sep 2014 18:46:39 +1000 Subject: [PATCH 4/5] improve new code for cancelling quests in invitation stage and for handling the cases where the quest owner is no longer in the quest or in the party --- public/js/controllers/groupsCtrl.js | 24 +++++++++++++++ src/controllers/groups.js | 24 +++++++++++---- views/options/social/boss.jade | 46 ++++++++++++++++------------- 3 files changed, 69 insertions(+), 25 deletions(-) diff --git a/public/js/controllers/groupsCtrl.js b/public/js/controllers/groupsCtrl.js index ca062e95e8..8d723ce055 100644 --- a/public/js/controllers/groupsCtrl.js +++ b/public/js/controllers/groupsCtrl.js @@ -3,6 +3,24 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '$http', 'API_URL', '$q', 'User', 'Members', '$state', function($scope, $rootScope, Shared, Groups, $http, API_URL, $q, User, Members, $state) { + $scope.isMemberOfPendingQuest = function(userid, group){ + if (!group.quest || !group.quest.members) return false; + if (group.quest.active) return false; // quest is started, not pending + return userid in group.quest.members && group.quest.members[userid] != false; + } + + $scope.isMemberOfRunningQuest = function(userid, group){ + if (!group.quest || !group.quest.members) return false; + if (!group.quest.active) return false; // quest is pending, not started + return group.quest.members[userid]; + } + + $scope.isMemberOfGroup = function(userid, group){ + if (!group.members) return false; + var memberIds = _.map(group.members, function(x){return x._id}); + return ~(memberIds.indexOf(userid)); + } + $scope.isMember = function(user, group){ return ~(group.members.indexOf(user._id)); } @@ -362,11 +380,17 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', ' User.set({'invitations.party':{}}); } + $scope.questCancel = function(){ + if (!confirm(window.env.t('sureCancel'))) return; + $rootScope.party.$questCancel(); + } + $scope.questAbort = function(){ if (!confirm(window.env.t('sureAbort'))) return; if (!confirm(window.env.t('doubleSureAbort'))) return; $rootScope.party.$questAbort(); } + } ]) diff --git a/src/controllers/groups.js b/src/controllers/groups.js index 6fd86bdd90..98398b329a 100644 --- a/src/controllers/groups.js +++ b/src/controllers/groups.js @@ -464,7 +464,15 @@ api.removeMember = function(req, res, next){ } if(_.contains(group.members, uuid)){ - Group.update({_id:group._id},{$pull:{members:uuid},$inc:{memberCount:-1}}, function(err, saved){ + var update = {$pull:{members:uuid}}; + if(group.quest && group.quest.members){ + // remove member from quest + update['$unset'] = {}; + update['$unset']['quest.members.' + uuid] = ""; + // TODO: run cleanQuestProgress and return scroll to member if member was quest owner + } + update['$inc'] = {memberCount: -1}; + Group.update({_id:group._id},update, function(err, saved){ if (err) return next(err); // Sending an empty 204 because Group.update doesn't return the group @@ -530,7 +538,6 @@ questStart = function(req, res, next) { quest = shared.content.quests[key], collected = quest.collect ? _.transform(quest.collect, function(m,v,k){m[k]=0}) : {}; - // TODO will this handle appropriately when people leave/join party between quest invite? _.each(group.members, function(m){ var updates = {$set:{},$inc:{'_v':1}}; if (m == group.quest.leader) @@ -611,12 +618,19 @@ api.questReject = function(req, res, next) { api.questCancel = function(req, res, next){ // Cancel a quest BEFORE it has begun (i.e., in the invitation stage) + // Quest scroll has not yet left quest owner's inventory so no need to return it. + // Do not wipe quest progress for members because they'll want it to be applied to the next quest that's started. var group = res.locals.group; async.parallel([ function(cb){ - group.quest = {key:null,progress:{},leader:null}; - group.markModified('quest'); - group.save(cb); + if (! group.quest.active) { + // Do not cancel active quests because this function does + // not do the clean-up required for that. + // TODO: return an informative error when quest is active + group.quest = {key:null,progress:{},leader:null}; + group.markModified('quest'); + group.save(cb); + } } ], function(err){ if (err) return next(err); diff --git a/views/options/social/boss.jade b/views/options/social/boss.jade index 837deff14d..833014dbce 100644 --- a/views/options/social/boss.jade +++ b/views/options/social/boss.jade @@ -9,25 +9,27 @@ mixin boss(tavern, mobile) table.table.table-striped tr(ng-repeat='member in group.members') td - span(ng-if=':: group.quest.leader && group.quest.leader==member._id') + span(ng-if=':: group.quest.leader && group.quest.leader==member._id && isMemberOfGroup(group.quest.leader,group) && isMemberOfPendingQuest(group.quest.leader,group)') |*  + // XXX does this trigger badly when ex-leader reenters group? {{::member.profile.name}} td {{group.quest.members[member._id] === true ? env.t('accepted') : group.quest.members[member._id] === false ? env.t('rejected') : env.t('pending')}} - //span(ng-if=':: group.quest.leader && group.members.indexOf(group.quest.leader) > -1') - |*  - =env.t('questOwner') - br - span(ng-if=':: !group.quest.leader') no quest leader - br - span(ng-if=':: group.members.indexOf(group.quest.leader) == -1') not in group - // span(ng-if=':: !group.quest.leader || group.members.indexOf(group.quest.leader) == -1')=env.t('questOwnerMIA') + + span(ng-if=':: group.quest.leader && isMemberOfGroup(group.quest.leader,group) && isMemberOfPendingQuest(group.quest.leader,group)') + |*  + =env.t('questOwner') + span(ng-if=':: group.quest.leader && isMemberOfGroup(group.quest.leader,group) && ! isMemberOfPendingQuest(group.quest.leader,group)') + =env.t('questOwnerNotInPendingQuest') + span(ng-if=':: ! group.quest.leader || ! isMemberOfGroup(group.quest.leader,group)') + =env.t('questOwnerNotInPendingQuestParty') hr .npc_ian.pull-left p=env.t('questStart') - button.btn.btn-sm.btn-warning(ng-if=':: !group.quest.leader || group.members.indexOf(group.quest.leader) == -1 || group.quest.leader==user._id', ng-click='party.$questAccept({"force":true})')=env.t('begin') - //button.btn.btn-sm.btn-warning(ng-if=':: !group.quest.leader || group.members.indexOf(group.quest.leader) == -1 || group.quest.leader==user._id', ng-click='party.$questCancel()')=env.t('cancel') - //-TODO Cancel button - //-TODO Both force-start & cancel should only be available to quest-initiator + + // only the quest owner sees the begin button: + button.btn.btn-sm.btn-warning(ng-if=':: group.quest.leader && group.quest.leader==user._id && isMemberOfGroup(group.quest.leader,group) && isMemberOfPendingQuest(group.quest.leader,group)', ng-click='party.$questAccept({"force":true})')=env.t('begin') + // only the quest owner sees the cancel button UNLESS the quest owner is no longer in the quest/party and then everyone sees it: + button.btn.btn-sm.btn-danger(ng-if=':: (group.quest.leader && group.quest.leader==user._id && isMemberOfGroup(group.quest.leader,group) && isMemberOfPendingQuest(group.quest.leader,group)) || (! group.quest.leader || ! isMemberOfGroup(group.quest.leader,group) || ! isMemberOfPendingQuest(group.quest.leader,group))', ng-click='questCancel()')=env.t('cancel') div(ng-if='group.quest.active==true') div(ng-if='::Content.quests[group.quest.key].boss',ng-init='boss=Content.quests[group.quest.key].boss;progress=group.quest.progress') @@ -69,13 +71,17 @@ mixin boss(tavern, mobile) table.table.table-striped tr(ng-repeat='(k,v) in group.members', ng-show='group.quest.members[v._id]') td - span(ng-if=':: group.quest.leader==v._id') + span(ng-if=':: group.quest.leader && group.quest.leader==v._id && isMemberOfGroup(group.quest.leader,group) && isMemberOfRunningQuest(group.quest.leader,group)') |*  {{::v.profile.name}} - // span(ng-if=':: group.quest.leader && group.members.indexOf(group.quest.leader) > -1') - // |*  - // =env.t('questOwner') - // span(ng-if=':: !group.quest.leader || group.members.indexOf(group.quest.leader) == -1')=env.t('questOwnerMIA') + span(ng-if=':: group.quest.leader && isMemberOfGroup(group.quest.leader,group) && isMemberOfRunningQuest(group.quest.leader,group)') + |*  + =env.t('questOwner') + span(ng-if=':: group.quest.leader && isMemberOfGroup(group.quest.leader,group) && ! isMemberOfRunningQuest(group.quest.leader,group)') + =env.t('questOwnerNotInRunningQuest') + span(ng-if=':: ! group.quest.leader || ! isMemberOfGroup(group.quest.leader,group)') + =env.t('questOwnerNotInRunningQuestParty') + quest-rewards(key='{{::group.quest.key}}') div(ng-if='::Content.quests[group.quest.key].boss') @@ -93,6 +99,6 @@ mixin boss(tavern, mobile) br p=env.t('bossColl2') - unless tavern - button.btn.btn-xs.btn-danger(ng-if='::!group.quest.leader || group.members.indexOf(group.quest.leader) == -1 || group.quest.leader==user._id', ng-click='questAbort()')=env.t('abort') + // only the quest owner sees the abort button UNLESS the quest owner is no longer in the quest/party and then everyone sees it: + button.btn.btn-sm.btn-warning(ng-if=':: (group.quest.leader && group.quest.leader==user._id && isMemberOfGroup(group.quest.leader,group) && isMemberOfRunningQuest(group.quest.leader,group)) || ! group.quest.leader || ! isMemberOfGroup(group.quest.leader,group) || ! isMemberOfRunningQuest(group.quest.leader,group)', ng-click='questAbort()')=env.t('abort') From 9dcc2576c3cec86e3d3254e181b1d80a714f01bc Mon Sep 17 00:00:00 2001 From: Alice Harris Date: Mon, 8 Sep 2014 18:48:28 +1000 Subject: [PATCH 5/5] remove dev comment --- views/options/social/boss.jade | 1 - 1 file changed, 1 deletion(-) diff --git a/views/options/social/boss.jade b/views/options/social/boss.jade index 833014dbce..276a9a4abd 100644 --- a/views/options/social/boss.jade +++ b/views/options/social/boss.jade @@ -11,7 +11,6 @@ mixin boss(tavern, mobile) td span(ng-if=':: group.quest.leader && group.quest.leader==member._id && isMemberOfGroup(group.quest.leader,group) && isMemberOfPendingQuest(group.quest.leader,group)') |*  - // XXX does this trigger badly when ex-leader reenters group? {{::member.profile.name}} td {{group.quest.members[member._id] === true ? env.t('accepted') : group.quest.members[member._id] === false ? env.t('rejected') : env.t('pending')}}