Merge branch 'develop' of github.com:HabitRPG/habitrpg into Wrong-max-level-messages

This commit is contained in:
Amin Arria
2015-10-19 12:14:38 -04:30
30 changed files with 1950 additions and 1792 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

After

Width:  |  Height:  |  Size: 156 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 144 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

After

Width:  |  Height:  |  Size: 145 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -16,6 +16,7 @@
"justin": "Justin",
"ian": "Ian",
"ianText": "Welcome to the Quest Shop! Here you can use Quest Scrolls to battle monsters with your friends. Be sure to check out our fine array of Quest Scrolls for purchase on the right!",
"ianBrokenText": "Welcome to the Quest Shop... Here you can use Quest Scrolls to battle monsters with your friends... Be sure to check out our fine array of Quest Scrolls for purchase on the right...",
"USD": "USD",
"newStuff": "New Stuff",
"cool": "Tell Me Later",

View File

@@ -13,6 +13,7 @@
"youReceived": "You've Received",
"dropQuestCongrats": "Congratulations on earning this quest scroll! You can invite your party to begin the quest now, or come back to it any time in your Inventory > Quests.",
"questSend": "Clicking \"Invite\" will send an invitation to your party members. When all members have accepted or denied, the quest begins. See status under Social > Party.",
"questSendBroken": "Clicking \"Invite\" will send an invitation to your party members... When all members have accepted or denied, the quest begins... See status under Social > Party...",
"inviteParty": "Invite Party to Quest",
"questInvitation": "Quest Invitation: ",
"questInvitationTitle": "Quest Invitation",
@@ -24,6 +25,7 @@
"rejected": "Rejected",
"pending": "Pending",
"questStart": "Once all members have either accepted or rejected, the quest begins. Only those that clicked \"accept\" will be able to participate in the quest and receive the drops. If members are pending too long (inactive?), the quest owner can start the quest without them by clicking \"Begin\". The quest owner can also cancel the quest and regain the quest scroll by clicking \"Cancel\".",
"questStartBroken": "Once all members have either accepted or rejected, the quest begins... Only those that clicked \"accept\" will be able to participate in the quest and receive the drops... If members are pending too long (inactive?), the quest owner can start the quest without them by clicking \"Begin\"... The quest owner can also cancel the quest and regain the quest scroll by clicking \"Cancel\"...",
"begin": "Begin",
"bossHP": "Boss Health",
"bossStrength": "Boss Strength",
@@ -34,9 +36,14 @@
"itemsToCollect": "Items to Collect",
"bossDmg1": "Each completed Daily and To-Do and each positive Habit hurts the boss. Hurt it more with redder tasks or Brutal Smash and Burst of Flames. The boss will deal damage to every quest participant for every Daily you've missed (multiplied by the boss's Strength) in addition to your regular damage, so keep your party healthy by completing your Dailies! <strong>All damage to and from a boss is tallied on cron (your day roll-over).</strong>",
"bossDmg2": "Only participants will fight the boss and share in the quest loot.",
"bossDmg1Broken": "Each completed Daily and To-Do and each positive Habit hurts the boss... Hurt it more with redder tasks or Brutal Smash and Burst of Flames... The boss will deal damage to every quest participant for every Daily you've missed (multiplied by the boss's Strength) in addition to your regular damage, so keep your party healthy by completing your Dailies... <strong>All damage to and from a boss is tallied on cron (your day roll-over)...</strong>",
"bossDmg2Broken": "Only participants will fight the boss and share in the quest loot...",
"tavernBossInfo": "Complete Dailies and To-Dos and score positive Habits to damage the World Boss! Incomplete Dailies fill the Exhaust Strike Bar. When the Exhaust Strike bar is full, the World Boss will attack an NPC. A World Boss will never damage individual players or accounts in any way. Only active accounts not resting in the Inn will have their tasks tallied.",
"tavernBossInfoBroken": "Complete Dailies and To-Dos and score positive Habits to damage the World Boss... Incomplete Dailies fill the Exhaust Strike Bar... When the Exhaust Strike bar is full, the World Boss will attack an NPC... A World Boss will never damage individual players or accounts in any way... Only active accounts not resting in the Inn will have their tasks tallied...",
"bossColl1": "To collect items, do your positive tasks. Quest items drop just like normal items; however, you won't see the drops until the next day, then everything you've found will be tallied up and contributed to the pile.",
"bossColl2": "Only participants can collect items and share in the quest loot.",
"bossColl1Broken": "To collect items, do your positive tasks... Quest items drop just like normal items; however, you won't see the drops until the next day, then everything you've found will be tallied up and contributed to the pile...",
"bossColl2Broken": "Only participants can collect items and share in the quest loot...",
"abort": "Abort",
"leaveQuest": "Leave Quest",
"sureLeave": "Are you sure you want to leave the active quest? All your quest progress will be lost.",
@@ -63,6 +70,7 @@
"sureAbort": "Are you sure you want to abort this mission? It will abort it for everyone in your party and all progress will be lost. The quest scroll will be returned to the quest owner.",
"doubleSureAbort": "Are you double sure? Make sure they won't hate you forever!",
"questWarning": "If new players join the party before the quest starts, they will also receive an invitation. However once the quest has started, no new party members can join the quest.",
"questWarningBroken": "If new players join the party before the quest starts, they will also receive an invitation... However once the quest has started, no new party members can join the quest...",
"bossRageTitle": "Rage",
"bossRageDescription": "When this bar fills, the boss will unleash a special attack!",
"startAQuest": "START A QUEST",

View File

@@ -119,5 +119,26 @@
"promoCodeApplied": "Promo Code Applied! Check your inventory",
"promoPlaceholder": "Enter Promotion Code",
"couponText": "We sometimes have events and give out coupon codes for special gear. (eg, those who stop by our Wondercon booth)",
"displayInviteToPartyWhenPartyIs1": "Display Invite To Party button when party has 1 member."
"displayInviteToPartyWhenPartyIs1": "Display Invite To Party button when party has 1 member.",
"saveCustomDayStart": "Save Custom Day Start",
"registration": "Registration",
"addLocalAuth": "Add local authentication:",
"generateCodes": "Generate Codes",
"generate": "Generate",
"getCodes": "Get Codes",
"webhooks": "Webhooks",
"enabled": "Enabled",
"webhookURL": "Webhook URL",
"add": "Add",
"buyGemsGoldCap": "Cap raised to <%= amount %>",
"mysticHourglass": "<%= amount %> Mystic Hourglass",
"mysticHourglassText": "Mystic Hourglasses allow purchasing a previous month's Mystery Item set.",
"purchasedPlanId": "Recurring $<%= price %> each <%= months %> Month(s) (<%= plan %>)",
"purchasedPlanExtraMonths": "You have <%= months %> months of subscription credit.",
"consecutiveSubscription": "Consecutive Subscription",
"consecutiveMonths": "Consecutive Months:",
"gemCapExtra": "Gem Cap Extra:",
"mysticHourglasses": "Mystic Hourglasses:",
"paypal": "PayPal",
"amazonPayments": "Amazon Payments"
}

View File

@@ -1154,6 +1154,7 @@ api.specialMounts =
'Orca-Base': 'orca'
'Gryphon-RoyalPurple': 'royalPurpleGryphon'
'Phoenix-Base': 'phoenix'
'JackOLantern-Base': 'jackolantern'
api.timeTravelStable =
pets:

View File

@@ -27,7 +27,7 @@ import moment from 'moment';
// mystery: <mystery_set_key>
// }
//
// <gear_type> - What type of euqipment it is (armor, head, weapon, etc)
// <gear_type> - What type of equipment it is (armor, head, weapon, etc)
// <set_name> - What set this gear is a part of (special, mystery, warrior, etc)
// <index> - The order in this particular set
// <formatted_equipment_key> - CamelCased version of key

View File

@@ -6,7 +6,8 @@ let specialMounts = {
'Mammoth-Base': 'mammoth',
'Orca-Base': 'orca',
'Gryphon-RoyalPurple': 'royalPurpleGryphon',
'Phoenix-Base': 'phoenix'
'Phoenix-Base': 'phoenix',
'JackOLantern-Base': 'jackolantern'
}
export default specialMounts;

View File

@@ -0,0 +1,67 @@
var migrationName = 'new_stuff.js';
var authorName = 'Sabe'; // in case script author needs to know when their ...
var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
/*
* Award Jack-O'-Lantern mounts to users who already have the pet version, award pet if they don't
*/
var dbserver = 'localhost:27017'; // FOR TEST DATABASE
// var dbserver = 'username:password@ds031379-a0.mongolab.com:31379'; // FOR PRODUCTION DATABASE
var dbname = 'habitrpg';
var mongo = require('mongoskin');
var _ = require('lodash');
var dbUsers = mongo.db(dbserver + '/' + dbname + '?auto_reconnect').collection('users');
// specify a query to limit the affected users (empty for all users):
var query = {
};
// specify fields we are interested in to limit retrieved data (empty if we're not reading data):
var fields = {
'items.pets.JackOLantern-Base':1
};
console.warn('Updating users...');
var progressCount = 1000;
var count = 0;
dbUsers.findEach(query, fields, {batchSize:250}, function(err, user) {
if (err) { return exiting(1, 'ERROR! ' + err); }
if (!user) {
console.warn('All appropriate users found and modified.');
return displayData();
}
count++;
// specify user data to change:
var set = {};
if (user.items.pets['JackOLantern-Base']) {
set = {'migration':migrationName, 'items.mounts.JackOLantern-Base':true};
} else {
set = {'migration':migrationName, 'items.pets.JackOLantern-Base':5};
}
dbUsers.update({_id:user._id}, {$set:set});
if (count%progressCount == 0) console.warn(count + ' ' + user._id);
if (user._id == authorUuid) console.warn(authorName + ' processed');
});
function displayData() {
console.warn('\n' + count + ' users processed\n');
return exiting(0);
}
function exiting(code, msg) {
code = code || 0; // 0 = success
if (code && !msg) { msg = 'ERROR!'; }
if (msg) {
if (code) { console.error(msg); }
else { console.log( msg); }
}
process.exit(code);
}

View File

@@ -4,38 +4,10 @@ app = require("../../website/src/server")
describe "Site Status", ->
describe "Without token or user id", ->
describe "GET /status", ->
it "/api/v2/status", (done) ->
request.get(baseURL + "/status").set("Accept", "application/json").end (res) ->
expect(res.statusCode).to.equal 200
expect(res.body.status).to.equal "up"
done()
it "/api/v2/user", (done) ->
request
.get(baseURL + "/user")
.set("Accept", "application/json")
.set("X-API-User", '')
.set("X-API-Key", '')
.end (res) ->
expect(res.statusCode).to.equal 401
expect(res.body.err).to.equal "You must include a token and uid (user id) in your request"
done()
describe "With token or user id", ->
before (done) ->
registerNewUser(done, true)
it "/api/v2/status", (done) ->
request.get(baseURL + "/status").set("Accept", "application/json").end (res) ->
expect(res.statusCode).to.equal 200
expect(res.body.status).to.equal "up"
done()
it "/api/v2/user", (done) ->
request.get(baseURL + "/user").set("Accept", "application/json").end (res) ->
expect(res.statusCode).to.equal 200
expect(res.body._id).to.equal user._id
done()

View File

@@ -5,6 +5,30 @@ app = require("../../website/src/server")
describe "Users", ->
describe "GET /user without token or user id", ->
it "/api/v2/user", (done) ->
request
.get(baseURL + "/user")
.set("Accept", "application/json")
.set("X-API-User", '')
.set("X-API-Key", '')
.end (res) ->
expect(res.statusCode).to.equal 401
expect(res.body.err).to.equal "You must include a token and uid (user id) in your request"
done()
describe "GET /user with token or user id", ->
before (done) ->
registerNewUser(done, true)
it "/api/v2/user", (done) ->
request.get(baseURL + "/user").set("Accept", "application/json").end (res) ->
expect(res.statusCode).to.equal 200
expect(res.body._id).to.equal user._id
done()
describe "Deleting Users", ->
before (done) ->

View File

@@ -135,7 +135,7 @@ describe('Stats Service', function() {
});
describe('equipmentStatBonus', function() {
it('tallies up stats from euqipment that is equipped', function() {
it('tallies up stats from equipment that is equipped', function() {
var equippedGear = {
"weapon" : "weapon_special_1",
"shield" : "shield_special_1",

View File

@@ -68,6 +68,8 @@ echo Updating repositories...
apt-get update -qq
echo Installing node.js
apt-get install -qq nodejs
echo Updating npm...
npm install -g npm
cd /vagrant

View File

@@ -2,8 +2,8 @@
// Make user and settings available for everyone through root scope.
habitrpg.controller('SettingsCtrl',
['$scope', 'User', '$rootScope', '$http', 'ApiUrl', 'Guide', '$location', '$timeout', 'Notification', 'Shared',
function($scope, User, $rootScope, $http, ApiUrl, Guide, $location, $timeout, Notification, Shared) {
['$scope', 'User', '$rootScope', '$http', 'ApiUrl', 'Guide', '$location', '$timeout', 'Content', 'Notification', 'Shared',
function($scope, User, $rootScope, $http, ApiUrl, Guide, $location, $timeout, Content, Notification, Shared) {
// FIXME we have this re-declared everywhere, figure which is the canonical version and delete the rest
// $scope.auth = function (id, token) {
@@ -150,6 +150,7 @@ habitrpg.controller('SettingsCtrl',
Notification.text(env.t('promoCodeApplied'));
});
}
$scope.generateCodes = function(codes){
$http.post(ApiUrl.get() + '/api/v2/coupons/generate/'+codes.event+'?count='+(codes.count || 1))
.success(function(res,code){
@@ -158,6 +159,7 @@ habitrpg.controller('SettingsCtrl',
window.location.href = '/api/v2/coupons?limit='+codes.count+'&_id='+User.user._id+'&apiToken='+User.user.apiToken;
})
}
$scope.releasePets = function() {
User.user.ops.releasePets({});
$rootScope.$state.go('tasks');
@@ -194,12 +196,30 @@ habitrpg.controller('SettingsCtrl',
$http.get(ApiUrl.get() + '/api/v2/coupons/valid-discount/'+coupon)
.success(function(){
Notification.text("Coupon applied!");
var subs = $scope.Content.subscriptionBlocks;
var subs = Content.subscriptionBlocks;
subs["basic_6mo"].discount = true;
subs["google_6mo"].discount = false;
});
}
$scope.gemGoldCap = function(subscription) {
var baseCap = 25;
var gemCapExtra = User.user.purchased.plan.consecutive.gemCapExtra;
// @TODO: What are these magic numbers? 3? 5?
var blocks = Content.subscriptionBlocks[subscription.key].months / 3 * 5;
var flooredBlocks = Math.floor(blocks);
var userTotalDropCap = baseCap + gemCapExtra + flooredBlocks;
var maxDropCap = 50;
return [userTotalDropCap, maxDropCap];
};
$scope.numberOfMysticHourglasses = function(subscription) {
var numberOfHourglasses = Content.subscriptionBlocks[subscription.key].months / 3;
return Math.floor(numberOfHourglasses);
};
function _calculateNextCron() {
$scope.dayStart;

View File

@@ -67,8 +67,8 @@ api.authWithUrl = function(req, res, next) {
}
api.registerUser = function(req, res, next) {
var regEmail = RegexEscape(req.body.email),
regUname = RegexEscape(req.body.username);
var regUname = RegexEscape(req.body.username);
var email = req.body.email && req.body.email.toLowerCase();
async.auto({
validate: function(cb) {
if (!(req.body.username && req.body.password && req.body.email))
@@ -80,14 +80,14 @@ api.registerUser = function(req, res, next) {
cb();
},
findReg: function(cb) {
User.findOne({$or:[{'auth.local.email': regEmail}, {'auth.local.username': regUname}]}, {'auth.local':1}, cb);
User.findOne({$or:[{'auth.local.email': email}, {'auth.local.username': regUname}]}, {'auth.local':1}, cb);
},
findFacebook: function(cb){
User.findOne({_id: req.headers['x-api-user'], apiToken: req.headers['x-api-key']}, {auth:1}, cb);
},
register: ['validate', 'findReg', 'findFacebook', function(cb, data) {
if (data.findReg) {
if (regEmail.test(data.findReg.auth.local.email)) return cb({code:401, err:"Email already taken"});
if (email === data.findReg.auth.local.email) return cb({code:401, err:"Email already taken"});
if (regUname.test(data.findReg.auth.local.username)) return cb({code:401, err:"Username already taken"});
}
var salt = utils.makeSalt();
@@ -95,7 +95,7 @@ api.registerUser = function(req, res, next) {
auth: {
local: {
username: req.body.username,
email: req.body.email,
email: email,
salt: salt,
hashed_password: utils.encryptPassword(req.body.password, salt)
},
@@ -143,7 +143,7 @@ api.loginLocal = function(req, res, next) {
var username = req.body.username;
var password = req.body.password;
if (!(username && password)) return res.json(401, {err:'Missing :username or :password in request body, please provide both'});
var login = validator.isEmail(username) ? {'auth.local.email':username} : {'auth.local.username':username};
var login = validator.isEmail(username) ? {'auth.local.email':username.toLowerCase()} : {'auth.local.username':username};
User.findOne(login, {auth:1}, function(err, user){
if (err) return next(err);
if (!user) return res.json(401, {err:"Uh-oh - your username or password is incorrect.\n- Make sure your username or email is typed correctly.\n- You may have signed up with Facebook, not email. Double-check by trying Facebook login.\n- If you forgot your password, click \"Forgot Password\"."});
@@ -233,12 +233,12 @@ api.deleteSocial = function(req,res,next){
}
api.resetPassword = function(req, res, next){
var email = req.body.email,
var email = req.body.email && req.body.email.toLowerCase(),
salt = utils.makeSalt(),
newPassword = utils.makeSalt(), // use a salt as the new password too (they'll change it later)
hashed_password = utils.encryptPassword(newPassword, salt);
User.findOne({'auth.local.email': RegexEscape(email)}, function(err, user){
User.findOne({'auth.local.email': email}, function(err, user){
if (err) return next(err);
if (!user) return res.send(401, {err:"Sorry, we can't find a user registered with email " + email + "\n- Make sure your email address is typed correctly.\n- You may have signed up with Facebook, not email. Double-check by trying Facebook login."});
user.auth.local.salt = salt;
@@ -283,14 +283,15 @@ api.changeUsername = function(req, res, next) {
}
api.changeEmail = function(req, res, next){
var email = req.body.email && req.body.email.toLowerCase()
async.waterfall([
function(cb){
User.findOne({'auth.local.email': RegexEscape(req.body.email)}, {auth:1}, cb);
User.findOne({'auth.local.email': email}, {auth:1}, cb);
},
function(found, cb){
if(found) return cb({code:401, err: "Email already taken"});
if (invalidPassword(res.locals.user, req.body.password)) return cb(invalidPassword(res.locals.user, req.body.password));
res.locals.user.auth.local.email = req.body.email;
res.locals.user.auth.local.email = email;
res.locals.user.save(cb);
}
], function(err){

View File

@@ -9,7 +9,8 @@ include ../../shared/mixins
.arrow.hidden-xs
h3.popover-title=env.t('ian')
.popover-content
p=env.t('ianText')
p(ng-if='env.worldDmg.quests')=env.t('ianBrokenText')
p(ng-if='!env.worldDmg.quests')=env.t('ianText')
h3.equipment-title=env.t('yourQuests')
+ownedQuests(true,'right')

View File

@@ -103,19 +103,19 @@ script(type='text/ng-template', id='partials/options.settings.settings.html')
br.visible-xs
button.btn.btn-block.btn-primary(ng-click='openDayStartModal(dayStart)',
ng-disabled='dayStart == user.preferences.dayStart')
| Save Custom Day Start
=env.t('saveCustomDayStart')
.personal-options.col-md-6
.panel.panel-default
.panel-heading
span Registration
span=env.t('registration')
.panel-body
div(ng-if='user.auth.facebook.id')
button.btn.btn-primary(disabled='disabled', ng-if='!user.auth.local.username')=env.t('registeredWithFb')
button.btn.btn-danger(ng-click='http("delete","/api/v2/user/auth/social",null,"detachedFacebook")', ng-if='user.auth.local.username')=env.t('detachFacebook')
hr
div(ng-if='!user.auth.local.username')
p Add local authentication:
p=env.t('addLocalAuth')
form(ng-submit='http("post","/api/v2/register",localAuth,"addedLocalAuth")', ng-init='localAuth={}', name='localAuth', novalidate)
//-.alert.alert-danger(ng-messages='changeUsername.$error && changeUsername.submitted')=env.t('fillAll')
.form-group
@@ -189,15 +189,15 @@ script(type='text/ng-template', id='partials/options.settings.promo.html')
small= env.t('couponText')
div(ng-if='user.contributor.sudo')
hr
h4 Generate Codes
h4=env.t('generateCodes')
form.form(role='form',ng-submit='generateCodes(_codes)',ng-init='_codes={}')
.form-group
input.form-control(type='text',ng-model='_codes.event',placeholder="Event code (eg, 'wondercon')")
.form-group
input.form-control(type='number',ng-model='_codes.count',placeholder="Number of codes to generate (eg, 250)")
.form-group
button.btn.btn-primary(type='submit') Generate
a.btn.btn-default(href='/api/v2/coupons?_id={{user._id}}&apiToken={{user.apiToken}}') Get Codes
button.btn.btn-primary(type='submit')=env.t('generate')
a.btn.btn-default(href='/api/v2/coupons?_id={{user._id}}&apiToken={{user.apiToken}}')=env.t('getCodes')
script(type='text/ng-template', id='partials/options.settings.api.html')
.container-fluid
@@ -214,12 +214,12 @@ script(type='text/ng-template', id='partials/options.settings.api.html')
hr
h2 Webhooks
h2=env.t('webhooks')
table.table.table-striped
thead(ng-if='hasWebhooks')
tr
th Enabled
th Webhook URL
th=env.t('enabled')
th=env.t('webhookURL')
th
tbody
tr(ng-repeat="webhook in user.preferences.webhooks | toArray:true | orderBy:'sort'")
@@ -235,9 +235,9 @@ script(type='text/ng-template', id='partials/options.settings.api.html')
td(colspan=2)
form.form-horizontal(ng-submit='addWebhook(_newWebhook.url)')
.form-group.col-sm-10
input.form-control(type='url', ng-model='_newWebhook.url', placeholder='Webhook URL')
input.form-control(type='url', ng-model='_newWebhook.url', placeholder=env.t('webhookURL'))
.col-sm-2
button.btn.btn-sm.btn-primary(type='submit') Add
button.btn.btn-sm.btn-primary(type='submit')=env.t('add')
script(id='partials/options.settings.export.html', type="text/ng-template")
.container-fluid
@@ -258,7 +258,7 @@ mixin subPerks()
tr
td
span.hint(popover=env.t('buyGemsGoldText', {gemCost: "{{Shared.planGemLimits.convRate}}", gemLimit: "{{Shared.planGemLimits.convCap}}"}),popover-trigger='mouseenter',popover-placement='right') #{env.t('buyGemsGold')}&nbsp;
span.badge.badge-success(ng-show='_subscription.key!="basic_earned"') Cap raised to {{ [25 + user.purchased.plan.consecutive.gemCapExtra + Math.floor(Content.subscriptionBlocks[_subscription.key].months/3*5), 50] | min }}
span.badge.badge-success(ng-show='_subscription.key!="basic_earned"')=env.t('buyGemsGoldCap', {amount: '{{:: gemGoldCap(_subscription) | min }}'})
tr
td
span.hint(popover=env.t('retainHistoryText'),popover-trigger='mouseenter',popover-placement='right')=env.t('retainHistory')
@@ -269,8 +269,8 @@ mixin subPerks()
td
span.hint(popover=env.t('mysteryItemText'),popover-trigger='mouseenter',popover-placement='right') #{env.t('mysteryItem')}&nbsp;
div(ng-show='_subscription.key!="basic_earned"')
.badge.badge-success +{{Math.floor(Content.subscriptionBlocks[_subscription.key].months/3)}} Mystic Hourglass
.small.muted Mystic Hourglasses allow purchasing a previous month's Mystery Item set.
.badge.badge-success=env.t('mysticHourglass', {amount: '+{{:: numberOfMysticHourglasses(_subscription)}}'})
.small.muted=env.t('mysticHourglassText')
tr
td
span.hint(popover=env.t('supportDevsText'),popover-trigger='mouseenter',popover-placement='right')=env.t('supportDevs')
@@ -324,17 +324,17 @@ script(id='partials/options.settings.subscription.html',type='text/ng-template')
| #{env.t('subCanceled')} <strong>{{moment(user.purchased.plan.dateTerminated).format('MM/DD/YYYY')}}</strong>
tr(ng-if='!user.purchased.plan.dateTerminated'): td
h4=env.t('subscribed')
p(ng-if='user.purchased.plan.planId') Recurring ${{Content.subscriptionBlocks[user.purchased.plan.planId].price}} each {{Content.subscriptionBlocks[user.purchased.plan.planId].months}} Month(s) ({{user.purchased.plan.paymentMethod}})
p(ng-if='user.purchased.plan.planId')=env.t('purchasedPlanId', {price: '{{Content.subscriptionBlocks[user.purchased.plan.planId].price}}', months: '{{Content.subscriptionBlocks[user.purchased.plan.planId].months}}', plan: '{{user.purchased.plan.paymentMethod}}'})
tr(ng-if='user.purchased.plan.extraMonths'): td
span.glyphicon.glyphicon-credit-card
| You have {{user.purchased.plan.extraMonths | number:2}} months of subscription credit.
| &nbsp;#{env.t('purchasedPlanExtraMonths', {months: '{{user.purchased.plan.extraMonths | number:2}}'})}
tr(ng-if='user.purchased.plan.consecutive.count || user.purchased.plan.consecutive.offset'): td
span.glyphicon.glyphicon-forward
| &nbsp;Consecutive Subscription
| &nbsp;#{env.t('consecutiveSubscription')}
ul.list-unstyled
li Consecutive Months: {{user.purchased.plan.consecutive.count + user.purchased.plan.consecutive.offset}}
li Gem Cap Extra: {{user.purchased.plan.consecutive.gemCapExtra}}
li Mystic Hourglasses: {{user.purchased.plan.consecutive.trinkets}}
li #{env.t('consecutiveMonths')} {{user.purchased.plan.consecutive.count + user.purchased.plan.consecutive.offset}}
li #{env.t('gemCapExtra')} {{user.purchased.plan.consecutive.gemCapExtra}}
li #{env.t('mysticHourglasses')} {{user.purchased.plan.consecutive.trinkets}}
div(ng-if='!user.purchased.plan.customerId || (user.purchased.plan.customerId && user.purchased.plan.dateTerminated)')
.form-group
.radio(ng-repeat='block in Content.subscriptionBlocks | toArray | omit: "discount==true" | orderBy:"months"')
@@ -355,8 +355,8 @@ script(id='partials/options.settings.subscription.html',type='text/ng-template')
h3(ng-if='(user.purchased.plan.customerId && user.purchased.plan.dateTerminated)')= env.t("resubscribe")
a.btn.btn-primary(ng-click='Payments.showStripe({subscription:_subscription.key, coupon:_subscription.coupon})', ng-disabled='!_subscription.key')= env.t('card')
a.btn.btn-success(href='/paypal/subscribe?_id={{user._id}}&apiToken={{user.apiToken}}&sub={{_subscription.key}}{{_subscription.coupon ? "&coupon="+_subscription.coupon : ""}}', ng-disabled='!_subscription.key') PayPal
a.btn.btn-warning(ng-click="Payments.amazonPayments.init({type: 'subscription', subscription:_subscription.key, coupon:_subscription.coupon})") Amazon Payments
a.btn.btn-success(href='/paypal/subscribe?_id={{user._id}}&apiToken={{user.apiToken}}&sub={{_subscription.key}}{{_subscription.coupon ? "&coupon="+_subscription.coupon : ""}}', ng-disabled='!_subscription.key')=env.t('paypal')
a.btn.btn-warning(ng-click="Payments.amazonPayments.init({type: 'subscription', subscription:_subscription.key, coupon:_subscription.coupon})")=env.t('amazonPayments')
div(ng-if='user.purchased.plan.customerId')
.btn.btn-primary(ng-if='!user.purchased.plan.dateTerminated && user.purchased.plan.paymentMethod=="Stripe"', ng-click='Payments.showStripeEdit()')=env.t('subUpdateCard')
.btn.btn-sm.btn-danger(ng-if='!user.purchased.plan.dateTerminated', ng-click='Payments.cancelSubscription()')=env.t('cancelSub')

View File

@@ -2,13 +2,18 @@ div(class="#{env.worldDmg.quests ? 'npc_ian_broken' : 'npc_ian'}").pull-left
div(ng-if='::Content.quests[group.quest.key].boss')
if tavern
p!=env.t('tavernBossInfo')
p(ng-if='!env.worldDmg.quests')!=env.t('tavernBossInfo')
p(ng-if='env.worldDmg.quests')!=env.t('tavernBossInfoBroken')
else
p!=env.t('bossDmg1')
p(ng-if='!env.worldDmg.quests')!=env.t('bossDmg1')
p(ng-if='env.worldDmg.quests')!=env.t('bossDmg1Broken')
br
p=env.t('bossDmg2')
p(ng-if='!env.worldDmg.quests')=env.t('bossDmg2')
p(ng-if='env.worldDmg.quests')=env.t('bossDmg2Broken')
div(ng-if='::Content.quests[group.quest.key].collect')
p=env.t('bossColl1')
p(ng-if='!env.worldDmg.quests')=env.t('bossColl1')
p(ng-if='env.worldDmg.quests')=env.t('bossColl1Broken')
br
p=env.t('bossColl2')
p(ng-if='!env.worldDmg.quests')=env.t('bossColl2')
p(ng-if='env.worldDmg.quests')=env.t('bossColl2Broken')

View File

@@ -19,7 +19,8 @@ div(ng-if='group.quest.active===false')
hr
div(class="#{env.worldDmg.quests ? 'npc_ian_broken' : 'npc_ian'}").pull-left
p=env.t('questStart')
p(ng-if='!env.worldDmg.quests')=env.t('questStart')
p(ng-if='env.worldDmg.quests')=env.t('questStartBroken')
span(ng-if='user.party.quest.RSVPNeeded')
button.btn.btn-sm.btn-success(ng-click='questAccept()')=env.t('accept')

View File

@@ -39,8 +39,10 @@ script(type='text/ng-template', id='modals/showQuest.html')
+questInfo
hr
div(class="#{env.worldDmg.quests ? 'npc_ian_broken' : 'npc_ian'}").pull-left
p=env.t('questSend')
p=env.t('questWarning')
p(ng-if='!env.worldDmg.quests')=env.t('questSend')
p(ng-if='!env.worldDmg.quests')=env.t('questWarning')
p(ng-if='env.worldDmg.quests')=env.t('questSendBroken')
p(ng-if='env.worldDmg.quests')=env.t('questWarningBroken')
.modal-footer
button.btn.btn-default(ng-click='closeQuest(); $close()')=env.t('cancel')
button.btn.btn-primary(ng-click='questInit()')

View File

@@ -1,34 +1,53 @@
h2 10/8/2015 - WORLD BOSS REVEALED: BURNOUT!
h2 10/14/2015 - JACK O'LANTERN PETS AND MOUNTS! WORLD BOSS EXHAUST STRIKE!
hr
tr
td
.promo_burnout.pull-right
h3 Burnout, Scourge of the Flourishing Fields
p It is well past midnight, still and stiflingly hot, when Redphoenix and scout captain Kiwibot abruptly burst through the city gates. "We need to evacuate all the wooden buildings!" Redphoenix shouts. "Hurry!"
.Pet-JackOLantern-Base.pull-right
h3 Jack O'Lantern Pets and Mounts
p The Flourishing Fields are full of cute carved pumpkins - and it looks like one has followed you home! It must be frightened of Burnout and looking for an adventurer to protect it.
br
p Kiwibot grips the wall as she catches her breath."It's draining people and turning them into Exhaust Spirits! That's why everything was delayed. That's where the missing people have gone. It's been stealing their energy!"
p Those of you who weren't around last Fall Festival have received a pet Jack-O-Lantern, and those of you who got one last year have received a Jack-O-Lantern Mount! Happy Fall Festival.
p.small.muted by Lemoness
tr
td
.npc_ian_broken.pull-right
h3 World Boss: Burnout Uses Exhaust Strike!
p Oh no! Despite our best efforts, we've let some Dailies get away from us, and now Burnout is inflamed with energy! With a crackling snarl, it engulfs Ian the Quest Master in a surge of spectral fire. As fallen quest scrolls smolder, the smoke clears, and you see that Ian has been drained of energy and turned into a drifting Exhaust Spirit!
br
p "'It'?'" asks Lemoness.
p Only defeating Burnout can break the spell and save our beloved Quest Master. Let's keep our Dailies in check and defeat this monster before it attacks again!
br
p And then the heat takes form.
br
p It rises from the earth in a billowing, twisting mass, and the air chokes with the scent of smoke and sulphur. Flames lick across the molten ground and contort into limbs, writhing to horrific heights. Smoldering eyes snap open, and the creature lets out a deep and crackling cackle.
br
p Kiwibot whispers a single word.
br
p "Burnout."
hr
p Quickly, Habiticans, we have to save the Flourishing Fields from incineration! All of us will battle this World Boss together by completing tasks, but it won't attack us individually. However, the more Dailies we skip, the closer we get to triggering its fearsome Exhaust Strike, which will target our NPCs!
br
p Knowing your enemy is the first step to defeating it. <a href='http://habitica.wikia.com/wiki/Boss#World_Bosses' target='_blank'>Read more about World Bosses here</a>.
br
p.small.muted by Lemoness, SabreCat, and Kiwibot
p Late to the fight? Learn about Burnout and how to defeat World Bosses <a href='http://habitica.wikia.com/wiki/Boss#World_Bosses' target='_blank'>here</a>.
if menuItem !== 'oldNews'
hr
a(href='/static/old-news', target='_blank') Read older news
mixin oldNews
h2 10/8/2015 - WORLD BOSS REVEALED: BURNOUT!
tr
td
.promo_burnout.pull-right
h3 Burnout, Scourge of the Flourishing Fields
p It is well past midnight, still and stiflingly hot, when Redphoenix and scout captain Kiwibot abruptly burst through the city gates. "We need to evacuate all the wooden buildings!" Redphoenix shouts. "Hurry!"
br
p Kiwibot grips the wall as she catches her breath."It's draining people and turning them into Exhaust Spirits! That's why everything was delayed. That's where the missing people have gone. It's been stealing their energy!"
br
p "'It'?'" asks Lemoness.
br
p And then the heat takes form.
br
p It rises from the earth in a billowing, twisting mass, and the air chokes with the scent of smoke and sulphur. Flames lick across the molten ground and contort into limbs, writhing to horrific heights. Smoldering eyes snap open, and the creature lets out a deep and crackling cackle.
br
p Kiwibot whispers a single word.
br
p "Burnout."
hr
p Quickly, Habiticans, we have to save the Flourishing Fields from incineration! All of us will battle this World Boss together by completing tasks, but it won't attack us individually. However, the more Dailies we skip, the closer we get to triggering its fearsome Exhaust Strike, which will target our NPCs!
br
p Knowing your enemy is the first step to defeating it. <a href='http://habitica.wikia.com/wiki/Boss#World_Bosses' target='_blank'>Read more about World Bosses here</a>.
br
p.small.muted by Lemoness, SabreCat, and Kiwibot
h2 10/5/2015 - OCTOBER BACKGROUNDS REVEALED, COSTUME CHALLENGE BEGINS, AND FALL PLOT-LINE CONTINUES
tr
td