Merge branch 'parties'

Conflicts:
	src/app/helpers.coffee
	src/app/scoring.coffee
This commit is contained in:
Tyler Renelle 2013-02-06 14:16:52 -05:00
commit 906932090f
15 changed files with 561 additions and 197 deletions

View file

@ -0,0 +1,97 @@
// %mongo server:27017/dbname underscore.js my_commands.js
// %mongo server:27017/dbname underscore.js --shell
//db.users.find({'auth.facebook.email': 'tylerrenelle@gmail.com'}).forEach(function(user){
db.users.find().forEach(function(user){
if (!user._id) {
print("User has null _id");
return; // need to figure out how to delete these buggers if they don't have an id to delete from
}
if (user._id.indexOf("$") === 0) {
print("User id starts with $ (" + user._id + ")")
return;
}
// even though we're clobbering user later, sometimes these are undefined and crash the script
// this saves us some ternaries
user.stats = user.stats || {};
user.items = user.items || {};
user.preferences = user.preferences || {};
user.notifications = user.notifications || {};
user.flags = user.flags || {};
user.habitIds = user.habitIds || [];
user.dailyIds = user.dailyIds || [];
user.todoIds = user.todoIds || [];
user.rewardIds = user.rewardIds|| [];
_.each(user.tasks, function(task, key){
if (!task.type) {
delete user.tasks[key];
// idList will take care of itself on page-load
return
}
if (key == '$spec') {
print("$spec was found: " + user._id);
return
}
if (key.indexOf("$_") === 0) {
var newKey = key.replace("$_", ''),
index = user[task.type + "Ids"].indexOf(key)
user[task.type + "Ids"][index] = newKey;
task.id = newKey
user.tasks[newKey] = task
// TODO make sure this is ok, that we're not deleting the original
// Otherwise use lodash.cloneDeep
delete user.tasks[key]
}
});
// New user schema has public and private paths, so we can setup proper access control with racer
// Note 'public' and 'private' are reserved words
var newUser = {
auth: user.auth, // we need this top-level due to derby-auth
apiToken: user.preferences.api_token || null, // set on update, we need derby.uuid()
preferences: {
armorSet: user.preferences.armorSet || 'v1',
gender: user.preferences.gender || 'm'
},
balance: user.balance || 2,
lastCron: user.lastCron || +new Date,
history: user.history || [],
stats: {
gp: user.stats.money || 0,
hp: user.stats.hp || 50,
exp: user.stats.exp || 0,
lvl: user.stats.lvl || 1
},
items: {
armor: user.items.armor || 0,
weapon: user.items.weapon || 0
},
tasks: user.tasks || {},
idLists: {
habit: user.habitIds || [],
daily: user.dailyIds || [],
todo: user.todoIds || [],
reward: user.rewardIds || []
},
flags: {
partyEnabled: false,
itemsEnabled: user.items.itemsEnabled || false,
kickstarter: user.notifications.kickstarter || 'show',
ads: user.flags.ads || null // null because it's set on registration
},
party: {
current: null,
invitation: null
}
};
try {
db.users.update({_id:user._id}, newUser);
} catch(e) {
print(e);
}
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View file

@ -1,6 +1,37 @@
process.on('uncaughtException', function (exception) { process.on('uncaughtException', function (error) {
console.error(exception);
// don't crash for now function sendEmail(mailData) {
var nodemailer = require("derby-auth/node_modules/nodemailer");
// create reusable transport method (opens pool of SMTP connections)
// TODO derby-auth isn't currently configurable here, if you need customizations please send pull request
var smtpTransport = nodemailer.createTransport("SMTP",{
service: process.env.SMTP_SERVICE,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS
}
});
// send mail with defined transport object
smtpTransport.sendMail(mailData, function(error, response){
if(error){
console.log(error);
}else{
console.log("Message sent: " + response.message);
}
smtpTransport.close(); // shut down the connection pool, no more messages
});
}
sendEmail({
from: "HabitRPG <admin@habitrpg.com>",
to: "tylerrenelle@gmail.com",
subject: "HabitRPG Error",
text: error.stack
});
console.log(error.stack);
}); });
require('coffee-script') // remove intermediate compilation requirement require('coffee-script') // remove intermediate compilation requirement

View file

@ -1,4 +1,10 @@
content = require('./content') content = require('./content')
_ = require 'underscore'
module.exports.resetDom = (model) ->
window.DERBY.app.dom.clear()
window.DERBY.app.view.render(model)
model.fn '_tnl', '_user.stats.lvl', (lvl) -> (lvl*100)/5
### ###
Loads JavaScript files from (1) public/js/* and (2) external sources Loads JavaScript files from (1) public/js/* and (2) external sources
@ -54,7 +60,7 @@ module.exports.setupSortable = (model) ->
# Also, note that refList index arguments can either be an index # Also, note that refList index arguments can either be an index
# or the item's id property # or the item's id property
model.at("_#{type}List").pass(ignore: domId).move {id}, to model.at("_#{type}List").pass(ignore: domId).move {id}, to
setupSortable(type) for type in ['habit', 'daily', 'todo', 'reward'] _.each ['habit', 'daily', 'todo', 'reward'], (type) -> setupSortable(type)
module.exports.setupTooltips = (model) -> module.exports.setupTooltips = (model) ->
$('[rel=tooltip]').tooltip() $('[rel=tooltip]').tooltip()
@ -95,7 +101,7 @@ module.exports.setupGrowlNotifications = (model) ->
allow_dismiss: true allow_dismiss: true
stackup_spacing: 10 # spacing between consecutive stacecked growls. stackup_spacing: 10 # spacing between consecutive stacecked growls.
user.on 'set', 'items.itemsEnabled', (captures, args) -> user.on 'set', 'flags.itemsEnabled', (captures, args) ->
return unless captures == true return unless captures == true
message = "Congratulations, you have unlocked the Item Store! You can now buy weapons, armor, potions, etc. Read each item's comment for more information." message = "Congratulations, you have unlocked the Item Store! You can now buy weapons, armor, potions, etc. Read each item's comment for more information."
$('ul.items').popover $('ul.items').popover
@ -112,16 +118,16 @@ module.exports.setupGrowlNotifications = (model) ->
user.on 'set', 'flags.partyEnabled', (captures, args) -> user.on 'set', 'flags.partyEnabled', (captures, args) ->
return unless captures == true return unless captures == true
message = "Congratulations, you have unlocked the Party System! You can now group with your friends by adding their User Ids." message = "Congratulations, you have unlocked the Party System! You can now group with your friends by adding their User Ids."
$('#add-party-button').popover $('.main-avatar').popover
title: "Pary System Unlocked" title: "Pary System Unlocked"
placement: 'bottom' placement: 'bottom'
trigger: 'manual' trigger: 'manual'
html: true html: true
content: "<div class='party-system-popover'> content: "<div class='party-system-popover'>
<img src='/img/BrowserQuest/favicon.png' /> <img src='/img/party-unlocked.png' style='float:right;padding:5px;' />
#{message} <a href='#' onClick=\"$('#add-party-button').popover('hide');return false;\">[Close]</a> #{message} <a href='#' onClick=\"$('.main-avatar').popover('hide');return false;\">[Close]</a>
</div>" </div>"
$('#add-party-button').popover 'show' $('.main-avatar').popover 'show'
# Setup listeners which trigger notifications # Setup listeners which trigger notifications
@ -131,14 +137,14 @@ module.exports.setupGrowlNotifications = (model) ->
if num < 0 if num < 0
statsNotification "<i class='icon-heart'></i>HP -#{rounded}", 'error' # lost hp from purchase statsNotification "<i class='icon-heart'></i>HP -#{rounded}", 'error' # lost hp from purchase
user.on 'set', 'stats.money', (captures, args) -> user.on 'set', 'stats.gp', (captures, args) ->
num = captures - args num = captures - args
rounded = Math.abs(num.toFixed(1)) rounded = Math.abs(num.toFixed(1))
# made purchase # made purchase
if num < 0 if num < 0
# FIXME use 'warning' when unchecking an accidently completed daily/todo, and notify of exp too # FIXME use 'warning' when unchecking an accidently completed daily/todo, and notify of exp too
statsNotification "<i class='icon-star'></i>GP -#{rounded}", 'success' statsNotification "<i class='icon-star'></i>GP -#{rounded}", 'success'
# gained money (and thereby exp) # gained gp (and thereby exp)
else if num > 0 else if num > 0
num = Math.abs(num) num = Math.abs(num)
statsNotification "<i class='icon-star'></i>Exp,GP +#{rounded}", 'success' statsNotification "<i class='icon-star'></i>Exp,GP +#{rounded}", 'success'

13
src/app/debug.coffee Normal file
View file

@ -0,0 +1,13 @@
moment = require 'moment'
module.exports.app = (appExports, model) ->
user = model.at('_user')
appExports.emulateNextDay = ->
yesterday = +moment().subtract('days', 1).toDate()
user.set 'lastCron', yesterday
window.location.reload()
appExports.cheat = ->
user.incr 'stats.exp', 20
user.incr 'stats.gp', 1000

View file

@ -44,17 +44,18 @@ module.exports.viewHelpers = (view) ->
else else
return "0" return "0"
view.fn "money", (num) -> view.fn "gp", (num) ->
if num if num
return num.toFixed(2) return num.toFixed(2)
else else
return "0.00" return "0.00"
view.fn "lessThan", (a, b) -> view.fn "lt", (a, b) ->
a < b a < b
view.fn 'gt', (a, b) -> a > b
view.fn "tokens", (money) -> view.fn "tokens", (gp) ->
return money/0.25 return gp/0.25
view.fn 'currentArmor', (user) -> view.fn 'currentArmor', (user) ->
armor = user?.items?.armor || 0 armor = user?.items?.armor || 0

View file

@ -10,23 +10,15 @@ content = require './content'
scoring = require './scoring' scoring = require './scoring'
schema = require './schema' schema = require './schema'
helpers = require './helpers' helpers = require './helpers'
helpers.viewHelpers view
browser = require './browser' browser = require './browser'
party = require './party'
helpers.viewHelpers view
_ = require('underscore') _ = require('underscore')
setupListReferences = (model) -> setupListReferences = (model) ->
taskTypes = ['habit', 'daily', 'todo', 'reward'] taskTypes = ['habit', 'daily', 'todo', 'reward']
_.each taskTypes, (type) -> model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids" _.each taskTypes, (type) -> model.refList "_#{type}List", "_user.tasks", "_user.idLists.#{type}"
setupModelFns = (model) ->
model.fn '_user._tnl', '_user.stats.lvl', (lvl) ->
# see https://github.com/lefnire/habitrpg/issues/4
# also update in scoring.coffee. TODO create a function accessible in both locations
(lvl*100)/5
# model.fn '_party', '_user.party', (ids) ->
# model.fetch model.query('users').party(ids), (err, party) ->
# model.set '_view.party', party
# ========== ROUTES ========== # ========== ROUTES ==========
@ -39,20 +31,23 @@ get '/', (page, model, next) ->
#if req.headers['x-forwarded-proto']!='https' and process.env.NODE_ENV=='production' #if req.headers['x-forwarded-proto']!='https' and process.env.NODE_ENV=='production'
# return page.redirect 'https://' + req.headers.host + req.url # return page.redirect 'https://' + req.headers.host + req.url
#FIXME subscribing to this query causes "Fatal Error: Unauuthorized" after conncetion for a time (racer/lib/accessControl/accessControl.Store.js) # This used to be in party.server(model, cb), but was getting `TypeError: Object #<Model> has no method 'server'`
#q = model.query('users').withId(model.session.userId) # on the second load for some reason
q = "users.#{model.session.userId}" selfQ = model.query('users').withId(model.get('_userId') or model.session.userId)
model.subscribe q, (err, user) -> model.subscribe selfQ, (err, users) ->
#user = result.at(0) throw err if err
user = users.at(0)
model.ref '_user', user model.ref '_user', user
batch = new schema.BatchUpdate(model) batch = new schema.BatchUpdate(model)
batch.startTransaction() batch.startTransaction()
obj = batch.obj()
# Setup Item Store # Setup Item Store
items = user.get('items')
_view.items = _view.items =
armor: content.items.armor[parseInt(items?.armor || 0) + 1] armor: content.items.armor[parseInt(obj.items?.armor || 0) + 1]
weapon: content.items.weapon[parseInt(items?.weapon || 0) + 1] weapon: content.items.weapon[parseInt(obj.items?.weapon || 0) + 1]
potion: content.items.potion potion: content.items.potion
reroll: content.items.reroll reroll: content.items.reroll
@ -62,22 +57,18 @@ get '/', (page, model, next) ->
batch.commit() batch.commit()
setupListReferences(model) setupListReferences(model)
setupModelFns(model) model.fn '_tnl', '_user.stats.lvl', (lvl) ->
# see https://github.com/lefnire/habitrpg/issues/4
# also update in scoring.coffee. TODO create a function accessible in both locations
(lvl*100)/5
# Subscribe to friends if obj.party?.current?
if !_.isEmpty(user.get('party')) party.partySubscribe model, obj.party.current, (p) -> page.render()
model.subscribe model.query('users').party(user.get('party')), (err, party) -> else
model.ref '_party', party page.render()
page.render()
# ========== CONTROLLER FUNCTIONS ========== # ========== CONTROLLER FUNCTIONS ==========
resetDom = (model) ->
window.DERBY.app.dom.clear()
view.render(model)
setupModelFns(model)
ready (model) -> ready (model) ->
user = model.at('_user') user = model.at('_user')
scoring.setModel(model) scoring.setModel(model)
@ -87,7 +78,7 @@ ready (model) ->
user.set('lastCron', +new Date) if (!lastCron? or lastCron == 'new') user.set('lastCron', +new Date) if (!lastCron? or lastCron == 'new')
# Setup model in scoring functions # Setup model in scoring functions
scoring.cron(resetDom) scoring.cron()
# Load all the jQuery, Growl, Tour, etc # Load all the jQuery, Growl, Tour, etc
browser.loadJavaScripts(model) browser.loadJavaScripts(model)
@ -96,8 +87,12 @@ ready (model) ->
browser.setupTour(model) browser.setupTour(model)
browser.setupGrowlNotifications(model) unless model.get('_view.mobileDevice') browser.setupGrowlNotifications(model) unless model.get('_view.mobileDevice')
party.app(exports, model)
require('../server/private').app(exports, model) require('../server/private').app(exports, model)
require('./debug').app(exports, model)
user.on 'set', 'tasks.*.completed', (i, completed, previous, isLocal, passed) -> user.on 'set', 'tasks.*.completed', (i, completed, previous, isLocal, passed) ->
return if passed? && passed.cron # Don't do this stuff on cron return if passed? && passed.cron # Don't do this stuff on cron
direction = () -> direction = () ->
@ -168,14 +163,14 @@ ready (model) ->
# fix when query subscriptions implemented properly # fix when query subscriptions implemented properly
$('[rel=tooltip]').tooltip('hide') $('[rel=tooltip]').tooltip('hide')
ids = user.get("#{type}Ids") ids = user.get("idLists.#{type}")
ids.splice(ids.indexOf(id),1) ids.splice(ids.indexOf(id),1)
user.del('tasks.'+id) user.del('tasks.'+id)
user.set("#{type}Ids", ids) user.set("idLists.#{type}", ids)
exports.clearCompleted = (e, el) -> exports.clearCompleted = (e, el) ->
todoIds = user.get('todoIds') todoIds = user.get('idLists.todo')
removed = false removed = false
_.each model.get('_todoList'), (task) -> _.each model.get('_todoList'), (task) ->
if task.completed if task.completed
@ -183,7 +178,7 @@ ready (model) ->
user.del('tasks.'+task.id) user.del('tasks.'+task.id)
todoIds.splice(todoIds.indexOf(task.id), 1) todoIds.splice(todoIds.indexOf(task.id), 1)
if removed if removed
user.set('todoIds', todoIds) user.set('idLists.todo', todoIds)
exports.toggleDay = (e, el) -> exports.toggleDay = (e, el) ->
task = model.at(e.target) task = model.at(e.target)
@ -226,11 +221,11 @@ ready (model) ->
#TODO: this should be working but it's not. so instead, i'm passing all needed values as data-attrs #TODO: this should be working but it's not. so instead, i'm passing all needed values as data-attrs
# item = model.at(e.target) # item = model.at(e.target)
money = user.get 'stats.money' gp = user.get 'stats.gp'
[type, value, index] = [ $(el).attr('data-type'), $(el).attr('data-value'), $(el).attr('data-index') ] [type, value, index] = [ $(el).attr('data-type'), $(el).attr('data-value'), $(el).attr('data-index') ]
return if money < value return if gp < value
user.set 'stats.money', money - value user.set 'stats.gp', gp - value
if type == 'armor' if type == 'armor'
user.set 'items.armor', index user.set 'items.armor', index
model.set '_view.items.armor', content.items.armor[parseInt(index) + 1] model.set '_view.items.armor', content.items.armor[parseInt(index) + 1]
@ -254,7 +249,7 @@ ready (model) ->
# Reset stats # Reset stats
batch.set 'stats.hp', 50 batch.set 'stats.hp', 50
batch.set 'stats.lvl', 1 batch.set 'stats.lvl', 1
batch.set 'stats.money', 0 batch.set 'stats.gp', 0
batch.set 'stats.exp', 0 batch.set 'stats.exp', 0
# Reset items # Reset items
@ -276,40 +271,15 @@ ready (model) ->
batch.startTransaction() batch.startTransaction()
taskTypes = ['habit', 'daily', 'todo', 'reward'] taskTypes = ['habit', 'daily', 'todo', 'reward']
batch.set 'tasks', {} batch.set 'tasks', {}
_.each taskTypes, (type) -> batch.set "#{type}Ids", [] _.each taskTypes, (type) -> batch.set "idLists.#{type}", []
batch.set 'balance', 2 if user.get('balance') < 2 #only if they haven't manually bought tokens batch.set 'balance', 2 if user.get('balance') < 2 #only if they haven't manually bought tokens
revive(batch, true) revive(batch)
batch.commit() batch.commit()
resetDom(model) browser.resetDom(model)
exports.closeKickstarterNofitication = (e, el) -> exports.closeKickstarterNofitication = (e, el) -> user.set('flags.kickstarter', 'hide')
user.set('notifications.kickstarter', 'hide')
exports.setMale = -> user.set('preferences.gender', 'm') exports.setMale = -> user.set('preferences.gender', 'm')
exports.setFemale = -> user.set('preferences.gender', 'f') exports.setFemale = -> user.set('preferences.gender', 'f')
exports.setArmorsetV1 = -> user.set('preferences.armorSet', 'v1') exports.setArmorsetV1 = -> user.set('preferences.armorSet', 'v1')
exports.setArmorsetV2 = -> user.set('preferences.armorSet', 'v2') exports.setArmorsetV2 = -> user.set('preferences.armorSet', 'v2')
exports.addParty = ->
id = model.get('_newPartyMember').replace(/[\s"]/g, '')
debugger
return if _.isEmpty(id)
if user.get('party').indexOf(id) != -1
model.set "_view.addPartyError", "#{id} already in party."
return
query = model.query('users').party([id])
model.fetch query, (err, users) ->
partyMember = users.at(0).get()
if partyMember?.id?
user.push('party', id)
$('#add-party-modal').modal('hide')
window.location.reload() #TODO break old subscription, setup new subscript, remove this reload
model.set '_newPartyMember', ''
else
model.set "_view.addPartyError", "User with id #{id} not found."
exports.emulateNextDay = ->
yesterday = +moment().subtract('days', 1).toDate()
user.set 'lastCron', yesterday
window.location.reload()

150
src/app/party.coffee Normal file
View file

@ -0,0 +1,150 @@
_ = require('underscore')
schema = require './schema'
browser = require './browser'
_subscriptions =
party:
query: null
id: null
members:
query: null
ids: null
module.exports.partySubscribe = partySubscribe = (model, id, cb) ->
s = _subscriptions
###
# Note, this tries to unsubscribe from previous similar subscriptions so we don't have a memory leak. However,
# This causes the page to crash on refresh. We need to fix this in the future. Same goes for membersSubscribe
if s.party.query? and id == s.party.id
# No need to resubscribe, same parameters
return cb(model.at('_party'))
# already have a subscription, but we want a new one
if s.party.query? and s.party.id != id
s.party.query.unsubscribe()
s.party.query = null
###
# subscripe
s.party.query = model.query('parties').withId(id)
s.party.id = id
s.party.query.subscribe (err, res) ->
throw err if err
p = res.at(0)
model.ref '_party', p
# FIXME this is the kicker right here. This isn't getting triggered, and it's the reason why we have to refresh
# after every event. Get this working
# p.on '*', 'members', (ids) ->
# console.log("members listener got called")
# membersSubscribe model, ids
ids = p.get('members')
if !_.isEmpty(ids)
membersSubscribe model, ids, (m) ->
browser.resetDom(model) if window?
cb(p) if cb?
else
browser.resetDom(model) if window?
cb(p) if cb?
module.exports.membersSubscribe = membersSubscribe = (model, ids, cb) ->
s = _subscriptions
### @see above
if s.members.query? and !_.isEmpty(_.difference(s.members.ids, ids))
# No need to resubscribe, same parameters
return cb(model.at('_partyMembers'))
# already have a subscription, but we want a new one
if s.members.query? and _.isEmpty(_.difference(s.members.ids, ids))
s.members.query.unsubscribe()
s.members.query = null
###
# subscripe
s.members.query = model.query('users').party(ids)
s.members.ids = ids
s.members.query.subscribe (err, m) ->
throw err if err
model.ref '_partyMembers', m
# Here's a hack we need to get fixed (hopefully Lever will) - later model.queries override previous model.queries'
# returned fields. Aka, we need this here otherwise we only get the "public" fields for the current user, which
# are defined in model.query('users')party()
selfQ = model.query('users').withId(model.get('_userId') or model.session.userId)
model.subscribe selfQ, (err, users) ->
model.ref '_user', users.at(0)
cb(m) if cb?
module.exports.app = (appExports, model) ->
user = model.at('_user')
model.on 'set', '_user.party.invitation', (id) ->
partySubscribe(model, id) if id?
appExports.partyCreate = ->
newParty = model.get("_newParty")
id = model.add 'parties', { name: newParty, leader: user.get('id'), members: [user.get('id')], invites:[] }
user.set 'party', {current: id, invitation: null, leader: true}
partySubscribe model, id, -> $('#party-modal').modal('show')
appExports.partyInvite = ->
id = model.get('_newPartyMember').replace(/[\s"]/g, '')
return if _.isEmpty(id)
obj = user.get()
query = model.query('users').party([id])
model.fetch query, (err, res) ->
throw err if err
u = res.at(0).get()
if !u?.id?
model.set "_view.partyError", "User with id #{id} not found."
return
else if u.party.current? or u.party.invitation?
model.set "_view.partyError", "User already in a party or pending invitation."
return
else
p = model.at '_party'
p.push "invites", id
model.set "users.#{id}.party.invitation", p.get('id')
$.bootstrapGrowl "Invitation Sent."
$('#party-modal').modal('hide')
model.set '_newPartyMember', ''
membersSubscribe model, p.get('members'), ->
#window.location.reload(true)
appExports.partyAccept = ->
invitation = user.get('party.invitation')
partySubscribe model, invitation, (p) ->
p.push 'members', user.get('id')
user.set 'party.invitation', null
user.set 'party.current', p.get('id')
membersSubscribe model, p.get('members'), (m) ->
window.location.reload(true)
appExports.partyReject = ->
user.set 'party.invitation', null
model.set '_party', null
browser.resetDom(model)
# TODO splice parties.*.invites[key]
# TODO notify sender
appExports.partyLeave = ->
id = user.set 'party.current', null
p = model.at '_party'
members = p.get('members')
index = members.indexOf(user.get('id'))
members.splice(index,1)
p.set 'members', members
if (members.length == 0)
# last member out, kill the party
model.del "parties.#{id}"
#_subscriptions.party.query.unsubscribe()
#model.set '_party', null
#model.set '_partyMembers', null
#browser.resetDom()
setTimeout (-> window.location.reload true), 1
#exports.partyDisband = ->

View file

@ -5,62 +5,60 @@ lodash = require 'lodash'
derby = require 'derby' derby = require 'derby'
userSchema = userSchema =
# _id
stats: { gp: 0, exp: 0, lvl: 1, hp: 50 }
party: { current: null, invitation: null }
items: { armor: 0, weapon: 0 }
preferences: { gender: 'm', armorSet: 'v1' }
idLists:
habit: []
daily: []
todo: []
reward: []
apiToken: null # set in newUserObject below
lastCron: 'new' #this will be replaced with `+new Date` on first run lastCron: 'new' #this will be replaced with `+new Date` on first run
balance: 2 balance: 2
stats: { money: 0, exp: 0, lvl: 1, hp: 50 }
items: { itemsEnabled: false, armor: 0, weapon: 0 }
notifications: { kickstarter: 'show' }
preferences: { gender: 'm', armorSet: 'v1' }
flags: { partyEnabled: false }
party: []
tasks: {} tasks: {}
habitIds: [] flags:
dailyIds: [] partyEnabled: false
todoIds: [] itemsEnabled: false
rewardIds: [] kickstarter: 'show'
# ads: 'show' # added on registration
module.exports.newUserObject = -> module.exports.newUserObject = ->
# deep clone, else further new users get duplicate objects # deep clone, else further new users get duplicate objects
newUser = require('lodash').cloneDeep userSchema newUser = lodash.cloneDeep userSchema
newUser.apiToken = derby.uuid()
for task in content.defaultTasks for task in content.defaultTasks
guid = task.id = require('racer').uuid() guid = task.id = derby.uuid()
newUser.tasks[guid] = task newUser.tasks[guid] = task
switch task.type switch task.type
when 'habit' then newUser.habitIds.push guid when 'habit' then newUser.idLists.habit.push guid
when 'daily' then newUser.dailyIds.push guid when 'daily' then newUser.idLists.daily.push guid
when 'todo' then newUser.todoIds.push guid when 'todo' then newUser.idLists.todo.push guid
when 'reward' then newUser.rewardIds.push guid when 'reward' then newUser.idLists.reward.push guid
return newUser return newUser
module.exports.updateUser = (batch) -> module.exports.updateUser = (batch) ->
user = batch.user user = batch.user
obj = batch.obj()
batch.set('notifications.kickstarter', 'show') unless user.get('notifications.kickstarter') batch.set('apiToken', derby.uuid()) unless obj.apiToken
batch.set('party', []) unless !_.isEmpty(user.get('party'))
# Preferences, including API key
# Some side-stepping to avoid unecessary set (one day, model.update... one day..)
currentPrefs = _.clone user.get('preferences')
mergedPrefs = _.defaults currentPrefs, { gender: 'm', armorSet: 'v1', api_token: derby.uuid() }
batch.set('preferences', mergedPrefs)
## Task List Cleanup ## Task List Cleanup
# FIXME temporary hack to fix lists (Need to figure out why these are happening) # FIXME temporary hack to fix lists (Need to figure out why these are happening)
# FIXME consolidate these all under user.listIds so we can set them en-masse tasks = obj.tasks
tasks = user.get('tasks')
_.each ['habit','daily','todo','reward'], (type) -> _.each ['habit','daily','todo','reward'], (type) ->
path = "#{type}Ids"
# 1. remove duplicates # 1. remove duplicates
# 2. restore missing zombie tasks back into list # 2. restore missing zombie tasks back into list
taskIds = _.pluck( _.where(tasks, {type:type}), 'id') taskIds = _.pluck( _.where(tasks, {type:type}), 'id')
union = _.union user.get(path), taskIds union = _.union obj.idLists[type], taskIds
# 2. remove empty (grey) tasks # 2. remove empty (grey) tasks
preened = _.filter(union, (val) -> _.contains(taskIds, val)) preened = _.filter(union, (val) -> _.contains(taskIds, val))
# There were indeed issues found, set the new list # There were indeed issues found, set the new list
batch.set(path, preened) # if _.difference(preened, userObj[path]).length != 0 batch.set("idLists.#{type}", preened) # if _.difference(preened, userObj[path]).length != 0
module.exports.BatchUpdate = BatchUpdate = (model) -> module.exports.BatchUpdate = BatchUpdate = (model) ->
user = model.at("_user") user = model.at("_user")

View file

@ -75,24 +75,24 @@ updateStats = (newStats, batch) ->
if newStats.exp? if newStats.exp?
# level up & carry-over exp # level up & carry-over exp
tnl = user.get '_tnl' tnl = model.get '_tnl'
if newStats.exp >= tnl if newStats.exp >= tnl
newStats.exp -= tnl newStats.exp -= tnl
obj.stats.lvl++ obj.stats.lvl++
obj.stats.hp = 50 obj.stats.hp = 50
if !obj.items.itemsEnabled and obj.stats.lvl >= 2 if !obj.flags.itemsEnabled and obj.stats.lvl >= 2
# Set to object, then also send to browser right away to get model.on() subscription notification # Set to object, then also send to browser right away to get model.on() subscription notification
batch.set 'items.itemsEnabled', true batch.set 'flags.itemsEnabled', true
obj.items.itemsEnabled = true obj.flags.itemsEnabled = true
# if !obj.flags.partyEnabled and obj.stats.lvl >= 3 if !obj.flags.partyEnabled and obj.stats.lvl >= 3
# batch.set 'flags.partyEnabled', true batch.set 'flags.partyEnabled', true
# obj.flags.partyEnabled = true obj.flags.partyEnabled = true
obj.stats.exp = newStats.exp obj.stats.exp = newStats.exp
if newStats.money? if newStats.gp?
#FIXME what was I doing here? I can't remember, money isn't defined #FIXME what was I doing here? I can't remember, gp isn't defined
money = 0.0 if (!money? or money<0) gp = 0.0 if (!gp? or gp<0)
obj.stats.money = newStats.money obj.stats.gp = newStats.gp
# {taskId} task you want to score # {taskId} task you want to score
# {direction} 'up' or 'down' # {direction} 'up' or 'down'
@ -106,7 +106,7 @@ score = (taskId, direction, times, batch, cron) ->
batch.startTransaction() batch.startTransaction()
obj = batch.obj() obj = batch.obj()
{money, hp, exp, lvl} = obj.stats {gp, hp, exp, lvl} = obj.stats
taskPath = "tasks.#{taskId}" taskPath = "tasks.#{taskId}"
taskObj = obj.tasks[taskId] taskObj = obj.tasks[taskId]
@ -127,7 +127,7 @@ score = (taskId, direction, times, batch, cron) ->
addPoints = -> addPoints = ->
modified = expModifier(delta) modified = expModifier(delta)
exp += modified exp += modified
money += modified gp += modified
subtractPoints = -> subtractPoints = ->
modified = hpModifier(delta) modified = hpModifier(delta)
@ -162,17 +162,17 @@ score = (taskId, direction, times, batch, cron) ->
# Don't adjust values for rewards # Don't adjust values for rewards
calculateDelta(false) calculateDelta(false)
# purchase item # purchase item
money -= Math.abs(taskObj.value) gp -= Math.abs(taskObj.value)
num = parseFloat(taskObj.value).toFixed(2) num = parseFloat(taskObj.value).toFixed(2)
# if too expensive, reduce health & zero money # if too expensive, reduce health & zero gp
if money < 0 if gp < 0
hp += money # hp - money difference hp += gp # hp - gp difference
money = 0 gp = 0
taskObj.value = value taskObj.value = value
batch.set "#{taskPath}.value", taskObj.value batch.set "#{taskPath}.value", taskObj.value
origStats = _.clone obj.stats origStats = _.clone obj.stats
updateStats {hp: hp, exp: exp, money: money}, batch updateStats {hp: hp, exp: exp, gp: gp}, batch
if commit if commit
# newStats / origStats is a glorious hack to trick Derby into seeing the change in model.on(*) # newStats / origStats is a glorious hack to trick Derby into seeing the change in model.on(*)
newStats = _.clone batch.obj().stats newStats = _.clone batch.obj().stats
@ -186,7 +186,7 @@ score = (taskId, direction, times, batch, cron) ->
At end of day, add value to all incomplete Daily & Todo tasks (further incentive) At end of day, add value to all incomplete Daily & Todo tasks (further incentive)
For incomplete Dailys, deduct experience For incomplete Dailys, deduct experience
### ###
cron = (resetDom_cb) -> cron = () ->
today = +new Date today = +new Date
daysPassed = helpers.daysBetween(today, user.get('lastCron')) daysPassed = helpers.daysBetween(today, user.get('lastCron'))
if daysPassed > 0 if daysPassed > 0
@ -245,7 +245,7 @@ cron = (resetDom_cb) ->
batch.setStats() batch.setStats()
batch.set('history', obj.history) batch.set('history', obj.history)
batch.commit() batch.commit()
resetDom_cb(model) browser.resetDom(model)
setTimeout (-> user.set 'stats.hp', hpAfter), 1000 # animate hp loss setTimeout (-> user.set 'stats.hp', hpAfter), 1000 # animate hp loss

View file

@ -8,6 +8,7 @@ serverError = require './serverError'
MongoStore = require('connect-mongo')(express) MongoStore = require('connect-mongo')(express)
auth = require 'derby-auth' auth = require 'derby-auth'
priv = require './private' priv = require './private'
habitrpgStore = require('./store')
## Run server cron ## ## Run server cron ##
require('./cron').deleteStaleAccounts() require('./cron').deleteStaleAccounts()
@ -31,7 +32,6 @@ derby.use(require 'racer-db-mongo')
store = derby.createStore store = derby.createStore
db: {type: 'Mongo', uri: process.env.NODE_DB_URI, safe:true} db: {type: 'Mongo', uri: process.env.NODE_DB_URI, safe:true}
listen: server listen: server
require('./store')(store) #setup custom accessControl
ONE_YEAR = 1000 * 60 * 60 * 24 * 365 ONE_YEAR = 1000 * 60 * 60 * 24 * 365
root = path.dirname path.dirname __dirname root = path.dirname path.dirname __dirname
@ -48,6 +48,7 @@ options =
domain: process.env.BASE_URL || 'http://localhost:3000' domain: process.env.BASE_URL || 'http://localhost:3000'
allowPurl: true allowPurl: true
schema: require('../app/schema').newUserObject() schema: require('../app/schema').newUserObject()
customAccessControl: habitrpgStore.customAccessControl
mongo_store = new MongoStore {url: process.env.NODE_DB_URI}, -> mongo_store = new MongoStore {url: process.env.NODE_DB_URI}, ->
expressApp expressApp

View file

@ -37,7 +37,6 @@ module.exports.app = (appExports, model) ->
obj = model.get('_user') obj = model.get('_user')
batch.set 'balance', obj.balance-1 batch.set 'balance', obj.balance-1
_.each obj.tasks, (task) -> batch.set("tasks.#{task.id}.value", 0) unless task.type == 'reward' _.each obj.tasks, (task) -> batch.set("tasks.#{task.id}.value", 0) unless task.type == 'reward'
console.log(obj)
batch.commit() batch.commit()
module.exports.routes = (expressApp) -> module.exports.routes = (expressApp) ->
@ -52,6 +51,7 @@ module.exports.routes = (expressApp) ->
else else
model = req.getModel() model = req.getModel()
userId = model.session.userId userId = model.session.userId
req._isServer = true
model.fetch "users.#{userId}", (err, user) -> model.fetch "users.#{userId}", (err, user) ->
model.ref '_user', "users.#{userId}" model.ref '_user', "users.#{userId}"
model.set('_user.balance', model.get('_user.balance')+5) model.set('_user.balance', model.get('_user.balance')+5)

View file

@ -17,40 +17,40 @@ module.exports = (expressApp, root, derby) ->
deprecatedMessage = 'This API is no longer supported, see https://github.com/lefnire/habitrpg/wiki/API for new protocol' deprecatedMessage = 'This API is no longer supported, see https://github.com/lefnire/habitrpg/wiki/API for new protocol'
expressApp.get '/:uid/up/:score?', (req, res) -> res.send(500, deprecatedMessage) expressApp.get '/:uid/up/:score?', (req, res) -> res.send(500, deprecatedMessage)
expressApp.get '/:uid/down/:score?', (req, res) -> res.send(500, deprecatedMessage) expressApp.get '/:uid/down/:score?', (req, res) -> res.send(500, deprecatedMessage)
expressApp.post '/v1/users/:uid/tasks/:taskId/:direction', (req, res) -> res.send(500, deprecatedMessage) expressApp.post '/users/:uid/tasks/:taskId/:direction', (req, res) -> res.send(500, deprecatedMessage)
# ---------- v1 API ------------ # ---------- v1 API ------------
### ###
v1 API. Requires user-id and api_token, task-id, direction. Test with: v1 API. Requires user-id and apiToken, task-id, direction. Test with:
curl -X POST -H "Content-Type:application/json" -d '{"api_token":"{TOKEN}"}' localhost:3000/v1/users/{UID}/tasks/productivity/up curl -X POST -H "Content-Type:application/json" -d '{"apiToken":"{TOKEN}"}' localhost:3000/v1/users/{UID}/tasks/productivity/up
### ###
# TODO /v1/.. expressApp.post '/v1/users/:uid/tasks/:taskId/:direction', (req, res) ->
expressApp.post '/users/:uid/tasks/:taskId/:direction', (req, res) ->
{uid, taskId, direction} = req.params {uid, taskId, direction} = req.params
{api_token, title, service, icon} = req.body {apiToken, title, service, icon} = req.body
console.log {params:req.params, body:req.body} if process.env.NODE_ENV == 'development' console.log {params:req.params, body:req.body} if process.env.NODE_ENV == 'development'
# Send error responses for improper API call # Send error responses for improper API call
return res.send(500, 'request body "api_token" required') unless api_token return res.send(500, 'request body "apiToken" required') unless apiToken
return res.send(500, ':uid required') unless uid return res.send(500, ':uid required') unless uid
return res.send(500, ':taskId required') unless taskId return res.send(500, ':taskId required') unless taskId
return res.send(500, ":direction must be 'up' or 'down'") unless direction in ['up','down'] return res.send(500, ":direction must be 'up' or 'down'") unless direction in ['up','down']
model = req.getModel() model = req.getModel()
model.fetch model.query('users').withIdAndToken(uid, api_token), (err, result) -> req._isServer = true
model.fetch model.query('users').withIdAndToken(uid, apiToken), (err, result) ->
return res.send(500, err) if err return res.send(500, err) if err
user = result.at(0) user = result.at(0)
userObj = user.get() userObj = user.get()
if _.isEmpty(userObj) if _.isEmpty(userObj)
return res.send(500, "User with uid=#{uid}, token=#{api_token} not found. Make sure you're not using your username, but your User Id") return res.send(500, "User with uid=#{uid}, token=#{apiToken} not found. Make sure you're not using your username, but your User Id")
model.ref('_user', user) model.ref('_user', user)
# Create task if doesn't exist # Create task if doesn't exist
# TODO add service & icon to task # TODO add service & icon to task
unless model.get("_user.tasks.#{taskId}") unless model.get("_user.tasks.#{taskId}")
model.refList "_habitList", "_user.tasks", "_user.habitIds" model.refList "_habitList", "_user.tasks", "_user.idLists.habit"
model.at('_habitList').push { model.at('_habitList').push {
id: taskId id: taskId
type: 'habit' type: 'habit'
@ -59,7 +59,9 @@ module.exports = (expressApp, root, derby) ->
up: true up: true
down: true down: true
notes: "This task was created by a third-party service. Feel free to edit, it won't harm the connection to that service. Additionally, multiple services may piggy-back off this task." notes: "This task was created by a third-party service. Feel free to edit, it won't harm the connection to that service. Additionally, multiple services may piggy-back off this task."
} }, (a,b,c) ->
console.log {a:a,b:b,c:c}
scoring.setModel(model) scoring.setModel(model)
delta = scoring.score(taskId, direction) delta = scoring.score(taskId, direction)

View file

@ -2,39 +2,92 @@
Setup read / write access Setup read / write access
@param store @param store
### ###
module.exports = (store) ->
# store.writeAccess "*", "users.*.balance", (id, newBalance, next) -> module.exports.customAccessControl = (store) ->
# return unless @session and @session.userId # https://github.com/codeparty/racer/issues/37 userAccess(store)
# purchasingSomethingOnClient = newBalance < this.session.req._racerModel.get("users.#{id}.balance") partySystem(store)
# isServer = not @req.socket REST(store)
# next(purchasingSomethingOnClient or isServer)
###
General user access
###
userAccess = (store) ->
store.readPathAccess "users.*", -> # captures, next) ->
#return unless @session and @session.userId # https://github.com/codeparty/racer/issues/37
uid = arguments[0]
next = arguments[arguments.length - 1]
next (uid is @session.userId) or @session.req?._isServer
store.writeAccess "*", "users.*", -> # captures, value, next) ->
#return unless @session and @session.userId # https://github.com/codeparty/racer/issues/37
[captures, next] = [arguments[0].split('.'), arguments[arguments.length-1]]
uid = captures.shift()
attrPath = captures.join('.') # new array shifted left, after shift() was run
# public access to users.*.party.invitation (TODO, lock down a bit more)
if (attrPath == 'party.invitation')
return next(true)
# Same session (user.id = this.session.userId)
if (uid is @session.userId) or @session.req?._isServer
return next(true)
next(false)
store.writeAccess "*", "users.*.balance", (id, newBalance, next) ->
#return unless @session and @session.userId # https://github.com/codeparty/racer/issues/37
oldBalance = @session.req._racerModel?.get("users.#{id}.balance") || 0
purchasingSomethingOnClient = newBalance < oldBalance
next(purchasingSomethingOnClient or @session.req?._isServer)
store.writeAccess "*", "users.*.flags.ads", -> # captures, value, next -> store.writeAccess "*", "users.*.flags.ads", -> # captures, value, next ->
return unless @session and @session.userId # https://github.com/codeparty/racer/issues/37 #return unless @session and @session.userId # https://github.com/codeparty/racer/issues/37
next = arguments[arguments.length - 1] next = arguments[arguments.length - 1]
isServer = not @req.socket next(@session.req?._isServer)
next(isServer)
###
Get user with API token ###
### REST
store.query.expose "users", "withIdAndToken", (id, api_token) -> Get user with API token
###
REST = (store) ->
store.query.expose "users", "withIdAndToken", (id, apiToken) ->
@where("id").equals(id) @where("id").equals(id)
.where('preferences.api_token').equals(api_token) .where('apiToken').equals(apiToken)
.limit(1) .limit(1)
store.queryAccess "users", "withIdAndToken", (id, token, next) -> store.queryAccess "users", "withIdAndToken", (id, apiToken, next) ->
return next(false) unless @session and @session.userId # https://github.com/codeparty/racer/issues/37 #return next(false) unless @session and @session.userId # https://github.com/codeparty/racer/issues/37
isServer = not @req.socket next(true) # only user has id & token
next(isServer)
###
Party permissions ###
### Party permissions
###
partySystem = (store) ->
store.query.expose "users", "party", (ids) -> store.query.expose "users", "party", (ids) ->
@where("id").within(ids) @where("id").within(ids)
.only('stats', 'preferences.gender', 'preferences.armorSet', 'items', 'auth.local.username', 'auth.facebook.displayName') .only('stats',
'items',
'party',
'preferences.gender',
'preferences.armorSet',
'auth.local.username',
'auth.facebook.displayName')
store.queryAccess "users", "party", (ids, next) -> store.queryAccess "users", "party", (ids, next) ->
next(true) # no harm in public user stats next(true) # no harm in public user stats
store.query.expose "parties", "withId", (id) ->
@where("id").equals(id)
store.queryAccess "parties", "withId", (id, next) ->
next(true)
store.readPathAccess "parties.*", ->
next = arguments[arguments.length-1]
next(true)
store.writeAccess "*", "parties.*", ->
next = arguments[arguments.length-1]
next(true)

View file

@ -14,7 +14,7 @@
<pre class=prettyprint>{_user.id}</pre> <pre class=prettyprint>{_user.id}</pre>
<h6>API Token</h6> <h6>API Token</h6>
<pre class=prettyprint>{_user.preferences.api_token}</pre> <pre class=prettyprint>{_user.apiToken}</pre>
<hr/> <hr/>
<h4>Gender</h4> <h4>Gender</h4>
@ -40,6 +40,9 @@
</label> </label>
{/} {/}
<hr/>
<a class='btn btn-danger' data-target="#reset-modal" data-toggle="modal">Reset</a>
<@footer> <@footer>
<button class="btn" data-dismiss="modal" aria-hidden="true">Close</button> <button class="btn" data-dismiss="modal" aria-hidden="true">Close</button>
</@footer> </@footer>
@ -49,9 +52,9 @@
<p>This resets your entire account - your tasks will be deleted and your character will start over.</p> <p>This resets your entire account - your tasks will be deleted and your character will start over.</p>
<p>This is highly discouraged because you'll lose historical data, which is useful for graphing your progress over time. However, some people find it useful in the beginning after playing with the app for a while.</p> <p>This is highly discouraged because you'll lose historical data, which is useful for graphing your progress over time. However, some people find it useful in the beginning after playing with the app for a while.</p>
<@footer> <@footer>
<button class="btn" data-dismiss="modal" aria-hidden="true">Close</button> <button class="btn" data-dismiss="modal" aria-hidden="true">Close</button>
<button data-dismiss="modal" x-bind=click:reset class="btn btn-danger btn-large">Reset</button> <button data-dismiss="modal" x-bind=click:reset class="btn btn-danger btn-large">Reset</button>
</@footer> </@footer>
</app:myModal> </app:myModal>
<app:myModal modalId="why-ads-modal" header="Why Ads?"> <app:myModal modalId="why-ads-modal" header="Why Ads?">
@ -100,7 +103,7 @@
<app:userTokens/> <app:userTokens/>
<p>Highly discouraged because red tasks provide good incentive to improve (<a target="_blank" href="https://github.com/lefnire/habitrpg#all-my-tasks-are-red-im-dying-too-fast">read more</a>). However, this becomes necessary after long bouts of bad habits.</p> <p>Highly discouraged because red tasks provide good incentive to improve (<a target="_blank" href="https://github.com/lefnire/habitrpg#all-my-tasks-are-red-im-dying-too-fast">read more</a>). However, this becomes necessary after long bouts of bad habits.</p>
<@footer> <@footer>
{#if lessThan(_user.balance,1)} {#if lt(_user.balance,1)}
<a data-dismiss="modal" x-bind="click:showStripe" class="btn btn-danger btn-large">Buy More Tokens</a><span class='token-cost'>Not enough tokens</span> <a data-dismiss="modal" x-bind="click:showStripe" class="btn btn-danger btn-large">Buy More Tokens</a><span class='token-cost'>Not enough tokens</span>
{else} {else}
<a data-dismiss="modal" x-bind=click:buyReroll class="btn btn-danger btn-large">Re-Roll</a><span class='token-cost'>4 Tokens</span> <a data-dismiss="modal" x-bind=click:buyReroll class="btn btn-danger btn-large">Re-Roll</a><span class='token-cost'>4 Tokens</span>
@ -108,14 +111,46 @@
</@footer> </@footer>
</app:myModal> </app:myModal>
<app:myModal modalId="add-party-modal" header="Add Party Member"> <app:myModal modalId="party-modal">
<form x-bind="submit: addParty">
{#if _view.addPartyError} {#if _user.party.current}
<div class='alert alert-danger'>{_view.addPartyError}</div> <h2>{_party.name}</h2>
<table class="table table-striped">
{#each _partyMembers as :member}
<tr><td>{username(:member.auth)}</td><td>({:member.id})</td></tr>
{/}
</table>
<form class=form-inline x-bind="submit: partyInvite">
{#if _view.partyError}
<div class='alert alert-danger'>{_view.partyError}</div>
{/} {/}
<input type="text" class="input-medium search-query" value="{_newPartyMember}"> <div class='control-group'>
<input type="submit" class="btn" value="Add" /> <input type="text" class="input-medium" placeholder="User Id" value="{_newPartyMember}">
</form> <input type="submit" class="btn" value="Invite" />
</div>
</form>
<a class='btn btn-danger' x-bind="click: partyLeave">Leave</a>
{else if _user.party.invitation}
<!-- TODO show by whom -->
<h2>You're Invited To {_party.name}</h2>
<a class='btn btn-success' x-bind="click: partyAccept">Accept</a>
<a class='btn btn-danger' x-bind="click: partyReject">Reject</a>
{else}
<h2>Create A Party</h2>
<!-- Not in a party , no invites - create a new one -->
<form class=form-inline x-bind="submit: partyCreate">
{#if _view.partyError}
<div class='alert alert-danger'>{_view.partyError}</div>
{/}
<div class=control-group>
<input type="text" class="input-medium" placeholder="Party Name" value="{_newParty}">
<input type="submit" class="btn" value="Create" />
</div>
<form>
{/}
</app:myModal> </app:myModal>
<alerts:> <alerts:>
@ -125,7 +160,7 @@
</ul> </ul>
{/} {/}
{#if equal(_user.notifications.kickstarter,'show')} {#if equal(_user.flags.kickstarter,'show')}
<div class='alert alert-success'> <div class='alert alert-success'>
<a x-bind="click:closeKickstarterNofitication" class='pull-right'>Dismiss</a> <a x-bind="click:closeKickstarterNofitication" class='pull-right'>Dismiss</a>
Help Habit by backing the <strong><a href="http://kck.st/XoA3Yg">Kickstarter</a></strong>! Funds iPhone & Android apps, <a href="https://github.com/lefnire/habitrpg/issues?labels=critical&page=1&state=open">bug fixes</a>, and the <a href="https://github.com/lefnire/habitrpg/issues/58">Groups feature</a>. Help Habit by backing the <strong><a href="http://kck.st/XoA3Yg">Kickstarter</a></strong>! Funds iPhone & Android apps, <a href="https://github.com/lefnire/habitrpg/issues?labels=critical&page=1&state=open">bug fixes</a>, and the <a href="https://github.com/lefnire/habitrpg/issues/58">Groups feature</a>.
@ -142,13 +177,20 @@
<a href="#" class="btn btn-small btn-info" data-target="#login-modal" data-toggle="modal">Login / Register</a> <a href="#" class="btn btn-small btn-info" data-target="#login-modal" data-toggle="modal">Login / Register</a>
{else} {else}
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-small">{username(_user.auth)}</button> <button class="btn btn-small">
{#if _user.party.invitation}<span class="badge badge-success">1</span>{/}
{username(_user.auth)}
</button>
<button class="btn btn-small dropdown-toggle" data-toggle="dropdown"> <button class="btn btn-small dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span> <span class="caret"></span>
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a href="#" data-target="#settings-modal" data-toggle="modal">Settings</a></li> <li><a href="#" data-target="#settings-modal" data-toggle="modal">Settings</a></li>
<li><a href="#" data-target="#reset-modal" data-toggle="modal">Reset</a></li> {#if _user.flags.partyEnabled}
<li><a href="#" data-target="#party-modal" data-toggle="modal">
Party{#if _user.party.invitation}<span class="badge badge-success">1</span>{/}
</a></li>
{/}
<li><a href='/logout'>Logout</a></li> <li><a href='/logout'>Logout</a></li>
</ul> </ul>
</div> </div>
@ -158,7 +200,7 @@
<div class='container-fluid'> <div class='container-fluid'>
<div class='row-fluid'> <div class='row-fluid'>
<div id=character class='{#if _party}span9{else}span5{/}'> <div id=character class='{#if gt(_partyMembers.length,1)}span9{else}span5{/}'>
<table> <table>
<tr> <tr>
@ -172,26 +214,26 @@
</td> </td>
<!-- Progress Bars --> <!-- Progress Bars -->
<td id="bars" style="width:{#if _party}70%{else}90%{/};"> <td id="bars" style="width:{#if gt(_partyMembers.length,1)}80%{else}90%{/};">
<div class="progress progress-danger" rel=tooltip data-placement=bottom title="Health"> <div class="progress progress-danger" rel=tooltip data-placement=bottom title="Health">
<div class="bar" style="width: {percent(_user.stats.hp, 50)}%;"></div> <div class="bar" style="width: {percent(_user.stats.hp, 50)}%;"></div>
<span class="progress-text"><i class=icon-heart></i> {round(_user.stats.hp)} / 50</span> <span class="progress-text"><i class=icon-heart></i> {round(_user.stats.hp)} / 50</span>
</div> </div>
<div class="progress progress-warning" rel=tooltip data-placement=bottom title="Experience"> <div class="progress progress-warning" rel=tooltip data-placement=bottom title="Experience">
<div class="bar" style="width: {percent(_user.stats.exp,_user._tnl)}%;"></div> <div class="bar" style="width: {percent(_user.stats.exp,_tnl)}%;"></div>
<span class="progress-text"> <span class="progress-text">
{#if _user.history.exp} {#if _user.history.exp}
<a x-bind=click:toggleChart data-toggle-id="exp-chart" data-history-path="_user.history.exp" rel=tooltip title="Progress"><i class=icon-signal></i></a>&nbsp; <a x-bind=click:toggleChart data-toggle-id="exp-chart" data-history-path="_user.history.exp" rel=tooltip title="Progress"><i class=icon-signal></i></a>&nbsp;
{/} {/}
<i class=icon-star></i> {round(_user.stats.exp)} / {_user._tnl} <i class=icon-star></i> {round(_user.stats.exp)} / {_tnl}
</span> </span>
</div> </div>
</td> </td>
<!-- Party --> <!-- Party -->
{#if _user.flags.partyEnabled} {#each _partyMembers as :member}
{#each _party as :member} {#unless equal(:member.id, _userId)}
<td class="avatar party-avatar" rel="tooltip" title="{username(:member.auth)}" data-placement="bottom" > <td class="avatar party-avatar" rel="tooltip" title="{username(:member.auth)}" data-placement="bottom" >
<div class='avatar-sprites'> <div class='avatar-sprites'>
<img class='weapon weapon-{:member.items.weapon}' src="/img/BrowserQuest/habitrpg_mods/weapon{:member.items.weapon}.png" /> <img class='weapon weapon-{:member.items.weapon}' src="/img/BrowserQuest/habitrpg_mods/weapon{:member.items.weapon}.png" />
@ -199,8 +241,7 @@
</div> </div>
<div id="lvl"><span class="badge badge-info">Lvl {:member.stats.lvl}</span></div> <div id="lvl"><span class="badge badge-info">Lvl {:member.stats.lvl}</span></div>
</td> </td>
{/} {/}
<td><a class="btn" id="add-party-button" data-target="#add-party-modal" data-toggle="modal"><i class="icon-user"></i></a></td>
{/} {/}
</tr> </tr>
@ -281,7 +322,7 @@
<!--Title --> <!--Title -->
<div class="row-fluid"> <div class="row-fluid">
<div class="span6"><h2>Rewards</h2></div> <div class="span6"><h2>Rewards</h2></div>
<div class="span6" id="money">{gold(_user.stats.money)} <img src='/img/coin_single_gold.png'/> {silver(_user.stats.money)} <img src='/img/coin_single_silver.png'/></div> <div class="span6" id="money">{gold(_user.stats.gp)} <img src='/img/coin_single_gold.png'/> {silver(_user.stats.gp)} <img src='/img/coin_single_silver.png'/></div>
</div> </div>
<!-- Content --> <!-- Content -->
@ -289,7 +330,7 @@
{#each _rewardList as :task}<app:task />{/} {#each _rewardList as :task}<app:task />{/}
</ul> </ul>
{#if _user.items.itemsEnabled} {#if _user.flags.itemsEnabled}
<ul class='items'> <ul class='items'>
{#with _view.items.armor as :item}<app:item />{/} {#with _view.items.armor as :item}<app:item />{/}
{#with _view.items.weapon as :item}<app:item />{/} {#with _view.items.weapon as :item}<app:item />{/}
@ -308,9 +349,10 @@
<!-- Footer --> <!-- Footer -->
<footer class=footer> <footer class=footer>
<div class=container> <div class=container>
<!--<div class='pull-right'> <div class='pull-right'>
<button class='btn' x-bind="click:emulateNextDay">Emulate Next Day</button> <button class='btn' x-bind="click:emulateNextDay">Emulate Next Day</button>
</div>--> <button class='btn' x-bind="click:cheat">Add GP & Exp</button>
</div>
<div> <div>
<ul> <ul>
<li>Copyright &copy; 2012 OCDevel LLC</li> <li>Copyright &copy; 2012 OCDevel LLC</li>