diff --git a/.gitignore b/.gitignore index c0f0054b81..a7d8e7d2e6 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,5 @@ build src/*/*.map src/*/*/*.map test/*.js -test/*.map \ No newline at end of file +test/*.map +public/docs diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000000..83f4e22f0a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +language: node_js +node_js: + - '0.8' + - '0.10' +before_script: + - 'npm install -g bower grunt-cli' + - 'bower install' diff --git a/DOCS-README.md b/DOCS-README.md new file mode 100644 index 0000000000..e21c420b16 --- /dev/null +++ b/DOCS-README.md @@ -0,0 +1,77 @@ +# HabitRPG Docs Project + +Generated documentation for all of HabitRPG's source files will be kept in the folder and subfolders. If you would like to use the existing documentation, or contribute to the documentation efforts, read on. + +## Viewing Docs + +You're looking at it! + +Unless you are viewing this file directly from GitHub, you should see a list of files and folders to the left of this readme. + +If you are working locally, you can goto `localhost:3000/docs/` and view the Docs. + +All documentation is generated from comments in the code, into HTML files in the `public/docs/` folder. After you have cloned the HabitRPG repo locally, and done all the `npm install` goodness, the Docs should generate automagickly when you run `grunt run:dev` + +## What I do now? + +Well if you know Markdown, simply add detailed comments in the code using Markdown syntax. + +```` +/* +User.js +======= + +Defines the user data model (schema) for use via the API. +*/ + +// Dependencies +// ------------ +var mongoose = require("mongoose"); +var Schema = mongoose.Schema; +var helpers = require('habitrpg-shared/script/helpers'); +var _ = require('lodash');.... +```` + +As you can see, you can use both multiline style comments `/* fancy stuff */` and inline comments `// Ooooh my`. + +The exception being end of line comments +`text: String, // example: Wolf ` + +The above will not be on the "pretty print" side of the Docs, but will stay in the code. An example use case for end of line comments would be for FIXME notes. + +Add anything that would be helpful to a developer regarding how to use the functions, variables, and objects associated with HabitRPG. + +**All documentation should be committed as pull request to the `docs-project` branch of HabitRPG.** Since we are adding comments directly to the code, I don't want to be editing files used for beta or master. We can merge in the docs after we're sure we didn't break anything. + +### jsDoc Syntax + +Yes, the generator also supports jsDoc-style comments such as +```` +@param {Array} files Array of file paths relative to the `inDir` to generate documentation for. +```` + +**Important Note:** If you use the `@param` syntax, you must use multiline comment blocks (ie `/* stuff */`), otherwise they won't be parsed like parameters. + +This may or may not be useful for HabitRPG. Example use cases: +- Documenting the API +- Javascript Models + +## Okay, I added great comments. Now what? + +If you're running locally, just re-run `grunt run:dev`. Any changed docs will be automagickly updated. + +Once you're satisfied with the output, push your changes to your fork of HabitRPG and issue a Pull Request on the `docs-project` branch. + +It's that easy! + +## Tech Info + +The generator we are using is [Docker](https://github.com/jbt/docker), which is a fork of [Docco](http://jashkenas.github.io/docco/). Docker supports the same wide-range of filetypes, including being able to generate documentation for a whole project, including an index. + +We also use the [Grunt-Docker](https://github.com/Prevole/grunt-docker) node module for automatic processing. + +## Road Map + +- Customize CSS with HabitRPG specific Styling +- Explore possibilities of importing Wiki content +- Specify style guide for consistency of comments \ No newline at end of file diff --git a/Gruntfile.js b/Gruntfile.js index d9f0d64400..6c0d976cb7 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -4,6 +4,12 @@ module.exports = function(grunt) { // Project configuration. grunt.initConfig({ + karma: { + unit: { + configFile: 'karma.conf.js' + } + }, + clean: { build: ['build'] }, @@ -13,6 +19,7 @@ module.exports = function(grunt) { files: { 'build/app.js': [ 'public/bower_components/jquery/jquery.min.js', + 'public/bower_components/jquery.cookie/jquery.cookie.js', 'public/bower_components/bootstrap-growl/jquery.bootstrap-growl.min.js', 'public/bower_components/bootstrap-tour/build/js/bootstrap-tour.min.js', 'public/bower_components/angular/angular.min.js', @@ -75,7 +82,6 @@ module.exports = function(grunt) { 'public/bower_components/bootstrap/docs/assets/js/bootstrap.min.js', 'public/js/static.js', - 'public/js/services/memberServices.js', 'public/js/services/userServices.js', 'public/js/controllers/authCtrl.js' ] @@ -172,5 +178,6 @@ module.exports = function(grunt) { grunt.loadNpmTasks('grunt-concurrent'); grunt.loadNpmTasks('grunt-contrib-watch'); grunt.loadNpmTasks('grunt-hashres'); + grunt.loadNpmTasks('grunt-karma'); }; diff --git a/README.md b/README.md index 386f4715a2..c78cf4efbc 100644 --- a/README.md +++ b/README.md @@ -1,48 +1,27 @@ -HabitRPG Rewrite +HabitRPG =============== -HabitRPG Rewrite under development. Built using Angular, Express, Mongoose, Jade, Stylus, Coffeescript. +[HabitRPG](https://habitrpg.com) is an open source habit building program which treats your life like a Role Playing Game. Level up as you succeed, lose HP as you fail, earn money to buy weapons and armor. -**Note: This branch is under development, and these instructions may fall out of date. They were accurate as of August 5, 2013.** Should you encounter this, join #habitrpg on IRC (Freenode) and talk to litenull. - -Before starting install [MongoDB](http://www.mongodb.org/) and [NodeJS](http://nodejs.org/) - -The general steps are: - -1. Clone the repo -1. Install the global dependencies -1. Install all dependencies -1. Run the client - -Or, expressed in commands on the command line: - -1. `git clone --recursive -b develop https://github.com/lefnire/habitrpg.git` -1. 'npm install -g grunt-cli' (you may need to add `sudo` in front of it) -1. `cd habitrpg && npm install` -1. `grunt run:dev` - -To access the site, open http://localhost:3000 in your browser. - -There are a few other Grunt task avalaible: - -- `grunt build:dev` - Compile, concat and minify Stylus files -- `grunt build:prod` - Same as `grunt build:dev` but concat and minify Javascript files. -- `grunt nodemon` - Start the server with **nodemon**, restart when a file change but without compiling Stylus files - -# Technologies - -1. Angular, Express, Mongoose. Awesome, tried technologies. Read up on them. -1. CoffeeScript, Stylus, Jade - big debate. - 1. Jade. We need a server-side templating language so we can inject variables (`res.locals` from Express). Jade is great - because the "significant whitespace" paradigm protects you from HTML errors such as missing or mal-matched close tags, - which has been a pretty common error from multiple contribs on Habit. However, it's not very HTML-y, and makes people mad. - We'll re-visit this conversation after the rewrite is done. - 1. Stylus. We're either staying here or moving to LESS, but vanilla CSS isn't cutting it for our app. - 1. CoffeeScript. This is the hottest debate. I'm using it to rewrite, and Habit was written originally on CS. It's a - fantastic language, but it's a barrier-to-entry for potential contribs who don't know it. Will also revisit right after - the rewrite. +Built using Angular, Express, Mongoose, Jade, Stylus, Grunt and Bower. -# Windows Environment Install +# Set up HabitRPG locally + +**Windows** users should skip this section and read the one below with Windows-specific steps. + +Before starting make sure to have [MongoDB](http://www.mongodb.org/), [NodeJS and npm](http://nodejs.org/) and [Git](https://help.github.com/articles/set-up-git) installed and set up. + +1. [Fork the repository](https://help.github.com/articles/fork-a-repo) on your computer. +1. Checkout the **develop** branch where all the development happens: +`git checkout -b develop origin/develop` +1. Install **grunt-cli** npm package globally (on some systems you may need to add `sudo` in front of the command below): +`npm install -g grunt-cli bower` +1. Install the **npm** and **bower** packages: +`npm install` +1. Create a config file from the example one: +`cp config.json.example config.json` + +## Windows Environment Install 1. Set up MongoDB. Steps: 1. Download the latest production release of MongoDB from: http://www.mongodb.org/downloads @@ -50,27 +29,22 @@ There are a few other Grunt task avalaible: 1. Rename the folder from mongodb-win32-x86_64-2.4.6 to mongodb 1. Create a data\db directory under the application directory. Example: c:\apps\mongodb\data\db 1. Start up MongoDB using the following command: - 'c:\apps\mongodb\bin\mongod.exe --dbpath c:\apps\mongodb\data' + 'c:\apps\mongodb\bin\mongod.exe --dbpath c:\apps\mongodb\data' If MongoDB starts up successfully, you should see the following at the end of the logs: -Sun Sep 01 18:10:21.233 [initandlisten] waiting for connections on port 27017 +```Sun Sep 01 18:10:21.233 [initandlisten] waiting for connections on port 27017 Sun Sep 01 18:10:21.233 [websvr] admin web console waiting for connections on po -rt 28017 - +rt 28017``` + 1. Install Node.js (includes npm). Steps: 1. Download and run the latest Node.js msi installation file from http://nodejs.org/download/ -1. Create a fork of the habitrpg repository on github under your own account -1. Install Git and download angular_rewrite code repository. Steps: - 1. Install latest stable version of Git, found here: http://git-scm.com/downloads - 1. Make sure to select "Run Git from the Windows Command Prompt" during the installation process - 1. Open a command window. Navigate to the location where you would like the project files to live. Example: c:\projects - 1. Run git command to download angular_rewrite branch. - 'git clone --recursive -b develop https://github.com/ezinaz/habitrpg.git' (where 'ezinaz' is your account name) - 1. Run 'cd habitrpg' - 1. Create upstream remote: - 'git remote add upstream https://github.com/lefnire/habitrpg.git' - 1. Run 'git fetch upstream' - 1. Run 'npm install'. Read below for possible error message. +1. Install [Git](https://help.github.com/articles/set-up-git). +1. [Fork the repository](https://help.github.com/articles/fork-a-repo) on your computer. +1. Checkout the **develop** branch where all the development happens: +`git checkout -b develop origin/develop` +1. Install the **npm** packages: +`npm install` +Read below for possible error message. You might receive the following error during the 'npm install' command: > habitrpg@0.0.0-152 postinstall C:\Users\022498\Projects\habitrpg @@ -82,18 +56,29 @@ npm ERR! not ok code 0 Ignore this error and proceed with the following: -1. Run 'npm install -g grunt-cli' -1. Run 'npm install -g bower' -1. Run 'bower install -f' -1. Run 'copy config.json.example config.json' -1. `grunt run:dev` - -Open a browser to URL http://localhost:3000 to test the application. - +1. Install **grunt-cli** and **bower** npm packages globally +'npm install -g grunt-cli bower' +1. Install the **bower** packages: +'bower install -f' +1. Create a config file from the example one: +`copy config.json.example config.json` +# Run HabitRPG +HabitRPG uses [Grunt](http://gruntjs.com) as its build tool. +`grunt run:dev` compiles the **Stylus** files and start a web server. +It uses [Nodemon](https://github.com/remy/nodemon) and [grunt-contrib-watch](https://github.com/gruntjs/grunt-contrib-watch) to automatically restart the server and re-compile the files when a change is detected. +**Open a browser to URL http://localhost:3000 to test the application!** +# Technologies discussion +1. Angular, Express, Mongoose. Awesome, tried technologies. Read up on them. +1. Stylus, Jade - big debate. + 1. Jade. We need a server-side templating language so we can inject variables (`res.locals` from Express). Jade is great + because the "significant whitespace" paradigm protects you from HTML errors such as missing or mal-matched close tags, + which has been a pretty common error from multiple contribs on Habit. However, it's not very HTML-y, and makes people mad. + We'll re-visit this conversation after the rewrite is done. + 1. Stylus. We're either staying here or moving to LESS, but vanilla CSS isn't cutting it for our app. diff --git a/archive/derby_controllers/tasks.coffee b/archive/derby_controllers/tasks.coffee index 6452e72ac0..1a01c095e0 100644 --- a/archive/derby_controllers/tasks.coffee +++ b/archive/derby_controllers/tasks.coffee @@ -10,31 +10,6 @@ misc = require './misc' _.each completedIds, (id) -> user.del "tasks.#{id}"; true user.set 'todoIds', _.difference(todoIds, completedIds) - appExports.toggleChart = (e, el) -> - id = $(el).attr('data-id') - [historyPath, togglePath] = ['',''] - - switch id - when 'exp' - [togglePath, historyPath] = ['_page.charts.exp', '_user.history.exp'] - when 'todos' - [togglePath, historyPath] = ['_page.charts.todos', '_user.history.todos'] - else - [togglePath, historyPath] = ["_page.charts.#{id}", "_user.tasks.#{id}.history"] - model.set "_tasks.editing.#{id}", false - - history = model.get(historyPath) - model.set togglePath, !(model.get togglePath) - - matrix = [['Date', 'Score']] - _.each history, (obj) -> matrix.push([ moment(obj.date).format('MM/DD/YY'), obj.value ]) - data = google.visualization.arrayToDataTable matrix - options = - title: 'History' - backgroundColor: { fill:'transparent' } - chart = new google.visualization.LineChart $(".#{id}-chart")[0] - chart.draw(data, options) - ### Undo diff --git a/bower.json b/bower.json index 6411c5a5d5..e65d6e5edf 100644 --- a/bower.json +++ b/bower.json @@ -15,8 +15,8 @@ ], "dependencies": { "jquery": "~2.0.3", - "angular": "1.2.0-rc.1", - "angular-resource": "1.2.0-rc.1", + "angular": "1.2.0-rc.2", + "angular-resource": "1.2.0-rc.2", "angular-ui": "~0.4.0", "angular-bootstrap": "~0.5.0", "habitrpg-shared": "git://github.com/HabitRPG/habitrpg-shared.git#rewrite", @@ -29,12 +29,18 @@ "sticky": "*", "bootstrap-datepicker": "~1.2.0", "bootstrap": "v2.3.2", - "angular-route": "1.2.0-rc.1", + "angular-route": "1.2.0-rc.2", "angular-ui-utils": "~0.0.4", - "angular-sanitize": "1.2.0-rc.1", - "marked": "~0.2.9" + "angular-sanitize": "1.2.0-rc.2", + "marked": "~0.2.9", + "JavaScriptButtons": "git://github.com/paypal/JavaScriptButtons.git#master" }, "resolutions": { - "jquery": "~2.0.3" + "jquery": "~2.0.3", + "bootstrap": "v2.3.2", + "angular": "1.2.0-rc.2" + }, + "devDependencies": { + "angular-mocks": "1.2.0-rc.2" } } diff --git a/config.json.example b/config.json.example index c1cc52ad11..5cf560aae6 100644 --- a/config.json.example +++ b/config.json.example @@ -12,5 +12,6 @@ "SMTP_PASS":"password", "SMTP_SERVICE":"Gmail", "STRIPE_API_KEY":"aaaabbbbccccddddeeeeffff00001111", - "STRIPE_PUB_KEY":"22223333444455556666777788889999" -} \ No newline at end of file + "STRIPE_PUB_KEY":"22223333444455556666777788889999", + "PAYPAL_MERCHANT":"paypal-merchant@gmail.com" +} diff --git a/karma-e2e.conf.js b/karma-e2e.conf.js new file mode 100644 index 0000000000..fa01484a0a --- /dev/null +++ b/karma-e2e.conf.js @@ -0,0 +1,54 @@ +// 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/karma.conf.js b/karma.conf.js new file mode 100644 index 0000000000..59aff0d276 --- /dev/null +++ b/karma.conf.js @@ -0,0 +1,56 @@ +// 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: ['mocha', 'chai', 'chai-as-promised', 'sinon-chai'], + + // list of files / patterns to load in the browser + files: [ + 'public/bower_components/angular/angular.js', + 'public/bower_components/angular-resource/angular-resource.min.js', + 'public/bower_components/angular-mocks/angular-mocks.js', + 'public/bower_components/marked/lib/marked.js', + 'public/bower_components/habitrpg-shared/dist/habitrpg-shared.js', + 'public/js/*.js', + 'public/js/**/*.js', + 'test/mock/**/*.js', + 'test/spec/*.js', + 'test/spec/**/*.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 + }); +}; diff --git a/migrations/20131022_purchased_and_newStuff.js b/migrations/20131022_purchased_and_newStuff.js new file mode 100644 index 0000000000..862d9e107f --- /dev/null +++ b/migrations/20131022_purchased_and_newStuff.js @@ -0,0 +1,5 @@ +db.users.find().forEach(function(user){ + if (!user.purchased) user.purchased = {hair: {}, skin: {}}; + user.purchased.ads = user.flags && !!user.flags.ads; + db.users.update({_id:user._id}, {$set:{'purchased': user.purchased, 'flags.newStuff': true}, $unset: {'flags.ads':1}}); +}); \ No newline at end of file diff --git a/migrations/20131022_restore_ads.js b/migrations/20131022_restore_ads.js new file mode 100644 index 0000000000..6261990664 --- /dev/null +++ b/migrations/20131022_restore_ads.js @@ -0,0 +1,12 @@ +// node .migrations/20131022_restore_ads.js +var mongo = require('mongoskin'); +var _ = require('lodash'); +var dbBackup = mongo.db('localhost:27017/habitrpg?auto_reconnect'); +var dbLive = mongo.db('localhost:27017/habitrpg2?auto_reconnect'); +var count = 89474; +dbBackup.collection('users').findEach({$or: [{'flags.ads':'show'}, {'flags.ads': null}]}, {batchSize:10}, function(err, item) { + if (err) return console.error({err:err}); + if (!item || !item._id) return console.error('blank user'); + dbLive.collection('users').update({_id:item._id}, {$set:{'purchased.ads':false}, $unset: {'flags.ads': 1}}); + if (--count <= 0) console.log("DONE!"); +}); \ No newline at end of file diff --git a/migrations/new_stuff.js b/migrations/new_stuff.js index fa517671a6..df2d59e795 100644 --- a/migrations/new_stuff.js +++ b/migrations/new_stuff.js @@ -1 +1 @@ -db.users.update({},{$set:{'flags.newStuff':'show'}},{multi:true}) \ No newline at end of file +db.users.update({},{$set:{'flags.newStuff':true}},{multi:true}) \ No newline at end of file diff --git a/package.json b/package.json index ce0234c1a9..847988171e 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,8 @@ "passport": "~0.1.17", "validator": "~1.5.1", "nodemailer": "~0.5.2", - "grunt-cli": "~0.1.9" + "grunt-cli": "~0.1.9", + "paypal-ipn": "~1.0.1" }, "private": true, "subdomain": "habitrpg", @@ -57,5 +58,22 @@ "test": "mocha test/api.mocha.coffee", "start": "grunt run:dev", "postinstall": "./node_modules/bower/bin/bower install -f" + }, + "devDependencies": { + "karma-ng-scenario": "~0.1.0", + "grunt-karma": "~0.6.2", + "karma-script-launcher": "~0.1.0", + "karma-chrome-launcher": "~0.1.0", + "karma-firefox-launcher": "~0.1.0", + "karma-html2js-preprocessor": "~0.1.0", + "karma-coffee-preprocessor": "~0.1.0", + "karma-jasmine": "~0.1.3", + "karma-requirejs": "~0.1.0", + "karma-phantomjs-launcher": "~0.1.0", + "karma": "~0.10.2", + "karma-ng-html2js-preprocessor": "~0.1.0", + "karma-chai-plugins": "~0.1.0", + "mocha": "~1.12.1", + "karma-mocha": "~0.1.0" } } diff --git a/public/css/customizer.styl b/public/css/customizer.styl index 27c04f60ed..e61402f24b 100644 --- a/public/css/customizer.styl +++ b/public/css/customizer.styl @@ -52,70 +52,6 @@ menu } } -// narrower spriting for customize modal -.customize-menu - .m_hair_blond,.m_hair_black,.m_hair_brown,.m_hair_white, - .f_hair_blond,.f_hair_black,.f_hair_brown,.f_hair_white, - .m_skin_dead,.m_skin_orc,.m_skin_asian,.m_skin_black,.m_skin_white, - .f_skin_dead,.f_skin_orc,.f_skin_asian,.f_skin_black,.f_skin_white, - .m_head_0,.f_head_0 - width: 60px - height: 60px - - // head - .m_head_0 - background-position: -1557px -9px; - - // hair - .m_hair_blond - background-position: -1647px -4px; - .m_hair_black - background-position: -1737px -4px; - .m_hair_brown - background-position: -1827px -4px; - .m_hair_white - background-position: -1917px -4px; - - // skin - .m_skin_dead - background-position: -2547px -20px; - .m_skin_orc - background-position: -2637px -20px; - .m_skin_asian - background-position: -2727px -20px; - .m_skin_black - background-position: -2817px -20px; - .m_skin_white - background-position: -2907px -20px; - - // head - .f_head_0 - background-position: -1917px -9px; - - // hair - .f_hair_white - background-position: -2009px -8px; - .f_hair_brown - background-position: -2099px -8px; - .f_hair_black - background-position: -2189px -8px; - .f_hair_blond - background-position: -2279px -8px; - - // skin - .f_skin_dead - background-position: -2997px -20px; - .f_skin_orc - background-position: -3087px -20px; - .f_skin_asian - background-position: -3177px -20px; - .f_skin_black - background-position: -3267px -20px; - .f_skin_white - background-position: -3357px -20px; - - // starting armor - .f_armor_0_v2 - background-position: -2819px -38px; - .f_armor_0_v1 - background-position: -2909px -38px; \ No newline at end of file +.well.limited-edition + padding: 5px + margin: 0px \ No newline at end of file diff --git a/public/css/header.styl b/public/css/header.styl index a70a00fb46..a12f1b1408 100644 --- a/public/css/header.styl +++ b/public/css/header.styl @@ -7,8 +7,9 @@ background: #f5f5f5 border-bottom: 1px solid rgba(0,0,0,0.2) // margin-top: -1px - overflow: hidden - position relative + overflow-y: hidden + overflow-x: auto + // position relative /* login/menu buttons --------------------- */ @@ -17,7 +18,7 @@ top: 0.5em right: 0.5em font-size: 0.85em - z-index: 9000 + z-index: 1011 .site-nav margin-bottom: 0px diff --git a/public/css/index.styl b/public/css/index.styl index 11c21ff56f..444f3e49f4 100644 --- a/public/css/index.styl +++ b/public/css/index.styl @@ -143,7 +143,7 @@ hr position: fixed; top: 0; right: 0px; - z-index: 1001; + z-index: 1061; .modal-body margin: 10px @@ -168,4 +168,8 @@ a word-wrap: break-word .btn - margin-right: 5px \ No newline at end of file + margin-right: 5px + +.modal-indented-list + margin-left: 10px; + padding-left: 10px; \ No newline at end of file diff --git a/public/css/inventory.styl b/public/css/inventory.styl index 1c5fa3b1b4..fe5f84fd22 100644 --- a/public/css/inventory.styl +++ b/public/css/inventory.styl @@ -12,6 +12,9 @@ .tile opacity: 1 +.modal-header .gem-wallet + padding-top: 0px + // pets (this will all change when pet system is overhauled) .pet-grid @@ -36,6 +39,10 @@ width:6.5em height:2.5em +menu.pets div + display: inline-block + padding-top: -30px + .current-pet left: 0px bottom: 0px @@ -96,3 +103,10 @@ -moz-filter: brightness(0%) -o-filter: brightness(0%) -ms-filter: brightness(0%)*/ + +.selectableInventory + background-color: lightgreen !important + +.sell-inventory + width: 162px + height: 138px \ No newline at end of file diff --git a/public/css/npcs.styl b/public/css/npcs.styl index c9cc6d3bfc..5fc3bfc879 100644 --- a/public/css/npcs.styl +++ b/public/css/npcs.styl @@ -1,4 +1,4 @@ -.NPC-Alex, .NPC-Bailey, .NPC-Bailey-Head, .NPC-Daniel, .NPC-Justin, .NPC-Justin-Head, .NPC-Matt {background: url("/bower_components/habitrpg-shared/img/npcs/NPC-SpriteSheet.png") no-repeat} +.NPC-Alex, .NPC-Bailey, .NPC-Bailey-Head, .NPC-Daniel, .NPC-Justin, .NPC-Justin-Head, .NPC-Matt {background: url("/bower_components/habitrpg-shared/img/npcs/halloween/NPC-SpriteSheet.png") no-repeat} .NPC-Alex {background-position: 0 0; width: 162px; height: 138px;} .NPC-Alex-container{margin-bottom: 20px;} @@ -18,17 +18,19 @@ width: 54px height: 30px position: absolute - bottom: 0px + top: 117px // 147 (header-height) - 30 (bailey height) right: 50px cursor: pointer + .static-popover z-index 0 display block position:relative // Tour (Justin) -.tour-tour .NPC-Justin +.NPC-Justin.float-left float: left margin-right: 5px + margin-bottom: 5px .popover-navigation {clear:both;} \ No newline at end of file diff --git a/public/js/controllers/authCtrl.js b/public/js/controllers/authCtrl.js index f83db3397c..f1d4edecb3 100644 --- a/public/js/controllers/authCtrl.js +++ b/public/js/controllers/authCtrl.js @@ -4,8 +4,8 @@ The authentication controller (login & facebook) */ -habitrpg.controller("AuthCtrl", ['$scope', '$rootScope', 'User', '$http', '$location', 'API_URL', - function($scope, $rootScope, User, $http, $location, API_URL) { +habitrpg.controller("AuthCtrl", ['$scope', '$rootScope', 'User', '$http', '$location', '$window','API_URL', + function($scope, $rootScope, User, $http, $location, $window, API_URL) { var runAuth; var showedFacebookMessage; @@ -25,7 +25,7 @@ habitrpg.controller("AuthCtrl", ['$scope', '$rootScope', 'User', '$http', '$loca runAuth = function(id, token) { User.authenticate(id, token, function(err) { - window.location.href = '/'; + $window.location.href = '/'; //$rootScope.modals.login = false; }); }; @@ -41,22 +41,22 @@ habitrpg.controller("AuthCtrl", ['$scope', '$rootScope', 'User', '$http', '$loca runAuth(data.id, data.apiToken); }).error(function(data, status, headers, config) { if (status === 0) { - alert("Server not currently reachable, try again later"); + $window.alert("Server not currently reachable, try again later"); } else if (!!data && !!data.err) { - alert(data.err); + $window.alert(data.err); } else { - alert("ERROR: " + status); + $window.alert("ERROR: " + status); } }); }; function errorAlert(data, status, headers, config) { if (status === 0) { - alert("Server not currently reachable, try again later"); + $window.alert("Server not currently reachable, try again later"); } else if (!!data && !!data.err) { - alert(data.err); + $window.alert(data.err); } else { - alert("ERROR: " + status); + $window.alert("ERROR: " + status); } } diff --git a/public/js/controllers/footerCtrl.js b/public/js/controllers/footerCtrl.js index 7a31b9532c..3757f3cffb 100644 --- a/public/js/controllers/footerCtrl.js +++ b/public/js/controllers/footerCtrl.js @@ -14,7 +14,7 @@ habitrpg.controller("FooterCtrl", ['$scope', '$rootScope', 'User', '$http', $.getScript('//checkout.stripe.com/v2/checkout.js'); // Amazon Affiliate -// if ($rootScope.authenticated() && User.user.flags.ads !== 'hide') { +// if ($rootScope.authenticated() && !User.user.purchased.ads) { // $.getScript('//wms.assoc-amazon.com/20070822/US/js/link-enhancer-common.js?tag=ha0d2-20').fail(function() { // $('body').append(''); // }); diff --git a/public/js/controllers/groupsCtrl.js b/public/js/controllers/groupsCtrl.js index b5b38d9fa5..81b18f8dde 100644 --- a/public/js/controllers/groupsCtrl.js +++ b/public/js/controllers/groupsCtrl.js @@ -76,7 +76,7 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Groups', '$http', 'A } $scope.deleteChatMessage = function(group, message){ - if(message.uuid === User.user.id){ + if(message.uuid === User.user.id || (User.user.backer && User.user.backer.admin)){ group.$deleteChatMessage({messageId: message.id}, function(){ var i = _.indexOf(group.chat, message); if(i !== -1) group.chat.splice(i, 1); @@ -89,6 +89,7 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Groups', '$http', 'A } $scope.nameTagClasses = function(message){ + if (!message) return; // fixme what's triggering this? if (message.contributor) { if (message.contributor.match(/npc/i) || message.contributor.match(/royal/i)) { return 'label-royal'; @@ -113,7 +114,7 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Groups', '$http', 'A $scope.create = function(group){ if (User.user.balance < 1) { - return $rootScope.modals.moreGems = true; + return $rootScope.modals.buyGems = true; // $('#more-gems-modal').modal('show'); } @@ -142,6 +143,9 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Groups', '$http', 'A } $scope.leave = function(group){ + if (confirm("Are you sure you want to leave this guild?") !== true) { + return; + } group.$leave(); // var i = _.find($scope.groups.guilds, {_id:group._id}); // if (~i) $scope.groups.guilds.splice(i, 1); @@ -178,6 +182,9 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Groups', '$http', 'A }); } $scope.leave = function(group){ + if (confirm("Are you sure you want to leave this party?") !== true) { + return; + } group.$leave(function(){ Groups.groups.party = new Groups.Group(); }); diff --git a/public/js/controllers/inventoryCtrl.js b/public/js/controllers/inventoryCtrl.js index 862aa27d2c..6dc8d16955 100644 --- a/public/js/controllers/inventoryCtrl.js +++ b/public/js/controllers/inventoryCtrl.js @@ -1,32 +1,78 @@ habitrpg.controller("InventoryCtrl", ['$scope', 'User', function($scope, User) { - $scope.hatching = false; + // convenience vars since these are accessed frequently $scope.userEggs = User.user.items.eggs; $scope.userHatchingPotions = User.user.items.hatchingPotions; - $scope.chooseEgg = function(egg){ - if($scope.userHatchingPotions && $scope.userHatchingPotions.length < 1) { - return alert("You have no hatching potion!"); - } + $scope.selectedEgg = null; // {index: 1, name: "Tiger", value: 5} + $scope.selectedPotion = null; // {index: 5, name: "Red", value: 3} - $scope.selectedEgg = egg; - $scope.selectedPotion = $scope.userHatchingPotions[0]; - $scope.hatching = true; + $scope.chooseEgg = function(egg, $index){ + if ($scope.selectedEgg && $scope.selectedEgg.index == $index) { + return $scope.selectedEgg = null; // clicked same egg, unselect + } + var eggData = _.defaults({index:$index}, egg); + if (!$scope.selectedPotion) { + $scope.selectedEgg = eggData; + } else { + $scope.hatch(eggData, $scope.selectedPotion); + } } - $scope.pour = function(){ - var pet = $scope.selectedEgg.name + '-' + $scope.selectedPotion; + $scope.choosePotion = function(potion, $index){ + if ($scope.selectedPotion && $scope.selectedPotion.index == $index) { + return $scope.selectedPotion = null; // clicked same egg, unselect + } + // we really didn't think through the way these things are stored and getting passed around... + var potionData = _.findWhere(window.habitrpgShared.items.items.hatchingPotions, {name:potion}); + potionData = _.defaults({index:$index}, potionData); + if (!$scope.selectedEgg) { + $scope.selectedPotion = potionData; + } else { + $scope.hatch($scope.selectedEgg, potionData); + } + } - if(User.user.items.pets && ~User.user.items.pets.indexOf(pet)) { + $scope.sellInventory = function() { + if ($scope.selectedEgg) { + $scope.userEggs.splice($scope.selectedEgg.index, 1); + User.setMultiple({ + 'items.eggs': $scope.userEggs, + 'stats.gp': User.user.stats.gp + $scope.selectedEgg.value + }); + $scope.selectedEgg = null; + } else if ($scope.selectedPotion) { + $scope.userHatchingPotions.splice($scope.selectedPotion.index, 1); + User.setMultiple({ + 'items.hatchingPotions': $scope.userHatchingPotions, + 'stats.gp': User.user.stats.gp + $scope.selectedPotion.value + }); + $scope.selectedPotion = null; + } + } + + $scope.ownsPet = function(egg, potion){ + if (!egg || !potion) return; + var pet = egg.name + '-' + potion; + return User.user.items.pets && ~User.user.items.pets.indexOf(pet) + } + + $scope.selectableInventory = function(egg, potion, $index) { + if (!egg || !potion) return; + // FIXME this isn't updating the view for some reason + //if ($scope.selectedEgg && $scope.selectedEgg.index == $index) return 'selectableInventory'; + //if ($scope.selectedPotion && $scope.selectedPotion.index == $index) return 'selectableInventory'; + if (!$scope.ownsPet(egg, potion)) return 'selectableInventory'; + } + + $scope.hatch = function(egg, potion){ + if ($scope.ownsPet(egg, potion.name)){ return alert("You already have that pet, hatch a different combo.") } - - var i = _.indexOf($scope.userEggs, $scope.selectedEgg); - $scope.userEggs.splice(i, 1); - - i = _.indexOf($scope.userHatchingPotions, $scope.selectedPotion); - $scope.userHatchingPotions.splice(i, 1); + var pet = egg.name + '-' + potion.name; + $scope.userEggs.splice(egg.index, 1); + $scope.userHatchingPotions.splice(potion.index, 1); if(!User.user.items.pets) User.user.items.pets = []; User.user.items.pets.push(pet); @@ -40,7 +86,7 @@ habitrpg.controller("InventoryCtrl", ['$scope', 'User', alert("Your egg hatched! Visit your stable to equip your pet."); $scope.selectedEgg = null; - $scope.hatching = false; + $scope.selectedPotion = null; } }]); \ No newline at end of file diff --git a/public/js/controllers/marketCtrl.js b/public/js/controllers/marketCtrl.js index 68b11b79bc..c0cf429539 100644 --- a/public/js/controllers/marketCtrl.js +++ b/public/js/controllers/marketCtrl.js @@ -12,7 +12,7 @@ habitrpg.controller("MarketCtrl", ['$rootScope', '$scope', 'User', 'API_URL', '$ storePath = type === 'egg' ? 'items.eggs' : 'items.hatchingPotions' if(gems < item.value){ - return $rootScope.modals.moreGems = true; + return $rootScope.modals.buyGems = true; } var message = "Buy this " + (type == 'egg' ? 'egg' : 'hatching potion') + " with " + item.value + " of your " + gems + " Gems?" diff --git a/public/js/controllers/menuCtrl.js b/public/js/controllers/menuCtrl.js index aaf2760ac3..d8ddd61397 100644 --- a/public/js/controllers/menuCtrl.js +++ b/public/js/controllers/menuCtrl.js @@ -16,15 +16,22 @@ habitrpg.controller('MenuCtrl', } $scope.gotoOptions = function(){ - $scope.viewingOptions = true; $location.path('/options'); } $scope.gotoTasks = function(){ - $scope.viewingOptions = false; $location.path('/tasks') } + $scope.$on('$routeChangeSuccess', function(ev, current) { + if(!current.$$route) return; + if(current.$$route.originalPath === "/tasks"){ + $scope.viewingOptions = false; + }else if(current.$$route.originalPath === "/options"){ + $scope.viewingOptions = true; + } + }); + //FIXME where to implement this in rewrite? diff --git a/public/js/controllers/notificationCtrl.js b/public/js/controllers/notificationCtrl.js index d32d9db0bd..3f920be5df 100644 --- a/public/js/controllers/notificationCtrl.js +++ b/public/js/controllers/notificationCtrl.js @@ -1,120 +1,60 @@ 'use strict'; habitrpg.controller('NotificationCtrl', - ['$scope', '$rootScope', 'User', 'Guide', function ($scope, $rootScope, User, Guide) { + ['$scope', '$rootScope', 'User', 'Guide', 'Notification', function ($scope, $rootScope, User, Guide, Notification) { - Guide.initTour(); + $rootScope.$watch('user.stats.hp', function(after, before) { + if (after == before) return; + Notification.hp(after - before, 'hp'); + }); - function growlNotification(html, type) { - $.bootstrapGrowl(html, { - ele: '#notification-area', - type: type, //(null, 'info', 'error', 'success', 'gp', 'xp', 'hp', 'lvl','death') - top_offset: 20, - align: 'right', //('left', 'right', or 'center') - width: 250, //(integer, or 'auto') - delay: 3000, - allow_dismiss: true, - stackup_spacing: 10 // spacing between consecutive stacecked growls. - }); - }; + $rootScope.$watch('user.stats.exp', function(after, before) { + if (after == before) return; + Notification.exp(after - before); + }); - /* - Sets up "+1 Exp", "Level Up", etc notifications - */ - function setupGrowlNotifications() { + $rootScope.$watch('user.stats.gp', function(after, before) { + if (after == before) return; + var money = after - before; + Notification.gp(money); - function statsNotification(html, type) { - // don't show notifications if user dead - if (User.user.stats.lvl == 0) return; - growlNotification(html, type); - }; + //Append Bonus + var bonus = User.user._tmp.streakBonus; - /** - Show "+ 5 {gold_coin} 3 {silver_coin}" - */ - function showCoins(money) { - var absolute, gold, silver; - absolute = Math.abs(money); - gold = Math.floor(absolute); - silver = Math.floor((absolute - gold) * 100); - if (gold && silver > 0) { - return "" + gold + " " + silver + " "; - } else if (gold > 0) { - return "" + gold + " "; - } else if (silver > 0) { - return "" + silver + " "; - } - }; + if ((money > 0) && !!bonus) { + if (bonus < 0.01) bonus = 0.01; + Notification.text("+ " + Notification.coins(bonus) + " Streak Bonus!"); + } + }); - $rootScope.$watch('user.stats.hp', function(after, before) { + $rootScope.$watch('user._tmp.drop', function(after, before){ + if (after == before || !after) return; + $rootScope.modals.drop = true; + }); + + $rootScope.$watch('user.achievements.streak', function(after, before){ + if(after == before || after < before) return; + $rootScope.modals.achievements.streak = true; + }); + + // FIXME: this isn't working for some reason + /*_.each(['weapon', 'head', 'chest', 'shield'], function(watched){ + $rootScope.$watch('user.items.' + watched, function(before, after){ if (after == before) return; - var num = after - before; - var rounded = Math.abs(num.toFixed(1)); - if (num < 0) { - //lost hp from purchase - statsNotification(" - " + rounded + " HP", 'hp'); - } else if (num > 0) { - // gained hp from potion/level? - statsNotification(" + " + rounded + " HP", 'hp'); + if (+after < +before) { + Notification.death(); + //don't want to day "lost a head" + if (watched === 'head') watched = 'helm'; + Notification.text('Lost GP, 1 LVL, ' + watched); } - }); + }) + });*/ - $rootScope.$watch('user.stats.exp', function(after, before) { - if (after == before) return; - var num = after - before; - var rounded = Math.abs(num.toFixed(1)); - // TODO fix hackey negative notification supress - if (num < 0 && num > -50) { - statsNotification(" - " + rounded + " XP", 'xp'); - } else if (num > 0) { - statsNotification(" + " + rounded + " XP", 'xp'); - } - }); - - $rootScope.$watch('user.stats.gp', function(after, before) { - if (after == before) return; - var bonus, money, sign; - money = after - before; - - //why is this happening? gotta find where stats.gp is being set from (-)habit - //if (!money) { - // return; - //} - - sign = money < 0 ? '-' : '+'; - statsNotification("" + sign + " " + (showCoins(money)), 'gp'); - - //Append Bonus TODO - //bonus = model.get('_streakBonus'); - - if ((money > 0) && !!bonus) { - if (bonus < 0.01) { - bonus = 0.01; - } - statsNotification("+ " + (showCoins(bonus)) + " Streak Bonus!"); - //model.del('_streakBonus'); - } - }); - - // FIXME -// user.on('set', 'items.*', function(item, after, before) { -// if ((item === 'armor' || item === 'weapon' || item === 'shield' || item === 'head') && parseInt(after) < parseInt(before)) { -// //don't want to day "lost a head" -// if (item === 'head') { -// item = 'helm'; -// } -// return statsNotification(" Respawn!", "death"); -// } -// }); - - $rootScope.$watch('user.stats.lvl', function(after, before) { - if (after == before) return; - if (after > before) { - statsNotification(' Level Up!', 'lvl'); - } - }); - }; - - setupGrowlNotifications(); + $rootScope.$watch('user.stats.lvl', function(after, before) { + if (after == before) return; + if (after > before) { + Notification.lvl(); + } + }); } ]); diff --git a/public/js/controllers/petsCtrl.js b/public/js/controllers/petsCtrl.js index 1643d52d51..7d4e6ba5ed 100644 --- a/public/js/controllers/petsCtrl.js +++ b/public/js/controllers/petsCtrl.js @@ -5,6 +5,7 @@ habitrpg.controller("PetsCtrl", ['$scope', 'User', $scope.userCurrentPet = User.user.items.currentPet; $scope.pets = window.habitrpgShared.items.items.pets; $scope.hatchingPotions = window.habitrpgShared.items.items.hatchingPotions; + $scope.totalPets = $scope.pets.length * $scope.hatchingPotions.length; $scope.hasPet = function(name, potion){ if (!$scope.userPets) return false; diff --git a/public/js/controllers/rootCtrl.js b/public/js/controllers/rootCtrl.js index d73cdfe6c4..0726686180 100644 --- a/public/js/controllers/rootCtrl.js +++ b/public/js/controllers/rootCtrl.js @@ -6,6 +6,7 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$http', function($scope, $rootScope, $location, User, $http) { $rootScope.modals = {}; + $rootScope.modals.achievements = {}; $rootScope.User = User; $rootScope.user = User.user; $rootScope.settings = User.settings; @@ -42,25 +43,57 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$ } $rootScope.showStripe = function() { - var disableAds = User.user.flags.ads == 'hide' ? '' : 'Disable Ads, '; - StripeCheckout.open({ - key: window.env.STRIPE_PUB_KEY, - address: false, - amount: 500, - name: "Checkout", - description: "Buy 20 Gems, " + disableAds + "Support the Developers", - panelLabel: "Checkout", - token: function(data) { - $scope.$apply(function(){ - $http.post("/api/v1/user/buy-gems", data) - .success(function() { - window.location.href = "/"; - }).error(function(err) { - alert(err); - }); - }) - } - }); + StripeCheckout.open({ + key: window.env.STRIPE_PUB_KEY, + address: false, + amount: 500, + name: "Checkout", + description: "Buy 20 Gems, Disable Ads, Support the Developers", + panelLabel: "Checkout", + token: function(data) { + $scope.$apply(function(){ + $http.post("/api/v1/user/buy-gems", data) + .success(function() { + window.location.href = "/"; + }).error(function(err) { + alert(err); + }); + }) + } + }); } + + $rootScope.charts = {}; + $rootScope.toggleChart = function(id, task) { + var history = [], matrix, data, chart, options; + switch (id) { + case 'exp': + $rootScope.charts.exp = !$rootScope.charts.exp; + history = User.user.history.exp; + break; + case 'todos': + $rootScope.charts.todos = !$rootScope.charts.todos; + history = User.user.history.todos; + break; + default: + $rootScope.charts[id] = !$rootScope.charts[id]; + history = task.history; + if (task && task._editing) task._editing = false; + } + matrix = [['Date', 'Score']]; + _.each(history, function(obj) { + matrix.push([moment(obj.date).format('MM/DD/YY'), obj.value]); + }); + data = google.visualization.arrayToDataTable(matrix); + options = { + title: 'History', + backgroundColor: { + fill: 'transparent' + }, + width:300 + }; + chart = new google.visualization.LineChart($("." + id + "-chart")[0]); + chart.draw(data, options); + }; } ]); diff --git a/public/js/controllers/settingsCtrl.js b/public/js/controllers/settingsCtrl.js index bda590b2d2..11ad1c1a6b 100644 --- a/public/js/controllers/settingsCtrl.js +++ b/public/js/controllers/settingsCtrl.js @@ -59,9 +59,17 @@ habitrpg.controller('SettingsCtrl', }); } + $scope.restoreValues = {}; + $rootScope.$watch('modals.restore', function(value){ + if(value === true){ + $scope.restoreValues.stats = angular.copy(User.user.stats); + $scope.restoreValues.items = angular.copy(User.user.items); + } + }) + $scope.restore = function(){ - var stats = User.user.stats, - items = User.user.items; + var stats = $scope.restoreValues.stats, + items = $scope.restoreValues.items; User.setMultiple({ "stats.hp": stats.hp, "stats.exp": stats.exp, diff --git a/public/js/controllers/tasksCtrl.js b/public/js/controllers/tasksCtrl.js index eb29be6877..752d3cdd7c 100644 --- a/public/js/controllers/tasksCtrl.js +++ b/public/js/controllers/tasksCtrl.js @@ -32,43 +32,12 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User', ' } ]; $scope.score = function(task, direction) { - /*save current stats to compute the difference after scoring. - */ - - var oldStats, statsDiff; - statsDiff = {}; - oldStats = _.clone(User.user.stats); + if (task.type === "reward" && User.user.stats.gp < task.value){ + return Notification.text('Not enough GP.'); + } Algos.score(User.user, task, direction); - /*compute the stats change. - */ + User.log({op: "score",data: task, dir: direction}); - _.each(oldStats, function(value, key) { - var newValue; - newValue = User.user.stats[key]; - if (newValue !== value) { - statsDiff[key] = newValue - value; - } - }); - /*notify user if there are changes in stats. - */ - - if (Object.keys(statsDiff).length > 0) { - Notification.push({ - type: "stats", - stats: statsDiff - }); - } - if (task.type === "reward" && _.isEmpty(statsDiff)) { - Notification.push({ - type: "text", - text: "Not enough GP." - }); - } - User.log({ - op: "score", - data: task, - dir: direction - }); }; $scope.addTask = function(list) { @@ -101,6 +70,13 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User', ' op: "revive" }); }; + + $scope.toggleEdit = function(task){ + console.log(task) + task._editing = !task._editing; + if($rootScope.charts[task.id]) $rootScope.charts[task.id] = false; + }; + $scope.remove = function(task) { var tasks; if (confirm("Are you sure you want to delete this task?") !== true) { @@ -113,13 +89,14 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User', ' }); tasks.splice(tasks.indexOf(task), 1); }; + /* ------------------------ Items ------------------------ */ - $scope.$watch("user.items", function() { + var updateStore = function(){ var sorted, updated; updated = window.habitrpgShared.items.updateStore(User.user); /* Figure out whether we wanna put this in habitrpg-shared @@ -127,31 +104,24 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User', ' sorted = [updated.weapon, updated.armor, updated.head, updated.shield, updated.potion, updated.reroll]; $scope.itemStore = sorted; - }); + } + + updateStore(); + $scope.buy = function(type) { - var hasEnough; - hasEnough = window.habitrpgShared.items.buyItem(User.user, type); + var hasEnough = window.habitrpgShared.items.buyItem(User.user, type); if (hasEnough) { - User.log({ - op: "buy", - type: type - }); - Notification.push({ - type: "text", - text: "Item bought!" - }); + User.log({op: "buy",type: type}); + Notification.text("Item purchased."); + updateStore(); } else { - Notification.push({ - type: "text", - text: "Not enough GP." - }); + Notification.text("Not enough GP."); } }; - $scope.clearCompleted = function() { User.user.todos = _.reject(User.user.todos, {completed:true}); User.log({op: 'clear-completed'}); } -}]); + }]); diff --git a/public/js/controllers/userCtrl.js b/public/js/controllers/userCtrl.js index a71867f0a8..d306020935 100644 --- a/public/js/controllers/userCtrl.js +++ b/public/js/controllers/userCtrl.js @@ -1,21 +1,41 @@ "use strict"; -habitrpg.controller("UserCtrl", ['$scope', '$location', 'User', - function($scope, $location, User) { - $scope.profile = User.user; - $scope.hideUserAvatar = function() { - $(".userAvatar").hide(); - }; - $scope.toggleHelm = function(val){ - User.log({op:'set', data:{'preferences.showHelm':val}}); +habitrpg.controller("UserCtrl", ['$rootScope', '$scope', '$location', 'User', '$http', + function($rootScope, $scope, $location, User, $http) { + $scope.profile = User.user; + $scope.hideUserAvatar = function() { + $(".userAvatar").hide(); + }; + $scope.toggleHelm = function(val){ + User.log({op:'set', data:{'preferences.showHelm':val}}); + } + + $scope.$watch('_editing.profile', function(value){ + if(value === true) $scope.editingProfile = angular.copy(User.user.profile); + }); + + $scope.save = function(){ + var values = {}; + _.each($scope.editingProfile, function(value, key){ + // Using toString because we need to compare two arrays (websites) + var curVal = $scope.profile.profile[key]; + if(!curVal || $scope.editingProfile[key].toString() !== curVal.toString()) + values['profile.' + key] = value; + }); + User.setMultiple(values); + $scope._editing.profile = false; + } + + $scope.addWebsite = function(){ + if (!$scope.editingProfile.websites) $scope.editingProfile.websites = []; + $scope.editingProfile.websites.push($scope._newWebsite); + $scope._newWebsite = ''; + } + $scope.removeWebsite = function($index){ + $scope.editingProfile.websites.splice($index,1); + } + + $scope.unlock = User.unlock; + } - $scope.addWebsite = function(){ - if (!User.user.profile.websites) User.user.profile.websites = []; - User.user.profile.websites.push($scope._newWebsite); - User.set('profile.websites', User.user.profile.websites); - $scope._newWebsite = ''; - } - $scope.removeWebsite = function($index){ - User.user.profile.websites.splice($index,1); - } -}]); +]); diff --git a/public/js/directives/directives.js b/public/js/directives/directives.js index 639d615abb..3a1f2f302d 100644 --- a/public/js/directives/directives.js +++ b/public/js/directives/directives.js @@ -47,6 +47,7 @@ habitrpg.directive('habitrpgSortable', ['User', function(User) { return function($scope, element, attrs, ngModel) { $(element).sortable({ axis: "y", + distance: 5, start: function (event, ui) { ui.item.data('startIndex', ui.item.index()); }, diff --git a/public/js/filters/filters.js b/public/js/filters/filters.js index 0e0d79d0ae..65223c5957 100644 --- a/public/js/filters/filters.js +++ b/public/js/filters/filters.js @@ -1,10 +1,10 @@ angular.module('habitrpg') - .filter('gold', function () { - return function (gp) { - return Math.floor(gp); - } - }).filter('silver', function () { - return function (gp) { - return Math.floor((gp - Math.floor(gp))*100); - } - }); \ No newline at end of file + .filter('gold', function () { + return function (gp) { + return Math.floor(gp); + } + }).filter('silver', function () { + return function (gp) { + return Math.floor((gp - Math.floor(gp))*100); + } + }); diff --git a/public/js/services/guideServices.js b/public/js/services/guideServices.js index b85b2f7715..e0658a59b0 100644 --- a/public/js/services/guideServices.js +++ b/public/js/services/guideServices.js @@ -5,9 +5,15 @@ */ angular.module('guideServices', []). - factory('Guide', ['User', function(User) { + factory('Guide', ['$rootScope', 'User', 'Items', 'Helpers', function($rootScope, User, Items, Helpers) { - var initTour = function() { + /** + * Init and show the welcome tour. Note we do it listening to a $rootScope broadcasted 'userLoaded' message, + * this because we need to determine whether to show the tour *after* the user has been pulled from the server, + * otherwise it's always start off as true, and then get set to false later + */ + $rootScope.$on('userUpdated', initTour); + function initTour(){ if (User.user.flags.showTour === false) return; var tourSteps = [ { @@ -46,12 +52,12 @@ angular.module('guideServices', []). } ]; _.each(tourSteps, function(step){ - step.content = "
" + step.content + "
"; // add Justin NPC img + step.content = "
" + step.content + "
"; // add Justin NPC img }); $('.main-herobox').popover('destroy'); var tour = new Tour({ onEnd: function(){ - User.log({op:'set',data:{'flags.showTour':false}}); + User.set('flags.showTour', false); } }); tourSteps.forEach(function(step) { @@ -61,9 +67,90 @@ angular.module('guideServices', []). //tour.start(true); }; + var alreadyShown = function(before, after) { + return !(!before && after === true); + }; + + var showPopover = function(selector, title, html, placement) { + if (!placement) placement = 'bottom'; + $(selector).popover('destroy'); + var button = ""; + html = "
" + html + '
' + button + '
'; + $(selector).popover({ + title: title, + placement: placement, + trigger: 'manual', + html: true, + content: html + }).popover('show'); + }; + + $rootScope.$watch('user.flags.customizationsNotification', function(after, before) { + if (alreadyShown(before, after)) return; + showPopover('.main-herobox', 'Customize Your Avatar', "Click your avatar to customize your appearance.", 'bottom'); + }); + + $rootScope.$watch('user.flags.itemsEnabled', function(after, before) { + if (alreadyShown(before, after)) return; + var html = "Congratulations, you have unlocked the Item Store! You can now buy weapons, armor, potions, etc. Read each item's comment for more information."; + showPopover('div.rewards', 'Item Store Unlocked', html, 'left'); + }); + + $rootScope.$watch('user.flags.petsEnabled', function(after, before) { + //TODO is this ever used? I think dropsEnabled is the one that's used + if (alreadyShown(before, after)) return; + var html = "You have unlocked Pets! You can now buy pets with Gems (note, you replenish Gems with real-life money - so chose your pets wisely!)"; + showPopover('#rewardsTabs', 'Pets Unlocked', html, 'left'); + }); + + $rootScope.$watch('user.flags.partyEnabled', function(after, before) { + if (alreadyShown(before, after)) return; + var html = "Be social, join a party and play Habit with your friends! You'll be better at your habits with accountability partners. Click User -> Options -> Party, and follow the instructions. LFG anyone?"; + showPopover('.user-menu', 'Party System', html, 'bottom'); + }); + + $rootScope.$watch('user.flags.dropsEnabled', function(after, before) { + if (alreadyShown(before, after)) return; + var drop = Helpers.randomVal(Items.items.pets); + var eggs = User.user.items.eggs || []; + eggs.push(drop); // FIXME put this on server instead + User.set('items.eggs', eggs); + $rootScope.modals.dropsEnabled = true; + }); + + $rootScope.$watch('user.items.pets', function(after, before) { + if (User.user.achievements && User.user.achievements.beastMaster) return; + if (before >= 90) { + User.set('achievements.beastMaster', true); + $('#beastmaster-achievement-modal').modal('show'); // FIXME + } + }); + + $rootScope.$watch('user.items', function(after, before) { + if (User.user.achievements && User.user.achievements.ultimateGear) return; + var items = User.user.items; + if (+items.weapon >= 6 && +items.armor >= 5 && +items.head >= 5 && +items.shield >= 5) { + User.set('achievements.ultimateGear', true); // FIXME + $('#max-gear-achievement-modal').modal('show'); // FIXME + } + }); + + // FIXME how to handle tasks.*.streak? + // FIXME move to tasksCtrl + /*$rootScope.$watch('user.tasks.*.streak', function(id, after, before) { + if (after > 0) { + if ((after % 21) === 0) { + user.incr('achievements.streak', 1) + return $('#streak-achievement-modal').modal('show'); + } else if ((before - after === 1) && (before % 21 === 0)) { + return user.incr('achievements.streak', -1); + } + } + });*/ + return { - initTour: initTour - } + initTour:initTour + }; } ]); diff --git a/public/js/services/memberServices.js b/public/js/services/memberServices.js index f4cad5923b..5c16118b85 100644 --- a/public/js/services/memberServices.js +++ b/public/js/services/memberServices.js @@ -5,11 +5,11 @@ */ angular.module('memberServices', ['ngResource']). - factory('Members', ['API_URL', '$resource', - function(API_URL, $resource) { + factory('Members', ['$rootScope', 'API_URL', '$resource', + function($rootScope, API_URL, $resource) { var members = {}; var Member = $resource(API_URL + '/api/v1/members/:uid', {uid:'@_id'}); - return { + var memberServices = { members: members, @@ -69,5 +69,11 @@ angular.module('memberServices', ['ngResource']). } } } + + $rootScope.$on('userUpdated', function(event, user){ + memberServices.populate(user); + }) + + return memberServices; } ]); diff --git a/public/js/services/notificationServices.js b/public/js/services/notificationServices.js index 739683af61..655f38f20e 100644 --- a/public/js/services/notificationServices.js +++ b/public/js/services/notificationServices.js @@ -1,75 +1,71 @@ - -angular.module("notificationServices", []).factory("Notification", function() { - var active, data, fixMe, timer; - fixMe = { - push: function() {}, - get: function() {}, - animate: function() {}, - clearTimer: function() {}, - init: function() {} - }; - return fixMe; - data = { - message: "" - }; - active = false; - timer = null; - return { - hide: function() { - $("#notification").fadeOut(function() { - $("#notification").css("webkit-transform", "none"); - $("#notification").css("top", "-63px"); - $("#notification").css("left", "0px"); - setTimeout((function() { - $("#notification").show(); - }), 190); +/** + Set up "+1 Exp", "Level Up", etc notifications + */ +angular.module("notificationServices", []) + .factory("Notification", ['User', function(User) { + function growl(html, type) { + $.bootstrapGrowl(html, { + ele: '#notification-area', + type: type, //(null, 'info', 'error', 'success', 'gp', 'xp', 'hp', 'lvl','death') + top_offset: 20, + align: 'right', //('left', 'right', or 'center') + width: 250, //(integer, or 'auto') + delay: 3000, + allow_dismiss: true, + stackup_spacing: 10 // spacing between consecutive stacecked growls. }); - active = false; - timer = null; - }, - animate: function() { - if (timer) { - clearTimeout(timer); - timer = setTimeout(this.hide, 2000); + }; + + /** + Show "+ 5 {gold_coin} 3 {silver_coin}" + */ + function coins(money) { + var absolute, gold, silver; + absolute = Math.abs(money); + gold = Math.floor(absolute); + silver = Math.floor((absolute - gold) * 100); + if (gold && silver > 0) { + return "" + gold + " " + silver + " "; + } else if (gold > 0) { + return "" + gold + " "; + } else if (silver > 0) { + return "" + silver + " "; } - if (active === false) { - active = true; - $("#notification").transition({ - y: 63, - x: 0 - }); - timer = setTimeout(this.hide, 2000); - } - }, - push: function(message) { - data.message = ""; - switch (message.type) { - case "stats": - if ((message.stats.exp != null) && (message.stats.gp != null)) { - data.message = "Experience: " + message.stats.exp + "
GP: " + message.stats.gp.toFixed(2); - } - if (message.stats.hp) { - data.message = "HP: " + message.stats.hp.toFixed(2); - } - if (message.stats.gp && !(message.stats.exp != null)) { - data.message = "
GP: " + message.stats.gp.toFixed(2); - } - break; - case "text": - data.message = message.text; - } - this.animate(); - }, - get: function() { - return data; - }, - clearTimer: function() { - clearTimeout(timer); - timer = null; - active = false; - }, - init: function() { - timer = setTimeout(this.hide, 2000); + }; + + var sign = function(number){ + return number?number<0?'-':'+':'+'; } - }; -}); + + var round = function(number){ + return Math.abs(number.toFixed(1)); + } + + return { + coins: coins, + hp: function(val) { + // don't show notifications if user dead + if (User.user.stats.lvl == 0) return; + growl(" " + sign(val) + " " + round(val) + " HP", 'hp'); + }, + exp: function(val) { + if (User.user.stats.lvl == 0) return; + if (val < -50) return; // don't show when they level up (resetting their exp) + growl(" " + sign(val) + " " + round(val) + " XP", 'xp'); + }, + gp: function(val) { + if (User.user.stats.lvl == 0) return; + growl(sign(val) + " " + coins(val), 'gp'); + }, + text: function(val){ + growl(val); + }, + lvl: function(){ + growl(' Level Up!', 'lvl'); + }, + death: function(){ + growl(" Respawn!", "death"); + } + }; + } +]); diff --git a/public/js/services/userServices.js b/public/js/services/userServices.js index 1bb0e98001..e25cc89a5c 100644 --- a/public/js/services/userServices.js +++ b/public/js/services/userServices.js @@ -5,209 +5,235 @@ */ angular.module('userServices', []). - factory('User', ['$http', '$location', 'API_URL', 'STORAGE_USER_ID', 'STORAGE_SETTINGS_ID', 'Members', - function($http, $location, API_URL, STORAGE_USER_ID, STORAGE_SETTINGS_ID, Members) { - var authenticated = false, - defaultSettings = { - auth: { apiId: '', apiToken: ''}, - sync: { - queue: [], //here OT will be queued up, this is NOT call-back queue! - sent: [] //here will be OT which have been sent, but we have not got reply from server yet. - }, - fetching: false, // whether fetch() was called or no. this is to avoid race conditions - online: false - }, - settings = {}, //habit mobile settings (like auth etc.) to be stored here - user = {}; // this is stored as a reference accessible to all controllers, that way updates propagate + factory('User', ['$rootScope', '$http', '$location', '$window', 'API_URL', 'STORAGE_USER_ID', 'STORAGE_SETTINGS_ID', + function($rootScope, $http, $location, $window, API_URL, STORAGE_USER_ID, STORAGE_SETTINGS_ID) { + var authenticated = false, + defaultSettings = { + auth: { apiId: '', apiToken: ''}, + sync: { + queue: [], //here OT will be queued up, this is NOT call-back queue! + sent: [] //here will be OT which have been sent, but we have not got reply from server yet. + }, + fetching: false, // whether fetch() was called or no. this is to avoid race conditions + online: false + }, + settings = {}, //habit mobile settings (like auth etc.) to be stored here + user = {}; // this is stored as a reference accessible to all controllers, that way updates propagate - //first we populate user with schema - _.extend(user, window.habitrpgShared.helpers.newUser()); - user.apiToken = user._id = ''; // we use id / apitoken to determine if registered + //first we populate user with schema + _.extend(user, window.habitrpgShared.helpers.newUser()); + user.apiToken = user._id = ''; // we use id / apitoken to determine if registered - //than we try to load localStorage - - if (localStorage.getItem(STORAGE_USER_ID)) { - _.extend(user, JSON.parse(localStorage.getItem(STORAGE_USER_ID))); - } + //than we try to load localStorage + if (localStorage.getItem(STORAGE_USER_ID)) { + _.extend(user, JSON.parse(localStorage.getItem(STORAGE_USER_ID))); + } - var syncQueue = function (cb) { - if (!authenticated) { - alert("Not authenticated, can't sync, go to settings first."); - return; - } - - var queue = settings.sync.queue; - var sent = settings.sync.sent; - if (queue.length === 0) { - console.log('Sync: Queue is empty'); - return; - } - if (settings.fetching) { - console.log('Sync: Already fetching'); - return; - } - if (settings.online!==true) { - console.log('Sync: Not online'); - return; - } - - settings.fetching = true; -// move all actions from queue array to sent array - _.times(queue.length, function () { - sent.push(queue.shift()); - }); - - $http.post(API_URL + '/api/v1/user/batch-update', sent, {params: {data:+new Date, _v:user._v}}) - .success(function (data, status, heacreatingders, config) { - data.tasks = _.toArray(data.tasks); - //make sure there are no pending actions to sync. If there are any it is not safe to apply model from server as we may overwrite user data. - if (!queue.length) { - //we can't do user=data as it will not update user references in all other angular controllers. - - // the user has been modified from another application, sync up - if(data.wasModified) { - delete data.wasModified; - _.extend(user, data); - } - user._v = data._v; - Members.populate(user); - - // FIXME handle this somewhere else, we don't need to check every single time - var offset = moment().zone(); // eg, 240 - this will be converted on server as -(offset/60) - if (!user.preferences.timezoneOffset || user.preferences.timezoneOffset !== offset) { - userServices.log({op:'set', data: {'preferences.timezoneOffset': offset}}); - } - } - sent.length = 0; - settings.fetching = false; - save(); - if (cb) { - cb(false) - } - - syncQueue(); // call syncQueue to check if anyone pushed more actions to the queue while we were talking to server. - }) - .error(function (data, status, headers, config) { - //move sent actions back to queue - _.times(sent.length, function () { - queue.push(sent.shift()) - }); - settings.fetching = false; - //Notification.push({type:'text', text:"We're offline"}) - - }); - - } - - - var save = function () { - localStorage.setItem(STORAGE_USER_ID, JSON.stringify(user)); - localStorage.setItem(STORAGE_SETTINGS_ID, JSON.stringify(settings)); - }; - var userServices = { - user: user, - online: function (status) { - if (status===true) { - settings.online = true; - syncQueue(); - } else { - settings.online = false; - }; - }, - - authenticate: function (uuid, token, cb) { - if (!!uuid && !!token) { - $http.defaults.headers.common['x-api-user'] = uuid; - $http.defaults.headers.common['x-api-key'] = token; - authenticated = true; - settings.auth.apiId = uuid; - settings.auth.apiToken = token; - settings.online = true; - if (user && user._v) user._v--; // shortcut to always fetch new updates on page reload - this.log({}, cb); - } else { - alert('Please enter your ID and Token in settings.') - } - }, - - authenticated: function(){ - return this.settings.auth.apiId !== ""; - }, - - log: function (action, cb) { - //push by one buy one if an array passed in. - if (_.isArray(action)) { - action.forEach(function (a) { - settings.sync.queue.push(a); - }); - } else { - settings.sync.queue.push(action); - } - - save(); - syncQueue(cb); - }, - - /* - Very simple path-set. `set('preferences.gender','m')` for example. We'll deprecate this once we have a complete API - */ - set: function(k, v) { - var self = userServices; - var log = { op: 'set', data: {} }; - window.habitrpgShared.helpers.dotSet(k, v, userServices.user); - log.data[k] = v; - userServices.log(log); - }, - - setMultiple: function(obj){ - var self = this; - var log = { op: 'set', data: {} }; - _.each(obj, function(v,k){ - window.habitrpgShared.helpers.dotSet(k, v, userServices.user); - log.data[k] = v; - }) - userServices.log(log); - }, - - save: save, - - settings: settings - }; - - - //load settings if we have them - if (localStorage.getItem(STORAGE_SETTINGS_ID)) { - //use extend here to make sure we keep object reference in other angular controllers - _.extend(settings, JSON.parse(localStorage.getItem(STORAGE_SETTINGS_ID))); - - //if settings were saved while fetch was in process reset the flag. - settings.fetching = false; - //create and load if not - } else { - localStorage.setItem(STORAGE_SETTINGS_ID, JSON.stringify(defaultSettings)); - _.extend(settings, defaultSettings); + var syncQueue = function (cb) { + if (!authenticated) { + $window.alert("Not authenticated, can't sync, go to settings first."); + return; } - //If user does not have ApiID that forward him to settings. - if (!settings.auth.apiId || !settings.auth.apiToken) { - //var search = $location.search(); // FIXME this should be working, but it's returning an empty object when at a root url /?_id=... - var search = $location.search(window.location.search.substring(1)).$$search; // so we use this fugly hack instead - if (search.err) return alert(search.err); - if (search._id && search.apiToken) { - userServices.authenticate(search._id, search.apiToken, function(){ - window.location.href='/'; + var queue = settings.sync.queue; + var sent = settings.sync.sent; + if (queue.length === 0) { + // Sync: Queue is empty + return; + } + if (settings.fetching) { + // Sync: Already fetching + return; + } + if (settings.online!==true) { + // Sync: Not online + return; + } + + settings.fetching = true; + // move all actions from queue array to sent array + _.times(queue.length, function () { + sent.push(queue.shift()); + }); + + $http.post(API_URL + '/api/v1/user/batch-update', sent, {params: {data:+new Date, _v:user._v}}) + .success(function (data, status, heacreatingders, config) { + data.tasks = _.toArray(data.tasks); + //make sure there are no pending actions to sync. If there are any it is not safe to apply model from server as we may overwrite user data. + if (!queue.length) { + //we can't do user=data as it will not update user references in all other angular controllers. + + // the user has been modified from another application, sync up + if(data.wasModified) { + delete data.wasModified; + _.extend(user, data); + $rootScope.$emit('userUpdated', user); + } + user._v = data._v; + + // FIXME handle this somewhere else, we don't need to check every single time + var offset = moment().zone(); // eg, 240 - this will be converted on server as -(offset/60) + if (!user.preferences.timezoneOffset || user.preferences.timezoneOffset !== offset) { + userServices.log({op:'set', data: {'preferences.timezoneOffset': offset}}); + } + } + sent.length = 0; + settings.fetching = false; + save(); + if (cb) { + cb(false) + } + + syncQueue(); // call syncQueue to check if anyone pushed more actions to the queue while we were talking to server. + }) + .error(function (data, status, headers, config) { + //move sent actions back to queue + _.times(sent.length, function () { + queue.push(sent.shift()) + }); + settings.fetching = false; + //Notification.push({type:'text', text:"We're offline"}) + }); + } + + + var save = function () { + localStorage.setItem(STORAGE_USER_ID, JSON.stringify(user)); + localStorage.setItem(STORAGE_SETTINGS_ID, JSON.stringify(settings)); + }; + var userServices = { + user: user, + online: function (status) { + if (status===true) { + settings.online = true; + syncQueue(); + } else { + settings.online = false; + }; + }, + + authenticate: function (uuid, token, cb) { + if (!!uuid && !!token) { + $http.defaults.headers.common['x-api-user'] = uuid; + $http.defaults.headers.common['x-api-key'] = token; + authenticated = true; + settings.auth.apiId = uuid; + settings.auth.apiToken = token; + settings.online = true; + if (user && user._v) user._v--; // shortcut to always fetch new updates on page reload + userServices.log({}, cb); + } else { + alert('Please enter your ID and Token in settings.') + } + }, + + authenticated: function(){ + return this.settings.auth.apiId !== ""; + }, + + log: function (action, cb) { + //push by one buy one if an array passed in. + if (_.isArray(action)) { + action.forEach(function (a) { + settings.sync.queue.push(a); }); } else { - if (window.location.pathname.indexOf('/static') !== 0){ - localStorage.clear(); - window.location.href = '/logout'; + settings.sync.queue.push(action); + } + + save(); + syncQueue(cb); + }, + + /* + Very simple path-set. `set('preferences.gender','m')` for example. We'll deprecate this once we have a complete API + */ + set: function(k, v) { + var log = { op: 'set', data: {} }; + $window.habitrpgShared.helpers.dotSet(k, v, this.user); + log.data[k] = v; + userServices.log(log); + }, + + setMultiple: function(obj){ + var log = { op: 'set', data: {} }; + _.each(obj, function(v,k){ + $window.habitrpgShared.helpers.dotSet(k, v, userServices.user); + log.data[k] = v; + }); + userServices.log(log); + }, + + /** + * For gem-unlockable preferences, (a) if owned, select preference (b) else, purchase + * @param path: User.preferences <-> User.purchased maps like User.preferences.skin=abc <-> User.purchased.skin.abc. + * Pass in this paramater as "skin.abc". Alternatively, pass as an array ["skin.abc", "skin.xyz"] to unlock sets + */ + unlock: function(path){ + var self = userServices; //this; // why isn't this working? + + if (_.isArray(path)) { + if (confirm("Purchase for 5 Gems?") !== true) return; + if (user.balance < 1.25) return $rootScope.modals.buyGems = true; + path = path.join(','); + } else { + if (window.habitrpgShared.helpers.dotGet('purchased.' + path, user)) { + var pref = path.split('.')[0], + val = path.split('.')[1]; + return self.set('preferences.' + pref, val); + } else { + if (confirm("Purchase for 2 Gems?") !== true) return; + if (user.balance < 0.5) return $rootScope.modals.buyGems = true; } } + + $http.post(API_URL + '/api/v1/user/unlock?path=' + path) + .success(function(data, status, headers, config){ + self.log({}); // sync new unlocked & preferences + }).error(function(data, status, headers, config){ + alert(status + ': ' + data); + //FIXME use method used elsewhere for handling this error, this is temp while developing + }) + }, + + save: save, + + settings: settings + }; + + + //load settings if we have them + if (localStorage.getItem(STORAGE_SETTINGS_ID)) { + //use extend here to make sure we keep object reference in other angular controllers + _.extend(settings, JSON.parse(localStorage.getItem(STORAGE_SETTINGS_ID))); + + //if settings were saved while fetch was in process reset the flag. + settings.fetching = false; + //create and load if not + } else { + localStorage.setItem(STORAGE_SETTINGS_ID, JSON.stringify(defaultSettings)); + _.extend(settings, defaultSettings); + } + + //If user does not have ApiID that forward him to settings. + if (!settings.auth.apiId || !settings.auth.apiToken) { + //var search = $location.search(); // FIXME this should be working, but it's returning an empty object when at a root url /?_id=... + var search = $location.search($window.location.search.substring(1)).$$search; // so we use this fugly hack instead + if (search.err) return alert(search.err); + if (search._id && search.apiToken) { + userServices.authenticate(search._id, search.apiToken, function(){ + $window.location.href='/'; + }); } else { - userServices.authenticate(settings.auth.apiId, settings.auth.apiToken) + if ($window.location.pathname.indexOf('/static') !== 0){ + localStorage.clear(); + $window.location.href = '/logout'; + } } + } else { + userServices.authenticate(settings.auth.apiId, settings.auth.apiToken) + } - return userServices; - + return userServices; } ]); diff --git a/public/js/static.js b/public/js/static.js index f65568c182..4876031b17 100644 --- a/public/js/static.js +++ b/public/js/static.js @@ -1,6 +1,6 @@ "use strict"; -window.habitrpg = angular.module('habitrpg', ['userServices', 'memberServices']) +window.habitrpg = angular.module('habitrpg', ['userServices']) .constant("API_URL", "") .constant("STORAGE_USER_ID", 'habitrpg-user') .constant("STORAGE_SETTINGS_ID", 'habit-mobile-settings') \ No newline at end of file diff --git a/src/controllers/auth.js b/src/controllers/auth.js index 0d569b337c..dd4ace6c40 100644 --- a/src/controllers/auth.js +++ b/src/controllers/auth.js @@ -37,7 +37,8 @@ api.auth = function(req, res, next) { if (_.isEmpty(user)) { return res.json(401, NO_USER_FOUND); } - res.locals.wasModified = +user._v !== +req.query._v; + + res.locals.wasModified = req.query._v ? +user._v !== +req.query._v : true; res.locals.user = user; req.session.userId = user._id; return next(); diff --git a/src/controllers/groups.js b/src/controllers/groups.js index e464e330d8..93a8c85a0f 100644 --- a/src/controllers/groups.js +++ b/src/controllers/groups.js @@ -189,21 +189,18 @@ api.postChat = function(req, res, next) { api.deleteChatMessage = function(req, res, next){ var user = res.locals.user var group = res.locals.group; - var message = _.find(group.chat, {id: req.params.messageId, uuid: user.id}); + var message = _.find(group.chat, {id: req.params.messageId}); if(message === undefined) return res.json(404, {err: "Message not found!"}); - if(user.id !== message.uuid){ - if(!user.backer || (user.backer && !user.backer.admin)){ - return res.json(401, {err: "Not authorized to delete this message!"}) - } + if(user.id !== message.uuid && !(user.backer && user.backer.admin)){ + return res.json(401, {err: "Not authorized to delete this message!"}) } group.chat = _.without(group.chat, message); group.save(function(err, data){ if(err) return res.json(500, {err: err}); - res.send(204); }); } diff --git a/src/controllers/user.js b/src/controllers/user.js index 556183a963..ecd75a3d28 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -3,6 +3,8 @@ // fixme remove this junk, was coffeescript compiled (probably for IE8 compat) var __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; +var url = require('url'); +var ipn = require('paypal-ipn'); var _ = require('lodash'); var nconf = require('nconf'); var async = require('async'); @@ -202,13 +204,10 @@ api.scoreTask = function(req, res, next) { } task = user.tasks[id]; delta = algos.score(user, task, direction); - return user.save(function(err, saved) { - if (err) { - return res.json(500, { - err: err - }); - } - return res.json(200, _.extend({ + //user.markModified('flags'); + user.save(function(err, saved) { + if (err) return res.json(500, {err: err}); + res.json(200, _.extend({ delta: delta }, saved.toJSON().stats)); }); @@ -565,6 +564,49 @@ api['delete'] = function(req, res) { }) } +/* + ------------------------------------------------------------------------ + Unlock Preferences + ------------------------------------------------------------------------ + */ + +api.unlock = function(req, res) { + var user = res.locals.user; + var path = req.query.path; + var fullSet = ~path.indexOf(','); + + // 5G per set, 2G per individual + cost = fullSet ? 1.25 : 0.5; + + if (user.balance < cost) + return res.json(401, {err: 'Not enough gems'}); + + if (fullSet) { + var paths = path.split(','); + _.each(paths, function(p){ + helpers.dotSet('purchased.' + p, true, user); + }); + } else { + if (helpers.dotGet('purchased.' + path, user) === true) + return res.json(401, {err: 'User already purchased that'}); + helpers.dotSet('purchased.' + path, true, user); + } + + user.balance -= cost; + user.__v++; + user.markModified('purchased'); + user.save(function(err, saved){ + if (err) res.json(500, {err:err}); + res.send(200); + }) +} + +/* + ------------------------------------------------------------------------ + Buy Gems + ------------------------------------------------------------------------ + */ + /* Setup Stripe response when posting payment @@ -585,7 +627,7 @@ api.buyGems = function(req, res) { }, function(response, cb) { res.locals.user.balance += 5; - res.locals.user.flags.ads = 'hide'; + res.locals.user.purchased.ads = true; res.locals.user.save(cb); } ], function(err, saved){ @@ -594,6 +636,31 @@ api.buyGems = function(req, res) { }); }; +api.buyGemsPaypalIPN = function(req, res) { + res.send(200); + ipn.verify(req.body, function callback(err, msg) { + if (err) { + console.error(msg); + res.send(500, msg); + } else { + if (req.body.payment_status == 'Completed') { + //Payment has been confirmed as completed + var parts = url.parse(req.body.custom, true); + var uid = parts.query.uid; //, apiToken = query.apiToken; + if (!uid) throw new Error("uuid or apiToken not found when completing paypal transaction"); + User.findById(uid, function(err, user) { + if (err) throw err; + if (_.isEmpty(user)) throw "user not found with uuid " + uuid + " when completing paypal transaction" + user.balance += 5; + user.purchased.ads = true; + user.save(); + console.log('PayPal transaction completed and user updated'); + }); + } + } + }); +} + /* ------------------------------------------------------------------------ Tags @@ -712,7 +779,15 @@ api.batchUpdate = function(req, res, next) { } response = user.toJSON(); response.wasModified = res.locals.wasModified; - res.json(200, response); - return console.log("Reply sent"); + if (response._tmp && response._tmp.drop) response.wasModified = true; + + // Send the response to the server + if(response.wasModified){ + res.json(200, response); + }else{ + res.json(200, {_v: response._v}); + } + + return; }); }; \ No newline at end of file diff --git a/src/middleware.js b/src/middleware.js index f3180f737f..2b224ab85f 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -54,7 +54,6 @@ var walk = function(folder){ walk(path.join(__dirname, "/../build")); var getBuildUrl = function(url){ - console.log("CALLED ", url) if(buildFiles[url]) return buildFiles[url]; return url; @@ -64,6 +63,8 @@ module.exports.locals = function(req, res, next) { res.locals.habitrpg = res.locals.habitrpg || {} _.defaults(res.locals.habitrpg, { NODE_ENV: nconf.get('NODE_ENV'), + BASE_URL: nconf.get('BASE_URL'), + PAYPAL_MERCHANT: nconf.get('PAYPAL_MERCHANT'), IS_MOBILE: /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(req.header('User-Agent')), STRIPE_PUB_KEY: nconf.get('STRIPE_PUB_KEY'), getBuildUrl: getBuildUrl diff --git a/src/models/user.js b/src/models/user.js index 0ff0a580f2..587bcf22d1 100644 --- a/src/models/user.js +++ b/src/models/user.js @@ -1,9 +1,19 @@ +// User.js +// ======= +// Defines the user data model (schema) for use via the API. + +// Dependencies +// ------------ var mongoose = require("mongoose"); var Schema = mongoose.Schema; var helpers = require('habitrpg-shared/script/helpers'); var _ = require('lodash'); +// User Schema +// ----------- + var UserSchema = new Schema({ + // ### UUID and API Token _id: { type: String, 'default': helpers.uuid @@ -13,7 +23,8 @@ var UserSchema = new Schema({ 'default': helpers.uuid }, - //We want to know *every* time an object updates. Mongoose uses __v to designate when an object contains arrays which + // ### Mongoose Update Object + // We want to know *every* time an object updates. Mongoose uses __v to designate when an object contains arrays which // have been updated (http://goo.gl/gQLz41), but we want *every* update _v: { type: Number, @@ -42,15 +53,14 @@ var UserSchema = new Schema({ loggedin: Date } }, - /* TODO*/ - backer: Schema.Types.Mixed, - /* - # tier: Number - # admin: Boolean - # contributor: Boolean - # tokensApplieds: Boolean - */ + backer: { + tier: Number, + admin: Boolean, + npc: String, + contributor: String, + tokensApplied: Boolean + }, balance: Number, habitIds: Array, @@ -59,12 +69,18 @@ var UserSchema = new Schema({ rewardIds: Array, filters: {type: Schema.Types.Mixed, 'default': {}}, + purchased: { + ads: {type: Boolean, 'default': false}, + skin: {type: Schema.Types.Mixed, 'default': {}}, // eg, {skeleton: true, pumpkin: true, eb052b: true} + hair: {type: Schema.Types.Mixed, 'default': {}} + }, + flags: { + customizationsNotification: {type: Boolean, 'default': false}, showTour: {type: Boolean, 'default': true}, - ads: {type: String, 'default': 'show'}, // FIXME make this a boolean, run migration dropsEnabled: {type: Boolean, 'default': false}, itemsEnabled: {type: Boolean, 'default': false}, - newStuff: {type: String, 'default': 'hide'}, //FIXME to boolean (currently show/hide) + newStuff: {type: Boolean, 'default': false}, rewrite: {type: Boolean, 'default': true}, partyEnabled: Boolean, // FIXME do we need this? petsEnabled: {type: Boolean, 'default': false}, @@ -121,33 +137,38 @@ var UserSchema = new Schema({ eggs: [ { - dialog: String, //You've found a Wolf Egg! Find a hatching potion to pour on this egg, and one day it will hatch into a loyal pet - name: String, // Wolf - notes: String, //Find a hatching potion to pour on this egg, and one day it will hatch into a loyal pet. - text: String, // Wolf - //type: String, //Egg // this is forcing mongoose to return object as "[object Object]", but I don't think this is needed anyway? - value: Number //3 + // example: You've found a Wolf Egg! Find a hatching potion to pour on this egg, and one day it will hatch into a loyal pet + dialog: String, + // example: Wolf + name: String, + // example: Find a hatching potion to pour on this egg, and one day it will hatch into a loyal pet. + notes: String, + // example: Wolf + text: String, + /* type: String, //Egg // this is forcing mongoose to return object as "[object Object]", but I don't think this is needed anyway? */ + // example: 3 + value: Number } ], - hatchingPotions: Array, //["Base", "Skeleton",...] + hatchingPotions: Array, // ["Base", "Skeleton",...] lastDrop: { date: {type: Date, 'default': Date.now}, count: {type: Number, 'default': 0} }, - /* ["BearCub-Base", "Cactus-Base", ...]*/ + // ["BearCub-Base", "Cactus-Base", ...] pets: Array }, - /*FIXME store as Date?*/ + // FIXME store as Date? lastCron: { type: Date, 'default': Date.now }, - /* FIXME remove?*/ + // FIXME remove? party: { - //party._id //FIXME make these populate docs? + //party._id // FIXME make these populate docs? current: String, // party._id invitation: String, // party._id lastMessageSeen: String, @@ -155,19 +176,19 @@ var UserSchema = new Schema({ }, preferences: { armorSet: String, - dayStart: Number, - gender: String, - hair: String, - hideHeader: Boolean, - showHelm: Boolean, - skin: String, + dayStart: {type:Number, 'default': 0}, + gender: {type:String, 'default': 'm'}, + hair: {type:String, 'default':'blond'}, + hideHeader: {type:Boolean, 'default':false}, + showHelm: {type:Boolean, 'default':true}, + skin: {type:String, 'default':'white'}, timezoneOffset: Number }, profile: { blurb: String, imageUrl: String, name: String, - websites: Array //["http://ocdevel.com" ] + websites: Array // styled like --> ["http://ocdevel.com" ] }, stats: { hp: Number, @@ -177,15 +198,15 @@ var UserSchema = new Schema({ }, tags: [ { - /* FIXME use refs?*/ + // FIXME use refs? id: String, name: String } ], - /* - # We can't define `tasks` until we move off Derby, since Derby requires dictionary of objects. When we're off, migrate - # to array of subdocs - */ + + // ### Tasks Definition + // We can't define `tasks` until we move off Derby, since Derby requires dictionary of objects. When we're off, migrate + // to array of subdocs tasks: Schema.Types.Mixed /* @@ -208,12 +229,13 @@ var UserSchema = new Schema({ strict: true }); -/** - Derby requires a strange storage format for somethign called "refLists". Here we hook into loading the data, so we - can provide a more "expected" storage format for our various helper methods. Since the attributes are passed by reference, - the underlying data will be modified too - so when we save back to the database, it saves it in the way Derby likes. - This will go away after the rewrite is complete -*/ +// Legacy Derby Function? +// ---------------------- +// Derby requires a strange storage format for somethign called "refLists". Here we hook into loading the data, so we +// can provide a more "expected" storage format for our various helper methods. Since the attributes are passed by reference, +// the underlying data will be modified too - so when we save back to the database, it saves it in the way Derby likes. +// This will go away after the rewrite is complete + function transformTaskLists(doc) { _.each(['habit', 'daily', 'todo', 'reward'], function(type) { // we use _.transform instead of a simple _.where in order to maintain sort-order @@ -235,20 +257,23 @@ UserSchema.methods.toJSON = function() { doc.id = doc._id; transformTaskLists(doc); // we need to also transform for our server-side routes + // FIXME? Is this a reference to `doc.filters` or just disabled code? Remove? + /* // Remove some unecessary data as far as client consumers are concerned //_.each(['habit', 'daily', 'todo', 'reward'], function(type) { // delete doc["#{type}Ids"] //}); //delete doc.tasks + */ doc.filters = {}; + doc._tmp = this._tmp; // be sure to send down drop notifs return doc; }; -/* -# FIXME - since we're using special @post('init') above, we need to flag when the original path was modified. -# Custom setter/getter virtuals? -*/ + // FIXME - since we're using special @post('init') above, we need to flag when the original path was modified. + // Custom setter/getter virtuals? + UserSchema.pre('save', function(next) { this.markModified('tasks'); //our own version incrementer diff --git a/src/routes/api.js b/src/routes/api.js index 91346a424b..1eb4ade222 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -47,6 +47,8 @@ router.post('/user/revive', auth.auth, cron, user.revive); router.post('/user/batch-update', auth.auth, cron, user.batchUpdate); router.post('/user/reroll', auth.auth, cron, user.reroll); router.post('/user/buy-gems', auth.auth, user.buyGems); +router.post('/user/buy-gems/paypal-ipn', user.buyGemsPaypalIPN); +router.post('/user/unlock', auth.auth, cron, user.unlock); router.post('/user/reset', auth.auth, user.reset); router['delete']('/user', auth.auth, user['delete']); diff --git a/test/README.md b/test/README.md new file mode 100644 index 0000000000..7aea53a0db --- /dev/null +++ b/test/README.md @@ -0,0 +1,6 @@ +We need to clean up this directory. The *real* tests are in spec/ mock/ e2e/ and api.mocha.coffee. We want to: + +1. Move all old / deprecated tests from casper, test2, etc into spec, mock, e2e +1. Move api.mocha.coffee to vanilla javascript +1. Remove dependency of api.mocha.coffee on Derby, port it to Mongoose +1. Add better test-coverage \ No newline at end of file diff --git a/test/spec/authCtrlSpec.js b/test/spec/authCtrlSpec.js new file mode 100644 index 0000000000..b48d362b32 --- /dev/null +++ b/test/spec/authCtrlSpec.js @@ -0,0 +1,38 @@ +'use strict'; + +describe('Auth Controller', function() { + + beforeEach(module('habitrpg')); + + describe('AuthCtrl', function(){ + var scope, ctrl, user, $httpBackend, $window; + + beforeEach(inject(function(_$httpBackend_, $rootScope, $controller) { + $httpBackend = _$httpBackend_; + scope = $rootScope.$new(); + scope.loginUsername = 'user' + scope.loginPassword = 'pass' + $window = { location: { href: ""}, alert: sinon.spy() }; + user = { user: {}, authenticate: sinon.spy() }; + + ctrl = $controller('AuthCtrl', {$scope: scope, $window: $window, User: user}); + })); + + it('should log in users with correct uname / pass', function() { + $httpBackend.expectPOST('/api/v1/user/auth/local').respond({id: 'abc', token: 'abc'}); + scope.auth(); + $httpBackend.flush(); + expect(user.authenticate).to.have.been.calledOnce; + expect($window.alert).to.not.have.been.called; + }); + + it('should not log in users with incorrect uname / pass', function() { + $httpBackend.expectPOST('/api/v1/user/auth/local').respond(404, ''); + scope.auth(); + $httpBackend.flush(); + expect(user.authenticate).to.not.have.been.called; + expect($window.alert).to.have.been.calledOnce; + }); + }); + +}); \ No newline at end of file diff --git a/test/spec/memberServicesSpec.js b/test/spec/memberServicesSpec.js new file mode 100644 index 0000000000..47acf96364 --- /dev/null +++ b/test/spec/memberServicesSpec.js @@ -0,0 +1,40 @@ +'use strict'; + +describe('memberServices', function() { + var $httpBackend, members; + + beforeEach(module('memberServices')); + beforeEach(module('habitrpg')); + + beforeEach(inject(function(_$httpBackend_, Members) { + $httpBackend = _$httpBackend_; + members = Members; + })); + + it('has no members at the beginning', function() { + expect(members.members).to.be.an('object'); + expect(members.members).to.eql({}); + expect(members.selectedMember).to.be.undefined; + }); + + it('populates members', function(){ + var uid = 'abc'; + members.populate({ + members: [{ _id: uid }] + }); + expect(members.members).to.eql({ + abc: { _id: uid } + }); + }); + + it('selects a member', function(){ + var uid = 'abc'; + $httpBackend.expectGET('/api/v1/members/' + uid).respond({ _id: uid }); + members.selectMember(uid); + $httpBackend.flush(); + + expect(members.selectedMember._id).to.eql(uid); + expect(members.members).to.have.property(uid); + }); + +}); \ No newline at end of file diff --git a/test/spec/userServicesSpec.js b/test/spec/userServicesSpec.js new file mode 100644 index 0000000000..fa2c12a6df --- /dev/null +++ b/test/spec/userServicesSpec.js @@ -0,0 +1,49 @@ +'use strict'; + +describe('userServices', function() { + var $httpBackend, $window, user, STORAGE_USER_ID, STORAGE_SETTINGS_ID; + + beforeEach(module('habitrpg')); + + beforeEach(function(){ + module(function($provide){ + $window = {href: '', alert: sinon.spy(), location: {search: '', pathname: ''}}; + $provide.value('$window', $window); + }); + + inject(function(_$httpBackend_, User, _STORAGE_USER_ID_, _STORAGE_SETTINGS_ID_) { + $httpBackend = _$httpBackend_; + user = User; + STORAGE_USER_ID = _STORAGE_USER_ID_; + STORAGE_SETTINGS_ID = _STORAGE_SETTINGS_ID_; + }); + localStorage.removeItem(STORAGE_SETTINGS_ID); + localStorage.removeItem(STORAGE_USER_ID); + }); + + it('checks online status', function(){ + user.online(true); + expect(user.settings.online).to.be.true; + user.online(false); + expect(user.settings.online).to.be.false; + }) + + it('saves user data to local storage', function(){ + user.save(); + var settings = JSON.parse(localStorage[STORAGE_SETTINGS_ID]); + var user_id = JSON.parse(localStorage[STORAGE_USER_ID]); + expect(settings).to.eql(user.settings); + expect(user_id).to.eql(user.user); + }); + + it('alerts when not authenticated', function(){ + user.log(); + expect($window.alert).to.have.been.calledWith("Not authenticated, can't sync, go to settings first."); + }); + + it('puts items in que queue', function(){ + user.log({}); + //TODO where does that null comes from? + expect(user.settings.sync.queue).to.eql([null, {}]); + }); +}); \ No newline at end of file diff --git a/views/index.jade b/views/index.jade index 7e7cc338a4..1b6e26fab9 100644 --- a/views/index.jade +++ b/views/index.jade @@ -27,6 +27,7 @@ html - }else{ // Remember to update the file list in Gruntfile.js! script(type='text/javascript', src='/bower_components/jquery/jquery.min.js') + script(type='text/javascript', src='/bower_components/jquery.cookie/jquery.cookie.js') script(type='text/javascript', src='/bower_components/bootstrap-growl/jquery.bootstrap-growl.min.js') script(type='text/javascript', src='/bower_components/bootstrap-tour/build/js/bootstrap-tour.min.js') script(type='text/javascript', src='/bower_components/angular/angular.min.js') @@ -101,7 +102,7 @@ html div(ng-if='user.preferences.hideHeader') include ./shared/header/menu - .exp-chart(ng-show='page.charts.exp') + .exp-chart(ng-show='charts.exp') #main(ng-view) diff --git a/views/options/groups/chat-box.jade b/views/options/groups/chat-box.jade index 63b0427266..cfe3f6fb55 100644 --- a/views/options/groups/chat-box.jade +++ b/views/options/groups/chat-box.jade @@ -8,7 +8,7 @@ form(ng-submit='postChat(group,_chatMessage)') td input.btn.chat-btn(type='submit', value='Send Chat') td - button.btn - i(class='pull-right icon-refresh', ng-click='sync(group)', tooltip='Fetch Recent Messages') + button.btn(type="button", ng-click='sync(group)', tooltip='Fetch Recent Messages') + i(class='pull-right icon-refresh') diff --git a/views/options/groups/group.jade b/views/options/groups/group.jade index dfefaa5776..691e68dd97 100644 --- a/views/options/groups/group.jade +++ b/views/options/groups/group.jade @@ -1,4 +1,4 @@ -a.pull-right.gem-wallet(rel='popover', data-trigger='hover', data-title='Guild Bank', data-content='Gems which your Guild leader can use for prizes in the upcoming Challenges feature.', data-placement='bottom', data-html='true') +a.pull-right.gem-wallet(popover-trigger='mouseenter', popover-title='Guild Bank', popover='Gems which your Guild leader can use for prizes in the upcoming Challenges feature.', popover-placement='left', popover-html='true') // span.task-action-btn.tile.flush.neutral .Gems diff --git a/views/options/index.jade b/views/options/index.jade index 6d0e6a60b6..d27af6a886 100644 --- a/views/options/index.jade +++ b/views/options/index.jade @@ -42,13 +42,7 @@ include ./groups/index .tab-pane#inventory-tab - .row-fluid - .span6.border-right - h2 Inventory - include ./inventory - .span6 - h2 Market - include ./market + include ./inventory .tab-pane#stable-tab include ./pets diff --git a/views/options/inventory.jade b/views/options/inventory.jade index 39b0b39109..6fac392ac7 100644 --- a/views/options/inventory.jade +++ b/views/options/inventory.jade @@ -1,23 +1,54 @@ .row-fluid(ng-controller='InventoryCtrl') - div(ng-class='{span6: hatching}') + .span6.border-right + h2 Inventory + p.well Combine eggs with hatching potions to make pets by clicking an egg and then a potion. Sell unwanted drops to Alexander the Merchant. menu.inventory-list(type='list') li.customize-menu - menu.pets-menu(label='Eggs') + menu.pets-menu(label='Eggs ({{userEggs.length}})') p(ng-show='userEggs.length < 1') You don't have any eggs yet. div(ng-repeat='egg in userEggs track by $index') - button.customize-option(tooltip='{{egg.text}}', ng-click='chooseEgg(egg)', class='Pet_Egg_{{egg.name}}') + button.customize-option(tooltip='{{egg.text}}', ng-click='chooseEgg(egg, $index)', class='Pet_Egg_{{egg.name}}', ng-class='selectableInventory(egg, selectedPotion.name, $index)') p {{egg.text}} li.customize-menu - menu.hatchingPotion-menu(label='Hatching Potions') + menu.hatchingPotion-menu(label='Hatching Potions ({{userHatchingPotions.length}})') p(ng-show='userHatchingPotions.length < 1') You don't have any hatching potions yet. div(ng-repeat='hatchingPotion in userHatchingPotions track by $index') - button.customize-option(tooltip='{{hatchingPotion}}', class='Pet_HatchingPotion_{{hatchingPotion}}') + button.customize-option(tooltip='{{hatchingPotion}}', ng-click='choosePotion(hatchingPotion, $index)', class='Pet_HatchingPotion_{{hatchingPotion}}', ng-class='selectableInventory(selectedEgg, hatchingPotion, $index)') p {{hatchingPotion}} - .span6(ng-show='hatching') - h3 Hatch Your Egg - p(ng-show='userHatchingPotions.length < 1') You don't have any hatching potions yet. - div(ng-show='userHatchingPotions') - p. - Which hatching potion will you pour on your {{selectedEgg.text}} egg? - select(ng-options='hatchingPotion for hatchingPotion in userHatchingPotions', ng-model="selectedPotion") - button(ng-click="pour()") Pour + + .span6 + h2 Market + .row-fluid(ng-controller='MarketCtrl') + table.NPC-Alex-container + tr + td + .NPC-Alex.pull-left + td + .popover.static-popover.fade.right.in + .arrow + h3.popover-title + a(target='_blank', href='http://www.kickstarter.com/profile/523661924') Alexander the Merchant + .popover-content + p + | Welcome to the Market. Dying to get that particular pet you're after, but don't want to wait for it to drop? Buy it here! If you have unwanted drops, sell them to me. + p + button.btn.btn-primary(ng-show='selectedEgg', ng-click='sellInventory()') + | Sell {{selectedEgg.name}} for {{selectedEgg.value}} GP + button.btn.btn-primary(ng-show='selectedPotion', ng-click='sellInventory()') + | Sell {{selectedPotion.name}} for {{selectedPotion.value}} GP + + menu.inventory-list(type='list') + li.customize-menu + menu.pets-menu(label='Eggs') + div(ng-repeat='egg in eggs track by $index') + button.customize-option(tooltip='{{egg.text}} - {{egg.value}} Gem(s)', ng-click='buy("egg", egg)', class='Pet_Egg_{{egg.name}}') + p {{egg.text}} + + li.customize-menu + menu.pets-menu(label='Hatching Potions') + div(ng-repeat='hatchingPotion in hatchingPotions track by $index') + button.customize-option(tooltip='{{hatchingPotion.text}} - {{hatchingPotion.value}} Gem(s)', ng-click='buy("hatchingPotion", hatchingPotion)', class='Pet_HatchingPotion_{{hatchingPotion.name}}') + p {{hatchingPotion.text}} + + + diff --git a/views/options/market.jade b/views/options/market.jade deleted file mode 100644 index a0190f9d34..0000000000 --- a/views/options/market.jade +++ /dev/null @@ -1,28 +0,0 @@ -.row-fluid(ng-controller='MarketCtrl') - table.NPC-Alex-container - tr - td - .NPC-Alex.pull-left - td - .popover.static-popover.fade.right.in - .arrow - h3.popover-title - a(target='_blank', href='http://www.kickstarter.com/profile/523661924') Alexander Augustin - .popover-content - | Welcome to the Market! I'm the merchant, Alexander. Dying to get that particular pet you're after, but don't want to wait for it to drop? Buy it here! - - menu.inventory-list(type='list') - li.customize-menu - menu.pets-menu(label='Eggs') - div(ng-repeat='egg in eggs track by $index') - button.customize-option(tooltip='{{egg.text}}', ng-click='buy("egg", egg)', class='Pet_Egg_{{egg.name}}') - p {{egg.text}} - - li.customize-menu - menu.pets-menu(label='Hatching Potions') - div(ng-repeat='hatchingPotion in hatchingPotions track by $index') - button.customize-option(tooltip='{{hatchingPotion.text}}', ng-click='buy("hatchingPotion", hatchingPotion)', class='Pet_HatchingPotion_{{hatchingPotion.name}}') - p {{hatchingPotion.text}} - - - diff --git a/views/options/pets.jade b/views/options/pets.jade index 8afc98672c..80d8b1a899 100644 --- a/views/options/pets.jade +++ b/views/options/pets.jade @@ -11,22 +11,19 @@ | ! Until that day, a(target='_blank', href='https://f.cloud.github.com/assets/2374703/164631/3ed5fa6c-78cd-11e2-8743-f65ac477b55e.png') have a look-see | at all the pets you can collect. - h4 {{userPets.length}} / 90 Pets Found + h4 {{userPets.length}} / {{totalPets}} Pets Found - table.pets(ng-repeat='pet in pets') - tbody - tr - td(ng-repeat='potion in hatchingPotions') - button(class="pet-button Pet-{{pet.name}}-{{potion.name}}", ng-show='hasPet(pet.name, potion.name)', ng-class="{active: isCurrentPet(pet.name, potion.name)}", ng-click='choosePet(pet.name, potion.name)', tooltip='{{potion.name}} {{pet.name}}') + menu.pets(type='list') + li.customize-menu(ng-repeat='pet in pets') + menu + div(ng-repeat='potion in hatchingPotions', tooltip='{{potion.name}} {{pet.name}}') + button(class="pet-button Pet-{{pet.name}}-{{potion.name}}", ng-show='hasPet(pet.name, potion.name)', ng-class="{active: isCurrentPet(pet.name, potion.name)}", ng-click='choosePet(pet.name, potion.name)') button(class="pet-button pet-not-owned", ng-hide='hasPet(pet.name, potion.name)') - img(src='/bower_components/habitrpg-shared/img/PixelPaw.png', tooltip='{{potion.name}} {{pet.name}}') + img(src='/bower_components/habitrpg-shared/img/PixelPaw.png') h4 Rare Pets - table - tr - td - button(class="pet-button Pet-Wolf-Veteran", ng-show='hasPet("Wolf", "Veteran")', ng-class='{active: isCurrentPet("Wolf", "Veteran")}', ng-click='choosePet("Wolf", "Veteran")', tooltip='Veteran Wolf') - button(class="pet-button pet-not-owned", ng-hide='hasPet("Wolf", "Veteran")') - img(src='/bower_components/habitrpg-shared/img/PixelPaw.png') - td(ng-if='hasPet("Wolf", "Cerberus")') + menu + div(ng-if='hasPet("Wolf", "Veteran")') + button(class="pet-button Pet-Wolf-Veteran", ng-class='{active: isCurrentPet("Wolf", "Veteran")}', ng-click='choosePet("Wolf", "Veteran")', tooltip='Veteran Wolf') + div(ng-if='hasPet("Wolf", "Cerberus")') button(class="pet-button Pet-Wolf-Cerberus", ng-class='{active: isCurrentPet("Wolf", "Cerberus")}', ng-click='choosePet("Wolf", "Cerberus")', tooltip='Cerberus Pup') \ No newline at end of file diff --git a/views/options/profile.jade b/views/options/profile.jade index 293e3c8e1f..41a485fc42 100644 --- a/views/options/profile.jade +++ b/views/options/profile.jade @@ -6,11 +6,14 @@ // gender li.customize-menu menu(label='Head') + menu button.m_head_0.customize-option(type='button', ng-click='set("preferences.gender","m")') button.f_head_0.customize-option(type='button', ng-click='set("preferences.gender","f")') label.checkbox input(type='checkbox', ng-model='user.preferences.showHelm', ng-change='toggleHelm(user.preferences.showHelm)') | Show Helm + hr + // hair li.customize-menu menu(label='Hair') @@ -18,14 +21,53 @@ button(class='{{user.preferences.gender}}_hair_black customize-option', type='button', ng-click='set("preferences.hair","black")') button(class='{{user.preferences.gender}}_hair_brown customize-option', type='button', ng-click='set("preferences.hair","brown")') button(class='{{user.preferences.gender}}_hair_white customize-option', type='button', ng-click='set("preferences.hair","white")') + hr + // skin li.customize-menu - menu(label='Skin') - button.customize-option(class='{{user.preferences.gender}}_skin_dead', type='button', ng-click='set("preferences.skin","dead")') - button.customize-option(class='{{user.preferences.gender}}_skin_orc', type='button', ng-click='set("preferences.skin","orc")') - button.customize-option(class='{{user.preferences.gender}}_skin_asian', type='button', ng-click='set("preferences.skin","asian")') - button.customize-option(class='{{user.preferences.gender}}_skin_black', type='button', ng-click='set("preferences.skin","black")') - button.customize-option(class='{{user.preferences.gender}}_skin_white', type='button', ng-click='set("preferences.skin","white")') + menu(label='Basic Skins') + button.customize-option(type='button', class='{{user.preferences.gender}}_skin_asian', ng-click='set("preferences.skin","asian")') + button.customize-option(type='button', class='{{user.preferences.gender}}_skin_white', ng-click='set("preferences.skin","white")') + button.customize-option(type='button', class='{{user.preferences.gender}}_skin_ea8349', ng-click='set("preferences.skin","ea8349")') + button.customize-option(type='button', class='{{user.preferences.gender}}_skin_c06534', ng-click='set("preferences.skin","c06534")') + button.customize-option(type='button', class='{{user.preferences.gender}}_skin_98461a', ng-click='set("preferences.skin","98461a")') + button.customize-option(type='button', class='{{user.preferences.gender}}_skin_black', ng-click='set("preferences.skin","black")') + button.customize-option(type='button', class='{{user.preferences.gender}}_skin_dead', ng-click='set("preferences.skin","dead")') + button.customize-option(type='button', class='{{user.preferences.gender}}_skin_orc', ng-click='set("preferences.skin","orc")') + + // Rainbow Skin + h5. + Rainbow Skins - 2/skin + //menu(label='Rainbow Skins (2G / skin)') + menu + button.customize-option(type='button', class='{{user.preferences.gender}}_skin_eb052b', ng-class='{locked: !user.purchased.skin.eb052b}', ng-click='unlock("skin.eb052b")') + button.customize-option(type='button', class='{{user.preferences.gender}}_skin_f69922', ng-class='{locked: !user.purchased.skin.f69922}', ng-click='unlock("skin.f69922")') + button.customize-option(type='button', class='{{user.preferences.gender}}_skin_f5d70f', ng-class='{locked: !user.purchased.skin.f5d70f}', ng-click='unlock("skin.f5d70f")') + button.customize-option(type='button', class='{{user.preferences.gender}}_skin_0ff591', ng-class='{locked: !user.purchased.skin.0ff591}', ng-click='unlock("skin.0ff591")') + button.customize-option(type='button', class='{{user.preferences.gender}}_skin_2b43f6', ng-class='{locked: !user.purchased.skin.2b43f6}', ng-click='unlock("skin.2b43f6")') + button.customize-option(type='button', class='{{user.preferences.gender}}_skin_d7a9f7', ng-class='{locked: !user.purchased.skin.d7a9f7}', ng-click='unlock("skin.d7a9f7")') + button.customize-option(type='button', class='{{user.preferences.gender}}_skin_800ed0', ng-class='{locked: !user.purchased.skin.800ed0}', ng-click='unlock("skin.800ed0")') + button.customize-option(type='button', class='{{user.preferences.gender}}_skin_rainbow', ng-class='{locked: !user.purchased.skin.rainbow}', ng-click='unlock("skin.rainbow")') + button.btn.btn-small.btn-primary(ng-hide='user.purchased.skin.eb052b && user.purchased.skin.f69922 && user.purchased.skin.f5d70f && user.purchased.skin.0ff591 && user.purchased.skin.2b43f6 && user.purchased.skin.d7a9f7 && user.purchased.skin.800ed0 && user.purchased.skin.rainbow', ng-click='unlock(["skin.eb052b", "skin.f69922", "skin.f5d70f", "skin.0ff591", "skin.2b43f6", "skin.d7a9f7", "skin.800ed0", "skin.rainbow"])') Unlock Set - 5 + + // Special Events + div.well.limited-edition + .label.label-info.pull-right(popover='Available for purchase until November 10th (but permanently in your options if purchased).', popover-title='Limited Edition', popover-placement='right', popover-trigger='mouseenter') + | Limited Edition  + i.icon.icon-question-sign + h5. + Spooky Skins - 2/skin + //menu(label='Spooky Skins (2 Gems / skin)') + menu + button.customize-option(type='button', class='{{user.preferences.gender}}_skin_monster', ng-class='{locked: !user.purchased.skin.monster}', ng-click='unlock("skin.monster")') + button.customize-option(type='button', class='{{user.preferences.gender}}_skin_pumpkin', ng-class='{locked: !user.purchased.skin.pumpkin}', ng-click='unlock("skin.pumpkin")') + button.customize-option(type='button', class='{{user.preferences.gender}}_skin_skeleton', ng-class='{locked: !user.purchased.skin.skeleton}', ng-click='unlock("skin.skeleton")') + button.customize-option(type='button', class='{{user.preferences.gender}}_skin_zombie', ng-class='{locked: !user.purchased.skin.zombie}', ng-click='unlock("skin.zombie")') + button.customize-option(type='button', class='{{user.preferences.gender}}_skin_ghost', ng-class='{locked: !user.purchased.skin.ghost}', ng-click='unlock("skin.ghost")') + button.customize-option(type='button', class='{{user.preferences.gender}}_skin_shadow', ng-class='{locked: !user.purchased.skin.shadow}', ng-click='unlock("skin.shadow")') + button.btn.btn-small.btn-primary(ng-hide='user.purchased.skin.monster && user.purchased.skin.pumpkin && user.purchased.skin.skeleton && user.purchased.skin.zombie && user.purchased.skin.ghost && user.purchased.skin.shadow', ng-click='unlock(["skin.monster", "skin.pumpkin", "skin.skeleton", "skin.zombie", "skin.ghost", "skin.shadow"])') Unlock Set - 5 + + menu(ng-show='user.preferences.gender=="f"', type='list') li.customize-menu menu(label='Clothing') @@ -39,7 +81,7 @@ // ------- Edit ------- .span4 button.btn.btn-default(ng-click='_editing.profile = true', ng-show='!_editing.profile') Edit - button.btn.btn-primary(ng-click='_editing.profile = false', ng-show='_editing.profile') Save + button.btn.btn-primary(ng-click='save()', ng-show='_editing.profile') Save div(ng-show='!_editing.profile') h4 Display Name span(ng-show='profile.profile.name') {{profile.profile.name}} @@ -55,23 +97,23 @@ //{{profile.profile.blurb | linky:'_blank'}} h4 Websites - ul(ng-show='profile.profile.websites') + ul(ng-show='profile.profile.websites.length > 0') // TODO let's remove links eventually, since we can do markdown on profiles li(ng-repeat='website in profile.profile.websites') a(target='_blank', ng-href='{{website}}') {{website}} - span.muted(ng-hide='profile.profile.websites') - None - + span.muted(ng-hide='profile.profile.websites.length > 0') - None - div.whatever-options(ng-show='_editing.profile') // TODO use photo-upload instead: https://groups.google.com/forum/?fromgroups=#!topic/derbyjs/xMmADvxBOak .control-group.option-large label.control-label Display Name - input.option-content(type='text', placeholder='Full Name', ng-model='user.profile.name', ng-blur='set("profile.name", user.profile.name)', ) + input.option-content(type='text', placeholder='Full Name', ng-model='editingProfile.name') .control-group.option-large label.control-label Photo Url - input.option-content(type='url', ng-model='user.profile.imageUrl', placeholder='Image Url', ng-blur='set("profile.imageUrl", user.profile.imageUrl)') + input.option-content(type='url', ng-model='editingProfile.imageUrl', placeholder='Image Url') .control-group.option-large label.control-label Blurb - textarea.option-content(style='height:15em;', placeholder='Blurb', ng-model='user.profile.blurb', ng-blur='set("profile.blurb", user.profile.blurb)') + textarea.option-content(style='height:15em;', placeholder='Blurb', ng-model='editingProfile.blurb') include ../shared/formatting-help .control-group.option-large label.control-label Websites @@ -79,7 +121,7 @@ input.option-content(type='url', ng-model='_newWebsite', placeholder='Add Website') ul // would prefer if there were and index in #each, instead using data-website to search with indexOf - li(ng-repeat='website in user.profile.websites') + li(ng-repeat='website in editingProfile.websites') | {{website}} a(ng-click='removeWebsite($index)') i.icon-remove diff --git a/views/shared/footer.jade b/views/shared/footer.jade index 8b2569e042..229da7168b 100644 --- a/views/shared/footer.jade +++ b/views/shared/footer.jade @@ -11,10 +11,14 @@ footer.footer(ng-controller='FooterCtrl') .span3 h4 Company ul.unstyled + li + .btn.btn-small.btn-success(ng-click='modals.buyGems = true') + i.icon-heart.icon-white + | Donate li a(href='/static/about') About li - a(target='_blank', href='http://habitrpg.tumblr.com/') Blog + a(target='_blank', href='http://blog.habitrpg.com/') Blog li a(href='/static/extensions') Extensions li diff --git a/views/shared/gems.jade b/views/shared/gems.jade index f276a47528..09c9c50dbf 100644 --- a/views/shared/gems.jade +++ b/views/shared/gems.jade @@ -1,5 +1,5 @@ -a.pull-right.gem-wallet(popover-trigger='mouseenter', popover-title='Gems', popover="Used for buying special items (reroll, eggs, hatching potions, etc). You'll need to unlock those features before being able to use Gems.", popover-placement='bottom') - span.task-action-btn.tile.flush.bright.add-gems-btn(ng-click='showStripe()') + +a.pull-right.gem-wallet(ng-click='modals.buyGems = true', popover-trigger='mouseenter', popover-title='Gems', popover="Used for buying special items (reroll, eggs, hatching potions, etc). You'll need to unlock those features before being able to use Gems.", popover-placement='bottom') + span.task-action-btn.tile.flush.bright.add-gems-btn + span.task-action-btn.tile.flush.neutral .Gems | {{user.balance * 4 | number:0}} Gems \ No newline at end of file diff --git a/views/shared/header/header.jade b/views/shared/header/header.jade index 2a93254e55..0570507780 100644 --- a/views/shared/header/header.jade +++ b/views/shared/header/header.jade @@ -23,7 +23,7 @@ | {{user.stats.exp | number:0}} / {{tnl(user.stats.lvl)}} // FIXME doesn't look great here, but the "Experience" CSS title rollover covers it where it was before span(ng-show='user.history.exp') - a(x-bind='click:toggleChart', ng-click='notPorted()', data-id='exp', tooltip='Progress') + a(ng-click='toggleChart("exp")', tooltip='Progress') i.icon-signal // party span(ng-controller='PartyCtrl') diff --git a/views/shared/header/menu.jade b/views/shared/header/menu.jade index 5d19ba2184..5908bf27aa 100644 --- a/views/shared/header/menu.jade +++ b/views/shared/header/menu.jade @@ -5,7 +5,7 @@ h1.task-action-btn.tile.solid.user-reporter {{username(user.auth, user.profile.name)}} ul.flyout-content.nav.stacked li - a.task-action-btn.tile.solid(x-bind='click:toggleGamePane') + a.task-action-btn.tile.solid span(ng-show='viewingOptions', ng-click='gotoTasks()') i.icon-ok | Tasks diff --git a/views/shared/modals/achievements.jade b/views/shared/modals/achievements.jade index a913128af7..a68e1e794c 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.modals.streakAchievement = false') Cancel + button.btn.btn-default.cancel(ng-click='modals.achievements.streak = false') Cancel // Max Gear div(modal='modals.achievements.maxGear') @@ -17,7 +17,7 @@ div(modal='modals.achievements.maxGear') .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.modals.streakAchievement = false') Cancel + button.btn.btn-default.cancel(ng-click='modals.achievements.maxGear = false') Cancel // Beast Master div(modal='modals.achievements.beastmaster') @@ -28,6 +28,6 @@ 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.modals.beastMaster = false') Cancel + button.btn.btn-default.cancel(ng-click='modals.achievements.beastmaster = false') Cancel diff --git a/views/shared/modals/buy-gems.jade b/views/shared/modals/buy-gems.jade new file mode 100644 index 0000000000..e041f2d0ae --- /dev/null +++ b/views/shared/modals/buy-gems.jade @@ -0,0 +1,32 @@ +div(modal='modals.buyGems') + .modal-header + include ../gems + h3 Buy Gems / Donate + + .modal-body + table + tr + td + .NPC-Justin + td + .popover.static-popover.fade.right.in.wide-popover + .arrow + h3.popover-title + a(target='_blank', href='https://twitter.com/bowenNstuff') Justin + .popover-content + p $5 USD will: + ul.modal-indented-list + li Add 20 gems to your account, which are used to buy special items. + li Disable ads. + li Donate to the developers (as an open source project, we can use all the help we can get). + br + .row-fluid + .span6.well + h3 Pay with Card + .btn.btn-primary(ng-click='showStripe()') Pay with Card + .span6.well + h3 Pay with PayPal + script(src='/bower_components/JavaScriptButtons/dist/paypal-button.min.js?merchant=#{env.PAYPAL_MERCHANT}', data-button='buynow', data-name='20 Gems, Disable Ads, Donation to the Developers', data-quantity='1', data-amount='5', data-currency='USD', data-tax='0', data-callback='#{env.BASE_URL}/api/v1/user/buy-gems/paypal-ipn', data-env="#{env.NODE_ENV == 'production' ? '' : 'sandbox'}", data-custom='?uid={{user._id}}&apiToken={{user.apiToken}}', data-return='#{env.BASE_URL}', data-rm='1', data-no_shipping='1') + + .modal-footer + button.btn.btn-default.cancel(ng-click='modals.buyGems = false') Cancel \ No newline at end of file diff --git a/views/shared/modals/index.jade b/views/shared/modals/index.jade index 3ff38b60ec..6d15f517b7 100644 --- a/views/shared/modals/index.jade +++ b/views/shared/modals/index.jade @@ -3,7 +3,7 @@ include ./login include ./reroll include ./death include ./new-stuff -include ./why-ads -include ./more-gems +include ./buy-gems include ./members -include ./settings \ No newline at end of file +include ./settings +include ./pets \ No newline at end of file diff --git a/views/shared/modals/more-gems.jade b/views/shared/modals/more-gems.jade deleted file mode 100644 index b33d4dd87f..0000000000 --- a/views/shared/modals/more-gems.jade +++ /dev/null @@ -1,10 +0,0 @@ -div(modal='modals.moreGems', header='Out Of Gems') - .modal-header - h3 Out Of Gems - .modal-body - include ../gems - p. - Oops, out of Gems, which are used to buy special items! Habit is an open source project, and can use all the help - it can get - buy more Gems to receive this pet, and consider it a donation to the contributors. - .modal-footer - button.btn.btn-default.cancel(ng-click='modals.moreGems = false') Cancel \ No newline at end of file diff --git a/views/shared/modals/new-stuff.jade b/views/shared/modals/new-stuff.jade index 68d01c62cc..ecbf986797 100644 --- a/views/shared/modals/new-stuff.jade +++ b/views/shared/modals/new-stuff.jade @@ -1,49 +1,3 @@ -div(modal='user.flags.rewrite !== false') - .modal-header - h3 Mega Ultra Update! - .modal-body - table - tr - td - .NPC-Bailey - td - .popover.static-popover.fade.right.in.wide-popover - .arrow - h3.popover-title - a(target='_blank', href='http://www.kickstarter.com/profile/mihakuu') Bailey - .popover-content - p. - Hello my Habiteers! I have some amazing news to share with you, it's huge! - Has Habit ever crashed for you? (Joke). Well we re-wrote the website from the ground up - to conquor those critical bugs once and for all (more from Tyler in a bit). If you haven't seen me for a while (due to a bug in the old site), be sure to catch up with me on the right side of the screen for any missed news. Importantly: - .alert.alert-success(style='margin-bottom:5px'). - Android & iOS Apps are out! - p. - They're open source, so help us make them awesome. As for the rewrite: not all features are yet ported, but don't worry - you're still getting drops and streak-bonuses in the background, even if you can't see them yet. - We'll be working hard to bring in all the missing features. And if you're not already, be sure to follow our updates on Tumblr (there are some fun member highlights recently). One more thing: if you are a Veteran of the old site, I have granted you a Veteran Wolf! Check your inventory :) - - table(style='clear:both;') - tr - td - .popover.static-popover.fade.left.in - .arrow - h3.popover-title - a(target='_blank', href='https://twitter.com/lefnire') Tyler - .popover-content - p. - JavaScript developers! To me! We must finish vanquishing the old site, as not all features have been ported. - We rewrote Habit on AngularJS + Express. - We desparately need your help porting the rest of the features, and polishing off the bugs. Read this guide to getting started. - Thanks everyone for all your support and patience! - - td - img.pull-right(src='/bower_components/habitrpg-shared/img/unprocessed/efbd21c4-82a1-11e2-8190-fbc609b5c58b.png', style='height:72px') - - .modal-footer - button.btn.btn-default.cancel(ng-click='user.flags.rewrite = false') Read Later - button.btn.btn-warning.cancel(ng-click='set("flags.rewrite", false)') Dismiss - - div(modal='modals.newStuff') .modal-header h3 New Stuff! @@ -56,34 +10,61 @@ div(modal='modals.newStuff') .popover.static-popover.fade.right.in.wide-popover .arrow h3.popover-title - a(target='_blank', href='http://www.kickstarter.com/profile/mihakuu') Bailey + a(target='_blank', href='https://twitter.com/Mihakuu') Bailey .popover-content - strong 09/01/2013 - p. - We re-wrote the website from the ground up - And in case you missed it, Android & iOS Apps are out! - Both apps and the website are open source, and we desparately need your help porting the rest of the features, and polishing off the bugs. Read this guide to getting started. - Each of your pull requests shall grant you a special Contributor Gear piece. + strong 10/22/2013 + p + ul.modal-indented-list + li TRICK OR TREAT! It's Habit Halloween! Some of the NPCs have decorated for the occasion. Can you spot us? + li Two gem-purchasable skin tones are now available! The Rainbow Skin Set is here to stay, but in honor of Halloween, we also have the LIMITED EDITION SPOOKY SKIN SET. You will only be able to purchase the Spooky Skin Set until November 10th, so if you want a monstrous avatar, now's the time to act! + li Do note, skins won't work on mobile until the app is updated. We'll update Android ASAP, iPhone usually takes ~1wk to approve. + hr h3 Archive p + h4 10/19/2013 + ul + li. + New custom skin colors are now available! Go check them out in the Profile section. Also, the new mobile update, 0.0.10, is now available to download! It includes the new skin tones and the ability to hide or show your helm, among other things. + li. + You can now sell un-wanted drops to Alex the Merchant. Trade those troves of eggs for gold! + h4 09/01/2013 + ul + li. + We re-wrote the website from the ground up + And in case you missed it, Android & iOS Apps are out! + Both apps and the website are open source, and we desparately need your help porting the rest of the features, and polishing off the bugs. Read this guide to getting started. + We're working on a system of Contributor Gear to reward the awesome people who help out, so stay tuned! + + h4 The Rewrite! (Mid August) + ul + li. + Hello my Habiteers! I have some amazing news to share with you, it's huge! + Has Habit ever crashed for you? (Joke). Well we re-wrote the website from the ground up + to conquor those critical bugs once and for all (more from Tyler in a bit). If you haven't seen me for a while (due to a bug in the old site), be sure to catch up with me on the right side of the screen for any missed news. Importantly: + .alert.alert-success(style='margin-bottom:5px'). + Android & iOS Apps are out! + li. + They're open source, so help us make them awesome. As for the rewrite: not all features are yet ported, but don't worry - you're still getting drops and streak-bonuses in the background, even if you can't see them yet. + We'll be working hard to bring in all the missing features. And if you're not already, be sure to follow our updates on Tumblr (there are some fun member highlights recently). One more thing: if you are a Veteran of the old site, I have granted you a Veteran Wolf! Check your inventory :) + li. + JavaScript developers! To me! We must finish vanquishing the old site, as not all features have been ported. + We rewrote Habit on AngularJS + Express. + We desparately need your help porting the rest of the features, and polishing off the bugs. Read this guide to getting started. + Thanks everyone for all your support and patience! + h4 8/20/2013 ul - li - | Timezone + custom day start issues fixed, your dailies should now reset properly and in your own timezone. (This was vexing - strong Android - | users particularly). If you're still experiencing issues, - a(target='_blank', href='https://github.com/HabitRPG/habitrpg-mobile/issues/73#issuecomment-22960877') chime in here. + li. + Timezone + custom day start issues fixed, your dailies should now reset properly and in your own timezone. (This was vexing Android users particularly). If you're still experiencing issues, chime in here. li | API developers, the above means that strong cron | is automatically run for your users! Weee, they no longer have to log into the website to reset their dailies! h4 8/18/2013 ul - li - a(target='_blank', href='https://play.google.com/store/apps/details?id=com.ocdevel.habitrpg') Android Mobile App is out! - | There's a bug with Android 2.3, but we're working to fix it now :) The iPhone App has been submitted, so we're just waiting on Apple to approve it, which could take a week or two. We'll let you know as soon as they release it! For more details, - a(target='_blank', href='http://habitrpg.tumblr.com/post/58449057415/android-mobile-app-released-iphone-app-coming-soon') see our Tumblr post + li. + The Mobile Apps are out! iOS app and Android. There's a bug with Android 2.3, follow the progress here. For more details, see our Tumblr post li | Hey guys! Long time no see :) We want to make sure you guys have a better idea of what's going on behind the scenes, so we're going to be releasing b weekly status reports @@ -154,7 +135,7 @@ div(modal='modals.newStuff') | (basic implementation, more to come) li | NPCs! - a(target='_blank', href='http://www.kickstarter.com/profile/mihakuu') Bailey + a(target='_blank', href='https://twitter.com/Mihakuu') Bailey | the Town Crier, a(target='_blank', href='http://www.kickstarter.com/profile/523661924') Alexander | the diff --git a/views/shared/modals/pets.jade b/views/shared/modals/pets.jade index 366ba58e50..aa60e7610b 100644 --- a/views/shared/modals/pets.jade +++ b/views/shared/modals/pets.jade @@ -1,21 +1,24 @@ -div(modal='modals.pets.dropsEnabled') +div(modal='modals.dropsEnabled') .modal-header h3 Drops Enabled! .modal-body p - span.item-drop-icon(ng-class='Pet_Egg_{{user.items.eggs.0.name}}') - | You've unlocked the Drop System! Now when you complete tasks, you have a small chance of finding an item. And guess what, you just found a - strong {{user.items.eggs.0.text}} egg - | ! {{user.items.eggs.0.notes}} + span.item-drop-icon(class='Pet_Egg_{{user.items.eggs.0.name}}', style='margin-left: 0px') + | You've unlocked the Drop System! Now when you complete tasks, you have a small chance of finding an item. You just found a + strong {{user.items.eggs.0.text}} Egg + | ! {{user.items.eggs.0.notes}}. + br + p. + If you've got your eye on a pet, but can't wait any longer for it to drop, use Gems in Options > Inventory to buy one! .modal-footer - button.btn.btn-default.cancel(ng-click='modals.pets.dropsEnabled=false') Close + button.btn.btn-default.cancel(ng-click='modals.dropsEnabled = false') Close -div(modal='modals.pets.itemDropped') +div(modal='modals.drop') .modal-header h3 An item has dropped! .modal-body p - span.item-drop-icon(ng-class='Pet_{{_drop.type}}_{{_drop.name}}') - | {{_drop.dialog}} + span.item-drop-icon(class='Pet_{{user._tmp.drop.type}}_{{user._tmp.drop.name}}') + | {{user._tmp.drop.dialog}} .modal-footer - button.btn.btn-default.cancel(ng-click='modals.pets.itemDropped=false') Close \ No newline at end of file + button.btn.btn-default.cancel(ng-click='modals.drop = false') Close \ No newline at end of file diff --git a/views/shared/modals/reroll.jade b/views/shared/modals/reroll.jade index 4f48f8f4a6..0941593b32 100644 --- a/views/shared/modals/reroll.jade +++ b/views/shared/modals/reroll.jade @@ -11,7 +11,7 @@ div(modal='modals.reroll') .modal-footer span(ng-if='user.balance < 1') - a.btn.btn-success.btn-large(ng-click="showStripe()") Buy More Gems + 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 >= 1', ng-controller='SettingsCtrl') a.btn.btn-danger.btn-large(ng-click='reroll()') Re-Roll diff --git a/views/shared/modals/settings.jade b/views/shared/modals/settings.jade index 3467a49fe5..6e96247eb1 100644 --- a/views/shared/modals/settings.jade +++ b/views/shared/modals/settings.jade @@ -18,29 +18,29 @@ div(ng-controller='SettingsCtrl') form#restore-form.form-horizontal h3 Stats .option-group.option-medium - input.option-content(type='number', data-for='stats.hp', ng-model='user.stats.hp') + input.option-content(type='number', step="any", data-for='stats.hp', ng-model='restoreValues.stats.hp') span.input-suffix HP .option-group.option-medium - input.option-content(type='number', data-for='stats.exp', ng-model='user.stats.exp') + input.option-content(type='number', step="any", data-for='stats.exp', ng-model='restoreValues.stats.exp') span.input-suffix Exp .option-group.option-medium - input.option-content(type='number', data-for='stats.gp', ng-model='user.stats.gp') + input.option-content(type='number', step="any", data-for='stats.gp', ng-model='restoreValues.stats.gp') span.input-suffix GP .option-group.option-medium - input.option-content(type='number', data-for='stats.lvl', ng-model='user.stats.lvl') + input.option-content(type='number', data-for='stats.lvl', ng-model='restoreValues.stats.lvl') span.input-suffix Level h3 Items .option-group.option-medium - input.option-content(type='number', data-for='items.weapon', ng-model='user.items.weapon') + input.option-content(type='number', data-for='items.weapon', ng-model='restoreValues.items.weapon') span.input-suffix Weapon .option-group.option-medium - input.option-content(type='number', data-for='items.armor', ng-model='user.items.armor') + input.option-content(type='number', data-for='items.armor', ng-model='restoreValues.items.armor') span.input-suffix Armor .option-group.option-medium - input.option-content(type='number', data-for='items.head', ng-model='user.items.head') + input.option-content(type='number', data-for='items.head', ng-model='restoreValues.items.head') span.input-suffix Helm .option-group.option-medium - input.option-content(type='number', data-for='items.shield', ng-model='user.items.shield') + input.option-content(type='number', data-for='items.shield', ng-model='restoreValues.items.shield') span.input-suffix Shield .modal-footer button.btn.btn-primary(ng-click='restore()') Save & Close diff --git a/views/shared/modals/why-ads.jade b/views/shared/modals/why-ads.jade deleted file mode 100644 index 38dd5a8214..0000000000 --- a/views/shared/modals/why-ads.jade +++ /dev/null @@ -1,11 +0,0 @@ -div(modal='modals.whyAds') - .modal-header - h3 Why Ads? - .modal-body - p. - Habit is an open source project, and can use all the help it can get - consider this a donation to the contributors. You also get 20 Gems from the purchase, which you can use to buy special items. - p - | "Hey, I backed the Kickstarter!" - follow - a(target='_blank', href='http://community.habitrpg.com/node/22') these instructions. - .modal-footer - button.btn.btn-default.cancel(ng-click='modals.whyAds = false') Ok \ No newline at end of file diff --git a/views/shared/profiles/achievements.jade b/views/shared/profiles/achievements.jade index 724829e975..3678d69696 100644 --- a/views/shared/profiles/achievements.jade +++ b/views/shared/profiles/achievements.jade @@ -64,7 +64,7 @@ Helped HabitRPG grow by filling out this survey. hr - div(ng-if='profile.achievements.originalUser || profile.achievements.veteranUser') + div(ng-if='profile.achievements.originalUser || profile.achievements.veteran') .achievement.achievement-cake div(ng-if='profile.achievements.veteran') h5 Veteran diff --git a/views/static/front.jade b/views/static/front.jade index 79dc758a8f..cc3eaffc6f 100644 --- a/views/static/front.jade +++ b/views/static/front.jade @@ -19,7 +19,7 @@ block content a.btn.btn-primary.btn-small(ng-click='playButtonClick()') Play .container p(style='height:600;') - iframe(src='//player.vimeo.com/video/57639356', width='100%', height='539', frameborder='0', webkitallowfullscreen='', mozallowfullscreen='', allowfullscreen='') + iframe(src='//player.vimeo.com/video/76557040', width='100%', height='539', frameborder='0', webkitallowfullscreen='', mozallowfullscreen='', allowfullscreen='') .modal.fade#login-modal(style='display:none') .modal-dialog diff --git a/views/static/layout.jade b/views/static/layout.jade index 813842b38a..2057c6ab30 100644 --- a/views/static/layout.jade +++ b/views/static/layout.jade @@ -32,7 +32,6 @@ html script(type='text/javascript', src='/bower_components/bootstrap/docs/assets/js/bootstrap.min.js') script(type='text/javascript', src='/js/static.js') - script(type='text/javascript', src='/js/services/memberServices.js') script(type='text/javascript', src='/js/services/userServices.js') script(type='text/javascript', src='/js/controllers/authCtrl.js') -} diff --git a/views/tasks/ads.jade b/views/tasks/ads.jade index 4337c96f8f..1622a3cb74 100644 --- a/views/tasks/ads.jade +++ b/views/tasks/ads.jade @@ -1,10 +1,7 @@ -div(ng-if='authenticated() && user.flags.ads!="hide"') +div(ng-if='authenticated() && !user.purchased.ads') span.pull-right(ng-if='list.type!="reward"') - a(ng-click='showStripe()', tooltip='Remove Ads') + a(ng-click='modals.buyGems=true', tooltip='Remove Ads') i.icon-remove - br - a(ng-click='modals.whyAds=true', tooltip='Why Ads?') - i.icon-question-sign div(ng-if='list.type=="habit"', habitrpg-adsense) script(async='async', src='//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js') diff --git a/views/tasks/index.jade b/views/tasks/index.jade index d56da854a2..a50a17b0cb 100644 --- a/views/tasks/index.jade +++ b/views/tasks/index.jade @@ -6,10 +6,10 @@ div(ng-controller='TasksCtrl') // Todos export/graph options span.option-box.pull-right(ng-if='list.main && list.type=="todo"') - a.option-action(ng-show='user.history.todos', x-bind='click:toggleChart', ng-click='notPorted()', data-id='todos', tooltip='Progress') + a.option-action(ng-show='user.history.todos', ng-click='toggleChart("todos")', tooltip='Progress') i.icon-signal //-a.option-action(ng-href='/v1/users/{{user.id}}/calendar.ics?apiToken={{user.apiToken}}', tooltip='iCal') - a.option-action(ng-click='notPorted()', tooltip='iCal') + a.option-action(ng-click='notPorted()', tooltip='iCal', ng-show='false') i.icon-calendar // @@ -26,7 +26,7 @@ div(ng-controller='TasksCtrl') h2.task-column_title {{list.header}} // Todo Chart - .todos-chart(ng-if='list.type == "todo"', ng-show='_page.charts.todos') + .todos-chart(ng-if='list.type == "todo"', ng-show='charts.todos') // Add New form.addtask-form.form-inline.new-task-form(name='new{{list.type}}form', ng-show='list.editable', ng-hide='list.showCompleted && list.type=="todo"', data-task-type='{{list.type}}', ng-submit='addTask(list)') diff --git a/views/tasks/task.jade b/views/tasks/task.jade index 266a7861d6..a2a2abdcde 100644 --- a/views/tasks/task.jade +++ b/views/tasks/task.jade @@ -12,8 +12,11 @@ li(ng-repeat='task in user[list.type + "s"]', class='task {{taskClasses(task,use i.icon-tags(tooltip='{{appliedTags(user.tags, task.tags)}}', ng-hide='noTags(task.tags)') // edit - a(ng-click='task._editing = !task._editing', tooltip='Edit') - i.icon-pencil + a(ng-hide='task._editing', ng-click='toggleEdit(task)', tooltip='Edit') + i.icon-pencil(ng-hide='task._editing') + // cancel + a(ng-hide='!task._editing', ng-click='toggleEdit(task)', tooltip='Cancel') + i.icon-remove(ng-hide='!task._editing') //- challenges // {{#if task.challenge}} // {{#if brokenChallengeLink(task)}} @@ -27,10 +30,10 @@ li(ng-repeat='task in user[list.type + "s"]', class='task {{taskClasses(task,use a(ng-click='remove(task)', tooltip='Delete') i.icon-trash // chart - a(ng-show='task.history', x-bind='click:toggleChart', ng-click='notPorted()', data-id='{{task.id}}', tooltip='Progress') + a(ng-show='task.history', ng-click='toggleChart(task.id, task)', tooltip='Progress') i.icon-signal // notes - span.task-notes(ng-show='task.notes', popover-trigger='mouseenter', popover-placement='left', popover='{{task.notes}}', popover-title='{{task.text}}') + span.task-notes(ng-show='task.notes && !task._editing', popover-trigger='mouseenter', popover-placement='left', popover='{{task.notes}}', popover-title='{{task.text}}') i.icon-comment // left-hand side checkbox @@ -39,8 +42,8 @@ li(ng-repeat='task in user[list.type + "s"]', class='task {{taskClasses(task,use // Habits span(ng-if='list.main && task.type=="habit"') // only allow scoring on main tasks, not when viewing others' public tasks or when creating challenges - a.task-action-btn(ng-show='task.up', ng-click='score(task,"up")') + - a.task-action-btn(ng-show='task.down', ng-click='score(task,"down")') - + a.task-action-btn(ng-if='task.up', ng-click='score(task,"up")') + + a.task-action-btn(ng-if='task.down', ng-click='score(task,"down")') - //span(ng-if='!list.main') // span.task-action-btn(ng-show='task.up') + // span.task-action-btn(ng-show='task.down') = @@ -86,7 +89,7 @@ li(ng-repeat='task in user[list.type + "s"]', class='task {{taskClasses(task,use fieldset.option-group // {{#unless taskInChallenge(task)}} label.option-title Text - input.option-content(type='text', ng-model='task.text') + input.option-content(type='text', ng-model='task.text', required) // {{/}} label.option-title Extra Notes // {{#if taskInChallenge(task)}} @@ -120,7 +123,7 @@ li(ng-repeat='task in user[list.type + "s"]', class='task {{taskClasses(task,use // if Reward, pricing fieldset.option-group.option-short(ng-if='task.type=="reward"') legend.option-title Price - input.option-content(type='number', size='16', min='0', ng-model='task.value') + input.option-content(type='number', size='16', min='0', step="any", ng-model='task.value') .money.input-suffix span.shop_gold // if Todos, the due date @@ -157,4 +160,4 @@ li(ng-repeat='task in user[list.type + "s"]', class='task {{taskClasses(task,use input.option-content(type='number', ng-model='task.streak') button.task-action-btn.tile.spacious(type='submit') Save & Close - div(class='{{task.id}}-chart', ng-show='_page.charts[task.id]') + div(class='{{task.id}}-chart', ng-show='charts[task.id]')