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) {
console.error(exception);
// don't crash for now
process.on('uncaughtException', function (error) {
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

View file

@ -1,4 +1,10 @@
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
@ -54,7 +60,7 @@ module.exports.setupSortable = (model) ->
# Also, note that refList index arguments can either be an index
# or the item's id property
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) ->
$('[rel=tooltip]').tooltip()
@ -95,7 +101,7 @@ module.exports.setupGrowlNotifications = (model) ->
allow_dismiss: true
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
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
@ -112,16 +118,16 @@ module.exports.setupGrowlNotifications = (model) ->
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
$('.main-avatar').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>
<img src='/img/party-unlocked.png' style='float:right;padding:5px;' />
#{message} <a href='#' onClick=\"$('.main-avatar').popover('hide');return false;\">[Close]</a>
</div>"
$('#add-party-button').popover 'show'
$('.main-avatar').popover 'show'
# Setup listeners which trigger notifications
@ -131,14 +137,14 @@ module.exports.setupGrowlNotifications = (model) ->
if num < 0
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
rounded = Math.abs(num.toFixed(1))
# made purchase
if num < 0
# FIXME use 'warning' when unchecking an accidently completed daily/todo, and notify of exp too
statsNotification "<i class='icon-star'></i>GP -#{rounded}", 'success'
# gained money (and thereby exp)
# gained gp (and thereby exp)
else if num > 0
num = Math.abs(num)
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
return "0"
view.fn "money", (num) ->
view.fn "gp", (num) ->
if num
return num.toFixed(2)
else
return "0.00"
view.fn "lessThan", (a, b) ->
view.fn "lt", (a, b) ->
a < b
view.fn 'gt', (a, b) -> a > b
view.fn "tokens", (money) ->
return money/0.25
view.fn "tokens", (gp) ->
return gp/0.25
view.fn 'currentArmor', (user) ->
armor = user?.items?.armor || 0

View file

@ -10,23 +10,15 @@ content = require './content'
scoring = require './scoring'
schema = require './schema'
helpers = require './helpers'
helpers.viewHelpers view
browser = require './browser'
party = require './party'
helpers.viewHelpers view
_ = require('underscore')
setupListReferences = (model) ->
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 ==========
@ -39,20 +31,23 @@ get '/', (page, model, next) ->
#if req.headers['x-forwarded-proto']!='https' and process.env.NODE_ENV=='production'
# 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)
#q = model.query('users').withId(model.session.userId)
q = "users.#{model.session.userId}"
model.subscribe q, (err, user) ->
#user = result.at(0)
# This used to be in party.server(model, cb), but was getting `TypeError: Object #<Model> has no method 'server'`
# on the second load for some reason
selfQ = model.query('users').withId(model.get('_userId') or model.session.userId)
model.subscribe selfQ, (err, users) ->
throw err if err
user = users.at(0)
model.ref '_user', user
batch = new schema.BatchUpdate(model)
batch.startTransaction()
obj = batch.obj()
# Setup Item Store
items = user.get('items')
_view.items =
armor: content.items.armor[parseInt(items?.armor || 0) + 1]
weapon: content.items.weapon[parseInt(items?.weapon || 0) + 1]
armor: content.items.armor[parseInt(obj.items?.armor || 0) + 1]
weapon: content.items.weapon[parseInt(obj.items?.weapon || 0) + 1]
potion: content.items.potion
reroll: content.items.reroll
@ -62,22 +57,18 @@ get '/', (page, model, next) ->
batch.commit()
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 !_.isEmpty(user.get('party'))
model.subscribe model.query('users').party(user.get('party')), (err, party) ->
model.ref '_party', party
page.render()
if obj.party?.current?
party.partySubscribe model, obj.party.current, (p) -> page.render()
else
page.render()
# ========== CONTROLLER FUNCTIONS ==========
resetDom = (model) ->
window.DERBY.app.dom.clear()
view.render(model)
setupModelFns(model)
ready (model) ->
user = model.at('_user')
scoring.setModel(model)
@ -87,7 +78,7 @@ ready (model) ->
user.set('lastCron', +new Date) if (!lastCron? or lastCron == 'new')
# Setup model in scoring functions
scoring.cron(resetDom)
scoring.cron()
# Load all the jQuery, Growl, Tour, etc
browser.loadJavaScripts(model)
@ -96,8 +87,12 @@ ready (model) ->
browser.setupTour(model)
browser.setupGrowlNotifications(model) unless model.get('_view.mobileDevice')
party.app(exports, model)
require('../server/private').app(exports, model)
require('./debug').app(exports, model)
user.on 'set', 'tasks.*.completed', (i, completed, previous, isLocal, passed) ->
return if passed? && passed.cron # Don't do this stuff on cron
direction = () ->
@ -168,14 +163,14 @@ ready (model) ->
# fix when query subscriptions implemented properly
$('[rel=tooltip]').tooltip('hide')
ids = user.get("#{type}Ids")
ids = user.get("idLists.#{type}")
ids.splice(ids.indexOf(id),1)
user.del('tasks.'+id)
user.set("#{type}Ids", ids)
user.set("idLists.#{type}", ids)
exports.clearCompleted = (e, el) ->
todoIds = user.get('todoIds')
todoIds = user.get('idLists.todo')
removed = false
_.each model.get('_todoList'), (task) ->
if task.completed
@ -183,7 +178,7 @@ ready (model) ->
user.del('tasks.'+task.id)
todoIds.splice(todoIds.indexOf(task.id), 1)
if removed
user.set('todoIds', todoIds)
user.set('idLists.todo', todoIds)
exports.toggleDay = (e, el) ->
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
# 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') ]
return if money < value
user.set 'stats.money', money - value
return if gp < value
user.set 'stats.gp', gp - value
if type == 'armor'
user.set 'items.armor', index
model.set '_view.items.armor', content.items.armor[parseInt(index) + 1]
@ -254,7 +249,7 @@ ready (model) ->
# Reset stats
batch.set 'stats.hp', 50
batch.set 'stats.lvl', 1
batch.set 'stats.money', 0
batch.set 'stats.gp', 0
batch.set 'stats.exp', 0
# Reset items
@ -276,40 +271,15 @@ ready (model) ->
batch.startTransaction()
taskTypes = ['habit', 'daily', 'todo', 'reward']
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
revive(batch, true)
revive(batch)
batch.commit()
resetDom(model)
browser.resetDom(model)
exports.closeKickstarterNofitication = (e, el) ->
user.set('notifications.kickstarter', 'hide')
exports.closeKickstarterNofitication = (e, el) -> user.set('flags.kickstarter', 'hide')
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.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'
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
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: []
flags:
partyEnabled: false
itemsEnabled: false
kickstarter: 'show'
# ads: 'show' # added on registration
module.exports.newUserObject = ->
# 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
guid = task.id = require('racer').uuid()
guid = task.id = derby.uuid()
newUser.tasks[guid] = task
switch task.type
when 'habit' then newUser.habitIds.push guid
when 'daily' then newUser.dailyIds.push guid
when 'todo' then newUser.todoIds.push guid
when 'reward' then newUser.rewardIds.push guid
when 'habit' then newUser.idLists.habit.push guid
when 'daily' then newUser.idLists.daily.push guid
when 'todo' then newUser.idLists.todo.push guid
when 'reward' then newUser.idLists.reward.push guid
return newUser
module.exports.updateUser = (batch) ->
user = batch.user
obj = batch.obj()
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..)
currentPrefs = _.clone user.get('preferences')
mergedPrefs = _.defaults currentPrefs, { gender: 'm', armorSet: 'v1', api_token: derby.uuid() }
batch.set('preferences', mergedPrefs)
batch.set('apiToken', derby.uuid()) unless obj.apiToken
## 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')
tasks = obj.tasks
_.each ['habit','daily','todo','reward'], (type) ->
path = "#{type}Ids"
# 1. remove duplicates
# 2. restore missing zombie tasks back into list
taskIds = _.pluck( _.where(tasks, {type:type}), 'id')
union = _.union user.get(path), taskIds
union = _.union obj.idLists[type], taskIds
# 2. remove empty (grey) tasks
preened = _.filter(union, (val) -> _.contains(taskIds, val))
# 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) ->
user = model.at("_user")

View file

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

View file

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

View file

@ -37,7 +37,6 @@ module.exports.app = (appExports, model) ->
obj = model.get('_user')
batch.set 'balance', obj.balance-1
_.each obj.tasks, (task) -> batch.set("tasks.#{task.id}.value", 0) unless task.type == 'reward'
console.log(obj)
batch.commit()
module.exports.routes = (expressApp) ->
@ -52,6 +51,7 @@ module.exports.routes = (expressApp) ->
else
model = req.getModel()
userId = model.session.userId
req._isServer = true
model.fetch "users.#{userId}", (err, user) ->
model.ref '_user', "users.#{userId}"
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'
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)
expressApp.post '/users/:uid/tasks/:taskId/:direction', (req, res) -> res.send(500, deprecatedMessage)
# ---------- 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
v1 API. Requires user-id and apiToken, task-id, direction. Test with:
curl -X POST -H "Content-Type:application/json" -d '{"apiToken":"{TOKEN}"}' localhost:3000/v1/users/{UID}/tasks/productivity/up
###
# TODO /v1/..
expressApp.post '/users/:uid/tasks/:taskId/:direction', (req, res) ->
expressApp.post '/v1/users/:uid/tasks/:taskId/:direction', (req, res) ->
{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'
# 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, ':taskId required') unless taskId
return res.send(500, ":direction must be 'up' or 'down'") unless direction in ['up','down']
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
user = result.at(0)
userObj = user.get()
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)
# Create task if doesn't exist
# TODO add service & icon to task
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 {
id: taskId
type: 'habit'
@ -59,7 +59,9 @@ module.exports = (expressApp, root, derby) ->
up: 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."
}
}, (a,b,c) ->
console.log {a:a,b:b,c:c}
scoring.setModel(model)
delta = scoring.score(taskId, direction)

View file

@ -2,39 +2,92 @@
Setup read / write access
@param store
###
module.exports = (store) ->
# store.writeAccess "*", "users.*.balance", (id, newBalance, next) ->
# return unless @session and @session.userId # https://github.com/codeparty/racer/issues/37
# purchasingSomethingOnClient = newBalance < this.session.req._racerModel.get("users.#{id}.balance")
# isServer = not @req.socket
# next(purchasingSomethingOnClient or isServer)
module.exports.customAccessControl = (store) ->
userAccess(store)
partySystem(store)
REST(store)
###
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 ->
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]
isServer = not @req.socket
next(isServer)
next(@session.req?._isServer)
###
Get user with API token
###
store.query.expose "users", "withIdAndToken", (id, api_token) ->
###
REST
Get user with API token
###
REST = (store) ->
store.query.expose "users", "withIdAndToken", (id, apiToken) ->
@where("id").equals(id)
.where('preferences.api_token').equals(api_token)
.where('apiToken').equals(apiToken)
.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)
store.queryAccess "users", "withIdAndToken", (id, apiToken, next) ->
#return next(false) unless @session and @session.userId # https://github.com/codeparty/racer/issues/37
next(true) # only user has id & token
###
Party permissions
###
###
Party permissions
###
partySystem = (store) ->
store.query.expose "users", "party", (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) ->
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>
<h6>API Token</h6>
<pre class=prettyprint>{_user.preferences.api_token}</pre>
<pre class=prettyprint>{_user.apiToken}</pre>
<hr/>
<h4>Gender</h4>
@ -40,6 +40,9 @@
</label>
{/}
<hr/>
<a class='btn btn-danger' data-target="#reset-modal" data-toggle="modal">Reset</a>
<@footer>
<button class="btn" data-dismiss="modal" aria-hidden="true">Close</button>
</@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 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>
<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>
</@footer>
<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>
</@footer>
</app:myModal>
<app:myModal modalId="why-ads-modal" header="Why Ads?">
@ -100,7 +103,7 @@
<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>
<@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>
{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>
@ -108,14 +111,46 @@
</@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>
<app:myModal modalId="party-modal">
{#if _user.party.current}
<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}">
<input type="submit" class="btn" value="Add" />
</form>
<div class='control-group'>
<input type="text" class="input-medium" placeholder="User Id" value="{_newPartyMember}">
<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>
<alerts:>
@ -125,7 +160,7 @@
</ul>
{/}
{#if equal(_user.notifications.kickstarter,'show')}
{#if equal(_user.flags.kickstarter,'show')}
<div class='alert alert-success'>
<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>.
@ -142,13 +177,20 @@
<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">{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">
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<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>
</ul>
</div>
@ -158,7 +200,7 @@
<div class='container-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>
<tr>
@ -172,26 +214,26 @@
</td>
<!-- 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="bar" style="width: {percent(_user.stats.hp, 50)}%;"></div>
<span class="progress-text"><i class=icon-heart></i> {round(_user.stats.hp)} / 50</span>
</div>
<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">
{#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;
{/}
<i class=icon-star></i> {round(_user.stats.exp)} / {_user._tnl}
<i class=icon-star></i> {round(_user.stats.exp)} / {_tnl}
</span>
</div>
</td>
<!-- Party -->
{#if _user.flags.partyEnabled}
{#each _party as :member}
{#each _partyMembers as :member}
{#unless equal(:member.id, _userId)}
<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" />
@ -199,8 +241,7 @@
</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>
@ -281,7 +322,7 @@
<!--Title -->
<div class="row-fluid">
<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>
<!-- Content -->
@ -289,7 +330,7 @@
{#each _rewardList as :task}<app:task />{/}
</ul>
{#if _user.items.itemsEnabled}
{#if _user.flags.itemsEnabled}
<ul class='items'>
{#with _view.items.armor as :item}<app:item />{/}
{#with _view.items.weapon as :item}<app:item />{/}
@ -308,9 +349,10 @@
<!-- Footer -->
<footer class=footer>
<div class=container>
<!--<div class='pull-right'>
<div class='pull-right'>
<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>
<ul>
<li>Copyright &copy; 2012 OCDevel LLC</li>