diff --git a/.travis.yml b/.travis.yml index 9f977e2505..7edd7c20b0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,10 @@ language: node_js node_js: - '0.10' +services: + - mongodb before_script: - - 'npm install -g bower grunt-cli' - - 'bower install' + - 'npm install -g grunt-cli' - export DISPLAY=:99.0 - sh -e /etc/init.d/xvfb start + - cp config.json.example config.json diff --git a/bower.json b/bower.json index 7433383b0d..2769a82370 100644 --- a/bower.json +++ b/bower.json @@ -21,7 +21,6 @@ "angular-sanitize": "~1.2.1", "angular-resource": "~1.2.1", "angular-ui": "~0.4.0", - "angular-ui-utils": "~0.0.4", "angular-bootstrap": "~0.5.0", "angular-ui-router": "ca3b4777a603df8f86cfd653c8f6c38b2ae05d89", "angular-loading-bar": "~0.0.5", @@ -38,13 +37,14 @@ "js-emoji": "git://github.com/snicker/js-emoji#master", "gemoji": "git://github.com/github/gemoji", "sticky": "*", - "bootstrap-tour": "~0.8.0" + "bootstrap-tour": "~0.8.0", + "angular-ui-utils": "~0.1.0" }, "resolutions": { "jquery": "~2.0.3", "bootstrap": "v2.3.2", "angular": "~1.2.1", - "bootstrap-tour": "~0.8.0" + "angular-ui-utils": "~0.1.0" }, "devDependencies": { "angular-mocks": "~1.2.1" diff --git a/karma-e2e.conf.js b/karma-e2e.conf.js deleted file mode 100644 index fa01484a0a..0000000000 --- a/karma-e2e.conf.js +++ /dev/null @@ -1,54 +0,0 @@ -// Karma configuration -// http://karma-runner.github.io/0.10/config/configuration-file.html - -module.exports = function(config) { - config.set({ - // base path, that will be used to resolve files and exclude - basePath: '', - - // testing framework to use (jasmine/mocha/qunit/...) - frameworks: ['ng-scenario'], - - // list of files / patterns to load in the browser - files: [ - 'test/e2e/**/*.js' - ], - - // list of files / patterns to exclude - exclude: [], - - // web server port - port: 8080, - - // level of logging - // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG - logLevel: config.LOG_INFO, - - - // enable / disable watching file and executing tests whenever any file changes - autoWatch: false, - - - // Start these browsers, currently available: - // - Chrome - // - ChromeCanary - // - Firefox - // - Opera - // - Safari (only Mac) - // - PhantomJS - // - IE (only Windows) - browsers: ['Chrome'], - - - // Continuous Integration mode - // if true, it capture browsers, run tests and exit - singleRun: false - - // Uncomment the following lines if you are using grunt's server to run the tests - // proxies: { - // '/': 'http://localhost:9000/' - // }, - // URL root prevent conflicts with the site root - // urlRoot: '_karma_' - }); -}; diff --git a/package.json b/package.json index 9e2ac50493..a81bbd57d2 100644 --- a/package.json +++ b/package.json @@ -58,12 +58,12 @@ "npm": "1.2.x" }, "scripts": { - "test": "grunt karma:continuous", + "test": "./test/run_tests.sh", "start": "grunt run:dev", "postinstall": "./node_modules/bower/bin/bower install -f" }, "devDependencies": { - "karma-ng-scenario": "~0.1.0", + "protractor": "~0.14.0", "grunt-karma": "~0.6.2", "karma-script-launcher": "~0.1.0", "karma-chrome-launcher": "~0.1.0", diff --git a/protractor.conf.js b/protractor.conf.js new file mode 100644 index 0000000000..32999a2233 --- /dev/null +++ b/protractor.conf.js @@ -0,0 +1,24 @@ +// An example configuration file. +exports.config = { + // The address of a running selenium server. + seleniumAddress: 'http://localhost:4444/wd/hub', + + // Capabilities to be passed to the webdriver instance. + capabilities: { + 'browserName': 'firefox' + }, + + // Spec patterns are relative to the current working directly when + // protractor is called. + specs: ['test/e2e/e2e.js'], + + // A base URL for your application under test. Calls to protractor.get() + // with relative paths will be prepended with this. + baseUrl: 'http://localhost:3001', + + // Options to be passed to Jasmine-node. + jasmineNodeOpts: { + showColors: true, + defaultTimeoutInterval: 30000 + } +}; diff --git a/public/css/index.styl b/public/css/index.styl index 796dc2e278..7fbb6f7615 100644 --- a/public/css/index.styl +++ b/public/css/index.styl @@ -1,4 +1,5 @@ -@import "nib/vendor" +@import "nib" +//@import "nib/vendor" // Vendor Includes - include first so we can override // Import only styles that do not have urls to images! Include them directly in the page! diff --git a/public/css/tasks.styl b/public/css/tasks.styl index 78a02d007e..c9e96e4359 100644 --- a/public/css/tasks.styl +++ b/public/css/tasks.styl @@ -407,4 +407,34 @@ form color: #333 &:hover border-top: 0 - margin-top: 2px \ No newline at end of file + margin-top: 2px + +// Checklists +// -------- +.checklist-form + /* Make an input blend into its parent */ + input.inline-edit + reset-box-model() + reset-font() + line-height: inherit + text-align: inherit + background-color:inherit + box-shadow: none + + /* Add interaction cues on hover and focus */ + input.inline-edit:hover,input.inline-edit:focus + background-color: #FFD + transition: background-color 0.5s + + .checklist-icons + opacity:0 + + li + min-height: 20px + padding-left: 10px + margin-bottom: 5px + + [type="checkbox"] + margin: 0px 10px 3px 0px + li:hover .checklist-icons + opacity:1 diff --git a/public/js/controllers/notificationCtrl.js b/public/js/controllers/notificationCtrl.js index 003615e52a..795cf91538 100644 --- a/public/js/controllers/notificationCtrl.js +++ b/public/js/controllers/notificationCtrl.js @@ -33,7 +33,7 @@ habitrpg.controller('NotificationCtrl', $rootScope.$watch('user.stats.mp', function(after,before) { if (after == before) return; - if (User.user.stats.lvl == 0) return; + if (!User.user.flags.classSelected || User.user.preferences.disableClasses) return; var mana = after - before; Notification.mp(mana); }); @@ -68,6 +68,11 @@ habitrpg.controller('NotificationCtrl', $rootScope.modals.achievements.beastMaster = true; }, true); + $rootScope.$watch('user.achievements.rebirths', function(after, before){ + if(after === before) return; + $rootScope.modals.achievements.rebirth = true; + }); + /*_.each(['weapon', 'head', 'chest', 'shield'], function(watched){ $rootScope.$watch('user.items.' + watched, function(before, after){ if (after == before) return; diff --git a/public/js/controllers/settingsCtrl.js b/public/js/controllers/settingsCtrl.js index 0efd6504c1..8ad6419a14 100644 --- a/public/js/controllers/settingsCtrl.js +++ b/public/js/controllers/settingsCtrl.js @@ -59,6 +59,12 @@ habitrpg.controller('SettingsCtrl', $rootScope.$state.go('tasks'); } + $scope.rebirth = function(){ + User.user.ops.rebirth({}); + $rootScope.modals.rebirth = false; + $rootScope.$state.go('tasks'); + } + $scope.changePassword = function(changePass){ if (!changePass.oldPassword || !changePass.newPassword || !changePass.confirmNewPassword) { return alert("Please fill out all fields"); diff --git a/public/js/controllers/tasksCtrl.js b/public/js/controllers/tasksCtrl.js index f2d82ff08b..dcaa278712 100644 --- a/public/js/controllers/tasksCtrl.js +++ b/public/js/controllers/tasksCtrl.js @@ -1,7 +1,7 @@ "use strict"; -habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','Notification', '$http', 'API_URL', - function($scope, $rootScope, $location, User, Notification, $http, API_URL) { +habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','Notification', '$http', 'API_URL', '$timeout', + function($scope, $rootScope, $location, User, Notification, $http, API_URL, $timeout) { $scope.obj = User.user; // used for task-lists $scope.user = User.user; @@ -42,9 +42,11 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','N User.user.ops.deleteTask({params:{id:list[$index].id}}) }; - $scope.saveTask = function(task) { + $scope.saveTask = function(task, stayOpen) { + if (task.checklist && task.checklist[0]) + task.checklist = _.filter(task.checklist,function(i){return !!i.text}); User.user.ops.updateTask({params:{id:task.id},body:task}); - task._editing = false; + if (!stayOpen) task._editing = false; }; /** @@ -68,6 +70,56 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','N }); }; + /* + ------------------------ + To-Dos + ------------------------ + */ + $scope._today = moment().add('days',1); + + /* + ------------------------ + Checklists + ------------------------ + */ + function focusChecklist(task,index) { + window.setTimeout(function(){ + $('#task-'+task.id+' .checklist-form .inline-edit')[index].focus(); + }); + } + $scope.addChecklist = function(task) { + task.checklist = [{completed:false,text:""}]; + focusChecklist(task,0); + } + $scope.addChecklistItem = function(task,$event,$index) { + if ($index == task.checklist.length-1){ + User.user.ops.updateTask({params:{id:task.id},body:task}); // don't preen the new empty item + task.checklist.push({completed:false,text:''}); + focusChecklist(task,task.checklist.length-1); + } else { + $scope.saveTask(task,true); + focusChecklist(task,$index+1); + } + } + $scope.removeChecklistItem = function(task,$index,force){ + var backspaced = !force && !task.checklist[$index].text + if (force || backspaced) { + task.checklist.splice($index,1); + $scope.saveTask(task,true); + if (backspaced) focusChecklist(task,$index-1); + } + } + $scope.navigateChecklist = function(task,$index,$event){ + focusChecklist(task, $event.keyCode == '40' ? $index+1 : $index-1); + } + $scope.checklistCompletion = function(checklist){ + return _.reduce(checklist,function(m,i){return m+(i.completed ? 1 : 0);},0) + } + $scope.collapseChecklist = function(task) { + task.collapseChecklist = !task.collapseChecklist; + $scope.saveTask(task,true); + } + /* ------------------------ Items diff --git a/public/js/filters/filters.js b/public/js/filters/filters.js index e20db4aadf..9d2255d9e2 100644 --- a/public/js/filters/filters.js +++ b/public/js/filters/filters.js @@ -8,10 +8,4 @@ angular.module('habitrpg') return function (gp) { return Math.floor((gp - Math.floor(gp))*100); } - }) - .filter('completedFilter', function(){ - return function(tasks,_showCompleted) { - if (!tasks[0] || tasks[0].type != 'todo') return tasks; - return _.where(tasks, {completed:!!_showCompleted}); - } }) \ No newline at end of file diff --git a/public/js/services/guideServices.js b/public/js/services/guideServices.js index 3535086828..96bc90f202 100644 --- a/public/js/services/guideServices.js +++ b/public/js/services/guideServices.js @@ -139,7 +139,10 @@ angular.module('guideServices', []). } }); - + $rootScope.$watch('user.flags.rebirthEnabled', function(after, before) { + if (alreadyShown(before, after)) return; + $rootScope.modals.rebirthEnabled = true; + }); /** @@ -174,7 +177,7 @@ angular.module('guideServices', []). }, { element: ".meter.mana", title: "Spells", - content: "You can now unlock class-specific spells. You'll see your first at level 6. Your mana replenishes 10 points per day, plus 1 point per completed To-Do." + content: "You can now unlock class-specific spells. You'll see your first at level 11. Your mana replenishes 10 points per day, plus 1 point per completed To-Do." }, { orphan: true, title: "Read More", diff --git a/public/js/services/notificationServices.js b/public/js/services/notificationServices.js index c55acfa90b..2d5b89eb16 100644 --- a/public/js/services/notificationServices.js +++ b/public/js/services/notificationServices.js @@ -6,11 +6,11 @@ angular.module("notificationServices", []) function growl(html, type) { $.bootstrapGrowl(html, { ele: '#notification-area', - type: type, //(null, 'info', 'error', 'success', 'gp', 'xp', 'hp', 'lvl','death') + type: type, //(null, 'text', 'error', 'success', 'gp', 'xp', 'hp', 'lvl', 'death', 'mp') top_offset: 20, align: 'right', //('left', 'right', or 'center') width: 250, //(integer, or 'auto') - delay: 3000, + delay: 7000, allow_dismiss: true, stackup_spacing: 10 // spacing between consecutive stacecked growls. }); diff --git a/public/manifest.json b/public/manifest.json index dee320860c..6fc51a7f19 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -11,7 +11,7 @@ "bower_components/angular-ui-router/release/angular-ui-router.js", "bower_components/angular-resource/angular-resource.min.js", "bower_components/angular-ui/build/angular-ui.js", - "bower_components/angular-ui-utils/modules/keypress/keypress.js", + "bower_components/angular-ui-utils/ui-utils.min.js", "bower_components/angular-loading-bar/build/loading-bar.js", "bower_components/Angular-At-Directive/src/at.js", "bower_components/Angular-At-Directive/src/caret.js", diff --git a/src/controllers/user.js b/src/controllers/user.js index baf8a37055..4937e57892 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -371,7 +371,7 @@ api.cast = function(req, res) { if (group) { series.push(function(cb2){ - var message = '`'+user.profile.name+' casts '+spell.text + (type=='user' ? ' on @'+found.profile.name : ' for the party')+'.`'; + var message = '`'+user.profile.name+' casts '+spell.text + (type=='user' ? ' on '+found.profile.name : ' for the party')+'.`'; group.sendChat(message); group.save(cb2); }) diff --git a/src/models/group.js b/src/models/group.js index 7ce332e39d..9d17f5d55d 100644 --- a/src/models/group.js +++ b/src/models/group.js @@ -182,7 +182,7 @@ GroupSchema.statics.collectQuest = function(user, progress, cb) { return m; }, []); foundText = foundText ? foundText.join(', ') : 'nothing'; - group.sendChat("`<" + user.profile.name + "> found "+foundText+".`"); + group.sendChat("`" + user.profile.name + " found "+foundText+".`"); group.markModified('quest.progress.collect'); // Still needs completing @@ -210,7 +210,7 @@ GroupSchema.statics.bossQuest = function(user, progress, cb) { 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 + "> for " + (progress.up.toFixed(1)) + " damage, <" + quest.boss.name + "> attacks party for " + Math.abs(down).toFixed(1) + " damage.`"); + 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.`"); // Everyone takes damage var series = [ diff --git a/src/models/task.js b/src/models/task.js index e160a09971..4fcd03ff54 100644 --- a/src/models/task.js +++ b/src/models/task.js @@ -39,6 +39,14 @@ var HabitSchema = new Schema( , { _id: 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'}, @@ -53,6 +61,8 @@ var DailySchema = new Schema( s: {type: Boolean, 'default': true}, su: {type: Boolean, 'default': true} }, + collapseChecklist:collapseChecklist, + checklist:checklist, streak: {type: Number, 'default': 0} }, TaskSchema) , { _id: false } @@ -62,7 +72,9 @@ var TodoSchema = new Schema( _.defaults({ type: {type:String, 'default': 'todo'}, completed: {type: Boolean, 'default': false}, - 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 + 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 } ); diff --git a/src/models/user.js b/src/models/user.js index 4638f8544b..ecd0fe38a6 100644 --- a/src/models/user.js +++ b/src/models/user.js @@ -46,7 +46,9 @@ var UserSchema = new Schema({ snowball: Number, streak: Number, challenges: Array, - quests: Schema.Types.Mixed + quests: Schema.Types.Mixed, + rebirths: Number, + rebirthLevel: Number }, auth: { facebook: Schema.Types.Mixed, @@ -95,7 +97,8 @@ var UserSchema = new Schema({ partyEnabled: Boolean, // FIXME do we need this? contributor: Boolean, classSelected: {type: Boolean, 'default': false}, - mathUpdates: Boolean + mathUpdates: Boolean, + rebirthEnabled: {type: Boolean, 'default': false} }, history: { exp: Array, // [{date: Date, value: Number}], // big peformance issues if these are defined diff --git a/src/routes/apiv2.js b/src/routes/apiv2.js index a44d5e8b1c..065d152c8c 100644 --- a/src/routes/apiv2.js +++ b/src/routes/apiv2.js @@ -65,7 +65,8 @@ router['delete']('/user', auth.auth, user['delete']); router.post('/user/revive', auth.auth, cron, user.revive); // Shared.ops router.post('/user/reroll', auth.auth, cron, user.reroll); // Shared.ops router.post('/user/reset', auth.auth, user.reset); // Shared.ops -router.post('/user/sleep', auth.auth, cron, user.sleep); //Shared.opss +router.post('/user/sleep', auth.auth, cron, user.sleep); //Shared.ops +router.post('/user/rebirth', auth.auth, cron, user.rebirth); //Shared.ops router.post('/user/class/change', auth.auth, cron, user.changeClass); //Shared.ops | query={class} router.post('/user/class/allocate', auth.auth, cron, user.allocate); //Shared.ops | query={stat} diff --git a/src/seed.js b/src/seed.js index 3acd06e47e..becd3cf94b 100644 --- a/src/seed.js +++ b/src/seed.js @@ -1,6 +1,7 @@ require('coffee-script') // for habitrpg-shared var nconf = require('nconf'); -require('./config'); +var utils = require('./utils'); +utils.setupConfig(); var async = require('async'); var mongoose = require('mongoose'); User = require('./models/user').model; @@ -31,4 +32,5 @@ async.waterfall([ ],function(err){ if (err) throw err; console.log("Done initializing database"); + mongoose.disconnect(); }) diff --git a/test/e2e/e2e.js b/test/e2e/e2e.js new file mode 100644 index 0000000000..8bbdbe8a62 --- /dev/null +++ b/test/e2e/e2e.js @@ -0,0 +1,61 @@ +'use strict'; + +describe('front page', function() { + beforeEach(function(){ + browser.ignoreSynchronization = true; + browser.get('/'); + browser.sleep(1000); + }); + + // based on https://github.com/angular/protractor/issues/114#issuecomment-29046939 + afterEach(function(){ + var currentSpec = jasmine.getEnv().currentSpec; + var passed = currentSpec.results().passed(); + if(!passed){ + var filename = 'exception_' + currentSpec.description + '.png'; + browser.takeScreenshot().then(function(png){ + var fs = require('fs'); + var buffer = new Buffer(png, 'base64'); + var stream = fs.createWriteStream(filename); + stream.write(buffer); + stream.end(); + }); + } + }); + + it('shows the front page', function(){ + var button = element(by.className('btn')); + expect(button.getText()).toEqual('Play'); + }); + + it("don't login when using wrong credentials", function(){ + var button = element(by.className('btn')); + button.click(); + browser.sleep(1000); + element(by.model('loginUsername')).sendKeys('username'); + element(by.model('loginPassword')).sendKeys('pass'); + var login = element(by.css("#login-tab input[value='Login']")); + login.click(); + var alertDialog = browser.switchTo().alert(); + expect(alertDialog.getText()).toMatch(/Username 'username' not found/); + alertDialog.accept(); + }); + + it('registers a new user', function(){ + var button = element(by.className('btn')); + button.click(); + browser.sleep(1000); + var registerTab = element(by.linkText('Register')); + registerTab.click(); + element(by.model('registerVals.username')).sendKeys('user'); + element(by.model('registerVals.email')).sendKeys('user@example.com'); + element(by.model('registerVals.password')).sendKeys('pass'); + element(by.model('registerVals.confirmPassword')).sendKeys('pass'); + var register = element(by.css("#register-tab input[value='Register']")); + register.click(); + browser.sleep(1000); + browser.getCurrentUrl().then(function(url){ + expect(url).not.toMatch(/static\/front/); + }); + }); +}); \ No newline at end of file diff --git a/test/run_tests.sh b/test/run_tests.sh new file mode 100755 index 0000000000..d9f588ea06 --- /dev/null +++ b/test/run_tests.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# Configuration +TEST_DB=habitrpg_test +TEST_DB_URI="mongodb://localhost/$TEST_DB" +TEST_SERVER_PORT=3001 + +# Build assets +grunt build:dev + +# Launch Node server and Selenium +echo "Recreating test database" +mongo "$TEST_DB" --eval "db.dropDatabase()" +./node_modules/protractor/bin/webdriver-manager update +./node_modules/protractor/bin/webdriver-manager start > /dev/null & +NODE_DB_URI="$TEST_DB_URI" PORT=$TEST_SERVER_PORT node ./src/server.js > /dev/null & +NODE_PID=$! +trap "kill $NODE_PID && curl http://localhost:4444/selenium-server/driver/?cmd=shutDownSeleniumServer" EXIT + +sleep 3 # Wait for Selenium +grunt karma:continuous && ./node_modules/protractor/bin/protractor protractor.conf.js diff --git a/views/options/inventory/inventory.jade b/views/options/inventory/inventory.jade index ce935dc15a..54f634682c 100644 --- a/views/options/inventory/inventory.jade +++ b/views/options/inventory/inventory.jade @@ -137,3 +137,8 @@ script(type='text/ng-template', id='partials/options.inventory.drops.html') p | {{Content.spells.special.snowball.value}} span.Pet_Currency_Gem1x.inline-gems + div(ng-show='user.flags.rebirthEnabled') + button.customize-option(popover='Begin a new character at Level 1 while retaining achievements, collectibles, and tasks with history.', popover-title='Orb of Rebirth', popover-trigger='mouseenter', popover-placement='left', ng-click='modals.rebirth = true', class='rebirth_orb') + p + | 8 + span.Pet_Currency_Gem1x.inline-gems diff --git a/views/options/profile.jade b/views/options/profile.jade index d813eefd00..53972570e5 100644 --- a/views/options/profile.jade +++ b/views/options/profile.jade @@ -167,25 +167,25 @@ script(id='partials/options.profile.stats.html', type='text/ng-template') table.table.table-striped tr td - i.icon-question-sign(popover-title='Strength', popover='Strength increases damage you inflict to tasks (reducing redness), and increases critical hit chance.', popover-trigger='mouseenter') + i.icon-question-sign(popover-title='Strength', popover='Strength reduces task threat (redness), increases the Gold and Experience boost from random "critical hits," and helps deal damage to boss monsters.', popover-trigger='mouseenter') | STR: {{user.stats.str}} td a.btn.btn-primary(ng-show='user.stats.points', ng-click='allocate("str")') + tr td - i.icon-question-sign(popover-title='Constitution', popover='Constitution decreases damage inflicted by tasks, and increases the effectiveness of healing and buff spells.', popover-trigger='mouseenter') + i.icon-question-sign(popover-title='Constitution', popover='Constitution reduces the damage you take, whether from negative Habits, missed Dailies, or the attacks of boss monsters.', popover-trigger='mouseenter') | CON: {{user.stats.con}} td a.btn.btn-primary(ng-show='user.stats.points', ng-click='allocate("con")') + tr td - i.icon-question-sign(popover-title='Perception', popover='Perception increases drops and gold, and can help with critical hits by spells.', popover-trigger='mouseenter') + i.icon-question-sign(popover-title='Perception', popover='Perception increases how much Gold you earn, and increases the chance of finding items when scoring tasks.', popover-trigger='mouseenter') | PER: {{user.stats.per}} td a.btn.btn-primary(ng-show='user.stats.points', ng-click='allocate("per")') + tr td - i.icon-question-sign(popover-title='Intelligence', popover='Intelligence increases your overall mana, spell effectiveness, and experience gain.', popover-trigger='mouseenter') + i.icon-question-sign(popover-title='Intelligence', popover='Intelligence increases how much Experience you earn, and determines your maximum Mana available for class abilities.', popover-trigger='mouseenter') | INT: {{user.stats.int}} td a.btn.btn-primary(ng-show='user.stats.points', ng-click='allocate("int")') + diff --git a/views/options/settings.jade b/views/options/settings.jade index b74cf26481..5a5f542f01 100644 --- a/views/options/settings.jade +++ b/views/options/settings.jade @@ -48,7 +48,8 @@ script(type='text/ng-template', id='partials/options.settings.settings.html') div(ng-if='!user.preferences.disableClasses') button.btn(ng-click='showClassesTour()', popover-trigger='mouseenter', popover-placement='right', popover="Show the tour for using the class system.") Show Classes Tour - div(ng-show='user.auth.local') + //- Why is ng-if='user.auth.local' validating for users *without* user.auth.local (facebook users)? adding .username here for extra + div(ng-if='user.auth.local.username') hr h4 Change Password form(ng-submit='changePassword(changePass)', ng-show='user.auth.local') diff --git a/views/shared/header/avatar.jade b/views/shared/header/avatar.jade index fd9ed71529..eb90fa239a 100644 --- a/views/shared/header/avatar.jade +++ b/views/shared/header/avatar.jade @@ -46,3 +46,4 @@ figure.herobox(ng-click='spell ? castEnd(profile, "user", $event) : clickMember( .avatar-level(ng-class='userLevelStyle(profile,"label")') i.icon-circle-arrow-up.icon-white(ng-show='profile.stats.buffs.str || profile.stats.buffs.per || profile.stats.buffs.con || profile.stats.buffs.int || profile.stats.buffs.stealth', tooltip='Buffed', style='margin-right:5px;') | Lvl {{profile.stats.lvl}} + i.icon-plus-sign.icon-white(ng-show='profile.achievements.rebirths', tooltip='Reborn, max level {{profile.achievements.rebirthLevel}}', style='margin-left:5px') diff --git a/views/shared/modals/achievements.jade b/views/shared/modals/achievements.jade index a30a15d2bd..ef14be21bf 100644 --- a/views/shared/modals/achievements.jade +++ b/views/shared/modals/achievements.jade @@ -7,7 +7,7 @@ div(modal='modals.achievements.streak') .achievement.achievement-thermometer | You have stacked your "Streaker" Achievement! Every 21 days of streak, you gain 1 achievement point here. .modal-footer - button.btn.btn-default.cancel(ng-click='modals.achievements.streak = false') Ok + button.btn.btn-default.cancel(ng-click='modals.achievements.streak = false') OK // Max Gear div(modal='modals.achievements.ultimateGear') @@ -17,7 +17,7 @@ div(modal='modals.achievements.ultimateGear') .achievement.achievement-armor | You have earned the "Ultimate Gear" Achievement for upgrading to the maximum gear set! .modal-footer - button.btn.btn-default.cancel(ng-click='modals.achievements.ultimateGear = false') Ok + button.btn.btn-default.cancel(ng-click='modals.achievements.ultimateGear = false') OK // Beast Master div(modal='modals.achievements.beastMaster') @@ -28,7 +28,7 @@ div(modal='modals.achievements.beastMaster') .achievement.achievement-rat | You have earned the "Beast Master" Achievement for collecting all the pets! .modal-footer - button.btn.btn-default.cancel(ng-click='modals.achievements.beastMaster = false') Ok + button.btn.btn-default.cancel(ng-click='modals.achievements.beastMaster = false') OK // Contributor div(modal='user.flags.contributor') @@ -40,7 +40,15 @@ div(modal='user.flags.contributor') {{user.profile.name}}, you awesome person! You're now a level {{user.contributor.level}} contributor for helping HabitRPG. See what prizes you've earned for your contribution! .modal-footer - button.btn.btn-default.cancel(ng-click='set({"flags.contributor":false})') Ok - + button.btn.btn-default.cancel(ng-click='set({"flags.contributor":false})') OK +//Rebirth +div(modal='modals.achievements.rebirth') + .modal-header + h3 Achievement! + .modal-body + .achievement.achievement-sun + | You've begun a new adventure! This is Rebirth {{user.achievements.rebirths}} for you, and the highest Level you've attained is {{user.achievements.rebirthLevel}}. To stack this Achievement, begin your next new adventure when you've reached an even higher Level! + .modal-footer + button.btn.btn-default.cancel(ng-click='modals.achievements.rebirth = false') OK diff --git a/views/shared/modals/classes.jade b/views/shared/modals/classes.jade index cc598ecac6..49c4ac808e 100644 --- a/views/shared/modals/classes.jade +++ b/views/shared/modals/classes.jade @@ -1,11 +1,10 @@ .modal(ng-if='!user.flags.classSelected && user.stats.lvl >= 10', data-backdrop=true, ng-controller='UserCtrl') .modal-header - h3 Choose Your Class! + h3 Choose Your Class! .modal-body.select-class - p Select your class. Click each class for more information. See Wikia for thorough details. .row-fluid .span3(ng-click='selectedClass = "warrior"') - h5 Warrior + h5 Warrior figure.herobox(ng-class='{"selected-class": selectedClass=="warrior"}') .character-sprites span(class='skin_{{user.preferences.skin}}') @@ -19,7 +18,7 @@ span(class='shield_warrior_5') span(class='weapon_warrior_6') .span3(ng-click='selectedClass = "wizard"') - h5 Mage + h5 Mage figure.herobox(ng-class='{"selected-class": selectedClass=="wizard"}') .character-sprites span(class='skin_{{user.preferences.skin}}') @@ -33,7 +32,7 @@ span(class='shield_wizard_5') span(class='weapon_wizard_6') .span3(ng-click='selectedClass = "rogue"') - h5 Rogue + h5 Rogue figure.herobox(ng-class='{"selected-class": selectedClass=="rogue"}') .character-sprites span(class='skin_{{user.preferences.skin}}') @@ -47,7 +46,7 @@ span(class='shield_rogue_6') span(class='weapon_rogue_6') .span3(ng-click='selectedClass = "healer"') - h5 Healer + h5 Healer figure.herobox(ng-class='{"selected-class": selectedClass=="healer"}') .character-sprites span(class='skin_{{user.preferences.skin}}') @@ -61,10 +60,10 @@ span(class='shield_healer_5') span(class='weapon_healer_6') br - .well(ng-show='selectedClass=="warrior"') Warriors deal damage to tasks (reducing redness), have moderate defense, and improved critical hits. - .well(ng-show='selectedClass=="wizard"') Mages deal damage to tasks (reducing redness), can debuff tasks, and they gain experience rapidly. - .well(ng-show='selectedClass=="rogue"') Rogues finds more drops and gold. They can also "go stealth" to avoid damage at the end of a day. - .well(ng-show='selectedClass=="healer"') Healers have high defense against damage, and can heal themselves and other players in the party, as well as buff players. + .well(ng-show='selectedClass=="warrior"') Warriors keep tasks in line, each success making later slip-ups less harsh. They also score impressive "critical hits," which randomly give bonus Gold and Experience for scoring a task. Play a Warrior if you don't want to be punished too hard for inconsistency, or if you find strong motivation from unpredictable jackpot-style rewards! + .well(ng-show='selectedClass=="wizard"') Mages learn swiftly, gaining Experience and Levels faster than other classes. They also get a great deal of Mana for using special abilities. Play a Mage if you enjoy the tactical game aspects of Habit, or if you are strongly motivated by leveling up and unlocking advanced features! + .well(ng-show='selectedClass=="rogue"') Rogues love to accumulate wealth, gaining more Gold than anyone else, and are adept at finding random items. Their iconic Stealth ability lets them duck the consequences of missed Dailies. Play a Rogue if you find strong motivation from Rewards and Achievements, striving for loot and badges! + .well(ng-show='selectedClass=="healer"') Healers stand impervious against harm, and extend that protection to others. Missed Dailies and bad Habits don't faze them much, and they have ways to recover Health from failure. Play a Healer if you enjoy assisting others in your Party, or if the idea of cheating Death through hard work inspires you! .modal-footer button.btn.btn-small.btn-danger.cancel(ng-click='user.ops.disableClasses({})', popover-placement='top', popover-trigger='mouseenter', popover="Can't be bothered with classes? Opt out - you'll be a warrior and your points handled automatically. You can enable classes later under Settings") Opt Out diff --git a/views/shared/modals/index.jade b/views/shared/modals/index.jade index bb7dd5ac8e..db481615bb 100644 --- a/views/shared/modals/index.jade +++ b/views/shared/modals/index.jade @@ -8,4 +8,5 @@ include ./members include ./settings include ./drops include ./classes -include ./quests \ No newline at end of file +include ./quests +include ./rebirth \ No newline at end of file diff --git a/views/shared/modals/new-stuff.jade b/views/shared/modals/new-stuff.jade index f9cb2aeaa9..4d774d588c 100644 --- a/views/shared/modals/new-stuff.jade +++ b/views/shared/modals/new-stuff.jade @@ -26,48 +26,73 @@ div(modal='modals.newStuff') a(target='_blank', href='https://twitter.com/Mihakuu') Bailey .popover-content - h3 Winter Wonderland Event! - - h4 Part 2: Rescue the Bears + h4 Winter Wonderland Event Part 3: Party! table.table.table-striped tr td - h5 Quests & Bosses! - p A beast is roaring in the distant mountains, mysterious tracks have appeared in the snow. A new feature has been unlocked, Quests & Bosses. As a holiday present, HabitRPG gives you your first quest: "Trapper Santa". Check your inventory, you have until Jan 31 to complete it! + h5 Happy New Year! + p Happy New Year! Join the NPCs and Staff in showing off your new Absurd party hat.... and have a great night! + small.muted by @lemoness + tr + td + h5 Rebirth + p Nothing says New Year like a fresh start. Now when you reach level 50, Ultimate Gear, or BeastMaster, you can begin anew with the most prestigious of achievements: Rebirth. Read more here. But take heed! Scouts have reported monster sightings, harbinged by Trapper Santa. You may need all the strength you can muster come late January, Rebirth is for the hard-core. + small.muted by @SabreCat + tr + td + h5 Checklists + p Checklists are here! You can break your Dailies and To-Dos down into bite-size chunks. Their game mechanic takes some learning, so read more here. + small.muted by @lefnire + tr + td + h5 Task Icons & Markdown + p Task titles now support Markdown and Emoji, so you can create something like this. Read more here. + small.muted by @lefnire - p By @lefnire, @pandoro, @Shaners - small.muted 12/25/2013 + small.muted 12/31/2013 - div.muted - hr - p It's time for HabitRPG's biggest event yet - Winter Wonderland! The fun starts today, on the first day of winter, and ends on January 31st - HabitRPG's birthday. - p Get prepared to build new habits, earn fun drops, hold your party members accountable for their tasks, and decorate your avatar. Various features will be rolling out over the course of the event, so expect many updates! For starters... - h4 Part 1: The Great Snowball Fight - table.table.table-striped - tr - td - h5 NPC Decorations - p Looks like everyone is really getting into the winter spirit! Check out the new NPC sprites. (And I heard a rumor that the final NPC might show up, just in time for the new year...) - tr - td - .customize-option.hair_bangs_1_winternight.pull-right - h5 Limited-Edition Holiday Hair-Colors - p Now your avatar can dye their hair Candy Cane, Frost, Winter Sky, or Holly! You'll only be able to purchase these hair colors until January 31st, when they will be retired. - tr - td - .shop_snowball.pull-right - h5 The Great HabitRPG Snowball Fight - p Yes, you can now buy snowballs and hurl them at all your friends... to, uh, help them improve their habits. How? Weeeeellll, let's just say that after getting walloped, they might find themselves needing some extra gold to escape their predicament... - //-span.shop_head_special_candycane.item-img.shop-sprite - tr - td - h5 More to Come - p A beast is roaring in the distant mountains, mysterious tracks have appeared in the snow, and Lemoness is furiously crocheting something sparkly. - p It's going to be a wild winter. - p By @lemoness - small.muted 12/20/2013 + hr + h5 12/25/2013 + h4 Winter Wonderland Event Part 2: Rescue the Bears + table.table.table-striped + tr + td + h5 Quests & Bosses! + p A beast is roaring in the distant mountains, mysterious tracks have appeared in the snow. A new feature has been unlocked, Quests & Bosses. As a holiday present, HabitRPG gives you your first quest: "Trapper Santa". Check your inventory, you have until Jan 31 to complete it! + p By @lefnire, @pandoro, @Shaners + + hr + + + h5 12/20/2013 + h4 Winter Wonderland Event Part 1: The Great Snowball Fight + p It's time for HabitRPG's biggest event yet - Winter Wonderland! The fun starts today, on the first day of winter, and ends on January 31st - HabitRPG's birthday. + p Get prepared to build new habits, earn fun drops, hold your party members accountable for their tasks, and decorate your avatar. Various features will be rolling out over the course of the event, so expect many updates! For starters... + table.table.table-striped + tr + td + h5 NPC Decorations + p Looks like everyone is really getting into the winter spirit! Check out the new NPC sprites. (And I heard a rumor that the final NPC might show up, just in time for the new year...) + tr + td + .customize-option.hair_bangs_1_winternight.pull-right + h5 Limited-Edition Holiday Hair-Colors + p Now your avatar can dye their hair Candy Cane, Frost, Winter Sky, or Holly! You'll only be able to purchase these hair colors until January 31st, when they will be retired. + tr + td + .shop_snowball.pull-right + h5 The Great HabitRPG Snowball Fight + p Yes, you can now buy snowballs and hurl them at all your friends... to, uh, help them improve their habits. How? Weeeeellll, let's just say that after getting walloped, they might find themselves needing some extra gold to escape their predicament... + //-span.shop_head_special_candycane.item-img.shop-sprite + tr + td + h5 More to Come + p A beast is roaring in the distant mountains, mysterious tracks have appeared in the snow, and Lemoness is furiously crocheting something sparkly. + p It's going to be a wild winter. + + p By @lemoness hr diff --git a/views/shared/modals/rebirth.jade b/views/shared/modals/rebirth.jade new file mode 100644 index 0000000000..0139276afd --- /dev/null +++ b/views/shared/modals/rebirth.jade @@ -0,0 +1,45 @@ +// Created by Sabe on 12/22/13. + +div(modal='modals.rebirthEnabled') + .modal-header + h3 Rebirth: New Adventure Available! + .modal-body + .rebirth_orb.pull-left(style='margin-right: 5px') + p + span. + You've unlocked Rebirth! This special Market item allows you to begin a new game at level 1 while keeping your tasks, achievements, pets, and more. Use it to breathe new life into HabitRPG if you feel you've achieved it all, or to experience new features with the fresh eyes of a beginning character! + .modal-footer + button.btn.btn-default.cancel(ng-click='modals.rebirthEnabled = false') Close + +div(modal='modals.rebirth') + .modal-header + h3 Rebirth: Begin a New Adventure + .modal-body + include ../gems + .rebirth_orb.pull-left(style='margin-right: 5px') + p Rebirth starts your character over from Level 1, as if you had created a new account. + br + p Your adventure begins anew with a clean slate: + .unordered + li You return to full Health. + li You have no Experience, Gold, or equipment. + li Your Habits, Dailies, and To-Dos reset to yellow, and streaks reset. + li You have the starting class of Warrior until you earn a new class. + br + p Your new character inherits a few things from their predecessor: + .unordered + li Tasks, history, and settings remain. + li Challenge, Guild, and Party memberships remain. + li Gems, backer tiers, and contributor levels remain. + li Items obtained from Gems or drops (such as pets and mounts) remain, though you cannot access them until you unlock them again. + br + .achievement-sun.pull-left(style='margin-right: 5px') + p You also earn an Achievement for beginning a new adventure! + .modal-footer + button.btn.btn-large.cancel(ng-click='modals.rebirth = false') Never mind + span(ng-if='user.balance < 2') + a.btn.btn-success.btn-large(ng-click="modals.buyGems = true") Buy More Gems + span.gem-cost Not enough Gems + span(ng-if='user.balance >= 2', ng-controller='SettingsCtrl') + a.btn.btn-danger.btn-large(ng-click='rebirth()') Be Reborn + span.gem-cost 8 Gems diff --git a/views/shared/profiles/achievements.jade b/views/shared/profiles/achievements.jade index ae71005565..0be4d10dcc 100644 --- a/views/shared/profiles/achievements.jade +++ b/views/shared/profiles/achievements.jade @@ -3,7 +3,6 @@ //
// // - // // // // @@ -57,6 +56,14 @@ small Has found all 90 pets (insanely difficult, give this user props!) hr + div(ng-if='profile.achievements.rebirths') + .achievement.achievement-sun + h5(ng-if='profile.achievements.rebirths == 1') Began a New Adventure + h5(ng-if='profile.achievements.rebirths > 1') Began {{profile.achievements.rebirths}} New Adventures + small. + Used an Orb of Rebirth to start over after attaining Level {{profile.achievements.rebirthLevel}}. + hr + div(ng-if='profile.achievements.helpedHabit') .achievement.achievement-tree h5 Helped Habit Grow @@ -100,4 +107,4 @@ .achievement.achievement-snowball h5 Annoying Friends small Got snowballed {{profile.achievements.snowball}} times by party members. - hr \ No newline at end of file + hr diff --git a/views/shared/tasks/lists.jade b/views/shared/tasks/lists.jade index 1de3dd95a7..be8b21e6cd 100644 --- a/views/shared/tasks/lists.jade +++ b/views/shared/tasks/lists.jade @@ -46,10 +46,10 @@ script(id='templates/habitrpg-tasks.html', type="text/ng-template") // Static Rewards ul.items.rewards(ng-if='main && list.type=="reward" && user.flags.itemsEnabled') - li.task.reward-item(ng-repeat='item in itemStore') + li.task.reward-item(ng-repeat='item in itemStore',popover-trigger='mouseenter', popover-placement='top', popover='{{item.notes}}') // right-hand side control buttons .task-meta-controls - span.task-notes(popover-trigger='mouseenter', popover-placement='left', popover='{{item.notes}}', popover-title='{{item.text}}') + span.task-notes i.icon-comment //left-hand size commands .task-controls @@ -62,9 +62,9 @@ script(id='templates/habitrpg-tasks.html', type="text/ng-template") // Winter Event ul.items.rewards(ng-if='main && list.type=="reward" && (user.items.special.snowball>0 || user.stats.buffs.snowball)') - li.task.reward-item(ng-if='user.items.special.snowball>0') + li.task.reward-item(ng-if='user.items.special.snowball>0',popover-trigger='mouseenter', popover-placement='top', popover='{{Content.spells.special.snowball.notes}}') .task-meta-controls - span.task-notes(popover-trigger='mouseenter', popover-placement='left', popover='{{Content.spells.special.snowball.notes}}', popover-title='{{Content.spells.special.snowball.text}}') + span.task-notes i.icon-comment //left-hand size commands .task-controls @@ -73,9 +73,9 @@ script(id='templates/habitrpg-tasks.html', type="text/ng-template") span.shop_snowball(style='display:inline-block;vertical-align:top;') // main content p.task-text {{Content.spells.special.snowball.text}} - li.task.reward-item(ng-if='user.stats.buffs.snowball') + li.task.reward-item(ng-if='user.stats.buffs.snowball',popover-trigger='mouseenter', popover-placement='top', popover='{{Content.spells.special.salt.notes}}') .task-meta-controls - span.task-notes(popover-trigger='mouseenter', popover-placement='left', popover='{{Content.spells.special.salt.notes}}', popover-title='{{Content.spells.special.salt.text}}') + span.task-notes i.icon-comment //left-hand size commands .task-controls @@ -88,9 +88,9 @@ script(id='templates/habitrpg-tasks.html', type="text/ng-template") // Spells ul.items(ng-if='main && list.type=="reward" && user.stats.class && !user.preferences.disableClasses') - li.task.reward-item(ng-repeat='(k,spell) in Content.spells[user.stats.class]', ng-show='user.stats.lvl >= spell.lvl') + li.task.reward-item(ng-repeat='(k,spell) in Content.spells[user.stats.class]', ng-if='user.stats.lvl >= spell.lvl',popover-trigger='mouseenter', popover-placement='top', popover='{{spell.notes}}') .task-meta-controls - span.task-notes(popover-trigger='mouseenter', popover-placement='left', popover='{{spell.notes}}', popover-title='{{spell.text}}') + span.task-notes i.icon-comment //left-hand size commands .task-controls diff --git a/views/shared/tasks/task.jade b/views/shared/tasks/task.jade index 5f4922267d..1b78413235 100644 --- a/views/shared/tasks/task.jade +++ b/views/shared/tasks/task.jade @@ -1,10 +1,10 @@ -li(bindonce='list', ng-repeat='task in obj[list.type+"s"] | completedFilter: list.showCompleted', class='task {{Shared.taskClasses(task, user.filters, user.preferences.dayStart, user.lastCron, list.showCompleted, main)}}', ng-click='spell && castEnd(task, "task", $event)', ng-class='{"cast-target":spell}') +li(bindonce='list', bo-id='"task-"+task.id', ng-repeat='task in obj[list.type+"s"]', class='task {{Shared.taskClasses(task, user.filters, user.preferences.dayStart, user.lastCron, list.showCompleted, main)}}', ng-click='spell && castEnd(task, "task", $event)', ng-class='{"cast-target":spell}', popover-trigger='mouseenter', popover-placement='top', popover='{{task.notes}}') // right-hand side control buttons .task-meta-controls // Due Date span.task-date(ng-if='task.type=="todo" && task.date') - | {{task.date}} + span(ng-class='{"label label-important":moment(task.date).isBefore(_today)}') {{moment(task.date).format('MM/DD')}} // Streak span(ng-show='task.streak') {{task.streak}} span(tooltip='Streak Counter') @@ -12,6 +12,9 @@ li(bindonce='list', ng-repeat='task in obj[list.type+"s"] | completedFilter: lis // Icons only available if you own the tasks (aka, hidden from challenge stats) span(ng-if='!obj._locked') + a.badge(ng-if='task.checklist[0]', ng-class='{"badge-success":checklistCompletion(task.checklist) == task.checklist.length}', ng-click='collapseChecklist(task)') + {{checklistCompletion(task.checklist)}}/{{task.checklist.length}} + i.icon-tags(tooltip='{{appliedTags(user.tags, task.tags)}}', ng-hide='Shared.noTags(task.tags)') // edit a(ng-hide='task._editing', ng-click='editTask(task)', tooltip='Edit') @@ -36,7 +39,7 @@ li(bindonce='list', ng-repeat='task in obj[list.type+"s"] | completedFilter: lis a(ng-show='task.history', ng-click='toggleChart(obj._id+task.id, task)', tooltip='Progress') i.icon-signal // notes - span.task-notes(ng-show='task.notes && !task._editing', popover-trigger='mouseenter', popover-placement='left', popover='{{task.notes}}', popover-title='{{task.text}}') + span.task-notes(ng-show='task.notes && !task._editing') i.icon-comment // left-hand side checkbox @@ -60,10 +63,16 @@ li(bindonce='list', ng-repeat='task in obj[list.type+"s"] | completedFilter: lis input.visuallyhidden.focusable(ng-if='!$state.includes("tasks")', id='box-{{obj._id}}_{{task.id}}', type='checkbox') label(for='box-{{obj._id}}_{{task.id}}') // main content - p.task-text + div.task-text(ng-dblclick='task._editing ? saveTask(task) : editTask(task)') markdown(ng-model='task.text',target='_blank') //-| {{task.text}} + div(ng-if='task.checklist && !$state.includes("options.social.challenges") && !task.collapseChecklist && !task._editing') + fieldset.option-group + label.checkbox(ng-repeat='item in task.checklist') + input(type='checkbox',ng-model='item.completed',ng-change='saveTask(task,true)') + | {{item.text}} + // edit/options dialog div(ng-if='task._editing') .task-options @@ -96,6 +105,23 @@ li(bindonce='list', ng-repeat='task in obj[list.type+"s"] | completedFilter: lis | | a(ng-click="unlink(task, 'remove-all')") Remove Them + // Checklists + a.btn.btn-small(ng-if='!task.checklist[0] && (task.type=="daily" || task.type=="todo")',ng-click='addChecklist(task)') + i.icon-tasks + | Add Checklist + form.checklist-form(ng-if='task.checklist') + fieldset.option-group.task-checklist(ng-if='!$state.includes("options.social.challenges")') + legend.option-title + | Checklist + i.icon.icon-question-sign(popover='For Dailies, partially-completed Dailies will cut damage (eg, a 4-item checklist with 3 completed, your Daily will hurt you 25% of the damage). For To-Dos, when completed, your points will be multiplied by the number of checklist-items (eg, a 4-item To-Do will be 4x the Exp & GP).',popover-trigger='mouseenter',popover-placement='bottom') + ul.unstyled + li(ng-repeat='item in task.checklist') + a.pull-right.checklist-icons(ng-click='removeChecklistItem(task,$index,true)') + i.icon-trash(tooltip='Delete') + input(type='checkbox',ng-model='item.completed',ng-change='saveTask(task,true)') + input.inline-edit(type='text',ng-model='item.text',ui-keyup="{13:'addChecklistItem(task,$event,$index)','8 49':'removeChecklistItem(task,$index)','38 40':'navigateChecklist(task,$index,$event)'}")//-,ng-blur='saveTask(task,true)') + + form(ng-submit='saveTask(task)') // text & notes fieldset.option-group @@ -138,7 +164,7 @@ li(bindonce='list', ng-repeat='task in obj[list.type+"s"] | completedFilter: lis // if Todos, the due date fieldset.option-group(ng-if='task.type=="todo" && !task.challenge.id') legend.option-title Due Date - input.option-content.datepicker(type='text', data-date-format='mm/dd/yyyy', ng-model='task.date') + input.option-content.datepicker(type='text', datepicker-popup='MM/dd/yyyy', ng-model='task.date') fieldset.option-group(ng-if='!$state.includes("options.social.challenges")') legend.option-title Tags @@ -159,9 +185,8 @@ li(bindonce='list', ng-repeat='task in obj[list.type+"s"] | completedFilter: lis button.task-action-btn.tile(type='button', ng-class='{active: task.priority==1.5}', ng-click='task.challenge.id || (task.priority=1.5)') Medium button.task-action-btn.tile(type='button', ng-class='{active: task.priority==2}', ng-click='task.challenge.id || (task.priority=2)') Hard //span(ng-if='task.type=="daily" && !task.challenge.id') - br span(ng-if='task.type=="daily"') - legend.option-title Restore Streak + legend.option-title.pull-left Restore Streak input.option-content(type='number', ng-model='task.streak') legend.option-title Attributes