Merge branch 'parties'
Conflicts: src/app/schema.coffee
This commit is contained in:
@@ -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;
|
||||
})
|
||||
+2
-2
@@ -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",
|
||||
|
||||
+20
-4
@@ -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: "<div class='item-store-popover'>
|
||||
<img src='/img/BrowserQuest/chest.png' />
|
||||
#{content.items.unlockedMessage.content} <a href='#' onClick=\"$('ul.items').popover('hide');return false;\">[Close]</a>
|
||||
</div>"
|
||||
<img src='/img/BrowserQuest/chest.png' />
|
||||
#{message} <a href='#' onClick=\"$('ul.items').popover('hide');return false;\">[Close]</a>
|
||||
</div>"
|
||||
$('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: "<div class='party-system-popover'>
|
||||
<img src='/img/BrowserQuest/favicon.png' />
|
||||
#{message} <a href='#' onClick=\"$('#add-party-button').popover('hide');return false;\">[Close]</a>
|
||||
</div>"
|
||||
$('#add-party-button').popover 'show'
|
||||
|
||||
|
||||
# Setup listeners which trigger notifications
|
||||
user.on 'set', 'stats.hp', (captures, args) ->
|
||||
num = captures - args
|
||||
|
||||
@@ -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}
|
||||
|
||||
+22
-1
@@ -54,4 +54,25 @@ module.exports.viewHelpers = (view) ->
|
||||
a < b
|
||||
|
||||
view.fn "tokens", (money) ->
|
||||
return money/0.25
|
||||
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'
|
||||
+59
-43
@@ -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')
|
||||
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()
|
||||
|
||||
|
||||
+87
-22
@@ -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
|
||||
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 = {}
|
||||
}
|
||||
|
||||
+93
-81
@@ -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
|
||||
|
||||
|
||||
@@ -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 ##
|
||||
|
||||
|
||||
@@ -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) ->
|
||||
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
|
||||
@@ -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)
|
||||
|
||||
|
||||
+24
-1
@@ -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)
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
+47
-8
@@ -8,10 +8,15 @@
|
||||
<modalDialogs:>
|
||||
{#if _loggedIn}
|
||||
<app:myModal modalId="settings-modal" header="Settings">
|
||||
<strong>User ID</strong><br/>
|
||||
<small>Copy this ID for use in third party applications.</small>
|
||||
<pre class=prettyprint>{_user.id}</pre><br/>
|
||||
<h4>API</h4>
|
||||
<small>Copy these for use in third party applications.</small>
|
||||
<h6>User ID</h6>
|
||||
<pre class=prettyprint>{_user.id}</pre>
|
||||
|
||||
<h6>API Token</h6>
|
||||
<pre class=prettyprint>{_user.preferences.api_token}</pre>
|
||||
|
||||
<hr/>
|
||||
<h4>Gender</h4>
|
||||
<label class="radio">
|
||||
<input type="radio" name="genderRadios" value="m" x-bind="click:setMale" checked="{equal(_user.preferences.gender,'m')}">
|
||||
@@ -56,6 +61,7 @@
|
||||
<button data-dismiss="modal" class="btn btn-success">Ok</button>
|
||||
</@footer>
|
||||
</app:myModal>
|
||||
|
||||
{else}
|
||||
<app:myModal modalId="login-modal" header="Login / Register">
|
||||
<a href="/auth/facebook"><img src='/img/facebook-login-register.jpeg' alt="Login / Register With Facebook"/></a>
|
||||
@@ -102,6 +108,16 @@
|
||||
</@footer>
|
||||
</app:myModal>
|
||||
|
||||
<app:myModal modalId="add-party-modal" header="Add Party Member">
|
||||
<form x-bind="submit: addParty">
|
||||
{#if _view.addPartyError}
|
||||
<div class='alert alert-danger'>{_view.addPartyError}</div>
|
||||
{/}
|
||||
<input type="text" class="input-medium search-query" value="{_newPartyMember}">
|
||||
<input type="submit" class="btn" value="Add" />
|
||||
</form>
|
||||
</app:myModal>
|
||||
|
||||
<alerts:>
|
||||
{#if _flash.error}
|
||||
<ul class="alert alert-error">
|
||||
@@ -126,7 +142,7 @@
|
||||
<a href="#" class="btn btn-small btn-info" data-target="#login-modal" data-toggle="modal">Login / Register</a>
|
||||
{else}
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-small">{_view.loginName}</button>
|
||||
<button class="btn btn-small">{username(_user.auth)}</button>
|
||||
<button class="btn btn-small dropdown-toggle" data-toggle="dropdown">
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
@@ -142,17 +158,21 @@
|
||||
<div class='container-fluid'>
|
||||
|
||||
<div class='row-fluid'>
|
||||
<div id=character class='span4'>
|
||||
<div id=character class='{#if _party}span9{else}span5{/}'>
|
||||
<table>
|
||||
<tr>
|
||||
<td id="avatar">
|
||||
|
||||
<!-- Avatar -->
|
||||
<td class="avatar main-avatar">
|
||||
<div class='avatar-sprites'>
|
||||
<img class='weapon weapon-{_user.items.weapon}' src="/img/BrowserQuest/habitrpg_mods/weapon{_user.items.weapon}.png" />
|
||||
<img class='armor armor-{_user.items.armor}' src="/img/BrowserQuest/habitrpg_mods/{_user._armor}" />
|
||||
<img class='armor armor-{_user.items.armor}' src="/img/BrowserQuest/habitrpg_mods/{currentArmor(_user.preferences.gender, _user.items.armor, _user.preferences.armorSet)}" />
|
||||
</div>
|
||||
<div id="lvl"><span class="badge badge-info">Lvl {_user.stats.lvl}</span></div>
|
||||
</td>
|
||||
<td id="bars">
|
||||
|
||||
<!-- Progress Bars -->
|
||||
<td id="bars" style="width:{#if _party}70%{else}90%{/};">
|
||||
<div class="progress progress-danger" rel=tooltip data-placement=bottom title="Health">
|
||||
<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>
|
||||
@@ -168,7 +188,23 @@
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Party -->
|
||||
{#if _user.flags.partyEnabled}
|
||||
{#each _party as :member}
|
||||
<td class="avatar party-avatar" rel="tooltip" title="{username(:member.auth)}" data-placement="bottom" >
|
||||
<div class='avatar-sprites'>
|
||||
<img class='weapon weapon-{:member.items.weapon}' src="/img/BrowserQuest/habitrpg_mods/weapon{:member.items.weapon}.png" />
|
||||
<img class='armor armor-{:member.items.armor}' src="/img/BrowserQuest/habitrpg_mods/{currentArmor(:member.preferences.gender, :member.items.armor, :member.preferences.armorSet)}" />
|
||||
</div>
|
||||
<div id="lvl"><span class="badge badge-info">Lvl {:member.stats.lvl}</span></div>
|
||||
</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>
|
||||
|
||||
</table>
|
||||
<div id="exp-chart" style="display:none;"></div>
|
||||
</div>
|
||||
@@ -272,6 +308,9 @@
|
||||
<!-- Footer -->
|
||||
<footer class=footer>
|
||||
<div class=container>
|
||||
<!--<div class='pull-right'>
|
||||
<button class='btn' x-bind="click:emulateNextDay">Emulate Next Day</button>
|
||||
</div>-->
|
||||
<div>
|
||||
<ul>
|
||||
<li>Copyright © 2012 OCDevel LLC</li>
|
||||
|
||||
Reference in New Issue
Block a user