diff --git a/migrations/20130204_count_habits.js b/migrations/20130204_count_habits.js
new file mode 100644
index 0000000000..1459a97133
--- /dev/null
+++ b/migrations/20130204_count_habits.js
@@ -0,0 +1,13 @@
+// %mongo server:27017/dbname underscore.js my_commands.js
+// %mongo server:27017/dbname underscore.js --shell
+var habits = 0,
+ dailies = 0,
+ todos = 0,
+ registered = { $or: [ { 'auth.local': { $exists: true } }, { 'auth.facebook': { $exists: true} } ]};
+
+db.user.find(registered).forEach(function(u){
+ //TODO this isn't working??
+ habits += _.where(u.tasks, {type:'habit'}).length;
+ dailies += _.where(u.tasks, {type:'daily'}).length;
+ todos += _.where(u.tasks, {type:'todo'}).length;
+})
diff --git a/package.json b/package.json
index ba7d6a4490..e4eb8e9832 100644
--- a/package.json
+++ b/package.json
@@ -5,8 +5,8 @@
"main": "./server.js",
"dependencies": {
"derby": "git://github.com/codeparty/derby#master",
- "racer": "git://github.com/lefnire/racer#master",
- "racer-db-mongo": "git://github.com/codeparty/racer-db-mongo#master",
+ "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",
"derby-auth": "git://github.com/lefnire/derby-auth#master",
"connect-mongo": "0.2.0",
diff --git a/src/app/browser.coffee b/src/app/browser.coffee
index e52e367bd6..6e302ddeef 100644
--- a/src/app/browser.coffee
+++ b/src/app/browser.coffee
@@ -97,17 +97,33 @@ module.exports.setupGrowlNotifications = (model) ->
user.on 'set', 'items.itemsEnabled', (captures, args) ->
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."
$('ul.items').popover
- title: content.items.unlockedMessage.title
+ title: "Item Store Unlocked"
placement: 'left'
trigger: 'manual'
html: true
content: "
-
- #{content.items.unlockedMessage.content}
[Close]
-
"
+
+ #{message} [Close]
+ "
$('ul.items').popover 'show'
+ user.on 'set', 'flags.partyEnabled', (captures, args) ->
+ return unless captures == true
+ message = "Congratulations, you have unlocked the Party System! You can now group with your friends by adding their User Ids."
+ $('#add-party-button').popover
+ title: "Pary System Unlocked"
+ placement: 'bottom'
+ trigger: 'manual'
+ html: true
+ content: ""
+ $('#add-party-button').popover 'show'
+
+
# Setup listeners which trigger notifications
user.on 'set', 'stats.hp', (captures, args) ->
num = captures - args
diff --git a/src/app/content.coffee b/src/app/content.coffee
index 6750047ee1..00791b068e 100644
--- a/src/app/content.coffee
+++ b/src/app/content.coffee
@@ -55,9 +55,6 @@ module.exports =
]
items:
- unlockedMessage:
- title: "Item Store Unlocked"
- content: "Congratulations, you have unlocked the Item Store! You can now buy weapons, armor, potions, etc. Read each item's comment for more information."
#TODO: figure out how to calculate index & type without having to store it in the JSON
weapon: [
{type: 'weapon', index: 0, text: "Sword 1", icon: "item-sword1", notes:'Training weapon.', value:0}
diff --git a/src/app/helpers.coffee b/src/app/helpers.coffee
index da74ee35dc..1429b50168 100644
--- a/src/app/helpers.coffee
+++ b/src/app/helpers.coffee
@@ -54,4 +54,25 @@ module.exports.viewHelpers = (view) ->
a < b
view.fn "tokens", (money) ->
- return money/0.25
\ No newline at end of file
+ return money/0.25
+
+ view.fn 'currentArmor', (gender, armor, armorSet) ->
+ if gender == 'f'
+ str = "armor#{armor}_f"
+ if parseInt(armor) > 1
+ armorSet = if armorSet then armorSet else 'v1'
+ str += '_' + armorSet
+ return "#{str}.png"
+ else
+ return "armor#{armor}_m.png"
+
+ view.fn "username", (auth) ->
+ if auth?.facebook?.displayName?
+ auth.facebook.displayName
+ else if auth?.facebook?
+ fb = auth.facebook
+ if fb._raw then "#{fb.name.givenName} #{fb.name.familyName}" else fb.name
+ else if auth?.local?
+ auth.local.username
+ else
+ 'Anonymous'
\ No newline at end of file
diff --git a/src/app/index.coffee b/src/app/index.coffee
index 9eeb2a7dfe..40d7b4d29c 100644
--- a/src/app/index.coffee
+++ b/src/app/index.coffee
@@ -24,15 +24,9 @@ setupModelFns = (model) ->
# also update in scoring.coffee. TODO create a function accessible in both locations
(lvl*100)/5
- model.fn '_user._armor', '_user.items.armor', '_user.preferences.armorSet', '_user.preferences.gender', (armor, armorSet, gender) ->
- if gender == 'f'
- str = "armor#{armor}_f"
- if parseInt(armor) > 1
- armorSet = if armorSet then armorSet else 'v1'
- str += '_' + armorSet
- return "#{str}.png"
- else
- "armor#{armor}_m.png"
+# model.fn '_party', '_user.party', (ids) ->
+# model.fetch model.query('users').party(ids), (err, party) ->
+# model.set '_view.party', party
# ========== ROUTES ==========
@@ -51,18 +45,11 @@ get '/', (page, model, next) ->
model.subscribe q, (err, user) ->
#user = result.at(0)
model.ref '_user', user
- userObj = user.get()
-
- return page.redirect '/500.html' unless userObj? #this should never happen, but it is. Looking into it
-
- # support legacy Everyauth schema (now using derby-auth, Passport)
- if username = userObj.auth?.local?.username
- _view.loginName = username
- else if fb = userObj.auth?.facebook
- _view.loginName = if fb._raw then "#{fb.name.givenName} #{fb.name.familyName}" else fb.name
+ batch = new schema.BatchUpdate(model)
+ batch.startTransaction()
# Setup Item Store
- items = userObj.items
+ items = user.get('items')
_view.items =
armor: content.items.armor[parseInt(items?.armor || 0) + 1]
weapon: content.items.weapon[parseInt(items?.weapon || 0) + 1]
@@ -71,10 +58,17 @@ get '/', (page, model, next) ->
model.set '_view', _view
- schema.updateUser(user, userObj)
+ schema.updateUser(batch)
+ batch.commit()
+
setupListReferences(model)
setupModelFns(model)
+ # Subscribe to friends
+ if !_.isEmpty(user.get('party'))
+ model.subscribe model.query('users').party(user.get('party')), (err, party) ->
+ model.ref '_party', party
+
page.render()
# ========== CONTROLLER FUNCTIONS ==========
@@ -86,13 +80,13 @@ resetDom = (model) ->
ready (model) ->
user = model.at('_user')
+ scoring.setModel(model)
#set cron immediately
lastCron = user.get('lastCron')
- 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
- scoring.setModel(model)
scoring.cron(resetDom)
# Load all the jQuery, Growl, Tour, etc
@@ -256,40 +250,38 @@ ready (model) ->
task = model.at $(el).parents('li')[0]
scoring.score(task.get('id'), direction)
- revive = (userObj, animateHp = false) ->
+ revive = (batch) ->
# Reset stats
- userObj.stats.hp = 50 unless animateHp # if we're animating hp-reset, we'll set to 50 ourselves later in our functions
- userObj.stats.lvl = 1; userObj.stats.money = 0; userObj.stats.exp = 0
+ batch.set 'stats.hp', 50
+ batch.set 'stats.lvl', 1
+ batch.set 'stats.money', 0
+ batch.set 'stats.exp', 0
# Reset items
- userObj.items.armor = 0; userObj.items.weapon = 0
+ batch.set 'items.armor', 0
+ batch.set 'items.weapon', 0
# Reset item store
model.set '_view.items.armor', content.items.armor[1]
model.set '_view.items.weapon', content.items.weapon[1]
exports.revive = (e, el) ->
- userObj = user.get()
- revive(userObj, true)
-
- user.set 'stats', userObj.stats
- user.set 'items', userObj.items
- # Re-render (since we replaced objects en-masse, see https://github.com/lefnire/habitrpg/issues/80)
+ batch = new schema.BatchUpdate(model)
+ batch.startTransaction()
+ revive(batch)
+ batch.commit()
resetDom(model)
- setTimeout (-> user.set 'stats.hp', 50), 0 # animate hp loss
exports.reset = (e, el) ->
- userObj = user.get()
+ batch = new schema.BatchUpdate(model)
+ batch.startTransaction()
taskTypes = ['habit', 'daily', 'todo', 'reward']
- userObj.tasks = {}
- _.each taskTypes, (type) -> userObj["#{type}Ids"] = []
- userObj.balance = 2 if userObj.balance < 2 #only if they haven't manually bought tokens
- revive(userObj, true)
-
- # Set new user
- model.set "users.#{userObj.id}", userObj
+ batch.set 'tasks', {}
+ _.each taskTypes, (type) -> batch.set "#{type}Ids", []
+ batch.set 'balance', 2 if user.get('balance') < 2 #only if they haven't manually bought tokens
+ revive(batch, true)
+ batch.commit()
resetDom(model)
- setTimeout (-> user.set 'stats.hp', 50), 0 # animate hp loss
exports.closeKickstarterNofitication = (e, el) ->
user.set('notifications.kickstarter', 'hide')
@@ -297,4 +289,28 @@ ready (model) ->
exports.setMale = -> user.set('preferences.gender', 'm')
exports.setFemale = -> user.set('preferences.gender', 'f')
exports.setArmorsetV1 = -> user.set('preferences.armorSet', 'v1')
- exports.setArmorsetV2 = -> user.set('preferences.armorSet', 'v2')
\ No newline at end of file
+ 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()
+
diff --git a/src/app/schema.coffee b/src/app/schema.coffee
index 8ef6d927c3..d8f601cdb1 100644
--- a/src/app/schema.coffee
+++ b/src/app/schema.coffee
@@ -1,22 +1,27 @@
content = require './content'
moment = require 'moment'
_ = require 'underscore'
+lodash = require 'lodash'
derby = require 'derby'
+userSchema =
+ lastCron: 'new' #this will be replaced with `+new Date` on first run
+ 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: {}
+ habitIds: []
+ dailyIds: []
+ todoIds: []
+ rewardIds: []
+
module.exports.newUserObject = ->
# deep clone, else further new users get duplicate objects
- newUser = require('lodash').cloneDeep
- lastCron: 'new' #this will be replaced with `+new Date` on first run
- 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' }
- tasks: {}
- habitIds: []
- dailyIds: []
- todoIds: []
- rewardIds: []
+ newUser = require('lodash').cloneDeep userSchema
for task in content.defaultTasks
guid = task.id = require('racer').uuid()
newUser.tasks[guid] = task
@@ -27,30 +32,90 @@ module.exports.newUserObject = ->
when 'reward' then newUser.rewardIds.push guid
return newUser
-module.exports.updateUser = (user, userObj) ->
- user.set 'notifications.kickstarter', 'show' unless userObj.notifications?.kickstarter?
+module.exports.updateUser = (batch) ->
+ user = batch.user
+
+ batch.set('notifications.kickstarter', 'show') unless user.get('notifications.kickstarter')
+ 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..)
- prefs = _.clone(userObj.preferences)
- prefs = _.defaults prefs, { gender: 'm', armorSet: 'v1', api_token: derby.uuid() }
- user.set 'preferences', prefs unless _.isEqual(prefs, userObj.preferences)
+ currentPrefs = _.clone user.get('preferences')
+ mergedPrefs = _.defaults currentPrefs, { gender: 'm', armorSet: 'v1', api_token: derby.uuid() }
+ batch.set('preferences', mergedPrefs)
## Task List Cleanup
# 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 = user.get('tasks')
_.each ['habit','daily','todo','reward'], (type) ->
path = "#{type}Ids"
# 1. remove duplicates
# 2. restore missing zombie tasks back into list
- where = {type:type}
- taskIds = _.pluck( _.where(userObj.tasks, where), 'id')
- union = _.union userObj[path], taskIds
+ taskIds = _.pluck( _.where(tasks, {type:type}), 'id')
+ union = _.union user.get(path), taskIds
# 2. remove empty (grey) tasks
preened = _.filter(union, (val) -> _.contains(taskIds, val))
# There were indeed issues found, set the new list
- # TODO _.difference might still be empty for duplicates in one list?
- user.set(path, preened) if _.difference(preened, userObj[path]).length != 0
\ No newline at end of file
+ batch.set(path, preened) # if _.difference(preened, userObj[path]).length != 0
+
+module.exports.BatchUpdate = BatchUpdate = (model) ->
+ user = model.at("_user")
+ transactionInProgress = false
+ obj = {}
+ updates = {}
+
+ {
+ user: user
+
+ obj: -> obj
+
+ startTransaction: ->
+ # start a batch transaction - nothing between now and @commit() will be set immediately
+ transactionInProgress = true
+ model._dontPersist = true
+
+ # Really strange, user.get() seems to only return attributes which have previously been accessed. So in
+ # many cases, userObj.tasks.{taskId}.value is undefined - so we manually .get() each attribute here.
+ # Additionally, for some reason after getting the user object, changing properies manually (userObj.stats.hp = 50)
+ # seems to actually run user.set('stats.hp',50) which we don't want to do - so we deepClone here
+ #_.each Object.keys(userSchema), (key) -> obj[key] = lodash.cloneDeep user.get(key)
+ obj = model.get('users.'+user.get('id'), true)
+
+ ###
+ Handles updating the user model. If this is an en-mass operation (eg, server cron), changes are queued
+ but not actually set to the model. It also modifies userObj in case you need to access properties manually later.
+ If transaction not in progress, it just runs standard model.set()
+ ###
+ set: (path, val) ->
+ updates[path] = val if transactionInProgress
+ user.set(path, val)
+
+ ###
+ Hack to get around dom bindings being lost if parent objects are replaced whole-sale
+ eg, user.set('stats', {hp:50, exp:10...}) will break dom bindings, but user.set('stats.hp',50) is ok
+ ###
+ setStats: (stats) ->
+ stats ?= obj.stats
+ that = @
+ _.each Object.keys(stats), (key) -> that.set "stats.#{key}", stats[key]
+
+# queue: (path, val) ->
+# # Special function for setting object properties by string dot-notation. See http://stackoverflow.com/a/6394168/362790
+# arr = path.split('.')
+# arr.reduce (curr, next, index) ->
+# if (arr.length - 1) == index
+# curr[next] = val
+# curr[next]
+# , obj
+
+ commit: ->
+ model._dontPersist = false
+ # some hackery in our own branched racer-db-mongo, see findAndModify of lefnire/racer-db-mongo#habitrpg index.js
+ user.set "update__", updates
+ transactionInProgress = false
+ updates = {}
+ }
diff --git a/src/app/scoring.coffee b/src/app/scoring.coffee
index c6f5dae618..6a3de2725e 100644
--- a/src/app/scoring.coffee
+++ b/src/app/scoring.coffee
@@ -4,7 +4,8 @@ _ = require 'underscore'
content = require './content'
helpers = require './helpers'
browser = require './browser'
-MODIFIER = .03 # each new level, armor, weapon add 3% modifier (this number may change)
+schema = require './schema'
+MODIFIER = .03 # each new level, armor, weapon add 3% modifier (this number may change)
user = undefined
model = undefined
@@ -52,73 +53,67 @@ taskDeltaFormula = (currentValue, direction) ->
delta = if (currentValue < 0) then (( -0.1 * currentValue + 1 ) * sign) else (( Math.pow(0.9,currentValue) ) * sign)
return delta
-
-###
- Handles updating the user model. If this is an en-mass operation (eg, server cron), pass the user object as {update}.
- otherwise, null means commit the changes immediately
-###
-userSet = (path, value, update) ->
- if update
- # Special function for setting object properties by string dot-notation. See http://stackoverflow.com/a/6394168/362790
- arr = path.split('.')
- arr.reduce (curr, next, index) ->
- if (arr.length - 1) == index
- curr[next] = value
- curr[next]
- , update
- else
- user.set path, value
-
###
Updates user stats with new stats. Handles death, leveling up, etc
{stats} new stats
{update} if aggregated changes, pass in userObj as update. otherwise commits will be made immediately
###
-updateStats = (newStats, update) ->
- userObj = update || user.get()
+updateStats = (newStats, batch) ->
+ obj = batch.obj()
# if user is dead, dont do anything
- return if userObj.stats.lvl == 0
-
+ return if obj.stats.lvl == 0
+
if newStats.hp?
# Game Over
if newStats.hp <= 0
- userSet 'stats.lvl', 0, update # signifies dead
- userSet 'stats.hp', 0, update
+ obj.stats.lvl = 0 # signifies dead
+ obj.stats.hp = 0
return
else
- userSet 'stats.hp', newStats.hp, update
+ obj.stats.hp = newStats.hp
if newStats.exp?
# level up & carry-over exp
tnl = user.get '_tnl'
if newStats.exp >= tnl
newStats.exp -= tnl
- userSet 'stats.lvl', userObj.stats.lvl + 1, update
- userSet 'stats.hp', 50, update
- if !userObj.items?.itemsEnabled and newStats.exp >=15
- user.set 'items.itemsEnabled', true #bit of trouble using userSet here
- userSet 'stats.exp', newStats.exp, update
+ obj.stats.lvl++
+ obj.stats.hp = 50
+ if !obj.items.itemsEnabled and obj.stats.lvl >= 2
+ # Set to object, then also send to browser right away to get model.on() subscription notification
+ batch.set 'items.itemsEnabled', true
+ obj.items.itemsEnabled = true
+# if !obj.flags.partyEnabled and obj.stats.lvl >= 3
+# batch.set 'flags.partyEnabled', true
+# obj.flags.partyEnabled = true
+ obj.stats.exp = newStats.exp
if newStats.money?
+ #FIXME what was I doing here? I can't remember, money isn't defined
money = 0.0 if (!money? or money<0)
- userSet 'stats.money', newStats.money, update
+ obj.stats.money = newStats.money
# {taskId} task you want to score
# {direction} 'up' or 'down'
# {times} # times to call score on this task (1 unless cron, usually)
# {update} if we're running updates en-mass (eg, cron on server) pass in userObj
-score = (taskId, direction, times, update) ->
- times ?= 1
+score = (taskId, direction, times, batch, cron) ->
+ commit = false
+ unless batch?
+ commit = true
+ batch = new schema.BatchUpdate(model)
+ batch.startTransaction()
+ obj = batch.obj()
- userObj = update or user.get()
- {money, hp, exp, lvl} = userObj.stats
+ {money, hp, exp, lvl} = obj.stats
taskPath = "tasks.#{taskId}"
- taskObj = userObj.tasks[taskId]
+ taskObj = obj.tasks[taskId]
{type, value} = taskObj
delta = 0
+ times ?= 1
calculateDelta = (adjustvalue=true) ->
# If multiple days have passed, multiply times days missed
_.times times, (n) ->
@@ -144,20 +139,23 @@ score = (taskId, direction, times, update) ->
adjustvalue = if (taskObj.up==false or taskObj.down==false) then false else true
calculateDelta(adjustvalue)
# Add habit value to habit-history (if different)
- historyEntry = { date: +new Date(), value: value } if taskObj.value != value
if (delta > 0) then addPoints() else subtractPoints()
- model.push "_user.#{taskPath}.history", historyEntry
+ taskObj.history ?= []
+ if taskObj.value != value
+ historyEntry = { date: +new Date, value: value }
+ taskObj.history.push historyEntry
+ batch.set "#{taskPath}.history", taskObj.history
when 'daily'
calculateDelta()
- if update? # cron
+ if cron? # cron
subtractPoints()
else
addPoints() # obviously for delta>0, but also a trick to undo accidental checkboxes
when 'todo'
calculateDelta()
- unless update? # don't touch stats on cron
+ unless cron? # don't touch stats on cron
addPoints() # obviously for delta>0, but also a trick to undo accidental checkboxes
when 'reward'
@@ -171,8 +169,17 @@ score = (taskId, direction, times, update) ->
hp += money # hp - money difference
money = 0
- userSet "#{taskPath}.value", value, update
- updateStats {hp: hp, exp: exp, money: money}, update
+ taskObj.value = value
+ batch.set "#{taskPath}.value", taskObj.value
+ origStats = _.clone obj.stats
+ updateStats {hp: hp, exp: exp, money: money}, batch
+ if commit
+ # newStats / origStats is a glorious hack to trick Derby into seeing the change in model.on(*)
+ newStats = _.clone batch.obj().stats
+ _.each Object.keys(origStats), (key) -> obj.stats[key] = origStats[key]
+ batch.setStats(newStats)
+# batch.setStats()
+ batch.commit()
return delta
###
@@ -183,56 +190,61 @@ cron = (resetDom_cb) ->
today = +new Date
daysPassed = helpers.daysBetween(today, user.get('lastCron'))
if daysPassed > 0
- user.set 'lastCron', today
- userObj = user.get()
- hpBefore = userObj.stats.hp #we'll use this later so we can animate hp loss
+ batch = new schema.BatchUpdate(model)
+ batch.startTransaction()
+ batch.set 'lastCron', today
+ obj = batch.obj()
+ hpBefore = obj.stats.hp #we'll use this later so we can animate hp loss
# Tally each task
todoTally = 0
- _.each userObj.tasks, (taskObj) ->
- #FIXME remove broken tasks
- if taskObj.id? # a task had a null id during cron, this should not be happening
- {id, type, completed, repeat} = taskObj
- if type in ['todo', 'daily']
- # Deduct experience for missed Daily tasks,
- # but not for Todos (just increase todo's value)
- unless completed
- # for todos & typical dailies, these are equivalent
- daysFailed = daysPassed
- # however, for dailys which have repeat dates, need
- # to calculate how many they've missed according to their own schedule
- if type=='daily' && repeat
- daysFailed = 0
- _.times daysPassed, (n) ->
- thatDay = moment().subtract('days', n+1)
- if repeat[helpers.dayMapping[thatDay.day()]]==true
- daysFailed++
- score id, 'down', daysFailed, userObj
+ _.each obj.tasks, (taskObj) ->
+ unless taskObj.id?
+ console.error "a task had a null id during cron, this should not be happening"
+ return
- value = taskObj.value #get updated value
- if type == 'daily'
- taskObj.history ?= []
- taskObj.history.push { date: today, value: value }
- taskObj.completed = false
- else
- absVal = if (completed) then Math.abs(value) else value
- todoTally += absVal
- user.set 'tasks.' + taskObj.id, taskObj
+ {id, type, completed, repeat} = taskObj
+ if type in ['todo', 'daily']
+ # Deduct experience for missed Daily tasks,
+ # but not for Todos (just increase todo's value)
+ unless completed
+ # for todos & typical dailies, these are equivalent
+ daysFailed = daysPassed
+ # however, for dailys which have repeat dates, need
+ # to calculate how many they've missed according to their own schedule
+ if type=='daily' && repeat
+ daysFailed = 0
+ _.times daysPassed, (n) ->
+ thatDay = moment().subtract('days', n+1)
+ if repeat[helpers.dayMapping[thatDay.day()]]==true
+ daysFailed++
+ score id, 'down', daysFailed, batch, true
+
+ if type == 'daily'
+ taskObj.history ?= []
+ taskObj.history.push { date: +new Date, value: value }
+ batch.set "tasks.#{taskObj.id}.history", taskObj.history
+ batch.set "tasks.#{taskObj.id}.completed", false
+ else
+ value = obj.tasks[taskObj.id].value #get updated value
+ absVal = if (completed) then Math.abs(value) else value
+ todoTally += absVal
# Finished tallying
- userObj.history ?= {}; userObj.history.todos ?= []; userObj.history.exp ?= []
- userObj.history.todos.push { date: today, value: todoTally }
+ obj.history ?= {}; obj.history.todos ?= []; obj.history.exp ?= []
+ obj.history.todos.push { date: today, value: todoTally }
# tally experience
- expTally = userObj.stats.exp
+ expTally = obj.stats.exp
lvl = 0 #iterator
- while lvl < (userObj.stats.lvl-1)
+ while lvl < (obj.stats.lvl-1)
lvl++
expTally += (lvl*100)/5
- userObj.history.exp.push { date: today, value: expTally }
+ obj.history.exp.push { date: today, value: expTally }
# Set the new user specs, and animate HP loss
- [hpAfter, userObj.stats.hp] = [userObj.stats.hp, hpBefore]
- user.set 'stats', userObj.stats
- user.set 'history', userObj.history
+ [hpAfter, obj.stats.hp] = [obj.stats.hp, hpBefore]
+ batch.setStats()
+ batch.set('history', obj.history)
+ batch.commit()
resetDom_cb(model)
setTimeout (-> user.set 'stats.hp', hpAfter), 1000 # animate hp loss
diff --git a/src/server/index.coffee b/src/server/index.coffee
index 1ac2071144..e7a4e5de14 100644
--- a/src/server/index.coffee
+++ b/src/server/index.coffee
@@ -10,16 +10,16 @@ auth = require 'derby-auth'
priv = require './private'
## Run server cron ##
-#require('./cron').deleteStaleAccounts()
+require('./cron').deleteStaleAccounts()
## RACER CONFIGURATION ##
racer = require 'racer'
racer.io.set('transports', ['xhr-polling'])
racer.set('bundleTimeout', 40000)
-unless process.env.NODE_ENV == 'production'
- racer.use(racer.logPlugin)
- derby.use(derby.logPlugin)
+#unless process.env.NODE_ENV == 'production'
+# racer.use(racer.logPlugin)
+# derby.use(derby.logPlugin)
## SERVER CONFIGURATION ##
diff --git a/src/server/private.coffee b/src/server/private.coffee
index 6a02c4c5ea..2b7898bb6a 100644
--- a/src/server/private.coffee
+++ b/src/server/private.coffee
@@ -1,6 +1,69 @@
+_ = require 'underscore'
+
module.exports.middleware = (req, res, next) ->
+ model = req.getModel()
+ model.set '_stripePubKey', process.env.STRIPE_PUB_KEY
return next()
-module.exports.app= (appExports, model) ->
+module.exports.app = (appExports, model) ->
-module.exports.routes = (expressApp) ->
\ No newline at end of file
+ appExports.showStripe = (e, el) ->
+ token = (res) ->
+ console.log(res);
+ $.ajax({
+ type:"POST",
+ url:"/charge",
+ data:res
+ }).success ->
+ window.location.href = "/"
+ .error (err) ->
+ alert err.responseText
+
+ StripeCheckout.open
+ key: model.get('_stripePubKey')
+ address: false
+ amount: 500
+ name: "Checkout"
+ description: "Removes ads and grants 20 additional tokens."
+ panelLabel: "Checkout"
+ token: token
+
+ ###
+ Buy Reroll Button
+ ###
+ appExports.buyReroll = (e, el, next) ->
+ user = model.at('_user')
+ tasks = user.get('tasks')
+ user.set('balance', user.get('balance')-1)
+ _.each tasks, (task) -> task.value = 0 unless task.type == 'reward'
+ user.set('tasks', tasks)
+ window.DERBY.app.dom.clear()
+ window.DERBY.app.view.render(model)
+
+module.exports.routes = (expressApp) ->
+ ###
+ Setup Stripe response when posting payment
+ ###
+ expressApp.post '/charge', (req, res) ->
+ stripeCallback = (err, response) ->
+ if err
+ console.error(err, 'Stripe Error')
+ return res.send(500, err.response.error.message)
+ else
+ model = req.getModel()
+ userId = model.session.userId
+ model.fetch "users.#{userId}", (err, user) ->
+ model.ref '_user', "users.#{userId}"
+ model.set('_user.balance', model.get('_user.balance')+5)
+ model.set('_user.flags.ads','hide')
+ return res.send(200)
+
+ api_key = process.env.STRIPE_API_KEY # secret stripe API key
+ stripe = require("stripe")(api_key)
+ token = req.body.id
+ # console.dir {token:token, req:req}, 'stripe'
+ stripe.charges.create
+ amount: "500" # $5
+ currency: "usd"
+ card: token
+ , stripeCallback
\ No newline at end of file
diff --git a/src/server/serverRoutes.coffee b/src/server/serverRoutes.coffee
index 5b5235eba4..abd0ca8ee2 100644
--- a/src/server/serverRoutes.coffee
+++ b/src/server/serverRoutes.coffee
@@ -12,35 +12,38 @@ module.exports = (expressApp, root, derby) ->
expressApp.get '/terms', (req, res) ->
staticPages.render 'terms', res
- # ---------- REST API ------------
+ # ---------- Deprecated Paths ------------
- # Deprecated API (will remove soon)
- deprecatedMessage = 'This REST resource is no longer supported, use /users/:uid/tasks/:taskId/:direction instead.'
- expressApp.get '/:uid/up/:score?', (req, res) ->
- res.send(200, deprecatedMessage)
- expressApp.get '/:uid/down/:score?', (req, res) ->
- res.send(200, deprecatedMessage)
+ 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/down/:score?', (req, res) -> res.send(500, deprecatedMessage)
+ expressApp.post '/v1/users/:uid/tasks/:taskId/:direction', (req, res) -> res.send(500, deprecatedMessage)
- # New API
- # test with `curl -X POST -H "Content-Type:application/json" localhost:3000/users/{uid}/tasks/productivity/up`
+ # ---------- v1 API ------------
+
+ ###
+ v1 API. Requires user-id and api_token, 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
+ ###
+ # TODO /v1/..
expressApp.post '/users/:uid/tasks/:taskId/:direction', (req, res) ->
{uid, taskId, direction} = req.params
- {title, service, icon} = req.body
+ {api_token, title, service, icon} = req.body
console.log {params:req.params, body:req.body} if process.env.NODE_ENV == 'development'
# Send error responses for improper API call
+ return res.send(500, 'request body "api_token" required') unless api_token
return res.send(500, ':uid required') unless uid
return res.send(500, ':taskId required') unless taskId
return res.send(500, ":direction must be 'up' or 'down'") unless direction in ['up','down']
model = req.getModel()
- model.fetch "users.#{uid}", (err, user) ->
+ model.fetch model.query('users').withIdAndToken(uid, api_token), (err, result) ->
return res.send(500, err) if err
+ user = result.at(0)
userObj = user.get()
- # Server crashes without this, I think some users are entering non-guid userIds and/or trying to use the API without having an account
- unless userObj && !_.isEmpty(userObj.stats)
- console.log {taskId:taskId, direction:direction, user:userObj, error: 'non-user attempted to score'} if process.env.NODE_ENV == 'development'
- return res.send(500, "User #{uid} not found")
+ 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")
model.ref('_user', user)
diff --git a/src/server/store.coffee b/src/server/store.coffee
index a7dedfaafb..d071a2a37e 100644
--- a/src/server/store.coffee
+++ b/src/server/store.coffee
@@ -14,4 +14,27 @@ module.exports = (store) ->
return unless @session and @session.userId # https://github.com/codeparty/racer/issues/37
next = arguments[arguments.length - 1]
isServer = not @req.socket
- next(isServer)
\ No newline at end of file
+ next(isServer)
+
+ ###
+ Get user with API token
+ ###
+ store.query.expose "users", "withIdAndToken", (id, api_token) ->
+ @where("id").equals(id)
+ .where('preferences.api_token').equals(api_token)
+ .limit(1)
+
+ store.queryAccess "users", "withIdAndToken", (id, token, next) ->
+ return next(false) unless @session and @session.userId # https://github.com/codeparty/racer/issues/37
+ isServer = not @req.socket
+ next(isServer)
+
+ ###
+ Party permissions
+ ###
+ store.query.expose "users", "party", (ids) ->
+ @where("id").within(ids)
+ .only('stats', 'preferences.gender', 'preferences.armorSet', 'items', 'auth.local.username', 'auth.facebook.displayName')
+
+ store.queryAccess "users", "party", (ids, next) ->
+ next(true) # no harm in public user stats
diff --git a/styles/app/index.styl b/styles/app/index.styl
index 79a0db9fb2..5ffb9dfa96 100644
--- a/styles/app/index.styl
+++ b/styles/app/index.styl
@@ -117,7 +117,10 @@ li:hover .task-meta-controls .hover-show
float:none
margin:0px auto
- td#avatar
+ td
+ padding-right: 2em
+
+ td.avatar
vertical-align: top
text-align: center
@@ -136,10 +139,9 @@ li:hover .task-meta-controls .hover-show
.weapon-6
left:-20px
td#bars
- padding: 10px 0 0 10px
+ padding-top: 10px
#bars
- width: 100%
.progress
position: relative
height: 25px
diff --git a/views/app/index.html b/views/app/index.html
index f9bd1f0309..66c9b504de 100644
--- a/views/app/index.html
+++ b/views/app/index.html
@@ -8,10 +8,15 @@
{#if _loggedIn}
- User ID
- Copy this ID for use in third party applications.
- {_user.id}
+ API
+ Copy these for use in third party applications.
+ User ID
+ {_user.id}
+ API Token
+ {_user.preferences.api_token}
+
+
Gender
@@ -56,6 +61,7 @@
Ok
@footer>
+
{else}
@@ -102,6 +108,16 @@
@footer>
+
+
+
+
{#if _flash.error}
@@ -126,7 +142,7 @@
Login / Register
{else}
-
{_view.loginName}
+
{username(_user.auth)}
@@ -142,17 +158,21 @@
-
+
-
+
+
+
Lvl {_user.stats.lvl}
-
+
+
+
{round(_user.stats.hp)} / 50
@@ -168,7 +188,23 @@
+
+
+ {#if _user.flags.partyEnabled}
+ {#each _party as :member}
+
+
+
+
+
+ Lvl {:member.stats.lvl}
+
+ {/}
+
+ {/}
+
+
@@ -272,6 +308,9 @@