This commit is contained in:
Matteo Pagliazzi 2013-11-11 21:20:12 +01:00
commit a1f9bf60fb
1132 changed files with 10739 additions and 240703 deletions

View file

@ -1,5 +0,0 @@
.DS_Store
public/gen/
#lib/
*.swp
.idea/

3
.bowerrc Normal file
View file

@ -0,0 +1,3 @@
{
"directory": "public/bower_components"
}

12
.gitignore vendored
View file

@ -1,8 +1,16 @@
.DS_Store
public/gen
node_modules
#lib/
*.swp
.idea*
config.json
npm-debug.log
npm-debug.log
lib
public/bower_components
build
src/*/*.map
src/*/*/*.map
test/*.js
test/*.map
public/docs

9
.gitmodules vendored
View file

@ -1,9 +0,0 @@
[submodule "public/vendor/github-buttons"]
path = public/vendor/github-buttons
url = https://github.com/mdo/github-buttons.git
[submodule "public/vendor/BrowserQuest"]
path = public/vendor/BrowserQuest
url = https://github.com/mozilla/BrowserQuest.git
[submodule "public/vendor/bootstrap-tour"]
path = public/vendor/bootstrap-tour
url = git://github.com/sorich87/bootstrap-tour.git

5
.nodemonignore Normal file
View file

@ -0,0 +1,5 @@
public/*
views/*
build
build/*
Gruntfile.js

8
.travis.yml Normal file
View file

@ -0,0 +1,8 @@
language: node_js
node_js:
- '0.10'
before_script:
- 'npm install -g bower grunt-cli'
- 'bower install'
- export DISPLAY=:99.0
- sh -e /etc/init.d/xvfb start

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

123
Gruntfile.js Normal file
View file

@ -0,0 +1,123 @@
/*global module:false*/
var _ = require('lodash');
module.exports = function(grunt) {
// Project configuration.
grunt.initConfig({
karma: {
unit: {
configFile: 'karma.conf.js'
},
continuous: {
configFile: 'karma.conf.js',
singleRun: true,
autoWatch: false
}
},
clean: {
build: ['build']
},
stylus: {
build: {
options: {
compress: false, // AFTER
'include css': true,
paths: ['public']
},
files: {
'build/app.css': ['public/css/index.styl'],
'build/static.css': ['public/css/static.styl']
}
}
},
copy: {
build: {
files: [{expand: true, cwd: 'public/', src: 'favicon.ico', dest: 'build/'}]
}
},
// UPDATE IT WHEN YOU ADD SOME FILES NOT ALREADY MATCHED!
hashres: {
build: {
options: {
fileNameFormat: '${name}-${hash}.${ext}'
},
src: [
'build/*.js', 'build/*.css', 'build/favicon.ico',
'build/bower_components/bootstrap/docs/assets/css/*.css',
'build/bower_components/habitrpg-shared/dist/*.css'
],
dest: 'make-sure-i-do-not-exist'
}
},
nodemon: {
dev: {
ignoredFiles: ['public/*', 'Gruntfile.js', 'views/*', 'build/*']
}
},
watch: {
dev: {
files: ['public/**/*.styl'], // 'public/**/*.js' Not needed because not in production
tasks: [ 'build:dev' ],
options: {
nospawn: true
}
}
},
concurrent: {
dev: ['nodemon', 'watch'],
options: {
logConcurrentOutput: true
}
}
});
//Load build files from public/manifest.json
grunt.registerTask('loadManifestFiles', 'Load all build files from public/manifest.json', function(){
var files = grunt.file.readJSON('./public/manifest.json');
var uglify = {};
var cssmin = {};
_.each(files, function(val, key){
var js = uglify['build/' + key + '.js'] = [];
_.each(files[key]['js'], function(val){
js.push('public/' + val);
});
_.each(files[key]['css'], function(val){
if(val == 'app.css' || val == 'static.css'){
cssmin['build/' + val] = ['build/' + val]
}else{
cssmin['build/' + val] = ['public/' + val]
}
});
});
grunt.config.set('uglify.build.files', uglify);
grunt.config.set('cssmin.build.files', cssmin);
});
// Register tasks.
grunt.registerTask('build:prod', ['loadManifestFiles', 'clean:build', 'uglify', 'stylus', 'cssmin', 'copy:build', 'hashres']);
grunt.registerTask('build:dev', ['loadManifestFiles', 'clean:build', 'stylus', 'cssmin', 'copy:build']);
grunt.registerTask('run:dev', [ 'build:dev', 'concurrent' ]);
// Load tasks
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-clean');
grunt.loadNpmTasks('grunt-contrib-stylus');
grunt.loadNpmTasks('grunt-contrib-cssmin');
grunt.loadNpmTasks('grunt-contrib-copy');
grunt.loadNpmTasks('grunt-nodemon');
grunt.loadNpmTasks('grunt-concurrent');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-hashres');
grunt.loadNpmTasks('grunt-karma');
};

View file

@ -1 +1 @@
web: node server.js
web: ./node_modules/.bin/grunt build:prod;./node_modules/.bin/grunt nodemon;

View file

@ -1,17 +1,89 @@
#[HabitRPG](http://habitrpg.com/)
HabitRPG
===============
HabitRPG is a 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.
[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.
[Read more](https://habitrpg.com/static/about)
Built using Angular, Express, Mongoose, Jade, Stylus, Grunt and Bower.
![Screenshot](https://raw.github.com/lefnire/habitrpg/master/public/img/screenshot.jpeg "Screenshot")
# Set up HabitRPG locally
##License
Code is licensed under GNU GPL v3. Content is licensed under CC-BY-SA 3.0.
See the LICENSE file for details.
**Windows** users should skip this section and read the one below with Windows-specific steps.
##Credits
Content comes from Mozilla's [BrowserQuest](http://browserquest.mozilla.org/)
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.
* [Mozilla](http://mozilla.org)
* [Little Workshop](http://www.littleworkshop.fr)
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`
1. Ensure that Mongo is running and seed the database with initial settings by running:
`node .\src\seed.js`.
## Windows Environment Install
1. Set up MongoDB. Steps:
1. Download the latest production release of MongoDB from: http://www.mongodb.org/downloads
1. Extract the zip file to the desired application directory. Example: c:\apps\mongodb-win32-x86_64-2.4.6
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'
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 [websvr] admin web console waiting for connections on po
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. 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
> ./node_modules/bower/bin/bower install -f
'.' is not recognized as an internal or external command,
operable program or batch file.
npm ERR! weird error 1
npm ERR! not ok code 0
Ignore this error and proceed with the following:
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`
1. Ensure that Mongo is running and seed the database with initial settings by r
unning:
`node ./src/seed.js`.
# 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.

8
archive/README.md Normal file
View file

@ -0,0 +1,8 @@
# What's This?
I'm consolidating @litenull's rewrite branch with the old code, and removing files from both sources once they've been
successfully merged into the new platform. While @litenull's "from scratch" approach was really clean, it will take
us longer to implemente all the original features. This approach will (1) let us leverage code we already have, (2) merge
in litenull's hard work from the last few weeks.
Once this archive/ directory is completely empty, we should be fully merged and ready to deploy the rewrite!

View file

@ -0,0 +1,46 @@
_ = require 'lodash'
moment = require 'moment'
###
Loads JavaScript files from public/vendor/*
Use require() to min / concatinate for faster page load
###
loadJavaScripts = (model) ->
# Turns out you can't have expressions in browserify require() statements
#vendor = '../../public/vendor'
#require "#{vendor}/jquery-ui-1.10.2/jquery-1.9.1"
###
Internal Scripts
###
require "../vendor/jquery.cookie.min.js"
require "../vendor/bootstrap/js/bootstrap.min.js"
require "../vendor/datepicker/js/bootstrap-datepicker"
require "../vendor/bootstrap-tour/bootstrap-tour"
unless (model.get('_mobileDevice') is true)
require "../vendor/sticky"
# note: external script loading is handled in app.on('render') near the bottom of this file (see https://groups.google.com/forum/?fromgroups=#!topic/derbyjs/x8FwdTLEuXo)
# jquery sticky header on scroll, no need for position fixed
initStickyHeader = (model) ->
$('.header-wrap').sticky({topSpacing:0})
module.exports.app = (appExports, model, app) ->
app.on 'render', (ctx) ->
#restoreRefs(model)
unless model.get('_mobileDevice')
setupTooltips(model)
initStickyHeader(model)
setupSortable(model)
$('.datepicker').datepicker({autoclose:true, todayBtn:true})
.on 'changeDate', (ev) ->
#for some reason selecting a date doesn't fire a change event on the field, meaning our changes aren't saved
model.at(ev.target).set 'date', moment(ev.date).format('MM/DD/YYYY')

View file

@ -0,0 +1,239 @@
_ = require 'lodash'
{helpers} = require 'habitrpg-shared'
async = require 'async'
module.exports.app = (app) ->
###
Sync any updates to challenges since last refresh. Do it after cron, so we don't punish them for new tasks
This is challenge->user sync. user->challenge happens when user interacts with their tasks
###
app.on 'ready', (model) ->
window.setTimeout ->
_.each model.get('groups'), (g) ->
if (@uid in g.members) and g.challenges
_.each(g.challenges, ->app.challenges.syncChalToUser g)
true
, 500
###
Sync user to challenge (when they score, add to statistics)
###
app.model.on "change", "_page.user.priv.tasks.*.value", (id, value, previous, passed) ->
### Sync to challenge, but do it later ###
async.nextTick =>
model = app.model
ctx = {model: model}
task = model.at "_page.user.priv.tasks.#{id}"
tobj = task.get()
pub = model.get "_page.user.pub"
if (chalTask = helpers.taskInChallenge.call ctx, tobj)? and chalTask.get()
chalTask.increment "value", value - previous
chal = model.at "groups.#{tobj.group.id}.challenges.#{tobj.challenge}"
chalUser = -> helpers.indexedAt.call(ctx, chal.path(), 'members', {id:pub.id})
cu = chalUser()
unless cu?.get()
chal.push "members", {id: pub.id, name: model.get(pub.profile.name)}
cu = model.at chalUser()
else
cu.set 'name', pub.profile.name # update their name incase it changed
cu.set "#{tobj.type}s.#{tobj.id}",
value: tobj.value
history: tobj.history
###
Render graphs for user scores when the "Challenges" tab is clicked
###
###
TODO
1) on main tab click or party
* sort & render graphs for party
2) guild -> all guilds
3) public -> all public
###
###
$('#profile-challenges-tab-link').on 'shown', ->
async.each _.toArray(model.get('groups')), (g) ->
async.each _.toArray(g.challenges), (chal) ->
async.each _.toArray(chal.tasks), (task) ->
async.each _.toArray(chal.members), (member) ->
if (history = member?["#{task.type}s"]?[task.id]?.history) and !!history
data = google.visualization.arrayToDataTable _.map(history, (h)-> [h.date,h.value])
options =
backgroundColor: { fill:'transparent' }
width: 150
height: 50
chartArea: width: '80%', height: '80%'
axisTitlePosition: 'none'
legend: position: 'bottom'
hAxis: gridlines: color: 'transparent' # since you can't seem to *remove* gridlines...
vAxis: gridlines: color: 'transparent'
chart = new google.visualization.LineChart $(".challenge-#{chal.id}-member-#{member.id}-history-#{task.id}")[0]
chart.draw(data, options)
###
app.fn
challenges:
###
Create
###
create: (e,el) ->
[type, gid] = [$(el).attr('data-type'), $(el).attr('data-gid')]
cid = @model.id()
@model.set '_page.new.challenge',
id: cid
name: ''
habits: []
dailys: []
todos: []
rewards: []
user:
uid: @uid
name: @pub.get('profile.name')
group: {type, id:gid}
timestamp: +new Date
###
Save
###
save: ->
newChal = @model.get('_page.new.challenge')
[gid, cid] = [newChal.group.id, newChal.id]
@model.push "_page.lists.challenges.#{gid}", newChal, ->
app.browser.growlNotification('Challenge Created','success')
app.challenges.discard()
app.browser.resetDom() # something is going absolutely haywire here, all model data at end of reflist after chal created
###
Toggle Edit
###
toggleEdit: (e, el) ->
path = "_page.editing.challenges.#{$(el).attr('data-id')}"
@model.set path, !@model.get(path)
###
Discard
###
discard: ->
@model.del '_page.new.challenge'
###
Delete
###
delete: (e) ->
return unless confirm("Delete challenge, are you sure?") is true
e.at().remove()
###
Add challenge name as a tag for user
###
syncChalToUser: (chal) ->
return unless chal
### Sync tags ###
tags = @priv.get('tags') or []
idx = _.findIndex tags, {id: chal.id}
if ~idx and (tags[idx].name isnt chal.name)
### update the name - it's been changed since ###
@priv.set "tags.#{idx}.name", chal.name
else
@priv.push 'tags', {id: chal.id, name: chal.name, challenge: true}
tags = {}; tags[chal.id] = true
_.each chal.habits.concat(chal.dailys.concat(chal.todos.concat(chal.rewards))), (task) =>
_.defaults task, { tags, challenge: chal.id, group: {id: chal.group.id, type: chal.group.type} }
path = "tasks.#{task.id}"
if @priv.get path
@priv.set path, _.defaults(@priv.get(path), task)
else
@model.push "_page.lists.tasks.#{@uid}.#{task.type}s", task
true
###
Subscribe
###
subscribe: (e) ->
chal = e.get()
### Add all challenge's tasks to user's tasks ###
currChallenges = @pub.get('challenges')
@pub.unshift('challenges', chal.id) unless currChallenges and ~currChallenges.indexOf(chal.id)
e.at().push "members",
id: @uid
name: @pub.get('profile.name')
app.challenges.syncChalToUser(chal)
###
--------------------------
Unsubscribe functions
--------------------------
###
unsubscribe: (chal, keep=true) ->
### Remove challenge from user ###
i = @pub.get('challenges')?.indexOf(chal.id)
if i? and ~i
@pub.remove("challenges", i, 1)
### Remove user from challenge ###
if ~(i = _.findIndex chal.members, {id: @uid})
@model.remove "groups.#{chal.group.id}.challenges.#{chal.id}.members", i, 1
### Remove tasks from user ###
async.each chal.habits.concat(chal.dailys.concat(chal.todos.concat(chal.rewards))), (task) =>
if keep is true
@priv.del "tasks.#{task.id}.challenge"
else
path = "_page.lists.tasks.#{@uid}.#{task.type}s"
if ~(i = _.findIndex(@model.get(path), {id:task.id}))
@model.remove(path, i, 1)
true
taskUnsubscribe: (e, el) ->
###
since the challenge was deleted, we don't have its data to unsubscribe from - but we have the vestiges on the task
FIXME this is a really dumb way of doing this
###
tasks = @priv.get('tasks')
tobj = tasks[$(el).attr("data-tid")]
deletedChal =
id: tobj.challenge
members: [@uid]
habits: _.where tasks, {type: 'habit', challenge: tobj.challenge}
dailys: _.where tasks, {type: 'daily', challenge: tobj.challenge}
todos: _.where tasks, {type: 'todo', challenge: tobj.challenge}
rewards: _.where tasks, {type: 'reward', challenge: tobj.challenge}
switch $(el).attr('data-action')
when 'keep'
@priv.del "tasks.#{tobj.id}.challenge"
@priv.del "tasks.#{tobj.id}.group"
when 'keep-all'
app.challenges.unsubscribe.call @, deletedChal, true
when 'remove'
path = "_page.lists.tasks.#{@uid}.#{tobj.type}s"
if ~(i = _.findIndex @model.get(path), {id: tobj.id})
@model.remove path, i
when 'remove-all'
app.challenges.unsubscribe.call @, deletedChal, false
challengeUnsubscribe: (e, el) ->
$(el).popover('destroy').popover({
html: true
placement: 'top'
trigger: 'manual'
title: 'Unsubscribe From Challenge And:'
content: """
<a class=challenge-unsubscribe-and-remove>Remove Tasks</a><br/>
<a class=challenge-unsubscribe-and-keep>Keep Tasks</a><br/>
<a class=challenge-unsubscribe-cancel>Cancel</a><br/>
"""
}).popover('show')
$('.challenge-unsubscribe-and-remove').click => app.challenges.unsubscribe.call @, e.get(), false
$('.challenge-unsubscribe-and-keep').click => app.challenges.unsubscribe.call @, e.get(), true
$('[class^=challenge-unsubscribe]').click => $(el).popover('destroy')

View file

@ -0,0 +1,22 @@
_ = require 'lodash'
module.exports.app = (appExports, model) ->
user = model.at('_user')
appExports.filtersDeleteTag = (e, el) ->
tags = user.get('tags')
tag = e.at "_user.tags." + $(el).attr('data-index')
tagId = tag.get('id')
###something got corrupted, let's clear the corrupt tags###
unless tagId
user.set 'tags', _.filter( tags, ((t)-> t?.id) )
user.set 'filters', {}
return
model.del "_user.filters.#{tagId}"
tag.remove()
### remove tag from all tasks###
_.each user.get("tasks"), (task) -> user.del "tasks.#{task.id}.tags.#{tagId}"; true

View file

@ -0,0 +1,139 @@
_ = require('lodash')
helpers = require('habitrpg-shared/script/helpers')
module.exports.app = (appExports, model, app) ->
browser = require './browser'
_currentTime = model.at '_currentTime'
_currentTime.setNull +new Date
# Every 60 seconds, reset the current time so that the chat can update relative times
setInterval (->_currentTime.set +new Date), 60000
appExports.groupCreate = (e,el) ->
type = $(el).attr('data-type')
newGroup =
name: model.get("_new.group.name")
description: model.get("_new.group.description")
leader: user.get('id')
members: [user.get('id')]
type: type
# parties - free
if type is 'party'
return model.add 'groups', newGroup, ->location.reload()
# guilds - 4G
unless user.get('balance') >= 1
return $('#more-gems-modal').modal 'show'
if confirm "Create Guild for 4 Gems?"
newGroup.privacy = (model.get("_new.group.privacy") || 'public') if type is 'guild'
newGroup.balance = 1 # they spent $ to open the guild, it goes into their guild bank
model.add 'groups', newGroup, ->
user.incr 'balance', -1, ->location.reload()
appExports.toggleGroupEdit = (e, el) ->
path = "_editing.groups.#{$(el).attr('data-gid')}"
model.set path, !model.get(path)
appExports.toggleLeaderMessageEdit = (e, el) ->
path = "_editing.leaderMessage.#{$(el).attr('data-gid')}"
model.set path, !model.get(path)
appExports.groupAddWebsite = (e, el) ->
test = e.get()
e.at().unshift 'websites', model.get('_newGroupWebsite')
model.del '_newGroupWebsite'
appExports.groupInvite = (e,el) ->
uid = model.get('_groupInvitee').replace(/[\s"]/g, '')
model.set '_groupInvitee', ''
return if _.isEmpty(uid)
model.query('users').publicInfo([uid]).fetch (err, profiles) ->
throw err if err
profile = profiles.at(0).get()
return model.set("_groupError", "User with id #{uid} not found.") unless profile
model.query('groups').withMember(uid).fetch (err, g) ->
throw err if err
group = e.get(); groups = g.get()
{type, name} = group; gid = group.id
groupError = (msg) -> model.set("_groupError", msg)
invite = ->
$.bootstrapGrowl "Invitation Sent."
switch type
when 'guild' then model.push "users.#{uid}.invitations.guilds", {id:gid, name}, ->location.reload()
when 'party' then model.set "users.#{uid}.invitations.party", {id:gid, name}, ->location.reload()
switch type
when 'guild'
if profile.invitations?.guilds and _.find(profile.invitations.guilds, {id:gid})
return groupError("User already invited to that group")
else if uid in group.members
return groupError("User already in that group")
else invite()
when 'party'
if profile.invitations?.party
return groupError("User already pending invitation.")
else if _.find(groups, {type:'party'})
return groupError("User already in a party.")
else invite()
appExports.acceptInvitation = (e,el) ->
gid = e.get('id')
if $(el).attr('data-type') is 'party'
user.set 'invitations.party', null, ->joinGroup(gid)
else
e.at().remove ->joinGroup(gid)
appExports.rejectInvitation = (e, el) ->
clear = -> browser.resetDom(model)
if e.at().path().indexOf('party') != -1
model.del e.at().path(), clear
else e.at().remove clear
appExports.groupLeave = (e,el) ->
if confirm("Leave this group, are you sure?") is true
uid = user.get('id')
group = model.at "groups.#{$(el).attr('data-id')}"
index = group.get('members').indexOf(uid)
if index != -1
group.remove 'members', index, 1, ->
updated = group.get()
# last member out, delete the party
if _.isEmpty(updated.members) and (updated.type is 'party')
group.del ->location.reload()
# assign new leader, so the party is editable #TODO allow old leader to assign new leader, this is just random
else if (updated.leader is uid)
group.set "leader", updated.members[0], ->location.reload()
else location.reload()
###
Chat Functionality
###
model.on 'unshift', '_party.chat', -> $('.chat-message').tooltip()
model.on 'unshift', '_habitrpg.chat', -> $('.chat-message').tooltip()
appExports.chatKeyup = (e, el, next) ->
return next() unless e.keyCode is 13
appExports.sendChat(e, el)
appExports.deleteChatMessage = (e) ->
if confirm("Delete chat message?") is true
e.at().remove() #requires the {#with}
app.on 'render', (ctx) ->
$('#party-tab-link').on 'shown', (e) ->
messages = model.get('_party.chat')
return false unless messages?.length > 0
model.set '_user.party.lastMessageSeen', messages[0].id
appExports.gotoPartyChat = ->
model.set '_gamePane', true, ->
$('#party-tab-link').tab('show')
appExports.assignGroupLeader = (e, el) ->
newLeader = model.get('_new.groupLeader')
if newLeader and (confirm("Assign new leader, you sure?") is true)
e.at().set('leader', newLeader, ->browser.resetDom(model)) if newLeader

View file

@ -0,0 +1,20 @@
# Translations
i18n = require './i18n'
i18n.localize app,
availableLocales: ['en', 'he', 'bg', 'nl']
defaultLocale: 'en'
urlScheme: false
checkHeader: true
# ========== CONTROLLER FUNCTIONS ==========
ready (model) ->
misc.fixCorruptUser(model) # https://github.com/lefnire/habitrpg/issues/634
# used for things like remove website, chat, etc
exports.removeAt = (e, el) ->
if (confirmMessage = $(el).attr 'data-confirm')?
return unless confirm(confirmMessage) is true
e.at().remove()
browser.resetDom(model) if $(el).attr('data-refresh')

View file

@ -0,0 +1,39 @@
items = require 'habitrpg-shared/script/items'
_ = require 'lodash'
updateStore = (model) ->
nextItems = items.updateStore(model.get('_user'))
_.each nextItems, (v,k) -> model.set("_items.next.#{k}",v); true
###
server exports
###
module.exports.server = (model) ->
model.set '_items', items.items
updateStore(model)
###
app exports
###
module.exports.app = (appExports, model) ->
misc = require './misc'
model.on "set", "_user.items.*", -> updateStore(model)
appExports.buyItem = (e, el) ->
misc.batchTxn model, (uObj, paths) ->
ret = items.buyItem uObj, $(el).attr('data-type'), {paths}
alert("Not enough GP") if ret is false
appExports.activateRewardsTab = ->
model.set '_activeTabRewards', true
model.set '_activeTabPets', false
appExports.activatePetsTab = ->
model.set '_activeTabPets', true
model.set '_activeTabRewards', false

View file

@ -0,0 +1,152 @@
_ = require 'lodash'
algos = require 'habitrpg-shared/script/algos'
items = require('habitrpg-shared/script/items').items
helpers = require('habitrpg-shared/script/helpers')
#TODO put this in habitrpg-shared
###
We can't always use refLists, but we often still need to get a positional path by id: eg, users.1234.tasks.5678.value
For arrays (which use indexes, not id-paths), here's a helper function so we can run indexedPath('users',:user.id,'tasks',:task.id,'value)
###
indexedPath = ->
_.reduce arguments, (m,v) =>
return v if !m #first iteration
return "#{m}.#{v}" if _.isString v #string paths
return "#{m}." + _.findIndex(@model.get(m),v)
, ''
taskInChallenge = (task) ->
return undefined unless task?.challenge
@model.at indexedPath.call(@, "groups.#{task.group.id}.challenges", {id:task.challenge}, "#{task.type}s", {id:task.id})
###
algos.score wrapper for habitrpg-helpers to work in Derby. We need to do model.set() instead of simply setting the
object properties, and it's very difficult to diff the two objects and find dot-separated paths to set. So we to first
clone our user object (if we don't do that, it screws with model.on() listeners, ping Tyler for an explaination),
perform the updates while tracking paths, then all the values at those paths
###
module.exports.score = (model, taskId, direction, allowUndo=false) ->
drop = undefined
delta = batchTxn model, (uObj, paths) ->
tObj = uObj.tasks[taskId]
# Stuff for undo
if allowUndo
tObjBefore = _.cloneDeep tObj
tObjBefore.completed = !tObjBefore.completed if tObjBefore.type in ['daily', 'todo']
previousUndo = model.get('_undo')
clearTimeout(previousUndo.timeoutId) if previousUndo?.timeoutId
timeoutId = setTimeout (-> model.del('_undo')), 20000
model.set '_undo', {stats:_.cloneDeep(uObj.stats), task:tObjBefore, timeoutId: timeoutId}
delta = algos.score(uObj, tObj, direction, {paths})
model.set('_streakBonus', uObj._tmp.streakBonus) if uObj._tmp?.streakBonus
drop = uObj._tmp?.drop
# Update challenge statistics
# FIXME put this in it's own batchTxn, make batchTxn model.at() ref aware (not just _user)
# FIXME use reflists for users & challenges
if (chalTask = taskInChallenge.call({model}, tObj)) and chalTask?.get()
model._dontPersist = false
chalTask.incr "value", delta
chal = model.at indexedPath.call({model}, "groups.#{tObj.group.id}.challenges", {id:tObj.challenge})
chalUser = -> indexedPath.call({model}, chal.path(), 'users', {id:uObj.id})
cu = model.at chalUser()
unless cu?.get()
chal.push "users", {id: uObj.id, name: helpers.username(uObj.auth, uObj.profile?.name)}
cu = model.at chalUser()
else
cu.set 'name', helpers.username(uObj.auth, uObj.profile?.name) # update their name incase it changed
cu.set "#{tObj.type}s.#{tObj.id}",
value: tObj.value
history: tObj.history
model._dontPersist = true
, done:->
if drop and $?
model.set '_drop', drop
$('#item-dropped-modal').modal 'show'
delta
###
Cleanup task-corruption (null tasks, rogue/invisible tasks, etc)
Obviously none of this should be happening, but we'll stop-gap until we can find & fix
Gotta love refLists! see https://github.com/lefnire/habitrpg/issues/803 & https://github.com/lefnire/habitrpg/issues/6343
###
module.exports.fixCorruptUser = (model) ->
user = model.at('_user')
tasks = user.get('tasks')
## Remove corrupted tasks
_.each tasks, (task, key) ->
unless task?.id? and task?.type?
user.del("tasks.#{key}")
delete tasks[key]
true
resetDom = false
batchTxn model, (uObj, paths, batch) ->
## fix https://github.com/lefnire/habitrpg/issues/1086
uniqPets = _.uniq(uObj.items.pets)
batch.set('items.pets', uniqPets) if !_.isEqual(uniqPets, uObj.items.pets)
if uObj.invitations?.guilds
uniqInvites = _.uniq(uObj.invitations.guilds)
batch.set('invitations.guilds', uniqInvites) if !_.isEqual(uniqInvites, uObj.invitations.guilds)
## Task List Cleanup
['habit','daily','todo','reward'].forEach (type) ->
# 1. remove duplicates
# 2. restore missing zombie tasks back into list
idList = uObj["#{type}Ids"]
taskIds = _.pluck( _.where(tasks, {type}), 'id')
union = _.union idList, taskIds
# 2. remove empty (grey) tasks
preened = _.filter union, (id) -> id and _.contains(taskIds, id)
# There were indeed issues found, set the new list
if !_.isEqual(idList, preened)
batch.set("#{type}Ids", preened)
console.error uObj.id + "'s #{type}s were corrupt."
true
resetDom = !_.isEmpty(paths)
require('./browser').resetDom(model) if resetDom
module.exports.viewHelpers = (view) ->
#misc
view.fn "percent", (x, y) ->
x=1 if x==0
Math.round(x/y*100)
view.fn 'indexOf', (str1, str2) ->
return false unless str1 && str2
str1.indexOf(str2) != -1
view.fn "round", Math.round
view.fn "floor", Math.floor
view.fn "ceil", Math.ceil
view.fn "truarr", (num) -> num-1
view.fn 'count', (arr) -> arr?.length or 0
view.fn 'int',
get: (num) -> num
set: (num) -> [parseInt(num)]
view.fn 'indexedPath', indexedPath
#iCal
view.fn "encodeiCalLink", helpers.encodeiCalLink
#User
view.fn "gems", (balance) -> balance * 4
#Challenges
view.fn 'taskInChallenge', (task) ->
taskInChallenge.call(@,task)?.get()
view.fn 'taskAttrFromChallenge', (task, attr) ->
taskInChallenge.call(@,task)?.get(attr)
view.fn 'brokenChallengeLink', (task) ->
task?.challenge and !(taskInChallenge.call(@,task)?.get())
view.fn 'challengeMemberScore', (member, tType, tid) ->
Math.round(member["#{tType}s"]?[tid]?.value)

View file

@ -1,6 +1,6 @@
_ = require 'underscore'
{ randomVal } = require './helpers'
{ pets, hatchingPotions } = require('./items').items
_ = require 'lodash'
{ randomVal } = require 'habitrpg-shared/script/helpers'
{ pets, hatchingPotions } = require('habitrpg-shared/script/items').items
###
app exports
@ -25,12 +25,11 @@ module.exports.app = (appExports, model) ->
return alert "You don't own that egg yet, complete more tasks!" if eggIdx is -1
return alert "You already have that pet, hatch a different combo." if myPets and myPets.indexOf("#{egg.name}-#{hatchingPotionName}") != -1
user.push 'items.pets', egg.name + '-' + hatchingPotionName
eggs.splice eggIdx, 1
myHatchingPotion.splice hatchingPotionIdx, 1
user.set 'items.eggs', eggs
user.set 'items.hatchingPotions', myHatchingPotion
user.push 'items.pets', egg.name + '-' + hatchingPotionName, ->
eggs.splice eggIdx, 1
myHatchingPotion.splice hatchingPotionIdx, 1
user.set 'items.eggs', eggs
user.set 'items.hatchingPotions', myHatchingPotion
alert 'Your egg hatched! Visit your stable to equip your pet.'
@ -46,14 +45,14 @@ module.exports.app = (appExports, model) ->
return user.set 'items.currentPet', {} if user.get('items.currentPet.str') is petStr
[name, modifier] = petStr.split('-')
pet = _.findWhere pets, name: name
pet = _.find pets, {name: name}
pet.modifier = modifier
pet.str = petStr
user.set 'items.currentPet', pet
appExports.buyHatchingPotion = (e, el) ->
name = $(el).attr 'data-hatchingPotion'
newHatchingPotion = _.findWhere hatchingPotions, name: name
newHatchingPotion = _.find hatchingPotions, {name: name}
gems = user.get('balance') * 4
if gems >= newHatchingPotion.value
if confirm "Buy this hatching potion with #{newHatchingPotion.value} of your #{gems} Gems?"
@ -64,7 +63,7 @@ module.exports.app = (appExports, model) ->
appExports.buyEgg = (e, el) ->
name = $(el).attr 'data-egg'
newEgg = _.findWhere pets, name: name
newEgg = _.find pets, {name: name}
gems = user.get('balance') * 4
if gems >= newEgg.value
if confirm "Buy this egg with #{newEgg.value} of your #{gems} Gems?"

View file

@ -0,0 +1,29 @@
algos = require 'habitrpg-shared/script/algos'
helpers = require 'habitrpg-shared/script/helpers'
_ = require 'lodash'
moment = require 'moment'
misc = require './misc'
appExports.clearCompleted = (e, el) ->
completedIds = _.pluck( _.where(model.get('_todoList'), {completed:true}), 'id')
todoIds = user.get('todoIds')
_.each completedIds, (id) -> user.del "tasks.#{id}"; true
user.set 'todoIds', _.difference(todoIds, completedIds)
###
Undo
###
appExports.undo = () ->
undo = model.get '_undo'
clearTimeout(undo.timeoutId) if undo?.timeoutId
model.del '_undo'
_.each undo.stats, (val, key) -> user.set "stats.#{key}", val; true
taskPath = "tasks.#{undo.task.id}"
_.each undo.task, (val, key) ->
return true if key in ['id', 'type'] # strange bugs in this world: https://workflowy.com/shared/a53582ea-43d6-bcce-c719-e134f9bf71fd/
if key is 'completed'
user.pass({cron:true}).set("#{taskPath}.completed",val)
else
user.set "#{taskPath}.#{key}", val
true

View file

@ -1,6 +1,6 @@
_ = require 'underscore'
{ randomVal } = require './helpers'
{ pets, hatchingPotions } = require('./items').items
_ = require 'lodash'
{ randomVal } = require 'habitrpg-shared/script/helpers'
{ pets, hatchingPotions } = require('habitrpg-shared/script/items').items
###
Listeners to enabled flags, set notifications to the user when they've unlocked features
@ -75,9 +75,9 @@ module.exports.app = (appExports, model) ->
user.on 'set', 'items.*', (after, before) ->
return if user.get('achievements.ultimateGear')
items = user.get('items')
if parseInt(items.weapon) == 6 and parseInt(items.armor) == 5 and parseInt(items.head) == 5 and parseInt(items.shield) == 5
dontPersist = model._dontPersist; model._dontPersist = false
user.set 'achievements.ultimateGear', true, (-> model._dontPersist = dontPersist)
if parseInt(items.weapon) >= 6 and parseInt(items.armor) >= 5 and parseInt(items.head) >= 5 and parseInt(items.shield) >= 5
dontPersist = model._dontPersist; model._dontPersist = false
user.set 'achievements.ultimateGear', true, (->model._dontPersist = dontPersist)
$('#max-gear-achievement-modal').modal('show')
user.on 'set', 'tasks.*.streak', (id, after, before) ->

45
bower.json Normal file
View file

@ -0,0 +1,45 @@
{
"name": "HabitRPG",
"version": "0.1.1",
"homepage": "https://github.com/lefnire/habitrpg",
"authors": [
"Tyler Renelle <tylerrenelle@gmail.com>"
],
"private": true,
"ignore": [
"**/.*",
"node_modules",
"public/bower_components",
"test",
"tests"
],
"dependencies": {
"jquery": "~2.0.3",
"jquery.cookie": "~1.4.0",
"jquery-ui": "~1.10.3",
"angular": "1.2.0-rc.3",
"angular-sanitize": "1.2.0-rc.3",
"angular-resource": "1.2.0-rc.3",
"angular-ui": "~0.4.0",
"angular-ui-utils": "~0.0.4",
"angular-bootstrap": "~0.5.0",
"angular-ui-router": "ca3b4777a603df8f86cfd653c8f6c38b2ae05d89",
"angular-loading-bar": "~0.0.5",
"bootstrap": "v2.3.2",
"bootstrap-datepicker": "~1.2.0",
"bootstrap-tour": "0.5.0",
"bootstrap-growl": "~1.1.0",
"habitrpg-shared": "git://github.com/HabitRPG/habitrpg-shared.git#master",
"BrowserQuest": "https://github.com/mozilla/BrowserQuest.git",
"github-buttons": "git://github.com/mdo/github-buttons.git",
"marked": "~0.2.9",
"JavaScriptButtons": "git://github.com/paypal/JavaScriptButtons.git#master"
},
"resolutions": {
"jquery": "~2.0.3",
"bootstrap": "v2.3.2"
},
"devDependencies": {
"angular-mocks": "1.2.0-rc.3"
}
}

View file

@ -7,9 +7,11 @@
"NODE_DB_URI":"mongodb://localhost/habitrpg",
"NODE_ENV":"development",
"SESSION_SECRET":"YOUR SECRET HERE",
"ADMIN_EMAIL": "you@yours.com",
"SMTP_USER":"user@domain.com",
"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_'
});
};

57
karma.conf.js Normal file
View file

@ -0,0 +1,57 @@
// 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-loading-bar/build/loading-bar.min.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: true,
// Start these browsers, currently available:
// - Chrome
// - ChromeCanary
// - Firefox
// - Opera
// - Safari (only Mac)
// - PhantomJS
// - IE (only Windows)
browsers: ['Firefox'],
// Continuous Integration mode
// if true, it capture browsers, run tests and exit
singleRun: false
});
};

View file

@ -19,7 +19,7 @@
"tour4Title" : "Todos",
"tour4Text" : "Todos are one-off goals which need to be completed eventually. ",
"tour5Title" : "Rewards",
"tour5Text" : "As you complete goals, you earn gold to buy rewards. Buy them liberally - rewards are integrals in forming good habits. ",
"tour5Text" : "As you complete goals, you earn gold to buy rewards. Buy them liberally - rewards are integral in forming good habits. ",
"tour6Title" : "Hover over comments",
"tour6Text" : "Different task-types have special properties. Hover over each task's comment for more information. When you're ready to get started, delete the existing tasks and add your own.",
@ -71,6 +71,7 @@
"_commenttaskview": "TASK VIEW",
"habits": "Habits",
"Habits": "Habits",
"newHabit": "New Habit",
"edit": "Edit",
"text": "Text",
@ -87,6 +88,7 @@
"progress": "Progress",
"score": "Score",
"dailies": "Dailies",
"Dailies": "Dailies",
"newDaily": "New Daily",
"repeat": "Repeat",
"Su": "Su",
@ -97,11 +99,13 @@
"F": "F",
"S": "S",
"todos": "Todos",
"Todos": "Todos",
"newTodo": "New Todo",
"dueDate": "Due Date",
"remaining": "Remaining",
"complete": "Complete",
"rewards": "Rewards",
"Rewards": "Rewards",
"gold": "Gold",
"silver": "Silver",
"newReward": "New Reward",
@ -282,7 +286,7 @@
"sheild1Text": "Decreases HP loss by 3%",
"sheild2Name" : "Buckler",
"sheild2Text": "Decreases HP loss by 4%",
"sheild3Name" : "Enforced Shield",
"sheild3Name" : "Reinforced Shield",
"sheild3Text": "Decreases HP loss by 5%",
"sheild4Name" : "Red Shield",
"sheild4Text": "Decreases HP loss by 7%",
@ -337,9 +341,15 @@
"_commentnpcsandchars": "NPCS & CHARACTERS",
"_commentNPCS" : "NPCS",
"NPCBaileyText1" : "the Town Crier here! Announcing new stuff!",
<<<<<<< HEAD
"NPCAugustinText1" : "Welcome to the Market! I'm the merchant;",
"NPCAugustinText2" : "Dying to get that particular pet you're after, but don't want to wait for it to drop? Buy it here!",
"NPCJohanssonText1" : "Welcome to the Tavern! I'm",
=======
"NPCAugustinText1" : "Welcome to the Market! I'm the merchant;",
"NPCAugustinText2" : "Dying to get that particular pet you're after, but don't want to wait for it to drop? Buy it here!",
"NPCJohanssonText1" : "Welcome to the Tavern! I'm",
>>>>>>> develop
"NPCJohanssonText2": "the bar keep. If you want to rest a while (going on vacation? sudden illness?), I'll set you up at the inn - dailies won't hurt you while you're resting. Stay a while & meet the locals.",
"NPCMelchiorText1" : "",
"NPCBowenText1" : "",
@ -379,7 +389,11 @@
"_commentmisc": "MISC & GLOBAL",
"removeAds": "Remove Ads",
"whyAds": "Why Ads?",
<<<<<<< HEAD
"whyAdsContent1": "Habit 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.",
=======
"whyAdsContent1": "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.",
>>>>>>> develop
"whyAdsContent2": "'Hey, I backed the Kickstarter!' - follow",
"whyAdsContent3": "these instructions",
"_commentbuttons": "BUTTONS",

View file

@ -1,56 +0,0 @@
/**
* Set this up as a midnight cron script
*
* mongo habitrpg node_modules/moment/moment.js migrations/json.js migrations/20130212_preen_cron.js
*/
/*
Users are allowed to experiment with the site before registering. Every time a new browser visits habitrpg, a new
"staged" account is created - and if the user later registeres, that staged account is considered a "production" account.
This function removes all staged accounts that have been abandoned - either older than a month, or corrupted in some way (lastCron==undefined)
*/
var un_registered = {
"auth.local": {$exists: false},
"auth.facebook": {$exists: false}
};
var registered = {
$or: [
{ 'auth.local': { $exists: true }},
{ 'auth.facebook': { $exists: true }}
]
};
var today = +(new Date);
// isValidDate = (d) ->
// return false if Object::toString.call(d) isnt "[object Date]"
// not isNaN(d.getTime())
db.users.find(un_registered).forEach(function(user) {
var diff, lastCron;
if (!user) return;
if (!!user.lastCron) {
lastCron = new Date(user.lastCron);
diff = Math.abs(moment(today).startOf('day').diff(moment(lastCron).startOf('day'), "days"));
if (diff > 3) {
return db.users.remove({_id:user._id});
}
} else {
return db.users.update({_id: user._id}, {$set: {lastCron: today}});
}
});
/**
* Don't remove missing user auths anymore. This was previously necessary due to data corruption,
* revisit if needs be
*/
/*db.sessions.find().forEach(function(sess){
var uid = JSON.parse(sess.session).userId;
if (!uid || db.users.count({_id:uid}) === 0) {
db.sessions.remove({_id:sess._id});
}
});*/

View file

@ -8,7 +8,7 @@
// since our primary subscription will first hit parties now, we *definitely* need an index there
db.parties.ensureIndex( { 'members': 1, 'background': 1} );
db.parties.ensureIndex( { 'members': 1}, {background: true} );
db.parties.find().forEach(function(party){

View file

@ -0,0 +1,48 @@
/**
* In adding the Guilds feature (which supports the Challenges feature), we are consolidating parties and guilds
* into one collection: groups, with group.type either 'party' or 'guild'. We are also creating the 'habitrpg' guild,
* which everyone is auto-subscribed to, and moving tavern chat into that guild
*
* mongo habitrpg ./node_modules/lodash/lodash.js ./migrations/20130518_setup_groups.js
*/
/**
* TODO
* 1) rename collection parties => groups
* 2) add group.type = 'party' for each current group
* 3) create habitrpg group, .type='guild'
* 4) move tavern.chat.chat into habitrpg guild
* 5) subscribe everyone to habitrpg (be sure to set that for default user too!)
*/
db.parties.renameCollection('groups',true);
//db.parties.dropCollection(); // doesn't seem to do this step during rename...
//db.parties.ensureIndex( { 'members': 1, 'background': 1} );
db.groups.update({}, {$set:{type:'party'}}, {multi:true});
//migrate invitation mechanisms
db.users.update(
{},
{
$remove:{party:1},
$set:{invitations:{party:null,guilds:[]}}
},
{multi:1}
);
tavern = db.tavern.findOne();
db.tavern.drop();
//TODO make as a callback of previous, or make sure group.type is still 'guild' for habitrpg in the end
db.groups.insert({
_id: "habitrpg",
leader: '9',
type: 'guild',
name: "HabitRPG",
chat: tavern.messages,
info: {
blurb: '',
websites: []
}
});

View file

@ -0,0 +1,25 @@
//mongo habitrpg ./node_modules/lodash/lodash.js migrations/20130602_survey_rewards.js
var members = []
members = _.uniq(members);
var query = {
_id: {$exists:1},
$or:[
{_id: {$in: members}},
//{'profile.name': {$in: members}},
{'auth.facebook.name': {$in: members}},
{'auth.local.username': {$in: members}},
{'auth.local.email': {$in: members}}
]
};
print(db.users.count(query));
db.users.update(query,
{
$set: { 'achievements.helpedHabit': true },
$inc: { balance: 2.5 }
},
{multi:true}
)

View file

@ -0,0 +1,9 @@
//mongo habitrpg migrations/20130612_survey_rewards_individual.js
var query = {_id: ""};
db.users.update(query,
{
$set: { 'achievements.helpedHabit': true },
$inc: { balance: 2.5 }
})

View file

@ -0,0 +1,4 @@
db.users.ensureIndex( { _id: 1, apiToken: 1 }, {background: true} )
db.groups.ensureIndex( { members: 1 }, {background: true} )
db.groups.ensureIndex( { type: 1 }, {background: true} )
db.groups.ensureIndex( { type: 1, privacy: 1 }, {background: true} )

View file

@ -0,0 +1,16 @@
//mongo habitrpg ./node_modules/lodash/lodash.js migrations/20130908_cleanup_corrupt_tags.js
// Racer was notorious for adding duplicates, randomly deleting documents, etc. Once we pull the plug on old.habit,
// run this migration to cleanup all the corruption
db.users.find().forEach(function(user){
user.tags = _.filter(user.tags, (function(t) {
return !!t ? t.id : false;
}));
try {
db.users.update({_id:user._id}, {$set:{tags:user.tags}});
} catch(e) {
print(e);
}
})

View file

@ -0,0 +1,51 @@
//mongo habitrpg ./node_modules/lodash/lodash.js migrations/20130908_cleanup_derby_corruption.js
// Racer was notorious for adding duplicates, randomly deleting documents, etc. Once we pull the plug on old.habit,
// run this migration to cleanup all the corruption
db.users.find().forEach(function(user){
// remove corrupt tasks, which will either be null-value or no id
user.tasks = _.reduce(user.tasks, function(m,task,k) {
if (!task || !task.id) return m;
if (isNaN(+task.value)) task.value = 0;
m[k] = task;
return m;
}, {});
// fix NaN stats
_.each(user.stats, function(v,k) {
if (!v || isNaN(+v)) user.stats[k] = 0;
return true;
});
// remove duplicates, restore ghost tasks
['habit', 'daily', 'todo', 'reward'].forEach(function(type) {
var idList = user[type + "Ids"];
var taskIds = _.pluck(_.where(user.tasks, {type: type}), 'id');
var union = _.union(idList, taskIds);
var preened = _.filter(union, function(id) {
return id && _.contains(taskIds, id);
});
if (!_.isEqual(idList, preened)) {
user[type + "Ids"] = preened;
}
});
// temporarily remove broken eggs. we'll need to write a migration script to grant gems for and remove these instead
if (user.items && user.items.eggs) {
user.items.eggs = _.filter(user.items.eggs,function(egg){
if (_.isString(egg)) {
user.balance += 0.75; // give them 3 gems for each broken egg
} else {
return true;
}
})
}
try {
db.users.update({_id:user._id}, user);
} catch(e) {
print(e);
}
})

View file

@ -0,0 +1,37 @@
/**
* Set this up as a midnight cron script
*
* mongo habitrpg ./migrations/20130908_remove_staged_users.js
*/
/**
* If we experience any troubles with removed staging users, come back to a snapshot and restore accounts. This will
* give a peak into possible conflict accounts:
*/
/*db.users.count({
"auth.local": {$exists: false},
"auth.facebook": {$exists: false},
"history.exp.5": {$exists: 1},
$where: "this.todoIds.length != 1 && this.dailyIds.length != 3 && this.habitIds.length != 3 && this.rewardIds.length != 2"
})*/
/**
* Users used to be allowed to experiment with the site before registering. Every time a new browser visits habitrpg, a new
* "staged" account is created - and if the user later registers, that staged account is considered a "production" account.
* This function removes all staged accounts, since the new site doesn't supported staged accounts, and when we add that feature
* in we'll be using localStorage anyway instead of creating a new database record
*/
db.users.remove({
// Un-registered users
"auth.local": {$exists: false},
"auth.facebook": {$exists: false}
});
/**
* Remove empty parties
* Another vestige of Racer. Empty parties shouldn't be being created anymore in the new site
*/
db.groups.remove({
'type': 'party',
$where: "return this.members.length === 0"
});

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

@ -0,0 +1,126 @@
// mongo habitrpg ./node_modules/lodash/lodash.js ./migrations/20131028_task_subdocs_tags_invites.js
// TODO it might be better we just find() and save() all user objects using mongoose, and rely on our defined pre('save')
// and default values to "migrate" users. This way we can make sure those parts are working properly too
// @see http://stackoverflow.com/questions/14867697/mongoose-full-collection-scan
//Also, what do we think of a Mongoose Migration module? something like https://github.com/madhums/mongoose-migrate
db.users.find().forEach(function(user){
// Add invites to groups
// -------------------------
if(user.invitations){
if(user.invitations.party){
db.groups.update({_id: user.invitations.party.id}, {$addToSet:{invites:user._id}});
}
if(user.invitations.guilds){
_.each(user.invitations.guilds, function(guild){
db.groups.update({_id: guild.id}, {$addToSet:{invites:user._id}});
});
}
}
// Cleanup broken tags
// -------------------------
_.each(user.tasks, function(task){
_.each(task.tags, function(val, key){
_.each(user.tags, function(tag){
if(key == tag.id) delete task.tags[key];
});
});
});
// Fix corrupt dates
// -------------------------
user.lastCron = new Date(user.lastCron);
if (user.lastCron == 'Invalid Date') user.lastCron = new Date();
if (user.auth) { // what to do with !auth?
_.defaults(user.auth, {timestamps: {created:undefined, loggedin: undefined}});
_.defaults(user.auth.timestamps, {created: new Date(user.lastCron), loggedin: new Date(user.lastCron)});
}
// Fix missing history
// -------------------------
_.defaults(user, {history:{}});
_.defaults(user.history,{exp:[], todos:[]});
// Add username
// -------------------------
if (!user.profile) user.profile = {name:undefined};
if (_.isEmpty(user.profile.name) && user.auth) {
var fb = user.auth.facebook;
user.profile.name =
(user.auth.local && user.auth.local.username) ||
(fb && (fb.displayName || fb.name || fb.username || (fb.first_name && fb.first_name + ' ' + fb.last_name))) ||
'Anonymous';
}
// Migrate to TaskSchema Sub-Docs!
// -------------------------
if (!user.tasks) {
// So evidentaly users before 02/2013 were ALREADY setup based on habits[], dailys[], etcs... I don't remember our schema
// ever being that way... Anyway, print busted users here (they don't have tasks, but also don't have the right schema)
if (!user.habits || !user.dailys || !user.todos || !user.rewards) {
print(user._id);
}
} else {
_.each(['habit', 'daily', 'todo', 'reward'], function(type) {
// we use _.transform instead of a simple _.where in order to maintain sort-order
user[type + "s"] = _.reduce(user[type + "Ids"], function(m, tid) {
var task = user.tasks[tid],
newTask = {};
if (!task) return m; // remove null tasks
// Cleanup tasks for TaskSchema
newTask._id = newTask.id = task.id;
newTask.text = (_.isString(task.text)) ? task.text : '';
if (_.isString(task.notes)) newTask.notes = task.notes;
newTask.tags = (_.isObject(task.tags)) ? task.tags : {};
newTask.type = (_.isString(task.type)) ? task.type : 'habit';
newTask.value = (_.isNumber(task.value)) ? task.value : 0;
newTask.priority = (_.isString(task.priority)) ? task.priority : '!';
switch (newTask.type) {
case 'habit':
newTask.up = (_.isBoolean(task.up)) ? task.up : true;
newTask.down = (_.isBoolean(task.down)) ? task.down : true;
newTask.history = (_.isArray(task.history)) ? task.history : [];
break;
case 'daily':
newTask.repeat = (_.isObject(task.repeat)) ? task.repeat : {m:1, t:1, w:1, th:1, f:1, s:1, su:1};
newTask.streak = (_.isNumber(task.streak)) ? task.streak : 0;
newTask.completed = (_.isBoolean(task.completed)) ? task.completed : false;
newTask.history = (_.isArray(task.history)) ? task.history : [];
break;
case 'todo':
newTask.completed = (_.isBoolean(task.completed)) ? task.completed : false;
break;
}
m.push(newTask);
return m;
}, []);
delete user[type + 'Ids'];
});
delete user.tasks;
}
try {
db.users.update({_id:user._id}, user);
} catch(e) {
print(e);
}
});
// Remove old groups.*.challenges, they're not compatible with the new system, set member counts
// -------------------------
db.groups.find().forEach(function(group){
db.groups.update({_id:group._id}, {
$set:{memberCount: _.size(group.members)},
$pull:{challenges:1}
})
});
// HabitRPG => Tavern
// -------------------------
db.groups.update({_id:'habitrpg'}, {$set:{name:'Tavern'}});

View file

@ -0,0 +1,25 @@
// mongo habitrpg ./node_modules/lodash/lodash.js ./migrations/20131028_task_subdocs_tags_invites.js
db.challenges.find().forEach(function(chal){
_.each(chal.habits.concat(chal.dailys).concat(chal.todos).concat(chal.rewards), function(task){
task.id = task.id || task._id;
})
try {
db.challenges.update({_id:chal._id}, chal);
db.groups.update({_id:chal.group}, {$addToSet:{challenges:chal._id}})
} catch(e) {
print(e);
}
});
db.users.find().forEach(function(user){
_.each(user.habits.concat(user.dailys).concat(user.todos).concat(user.rewards), function(task){
task.id = task.id || task._id;
})
try {
db.users.update({_id:user._id}, user);
} catch(e) {
print(e);
}
});

View file

@ -0,0 +1,7 @@
db.users.find({},{todos:1}).forEach(function(user){
_.each(user.todos, function(task){
if (moment(task.date).toDate() == 'Invalid Date')
task.date = moment().format('MM/DD/YYYY');
})
db.users.update({_id:user._id}, {$set:{todos: user.todos}});
});

View file

@ -0,0 +1,54 @@
// node .migrations/20131104_restore_lost_task_data.js
/**
* After the great challenges migration, quite a few things got inadvertently dropped from tasks since their
* schemas became more strict. See conversation at https://github.com/HabitRPG/habitrpg/issues/1712 ,
* this restores task tags, streaks, due-dates, values
*/
var mongo = require('mongoskin');
var _ = require('lodash');
var backupUsers = mongo.db('localhost:27017/habitrpg_old?auto_reconnect').collection('users');
var liveUsers = mongo.db('localhost:27017/habitrpg_new?auto_reconnect').collection('users');
backupUsers.count(function(err, count){
if (err) return console.error(err);
backupUsers.findEach({}, {batchSize:10}, function(err, before){
if (err) return console.error(err);
if (!before) return console.log('!before');
liveUsers.findById(before._id, function(err, after){
if (err) return console.error(err);
if (!after) {
count--;
return console.log(before._id + ' deleted?');
}
if (before._id == '9') console.log('lefnire processed');
_.each(before.tasks, function(tBefore){
var tAfter = _.find(after[tBefore.type+'s'], {id:tBefore.id});
if (!tAfter) return; // task has been deleted since launch
// Restore deleted tags
if (!_.isEmpty(tBefore.tags) && _.isEmpty(tAfter.tags))
tAfter.tags = tBefore.tags;
// Except tags which are no longer available on the updated user
_.each(tAfter.tags, function(v,k){ //value is true, key is tag.id
if (!_.find(after.tags,{id:k})) delete tAfter.tags[k];
})
// Restore deleted streaks
if (+tBefore.streak > tAfter.streak)
tAfter.streak = +tBefore.streak;
if (!!tBefore.date && !tAfter.date)
tAfter.date = tBefore.date;
// Restore deleted values
if (+tBefore.value != 0 && tAfter.value == 0)
tAfter.value = +tBefore.value;
})
after._v++;
liveUsers.update({_id:after._id}, after);
if (--count <= 0) console.log("DONE!");
})
});
});

View file

@ -0,0 +1,21 @@
function deleteId(h){
delete h._id;
}
db.users.find({},{habits:1,dailys:1,history:1}).forEach(function(user){
if (user.history) {
_.each(['todos','exp'], function(type){
if (user.history[type]) {
_.each(user.history.exp, deleteId);
}
})
} else {
user.history = {exp:[],todos:[]};
}
_.each(['habits', 'dailys'], function(type){
_.each(user[type].history, deleteId);
});
db.users.update({_id:user._id}, {$set:{history: user.history, habits: user.habits, dailys: user.dailys}});
});

View file

@ -0,0 +1,18 @@
db.users.find({
$or: [
{'backer.admin':{$exists:1}},
{'backer.contributor':{$exists:1}}
]
},{backer:1}).forEach(function(user){
user.contributor = {};
user.contributor.admin = user.backer.admin;
delete user.backer.admin;
// this isnt' the proper storage format, but I'm going to be going through the admin utility manually and setting things properly
if (user.backer.contributor) {
user.contributor.text = user.backer.contributor;
delete user.backer.contributor;
}
db.users.update({_id:user._id}, {$set:{backer:user.backer, contributor:user.contributor}});
});

View file

@ -0,0 +1,4 @@
// Increase everyone's gems per their contribution level
db.users.find({'contributor.level':{$gt:0}},{contributor:1, balance:1}).forEach(function(user){
db.users.update({_id:user._id}, {$inc: {balance: (user.contributor.level * .5)} });
});

View file

@ -1,10 +1,10 @@
import csv
data = csv.reader(open('/home/slappybag/backrs/800dollar.csv', 'rb'), delimiter=",", quotechar='|')
column = []
with open(r"/home/slappybag/Documents/SurveyScrape.csv") as f:
reader = csv.reader(f, delimiter=',', quotechar='"')
column = []
for row in reader:
if row:
column.append(row[4])
for row in data:
column.append(row[9])
print "one:"
print column

View file

@ -0,0 +1,10 @@
var oldId = "",
newId = "",
newUser = db.users.findOne({_id: newId})
db.users.update({_id: oldId}, {$set:{auth: newUser.auth}});
// remove the auth on the new user (which is a template account). The account will be preened automatically later,
// this allows us to keep the account around a few days in case there was a mistake
db.users.update({_id: newId}, {$unset:{auth:1}});

View file

@ -1,4 +1,4 @@
// mongo habitrpg ./node_modules/underscore/underscore.js ./migrations/find_unique_user.js
// mongo habitrpg ./node_modules/lodash/index.js ./migrations/find_unique_user.js
/**
* There are some rare instances of lost user accounts, due to a corrupt user auth variable (see https://github.com/lefnire/habitrpg/wiki/User-ID)
@ -6,6 +6,6 @@
*/
db.users.find().forEach(function(user){
var found = _.findWhere(user.tasks, {text: "Replace Me"})
var found = _.any(user.tasks, {text: "Replace Me"})
if (found) printjson({id:user._id, auth:user.auth});
})

86
migrations/metrics.js Normal file
View file

@ -0,0 +1,86 @@
// node habitrpg ./migrations/metrics.js
var EXPORT_EMAILS = true;
var mongo = require('mongoskin');
var csv = require('csv');
var _ = require('lodash');
var moment = require('moment');
var db = mongo.db('localhost:27017/habitrpg2?auto_reconnect');
var twoWeeksAgo = moment().subtract(14, 'days');
var angularRewrite = moment('07/09/2013');
var query = {auth: {'$exists':1}};
var fields = {lastCron:1, 'history.exp':1, 'auth.timestamps':1, 'auth.local.email':1};
db.collection('users').find(query, fields).toArray(function(err, items) {
if (err) return console.error({err:err});
var stats = {total: _.size(items), lostToDerby: 0, isActive: 0};
var emails = [];
_.each(items, function(item) {
//if (!item.history || !item.history.exp) console.log(item._id)
//var hasBeenActive = item.history && item.history.exp && item.history.exp.length > 7;
var hasBeenActive = item.auth.timestamps && item.auth.timestamps.created &&
(Math.abs(moment(item.lastCron).diff(item.auth.timestamps.created, 'd')) > 14);
if (/*hasBeenActive && */moment(item.lastCron).isBefore(angularRewrite)) {
stats.lostToDerby++;
if (item.auth.local)
emails.push([item.auth.local.email]);
// Facebook emails. Kinda dirty, and there's only ~30 available fb emails anyway.
// } else if (item.auth.facebook && item.auth.facebook.email) {
// emails.push([item.auth.facebook.email])
// } else if (item.auth.facebook && item.auth.facebook.emails && item.auth.facebook.emails[0] && !!item.auth.facebook.emails[0].value) {
// emails.push([item.auth.facebook.emails[0].value])
}
if (hasBeenActive && moment(item.lastCron).isAfter(twoWeeksAgo)) {
stats.isActive++;
}
})
stats.emails = _.size(emails);
console.log(stats);
if (EXPORT_EMAILS)
csv().from.array(emails).to.path(__dirname+'/emails.csv')
});
/*
load('./node_modules/moment/moment.js');
var today = +new Date,
twoWeeksAgo = +moment().subtract(14, 'days');
corrupt = {
$or: [
{lastCron: {$exists:false}},
{lastCron: 'new'}
]
}
un_registered = {
"auth.local": {$exists: false},
"auth.facebook": {$exists: false}
},
registered = {
$or: [
{ 'auth.local': { $exists: true }},
{ 'auth.facebook': { $exists: true }}
]
},
active = {
$or: [
{ 'auth.local': { $exists: true }},
{ 'auth.facebook': { $exists: true }}
],
$where: function(){
return this.history && this.history.exp && this.history.exp.length > 7;
},
'lastCron': {$gt: twoWeeksAgo}
};
print('corrupt: ' + db.users.count(corrupt));
print('unregistered: ' + db.users.count(un_registered));
print('registered: ' + db.users.count(registered));
print('active: ' + db.users.count(active));
*/

View file

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

View file

@ -1,32 +1,48 @@
{
"name": "habitrpg",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "0.0.0-151",
"main": "./server.js",
"version": "0.0.0-152",
"main": "./src/server.js",
"dependencies": {
"derby": "git://github.com/lefnire/derby#habitrpg",
"racer": "git://github.com/lefnire/racer#habitrpg",
"racer-db-mongo": "git://github.com/lefnire/racer-db-mongo#habitrpg",
"derby-ui-boot": "git://github.com/codeparty/derby-ui-boot#master",
"habitrpg-shared": "git://github.com/HabitRPG/habitrpg-shared#master",
"derby-auth": "git://github.com/lefnire/derby-auth#master",
"connect-mongo": "*",
"passport-facebook": "*",
"passport-facebook": "~1.0.0",
"express": "*",
"gzippo": "*",
"guid": "*",
"moment": "*",
"stripe": "*",
"lodash": "1.0.x",
"coffee-script": "1.4.x",
"underscore": "*",
"mongoskin": "*",
"coffee-script": "*",
"nconf": "*",
"icalendar": "git://github.com/lefnire/node-icalendar#master",
"superagent": "~0.12.4",
"resolve": "~0.2.3",
"expect.js": "~0.2.0",
"derby-i18n": "git://github.com/switz/derby-i18n#master",
"relative-date": "~1.1.1"
"relative-date": "~1.1.1",
"lodash": "~2.2.1",
"async": "~0.2.9",
"optimist": "~0.5.2",
"mongoose": "~3.6.20",
"stylus": "~0.37.0",
"grunt": "~0.4.1",
"grunt-contrib-uglify": "~0.2.4",
"grunt-contrib-stylus": "~0.8.0",
"grunt-contrib-clean": "~0.5.0",
"grunt-contrib-cssmin": "~0.6.1",
"grunt-contrib-watch": "~0.5.3",
"grunt-contrib-copy": "~0.4.1",
"grunt-hashres": "~0.3.2",
"grunt-nodemon": "~0.1.1",
"grunt-concurrent": "~0.3.1",
"bower": "~1.2.4",
"nib": "~1.0.1",
"jade": "~0.35.0",
"passport": "~0.1.17",
"validator": "~1.5.1",
"nodemailer": "~0.5.2",
"grunt-cli": "~0.1.9",
"paypal-ipn": "~1.0.1"
},
"private": true,
"subdomain": "habitrpg",
@ -35,11 +51,30 @@
"www.habitrpg.com"
],
"engines": {
"node": "0.8.x",
"npm": "1.1.x"
"node": "0.10.x",
"npm": "1.2.x"
},
"scripts": {
"start": "server.js",
"test": "mocha test/api.mocha.coffee"
"test": "grunt karma:continuous",
"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",
"csv": "~0.3.6"
}
}

View file

@ -11,7 +11,7 @@
<link href="/vendor/bootstrap/docs/assets/css/docs.css" rel="stylesheet">
<link href="/css/static-pages.css" rel="stylesheet">
<style type="text/css">
<!-- <style type="text/css">
.rotate-img {
transform:rotate(90deg);
-ms-transform:rotate(90deg); /* IE 9 */
@ -19,15 +19,17 @@
-webkit-transform:rotate(90deg); /* Safari and Chrome */
-o-transform:rotate(90deg); /* Opera */
}
</style>
</style> -->
</head>
<body>
<div class='container'>
<div class='marketing'>
<img class='rotate-img' src="/img/sprites/armor3_m.png" />
<h2>The server is experiencing issues.</h2>
<p><a href="/">Try again</a> in a few, the developer has been notified. The most likely culprit is <a href="https://github.com/lefnire/habitrpg/issues/165">this issue</a> which Tyler is working to fix. (Any memory leak experts?)</p>
<img src="/img/sprites/dead.png" />
<h2>The server is respawning.</h2>
<p><a href="/">Try again</a> in a few. We restart often due to <a href="https://github.com/lefnire/habitrpg/issues/165">this issue</a>, and we're <a href=http://habitrpg.tumblr.com/post/55655159428/0-5-upgrade-aborted-angularjs-future>rewriting the site</a> to fix it. (AngularJS developers, come <a href=https://github.com/lefnire/habitrpg/tree/angular_rewrite>join us</a>!)</p>
<p>If this page persists, the server may be experiencing issues; the developers have been notified. Try switching to <a href="https://beta.habitrpg.com/">the beta site</a> or <a href="https://habitrpg.com/">the main site</a>.</p>
</div>
</div>
</body>

1
public/css/README.md Normal file
View file

@ -0,0 +1 @@
We need to extract any sprite-sheet css in this directory (anything that's not habitrpg-web specific) and move it to habitrpg-shared

View file

@ -1,18 +1,20 @@
.new-stuff
position: fixed
text-align: center
top: 0
left: 0
width: 100%
height: 0
z-index: 1010
position: fixed
text-align: center
top: 0
left: 0
width: 100%
height: 0
z-index: 1010
.new-stuff> .alert
border-top: 0
border-radius: 0 0 4px 4px
padding-right: 14px
display: inline-block
border-top: 0
border-radius: 0 0 4px 4px
padding-right: 14px
display: inline-block
.wide-popover
max-width: 400px
// variables
@ -58,21 +60,21 @@ borderDarken = 20%
// alert icons
.icon-gold
background: url("img/coin_single_gold.png") no-repeat
background: url("/bower_components/habitrpg-shared/img/coin_single_gold.png") no-repeat
background-position: center center
background-size: 18px
width: 14px
height: 14px
.icon-silver
background: url("img/coin_single_silver.png") no-repeat
background: url("/bower_components/habitrpg-shared/img/coin_single_silver.png") no-repeat
background-position: center center
background-size: 18px
width: 14px
height: 14px
.icon-death
background: url("img/sprites/dead.png") no-repeat
background: url("/bower_components/habitrpg-shared/img/sprites/dead.png") no-repeat
background-position: center center
background-size: 14px
width: 14px
@ -83,18 +85,4 @@ borderDarken = 20%
z-index: 3000
position:absolute
left:5px
top:5px
// Bailey
.rewards-module
z-index:2000
.hiding-bailey
cursor: pointer
position:absolute
right: 50px
top: 117px //FIXME this isn't a good method, but i suck at css - tyler
.new-stuff-word-bubble-container
height: 90px
top:5px

View file

@ -40,7 +40,7 @@ future re: pets and whatnot, this is just temporary.
// always visible on the user's own avatar
.herobox:after
opacity: 0
.herobox[data-checkuser="isUser"]:after
.herobox.isUser:after
opacity: 1
// make it a bit special
background darken($better, 15%)
@ -54,18 +54,18 @@ future re: pets and whatnot, this is just temporary.
// vertically center avatar sprite
// depending on if they have a pet
.herobox[data-checkpet="hasPet"]
.herobox.hasPet
padding-top: 2em
.herobox:not([data-checkpet="hasPet"])
.herobox:not(.hasPet)
padding-top: 1.75em
// if it is the user's avatar, check if
// they have a pet and pad accordingly
.herobox[data-checkpet="hasPet"][data-checkuser="isUser"]
.herobox.hasPet.isUser
padding-top: 3.25em
// if not user's avatar, remove lines for party unity
.herobox:not([data-checkuser="isUser"])
.herobox:not(.isUser)
outline: 0
// expose hero info on hover, taking
@ -74,10 +74,10 @@ future re: pets and whatnot, this is just temporary.
background: desaturate(lighten($better, 30%), 10%)
&:after
opacity: 1
.herobox[data-checkpet="hasPet"]
.herobox.hasPet
&:hover, &:focus
padding-top: 3.25em
.herobox:not([data-checkpet="hasPet"])
.herobox:not(.hasPet)
&:hover, &:focus
padding-top: 2.5em

View file

@ -0,0 +1,21 @@
ul.challenge-accordion-header-specs
list-style:none
li
background-color: darken($neutral, 10%)
margin: 2px 5px
float:left
#create-challenge-btn
margin-bottom: 10px
#challenges-filters h3
margin-top: 0px;
// for things like challenges, member profiles, etc
@media (min-width: 767px)
.wide-modal
/* new custom width */
width: 1020px;
/* must be half of the width, minus scrollbar on the left (30px) */
margin-left: -480px;

View file

@ -50,4 +50,8 @@ menu
.customize-menu {
width: 100%;
}
}
}
.well.limited-edition
padding: 5px
margin: 0px

File diff suppressed because it is too large Load diff

60
public/css/game-pane.styl Normal file
View file

@ -0,0 +1,60 @@
.full-width
width 100%
.border-right
border-right 1px solid #ddd
.tab-notification
color #fff
background-color #51a351
.tavern-pane
position relative
height 250px
.tavern-chat, .party-chat
li
padding-top:15px
padding-bottom:15px
border-bottom 1px solid #ddd
word-wrap:break-word
&.highlight
background #EEE
label
margin-right:5px
.own-message
border-left: 4px solid #333
padding-left: 2px
// Name tags
.label-contributor-1, .label-contributor-2
background-color: #333;
.label-contributor-3, .label-contributor-4
background-color: #077409
color: white
.label-contributor-5, .label-contributor-6
background-color: #125BA2
color: white
.label-contributor-7
background-color: #7313B4
color: white
.label-contributor-8
background-color: #ff8000
color: white
.label-npc
background-color: indianred
color: white
#market-tab
position relative
height 500px
.buttonList li
margin: 5px
.option-group .option-time
padding: 0px 5px

View file

@ -7,7 +7,9 @@
background: #f5f5f5
border-bottom: 1px solid rgba(0,0,0,0.2)
// margin-top: -1px
overflow: hidden
overflow-y: hidden
overflow-x: auto
// position relative
/* login/menu buttons
--------------------- */
@ -16,6 +18,11 @@
top: 0.5em
right: 0.5em
font-size: 0.85em
z-index: 1011
.site-nav
margin-bottom: 0px
.tile
cursor: pointer
font-weight: 400

View file

@ -1,30 +1,28 @@
@import "nib/vendor";
@import "nib/vendor"
// Vendor Includes - include first so we can override
@import "../../public/vendor/bootstrap/css/bootstrap.min.css";
@import "../../public/vendor/bootstrap/css/bootstrap-responsive.min.css";
@import "../../public/vendor/datepicker/css/datepicker.css";
// Import only styles that do not have urls to images! Include them directly in the page!
@import "../bower_components/bootstrap-datepicker/css/datepicker.css"
@import "../bower_components/angular-ui/build/angular-ui.min.css"
@import "../bower_components/bootstrap/docs/assets/css/bootstrap-responsive.css"
@import "../bower_components/bootstrap-tour/build/css/bootstrap-tour.min.css"
@import "../bower_components/angular-loading-bar/build/loading-bar.css"
// Custom includes
@import "./female_sprites.styl";
@import "./male_sprites.styl";
@import "./shop_sprites.styl";
@import "./tasks.styl";
@import "./avatar.styl";
@import "./customizer.styl";
@import "./items.styl";
@import "./inventory.styl";
@import "./alerts.styl";
@import "../../public/img/sprites/pet_sprites.css";
@import "../../public/img/sprites/PetEggs.css";
@import "./helpers.styl";
@import "./responsive.styl";
@import "./header.styl";
@import "./filters.styl";
@import "./scrollbars.styl";
@import "./achievements.styl";
@import "./game-pane.styl";
@import "./backer.styl";
@import "./tasks.styl"
@import "./avatar.styl"
@import "./customizer.styl"
@import "./items.styl"
@import "./inventory.styl"
@import "./alerts.styl"
@import "./helpers.styl"
@import "./responsive.styl"
@import "./header.styl"
@import "./filters.styl"
@import "./scrollbars.styl"
@import "./game-pane.styl"
@import "./npcs.styl"
@import "./challenges.styl"
// fix exploding to very wide for some reason
.datepicker
@ -146,7 +144,33 @@ hr
position: fixed;
top: 0;
right: 0px;
z-index: 1001;
z-index: 1061;
.modal-body
margin: 10px
// TODO this is supposed to go in avatar.styl, but stylus is breaking if I even touch that file
.profile-modal-header
width:560px
margin: 0px auto
.avatar-level
position absolute
bottom 0px
right 0px
background-color #dfe9ea
padding 1px 3px 1px 3px
a
cursor: pointer
#modalMember p
word-wrap: break-word
.btn
margin-right: 5px
.modal-indented-list
margin-left: 10px;
padding-left: 10px;

View file

@ -1,5 +1,6 @@
.gem-wallet
cursor: pointer
padding-top: 10px;
.tile
background-color: darken($neutral, 10%)
.add-gems-btn
@ -11,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
@ -20,13 +24,24 @@
width: 100%
td
padding: 0.5em
width: 25%
//width: 25%
&.active-pet
background-color: $bad
outline: 1px solid rgba(0,0,0,0.1)
outline-offset: -1px
&:hover, &:focus
background-color: darken($better, 10%)
> div
margin:auto
margin-bottom:.5em
p
text-align:center
width:6.5em
height:2.5em
menu.pets div
display: inline-block
padding-top: -30px
.current-pet
left: 0px
@ -38,16 +53,42 @@
padding-bottom:20px
.Pet_Food_Base, .Pet_Food_Zombie, .Pet_Food_White, .Pet_Food_Veteran, .Pet_Food_Skeleton, .Pet_Food_Shade, .Pet_Food_Red, .Pet_Food_Golden, .Pet_Food_Desert, .Pet_Food_CottonCandyPink, .Pet_Food_CottonCandyBlue, .Pet_Food_Zombie, .Pet_Food_White, .Pet_Food_Skeleton, .Pet_Food_Shade, .Pet_Food_Red, .Pet_Food_Golden, .Pet_Food_Desert, .Pet_Food_CottonCandyPink, .Pet_Food_CottonCandyBlue, .Pet_Food_Base, .Pet_Food_Base, .Pet_Food_Zombie, .Pet_Food_White, .Pet_Food_Skeleton, .Pet_Food_Shade, .Pet_Food_Red, .Pet_Food_Golden, .Pet_Food_Desert, .Pet_Food_CottonCandyPink, .Pet_Food_CottonCandyBlue, .Pet_Food_Base, .Pet_Food_Zombie, .Pet_Food_White, .Pet_Food_Skeleton, .Pet_Food_Shade, .Pet_Food_Red, .Pet_Food_Golden, .Pet_Food_Desert, .Pet_Food_CottonCandyPink, .Pet_Food_CottonCandyBlue, .Pet_Food_Base, .Pet_Food_Zombie, .Pet_Food_White, .Pet_Food_Skeleton, .Pet_Food_Shade, .Pet_Food_Red, .Pet_Food_Golden, .Pet_Food_Desert, .Pet_Food_CottonCandyPink, .Pet_Food_CottonCandyBlue, .Pet_Food_Base, .Pet_Food_Zombie, .Pet_Food_White, .Pet_Food_Skeleton, .Pet_Food_Shade, .Pet_Food_Red, .Pet_Food_Golden, .Pet_Food_Desert, .Pet_Food_CottonCandyPink, .Pet_Food_CottonCandyBlue, .Pet_Food_Base, .Pet_Food_Zombie, .Pet_Food_White, .Pet_Food_Skeleton, .Pet_Food_Shade, .Pet_Food_Red, .Pet_Food_Golden, .Pet_Food_Desert, .Pet_Food_CottonCandyPink, .Pet_Food_CottonCandyBlue, .Pet_Food_Base, .Pet_Food_SugarCube, .Pet_Food_Strawberry, .Pet_Food_Rotten, .Pet_Food_Licorice, .Pet_Food_Golden, .Pet_Food_Cream, .Pet_Food_CottonCandyPink, .Pet_Food_CottonCandyBlue, .Pet_Food_Chocolate, .Pet_Food_Base, .Pet_Food_Zombie, .Pet_Food_White, .Pet_Food_Skeleton, .Pet_Food_Shade, .Pet_Food_Red, .Pet_Food_Golden, .Pet_Food_Desert, .Pet_Food_CottonCandyPink, .Pet_Food_CottonCandyBlue, .Pet_Food_Base
background: url("/img/hatching_powder.png") no-repeat
background: url("/bower_components/habitrpg-shared/img/hatching_powder.png") no-repeat
width:34px
height:34px
.Pet_Currency_Gem, .Pet_Currency_Gem2x, .Pet_Currency_Gem1x
background: url("/bower_components/habitrpg-shared/img/sprites/Egg_Sprite_Sheet.png") no-repeat
display:block
.Pet_Currency_Gem {background-position: 0px -510px; width: 51px; height: 45px} /* Not an egg or potion so has a different size */
.Pet_Currency_Gem2x {background-position: -55px -513px; width: 34px; height: 30px}
.Pet_Currency_Gem1x {background-position: -63px -542px; width: 19px; height: 17px}
.inventory-list p
//display: none
.inventory-list li
clear:both
.pets-menu > div
float:left
.hatchingPotions-menu > div
float:left
display:inline-block
vertical-align:top
padding:.3em
width:6em
margin-top:1em
p
text-align:center
width:6em
margin-top:-.5em
.hatchingPotion-menu > div
display:inline-block
vertical-align:top
padding:.3em
width:6em
margin-top:1em
p
text-align:center
width:6em
margin-top:-.5em
.pet-button
border: none
@ -62,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

36
public/css/npcs.styl Normal file
View file

@ -0,0 +1,36 @@
.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 {background-position: 0 0; width: 162px; height: 138px;}
.NPC-Alex-container{margin-bottom: 20px;}
.NPC-Daniel {background-position: -222px 0; width: 135px; height: 123px}
.NPC-Justin {background-position: -357px 0; width: 84px; height: 119px}
.NPC-Justin-Head {background-position: -396px 0; width: 36px; height: 96px}
.NPC-Matt {background-position: -441px 0; width: 100%; padding-left: 208px; padding-bottom: 20px;}
// Bailey
.NPC-Bailey
background-position: -162px -42px
width: 60px
height: 72px
float:left
.NPC-Bailey-Head
background-position: -168px -42px
width: 54px
height: 30px
position: absolute
top: 117px // 147 (header-height) - 30 (bailey height)
right: 50px
cursor: pointer
.static-popover
z-index 0
display block
position:relative
// Tour (Justin)
.NPC-Justin.float-left
float: left
margin-right: 5px
margin-bottom: 5px
.popover-navigation {clear:both;}

View file

@ -14,4 +14,8 @@
}
body {
padding-top:0px;
}
a {
cursor: pointer;
}

6
public/css/static.styl Normal file
View file

@ -0,0 +1,6 @@
// Vendor Includes - include first so we can override
// Import only styles that do not have urls to images! Include them directly in the page!
@import "../bower_components/angular-loading-bar/build/loading-bar.css"
@import "./static-pages.css"
@import "./footer.css"

View file

@ -71,7 +71,7 @@ for $stage in $stages
box-shadow: none
background-color: white
height: 3em
padding: 0 0 0 0.5em
padding: 0 3.3em 0 0.5em
width: 100%
&:focus
box-shadow: inset 0 0 3px darken($best, 20%),inset -1px 0 1px darken($best, 30%)
@ -117,6 +117,11 @@ for $stage in $stages
display: block
padding: 0.75em 0 0.75em 3.5em
line-height: 1.4
word-wrap: break-word
// task due date (for to-dos)
.task-date
font-size: 70%
.habit-wide .task-text
padding-left: 7em
@ -326,7 +331,7 @@ form
.option-short
.option-content
width: 3em
width: 4em
display: inline-block
.input-suffix
vertical-align: 20%

Binary file not shown.

Before

Width:  |  Height:  |  Size: 364 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 513 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 387 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 729 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Some files were not shown because too many files have changed in this diff Show more