Merge remote-tracking branch 'upstream/develop' into manual-due-date

This commit is contained in:
J. Ozymandias Fallick 2013-10-23 14:34:36 -04:00
commit 22b429242f
76 changed files with 1544 additions and 966 deletions

3
.gitignore vendored
View file

@ -12,4 +12,5 @@ build
src/*/*.map
src/*/*/*.map
test/*.js
test/*.map
test/*.map
public/docs

0
.gitmodules vendored
View file

7
.travis.yml Normal file
View file

@ -0,0 +1,7 @@
language: node_js
node_js:
- '0.8'
- '0.10'
before_script:
- 'npm install -g bower grunt-cli'
- 'bower install'

77
DOCS-README.md Normal file
View file

@ -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

View file

@ -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');
};

115
README.md
View file

@ -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.

View file

@ -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

View file

@ -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"
}
}

View file

@ -12,5 +12,6 @@
"SMTP_PASS":"password",
"SMTP_SERVICE":"Gmail",
"STRIPE_API_KEY":"aaaabbbbccccddddeeeeffff00001111",
"STRIPE_PUB_KEY":"22223333444455556666777788889999"
}
"STRIPE_PUB_KEY":"22223333444455556666777788889999",
"PAYPAL_MERCHANT":"paypal-merchant@gmail.com"
}

54
karma-e2e.conf.js Normal file
View file

@ -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_'
});
};

56
karma.conf.js Normal file
View file

@ -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
});
};

View file

@ -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}});
});

View file

@ -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!");
});

View file

@ -1 +1 @@
db.users.update({},{$set:{'flags.newStuff':'show'}},{multi:true})
db.users.update({},{$set:{'flags.newStuff':true}},{multi:true})

View file

@ -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"
}
}

View file

@ -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;
.well.limited-edition
padding: 5px
margin: 0px

View file

@ -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

View file

@ -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
margin-right: 5px
.modal-indented-list
margin-left: 10px;
padding-left: 10px;

View file

@ -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

View file

@ -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;}

View file

@ -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);
}
}

View file

@ -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('<img src="//wms.assoc-amazon.com/20070822/US/img/noscript.gif?tag=ha0d2-20" alt="" />');
// });

View file

@ -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();
});

View file

@ -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;
}
}]);

View file

@ -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?"

View file

@ -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?

View file

@ -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 + " <i class='icon-gold'></i> " + silver + " <i class='icon-silver'></i>";
} else if (gold > 0) {
return "" + gold + " <i class='icon-gold'></i>";
} else if (silver > 0) {
return "" + silver + " <i class='icon-silver'></i>";
}
};
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("<i class='icon-heart'></i> - " + rounded + " HP", 'hp');
} else if (num > 0) {
// gained hp from potion/level?
statsNotification("<i class='icon-heart'></i> + " + 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("<i class='icon-star'></i> - " + rounded + " XP", 'xp');
} else if (num > 0) {
statsNotification("<i class='icon-star'></i> + " + 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("<i class='icon-death'></i> Respawn!", "death");
// }
// });
$rootScope.$watch('user.stats.lvl', function(after, before) {
if (after == before) return;
if (after > before) {
statsNotification('<i class="icon-chevron-up"></i> Level Up!', 'lvl');
}
});
};
setupGrowlNotifications();
$rootScope.$watch('user.stats.lvl', function(after, before) {
if (after == before) return;
if (after > before) {
Notification.lvl();
}
});
}
]);

View file

@ -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;

View file

@ -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);
};
}
]);

View file

@ -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,

View file

@ -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'});
}
}]);
}]);

View file

@ -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);
}
}]);
]);

View file

@ -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());
},

View file

@ -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);
}
});
.filter('gold', function () {
return function (gp) {
return Math.floor(gp);
}
}).filter('silver', function () {
return function (gp) {
return Math.floor((gp - Math.floor(gp))*100);
}
});

View file

@ -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 = "<div><div class='NPC-Justin'></div>" + step.content + "</div>"; // add Justin NPC img
step.content = "<div><div class='NPC-Justin float-left'></div>" + step.content + "</div>"; // 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 = "<button class='btn btn-sm btn-default' onClick=\"$('" + selector + "').popover('hide');return false;\">Close</button>";
html = "<div><div class='NPC-Justin float-left'></div>" + html + '<br/>' + button + '</div>';
$(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
};
}
]);

View file

@ -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;
}
]);

View file

@ -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 + " <i class='icon-gold'></i> " + silver + " <i class='icon-silver'></i>";
} else if (gold > 0) {
return "" + gold + " <i class='icon-gold'></i>";
} else if (silver > 0) {
return "" + silver + " <i class='icon-silver'></i>";
}
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 + "<br />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 = "<br />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("<i class='icon-heart'></i> " + 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("<i class='icon-star'></i> " + 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('<i class="icon-chevron-up"></i> Level Up!', 'lvl');
},
death: function(){
growl("<i class='icon-death'></i> Respawn!", "death");
}
};
}
]);

View file

@ -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;
}
]);

View file

@ -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')

View file

@ -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();

View file

@ -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);
});
}

View file

@ -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;
});
};

View file

@ -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

View file

@ -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

View file

@ -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']);

6
test/README.md Normal file
View file

@ -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

38
test/spec/authCtrlSpec.js Normal file
View file

@ -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;
});
});
});

View file

@ -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);
});
});

View file

@ -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, {}]);
});
});

View file

@ -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)

View file

@ -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')

View file

@ -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 <a target=_blank href="https://trello.com/card/challenges-individual-party-guild-public/50e5d3684fe3a7266b0036d6/58">Challenges</a> 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 class="task-action-btn tile flush bright add-gems-btn"></span>
span.task-action-btn.tile.flush.neutral
.Gems

View file

@ -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

View file

@ -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 <b>{{selectedEgg.text}}</b> 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}}

View file

@ -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}}

View file

@ -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')

View file

@ -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<span class="Pet_Currency_Gem1x inline-gems"/>/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<span class="Pet_Currency_Gem1x inline-gems"/>
// 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&nbsp;
i.icon.icon-question-sign
h5.
Spooky Skins - 2<span class="Pet_Currency_Gem1x inline-gems"/>/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<span class="Pet_Currency_Gem1x inline-gems"/>
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

View file

@ -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

View file

@ -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

View file

@ -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')

View file

@ -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

View file

@ -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

View file

@ -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 <span class='label label-info'>$5 USD</span> 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

View file

@ -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
include ./settings
include ./pets

View file

@ -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

View file

@ -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 <u><a target='_blank' href="http://habitrpg.tumblr.com/post/59104876969/website-issues-what-were-doing">re-wrote the website</a></u> 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').
<a target='_blank' href='https://play.google.com/store/apps/details?id=com.ocdevel.habitrpg'>Android</a> & <a target='_blank' href='https://itunes.apple.com/us/app/habitrpg/id689569235?mt=8'>iOS</a> Apps are out!</u>
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 <a href="http://habitrpg.tumblr.com/" target="_blank">Tumblr</a> (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 <a target='_blank' href='http://angularjs.org/'>AngularJS</a> + <a target='_blank' href='http://expressjs.com/'>Express</a>.
We desparately need your help porting <a href='http://goo.gl/jYWTwl' target='_blank'>the rest of the features</a>, and polishing off the bugs. <a target='_blank' href='https://github.com/lefnire/habitrpg/wiki/Contributing#website'>Read this guide</a> 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 <a target='_blank' href="http://habitrpg.tumblr.com/post/59104876969/website-issues-what-were-doing">re-wrote the website from the ground up</a>
And in case you missed it, <a target='_blank' href='https://play.google.com/store/apps/details?id=com.ocdevel.habitrpg'>Android</a> & <a target='_blank' href='https://itunes.apple.com/us/app/habitrpg/id689569235?mt=8'>iOS</a> 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. <a target='_blank' href='https://github.com/lefnire/habitrpg/wiki/Contributing#website'>Read this guide</a> to getting started.
Each of your pull requests shall grant you a special <a href='https://github.com/lefnire/habitrpg/issues/946#issuecomment-18654292' target='_blank'>Contributor Gear piece</a>.
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 <a target='_blank' href="http://habitrpg.tumblr.com/post/59104876969/website-issues-what-were-doing">re-wrote the website from the ground up</a>
And in case you missed it, <a target='_blank' href='https://play.google.com/store/apps/details?id=com.ocdevel.habitrpg'>Android</a> & <a target='_blank' href='https://itunes.apple.com/us/app/habitrpg/id689569235?mt=8'>iOS</a> 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. <a target='_blank' href='https://github.com/lefnire/habitrpg/wiki/Contributing#website'>Read this guide</a> 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 <u><a target='_blank' href="http://habitrpg.tumblr.com/post/59104876969/website-issues-what-were-doing">re-wrote the website</a></u> 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').
<a target='_blank' href='https://play.google.com/store/apps/details?id=com.ocdevel.habitrpg'>Android</a> & <a target='_blank' href='https://itunes.apple.com/us/app/habitrpg/id689569235?mt=8'>iOS</a> Apps are out!</u>
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 <a href="http://habitrpg.tumblr.com/" target="_blank">Tumblr</a> (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 <a target='_blank' href='http://angularjs.org/'>AngularJS</a> + <a target='_blank' href='http://expressjs.com/'>Express</a>.
We desparately need your help porting <a href='http://goo.gl/jYWTwl' target='_blank'>the rest of the features</a>, and polishing off the bugs. <a target='_blank' href='https://github.com/lefnire/habitrpg/wiki/Contributing#website'>Read this guide</a> 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 <strong>Android</strong> 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</a>.
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! <a target='_blank' href="https://itunes.apple.com/us/app/habitrpg/id689569235">iOS app</a> and <a target='_blank' href="https://play.google.com/store/apps/details?id=com.ocdevel.habitrpg">Android</a>. There's a bug with Android 2.3, <a target='_blank' href='https://github.com/HabitRPG/habitrpg-mobile/issues/85'>follow the progress here</a>. For more details, see our <a target=_blank href="http://habitrpg.tumblr.com/post/58449057415/android-mobile-app-released-iphone-app-coming-soon">Tumblr post</a>
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

View file

@ -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.
<span class='Pet_Currency_Gem item-drop-icon'></span> If you've got your eye on a pet, but can't wait any longer for it to drop, use Gems in <strong>Options > Inventory</strong> 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
button.btn.btn-default.cancel(ng-click='modals.drop = false') Close

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -64,7 +64,7 @@
Helped HabitRPG grow by filling out <a href='http://community.habitrpg.com/node/290' target='_blank'>this survey.</a>
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

View file

@ -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

View file

@ -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')
-}

View file

@ -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')

View file

@ -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
// <a href="https://www.google.com/calendar/render?cid={{encodeiCalLink(_user.id, _user.apiToken)}}" rel=tooltip title="Google Calendar"><i class=icon-calendar></i></a>
@ -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)')

View file

@ -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]')