Quests: initial Bosses WIP - invite, accept, reject, etc

This commit is contained in:
Tyler Renelle
2013-12-20 12:42:36 -07:00
parent faeeecdf04
commit ecb354e2cc
13 changed files with 238 additions and 17 deletions

View File

@@ -180,3 +180,12 @@ a
.modal-indented-list
margin-left: 10px;
padding-left: 10px;
.inline-modal
position: relative
top: auto;
left: auto
right: auto
margin: 0 auto 20px
z-index: 1
max-width: 100%

View File

@@ -267,11 +267,11 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Groups', '$http', 'A
}
])
.controller("PartyCtrl", ['$scope', 'Groups', 'User', '$state',
function($scope, Groups, User, $state) {
.controller("PartyCtrl", ['$rootScope','$scope', 'Groups', 'User', '$state',
function($rootScope,$scope, Groups, User, $state) {
$scope.type = 'party';
$scope.text = 'Party';
$scope.group = Groups.party();
$scope.group = $rootScope.party = Groups.party();
$scope.newGroup = new Groups.Group({type:'party', leader: User.user._id, members: [User.user._id]});
$scope.create = function(group){
group.$save(function(newGroup){

View File

@@ -1,5 +1,5 @@
habitrpg.controller("InventoryCtrl", ['$rootScope', '$scope', 'User', 'API_URL', '$http', 'Notification',
function($rootScope, $scope, User, API_URL, $http, Notification) {
habitrpg.controller("InventoryCtrl", ['$rootScope', '$scope', 'User',
function($rootScope, $scope, User) {
var user = User.user;
var Content = $rootScope.Content;
@@ -17,6 +17,7 @@ habitrpg.controller("InventoryCtrl", ['$rootScope', '$scope', 'User', 'API_URL',
$scope.$watch('user.items.eggs', function(eggs){ $scope.eggCount = countStacks(eggs); }, true);
$scope.$watch('user.items.hatchingPotions', function(pots){ $scope.potCount = countStacks(pots); }, true);
$scope.$watch('user.items.food', function(food){ $scope.foodCount = countStacks(food); }, true);
$scope.$watch('user.items.quests', function(quest){ $scope.questCount = countStacks(quest); }, true);
$scope.$watch('user.items.gear', function(gear){
$scope.gear = {
@@ -114,5 +115,18 @@ habitrpg.controller("InventoryCtrl", ['$rootScope', '$scope', 'User', 'API_URL',
$scope.chooseMount = function(egg, potion) {
User.user.ops.equip({params:{type: 'mount', key: egg + '-' + potion}});
}
$scope.showQuest = function(quest) {
$rootScope.selectedQuest = Content.quests[quest];
$rootScope.modals.showQuest = true;
}
$scope.closeQuest = function(){
$rootScope.selectedQuest = undefined;
$rootScope.modals.showQuest = false;
}
$scope.questInit = function(){
$rootScope.party.$questAccept({key:$scope.selectedQuest.name});
$scope.closeQuest();
}
}
]);

View File

@@ -16,7 +16,8 @@ angular.module('groupServices', ['ngResource']).
join: {method: "POST", url: API_URL + '/api/v2/groups/:gid/join'},
leave: {method: "POST", url: API_URL + '/api/v2/groups/:gid/leave'},
invite: {method: "POST", url: API_URL + '/api/v2/groups/:gid/invite'},
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'}
});
// Defer loading everything until they're requested

View File

@@ -200,6 +200,15 @@ api.attachGroup = function(req, res, next) {
})
}
api.attachGroupPopulated = function(req, res, next) {
Group.findById(req.params.gid).populate('members').exec(function(err, group){
if(err) return res.json(500, {err:err});
if(!group) return res.json(404, {err: "Group not found"});
res.locals.group = group;
next();
})
}
/**
* TODO make this it's own ngResource so we don't have to send down group data with each chat post
*/
@@ -398,5 +407,100 @@ api.removeMember = function(req, res, next){
}else{
return res.json(400, {err: "User not found among group's members!"});
}
}
// ------------------------------------
// Quests
// ------------------------------------
questStart = function(req, res) {
var group = res.locals.group;
var user = res.locals.user;
var force = req.query.force;
group.markModified('quest');
// Not ready yet, wait till everyone's accepted, rejected, or we force-start
if (!force && _.findIndex(group.quest.members, function(m){
return m === undefined;
})) {
return group.save(function(err,saved){
if (err) return res.json(500,{err:err});
res.json(saved);
})
}
var parallel = [];
// TODO will this handle appropriately when people leave/join party between quest invite?
_.each(group.members, function(m){
if (m._id == user._id) m.items.quests[m.party.quest]--;
if (group.quest.members[m._id] == true) {
m.party.quest = group.quest.key;
} else {
m.party.quest = undefined;
delete group.quest.members[m._id];
}
parallel.push(function(cb2){m.save(cb2);});
})
group.quest.active = true;
group.quest.hp = shared.content.quests[group.quest.key].hp;
parallel.push(function(cb2){group.save(cb2);});
async.parallel(parallel,function(err, results){
if (err) return res.json(500,{err:err});
return res.json(group);
});
}
api.questAccept = function(req, res) {
var group = res.locals.group;
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 ?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) {
if (!shared.content.quests[key]) return res.json(404,{err:'Quest ' + key + ' not found'});
if (group.quest.key) return res.json(400, {err: 'Party already on a quest (and only have one quest at a time)'});
group.quest.key = key;
group.quest.members = {};
// Invite everyone. true means "accepted", false="rejected", undefined="pending". Once we click "start quest"
// or everyone has either accepted/rejected, then we store quest key in user object.
_.each(group.members, function(m){
if (m._id == user._id) {
group.quest.members[m._id] = true;
} else {
group.quest.members[m._id] = undefined;
}
});
// Party member accepting the invitation
} else {
if (!group.quest.key) return res.json(400,{err:'No quest invitation has been sent out yet.'});
group.quest.members[user._id] = true;
}
questStart(req,res);
}
api.questReject = function(req, res, next) {
var group = res.locals.group;
var user = res.locals.user;
if (!group.quest.key) return res.json(400,{err:'No quest invitation has been sent out yet.'});
group.quest.members[user._id] = false;
group.save(function(err,saved){
if (err) return res.json(500,{err:err});
return res.json(200,saved);
});
questStart(req,res);
}
//TODO
function questEnd(){}
function questAbort(){}

View File

@@ -30,7 +30,19 @@ var GroupSchema = new Schema({
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}))
challenges: [{type:'String', ref:'Challenge'}], // do we need this? could depend on back-ref instead (Challenge.find({group:GID}))
quest: {
key: String,
hp: Number,
active: {type:Boolean, 'default':false},
/*
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
}
}, {
strict: 'throw',
minimize: false // So empty objects are returned

View File

@@ -182,6 +182,12 @@ var UserSchema = new Schema({
),
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}
@@ -193,14 +199,14 @@ var UserSchema = new Schema({
'default': Date.now
},
// FIXME remove?
party: {
//party._id // FIXME make these populate docs?
current: String, // party._id
invitation: String, // party._id
lastMessageSeen: String,
leader: Boolean,
order: {type:String, 'default':'level'}
order: {type:String, 'default':'level'},
quest: String
},
preferences: {
armorSet: String,

View File

@@ -98,6 +98,8 @@ router.post('/groups/:gid/join', auth.auth, groups.attachGroup, groups.join);
router.post('/groups/:gid/leave', auth.auth, groups.attachGroup, groups.leave);
router.post('/groups/:gid/invite', auth.auth, groups.attachGroup, groups.invite);
router.post('/groups/:gid/removeMember', auth.auth, groups.attachGroup, groups.removeMember);
router.post('/groups/:gid/questAccept', auth.auth, groups.attachGroupPopulated, groups.questAccept); // query={key} (optional. if provided, trigger new invite, if not, accept existing invite)
router.post('/groups/:gid/questReject', auth.auth, groups.attachGroupPopulated, groups.questReject);
//GET /groups/:gid/chat
router.post('/groups/:gid/chat', auth.auth, groups.attachGroup, groups.postChat);

View File

@@ -41,7 +41,13 @@ script(type='text/ng-template', id='partials/options.inventory.drops.html')
div(ng-repeat='(pot,points) in ownedItems(user.items.hatchingPotions)')
button.customize-option(popover='{{Content.hatchingPotions[pot].notes}}', popover-title='{{Content.hatchingPotions[pot].text}} Potion', popover-trigger='mouseenter', popover-placement='right', ng-click='choosePotion(pot)', class='Pet_HatchingPotion_{{pot}}', ng-class='{selectableInventory: selectedEgg && !user.items.pets[selectedEgg.name+"-"+pot]}')
.badge.badge-info.stack-count {{points}}
//-p {{pot}}
li.customize-menu
menu.pets-menu(label='Quest Scrolls ({{questCount}})')
p(ng-show='questCount < 1') You don't have any quest scrolls.
div(ng-repeat='(quest,points) in ownedItems(user.items.quests)')
button.customize-option(popover='{{Content.quests[quest].notes}}', popover-title='{{Content.quests[quest].text}}', popover-trigger='mouseenter', popover-placement='right', ng-click='showQuest(quest)', class='inventory_quest_scroll')
.badge.badge-info.stack-count {{points}}
li.customize-menu
menu.pets-menu(label='Food ({{foodCount}})')
@@ -49,7 +55,6 @@ script(type='text/ng-template', id='partials/options.inventory.drops.html')
div(ng-repeat='(food,points) in ownedItems(user.items.food)')
button.customize-option(popover='{{Content.food[food].notes}}', popover-title='{{Content.food[food].text}}', popover-trigger='mouseenter', popover-placement='right', ng-click='chooseFood(food)', class='Pet_Food_{{food}}')
.badge.badge-info.stack-count {{points}}
//-p {{food}}
li.customize-menu(ng-if='user.items.special.snowball')
menu.pets-menu(label='Special')
@@ -105,6 +110,14 @@ script(type='text/ng-template', id='partials/options.inventory.drops.html')
| {{food.value}}
span.Pet_Currency_Gem1x.inline-gems
li.customize-menu
menu.pets-menu(label='Quests')
div(ng-repeat='quest in Content.quests')
button.customize-option(popover='{{quest.notes}}', popover-title='{{quest.text}}', popover-trigger='mouseenter', popover-placement='left', ng-click='purchase("quests", quest)', class='inventory_quest_scroll')
p
| {{quest.value}}
span.Pet_Currency_Gem1x.inline-gems
li.customize-menu
menu.pets-menu(label='Special')
div

View File

@@ -6,8 +6,39 @@ a.pull-right.gem-wallet(popover-trigger='mouseenter', popover-title='Guild Bank'
.row-fluid
.span4
// ------ Bosses -------
.modal.inline-modal(ng-if='group.type==="party" && group.quest.key && group.quest.active==false')
.modal-header(bindonce='group')
h3 Quest: {{Content.quests[group.quest.key].text}}
.modal-body
table.table.table-striped
tr(ng-repeat='member in group.members')
td {{member.profile.name}}
td {{group.quest.members[member._id] == undefined ? 'Pending' : k ? 'Rejected' : 'Accepted'}}
button.btn.btn-warning(ng-click='party.$questAccept({"force":true})') Force Start
.modal.inline-modal(ng-if='group.type=="party" && group.quest.key && group.quest.active==true')
.modal-header(bindonce='group')
h3 {{Content.quests[group.quest.key].text}}
.modal-body
div(class="quest_{{group.quest.key}}")
//-
.progress(style="height:10px")
.bar(style='width: {{Shared.percent(group.quest.hp, Content.quests[group.quest.key].hp)}}%;')
span.meter-text
i.icon-heart
| {{group.quest.hp | number:0}} / {{Content.quests[group.quest.key].hp}}
.hero-stats
.meter.health(title='Boss Health')
.bar(style='width: {{Shared.percent(group.quest.hp, Content.quests[group.quest.key].hp)}}%;')
span.meter-text
i.icon-heart
| {{group.quest.hp | number:0}} / {{Content.quests[group.quest.key].hp}}
p {{Content.quests[group.quest.key].notes}}
// ------ Information -------
.modal(style='position: relative;top: auto;left: auto;right: auto;margin: 0 auto 20px;z-index: 1;max-width: 100%;')
.modal.inline-modal
.modal-header(bindonce='group')
span(ng-if='group.leader == user.id')
button.btn.btn-primary.pull-right(ng-click='save(group)', ng-show='group._editing') Save
@@ -35,7 +66,7 @@ a.pull-right.gem-wallet(popover-trigger='mouseenter', popover-title='Guild Bank'
include ./challenge-box
// ------ Members -------
.modal(style='position: relative;top: auto;left: auto;right: auto;margin: 0 auto 20px;z-index: 1;max-width: 100%;')
.modal.inline-modal
.modal-header
h3 Members
.modal-body

View File

@@ -6,5 +6,6 @@ include ./new-stuff
include ./buy-gems
include ./members
include ./settings
include ./pets
include ./drops
include ./classes
include ./quests

View File

@@ -0,0 +1,28 @@
div(modal='modals.showQuest', ng-controller='InventoryCtrl')
.modal-header
h3 {{selectedQuest.text}}
.modal-body
table
tr
td
p {{selectedQuest.notes}}
td
div(class='quest_{{selectedQuest.name}}')
hr
div(style='clear:left;clear:right')
.npc_ian.pull-left
p Clicking "Invite" will send an invitation to your party members. When all members have accepted or denied, the quest begins. If they take to long, feel free to force-start the quest under Options > Social > Party
.modal-footer
button.btn.btn-default.btn-small.btn-cancel(ng-click='closeQuest()') Cancel
button.btn.btn-default.btn-primary(ng-click='questInit()') Invite Party
div(modal='party.quest.key && !questHold && party.quest.members[user._id] == undefined')
.modal-header
h3 Quest Invitation
.modal-body
p You have been invited to {{Content.quests[party.quest.key].text}}!
p (TODO list rewards)
.modal-footer
button.btn.btn-default.btn-small.btn-cancel(ng-click='questHold = true') Ask Later
button.btn.btn-default.btn-small.btn-cancel(ng-click='party.$questReject()') Reject
button.btn.btn-default.btn-primary(ng-click='party.$questAccept()') Accept