From f154a3ba66776252896ff09aa49d24025c0f48ec Mon Sep 17 00:00:00 2001 From: Tobias Leugger Date: Sat, 23 Feb 2013 18:52:26 +0100 Subject: [PATCH 01/14] Fixing all casper tests except registration and rest --- test/casper/dailies.casper.coffee | 2 +- test/casper/general.casper.coffee | 46 +++++++++++++------------- test/casper/habits.casper.coffee | 45 +++++++++++++------------ test/casper/helpers.coffee | 8 +++-- test/casper/items.casper.coffee | 22 ++++++++++++ test/casper/memoryleak.casper.coffee | 12 +++---- test/casper/registration.casper.coffee | 41 +++++++++++------------ test/casper/rewards.casper.coffee | 19 ----------- 8 files changed, 101 insertions(+), 94 deletions(-) create mode 100644 test/casper/items.casper.coffee delete mode 100644 test/casper/rewards.casper.coffee diff --git a/test/casper/dailies.casper.coffee b/test/casper/dailies.casper.coffee index add8608ba7..3730b91ad9 100644 --- a/test/casper/dailies.casper.coffee +++ b/test/casper/dailies.casper.coffee @@ -42,4 +42,4 @@ casper.then -> # ---------- Run ------------ casper.run -> - @test.renderResults true \ No newline at end of file + casper.test.renderResults true \ No newline at end of file diff --git a/test/casper/general.casper.coffee b/test/casper/general.casper.coffee index 3a8ea24443..8ce7280ab8 100644 --- a/test/casper/general.casper.coffee +++ b/test/casper/general.casper.coffee @@ -1,38 +1,38 @@ -helpers = require('./helpers') +helpers = new require('./test/casper/helpers')() casper = helpers.casper utils = helpers.utils -url = helpers.url +url = helpers.playUrl -# ---------- Init ------------ +# ---------- Basic Reset Test ------------ casper.start url, -> - @test.assertTitle "HabitRPG | Gamify Your Life", "[√] Page Title" + casper.test.assertTitle 'HabitRPG | Gamify Your Life', 'Page Title' -# ---------- Reset ------------ -# @then -> -# utils.dump @evaluate -> window.DERBY.app.model.get('_user.auth') -# @click '#reset-modal button:contains(Reset)' - -# Clear tasks +# Gain some GP and lose some HP casper.then -> - #TODO test after stats have been modified - casper.repeat 5, -> @click '.habits a[data-direction="down"]' - casper.repeat 5, -> @click '.habits a[data-direction="up"]' - userObj = @evaluate -> - window.DERBY.app.reset() - return window.DERBY.app.model.get('_user') + casper.repeat 5, -> casper.click '.habits a[data-direction="down"]' + casper.repeat 5, -> casper.click '.habits a[data-direction="up"]' + +# Reset +casper.then -> + helpers.reset() + +# Test that reset worked +casper.then -> + model = helpers.getModelDelayed (model) -> + casper.echo 'testing user after reset' + casper.test.assertEqual model._user.tasks, {}, 'no tasks' + casper.test.assertEqual model._user.stats, {hp:50, gp:0, exp:0, lvl:1}, 'stats' - @test.assertEqual userObj.tasks.length, 0 - @test.assertEqual userObj.stats.hp, 50 # ---------- Misc Pages ------------ -casper.thenOpen "#{url}/terms", -> - @test.assertTitle "Terms Of Use", "terms page works" +casper.thenOpen "#{helpers.baseUrl}/terms", -> + casper.test.assertTitle "Terms Of Use", "terms page works" -casper.thenOpen "#{url}/privacy", -> - @test.assertTitle "Privacy Policy", "privacy page works" +casper.thenOpen "#{helpers.baseUrl}/privacy", -> + casper.test.assertTitle "Privacy Policy", "privacy page works" # ---------- Run ------------ casper.run -> - @test.renderResults true \ No newline at end of file + casper.test.renderResults true \ No newline at end of file diff --git a/test/casper/habits.casper.coffee b/test/casper/habits.casper.coffee index 601ba7730e..075b2ba434 100644 --- a/test/casper/habits.casper.coffee +++ b/test/casper/habits.casper.coffee @@ -1,38 +1,39 @@ -helper = new require('./test/casper/helpers')() -casper = helper.casper -utils = helper.utils -url = helper.url +helpers = new require('./test/casper/helpers')() +casper = helpers.casper +utils = helpers.utils +url = helpers.playUrl -casper.start url + '/?play=1' +casper.start url -# ---------- Register ------------ -user = undefined -casper.then -> helper.register() -casper.then -> user = helper.getUser() +# # ---------- Register ------------ +# casper.then -> helpers.register() +# casper.then -> user = helpers.getUser() # ---------- Habits ------------ casper.then -> - helper.reset() - helper.addTasks() + helpers.reset() + helpers.addTasks(['habit']) casper.then -> - u = helper.userBeforeAfter (-> casper.click '.habits a[data-direction="down"]') - casper.test.assert u.before.stats.hp > u.after.stats.hp, '-habit -hp' - casper.test.assert u.before.stats.exp == u.after.stats.exp, '-habit =exp' + helpers.modelBeforeAfter (-> casper.click '.habits a[data-direction="down"]'), (model) -> + casper.test.assert model.before._user.stats.hp > model.after._user.stats.hp, '-habit -hp' + casper.test.assertEquals model.before._user.stats.exp, model.after._user.stats.exp, '-habit =exp' + casper.test.assertEquals model.before._user.stats.gp, model.after._user.stats.gp, '-habit =gp' casper.then -> - u = helper.userBeforeAfter (-> casper.click '.habits a[data-direction="up"]') - casper.test.assert u.before.stats.exp < u.after.stats.exp, '+habit +exp' - casper.test.assertEquals u.before.stats.hp, u.after.stats.hp, '+habit =hp' + helpers.modelBeforeAfter (-> casper.click '.habits a[data-direction="up"]'), (model) -> + casper.test.assert model.before._user.stats.exp < model.after._user.stats.exp, '+habit +exp' + casper.test.assert model.before._user.stats.gp < model.after._user.stats.gp, '+habit +gp' + casper.test.assertEquals model.before._user.stats.hp, model.after._user.stats.hp, '+habit =hp' # Test Death casper.then -> - @repeat 50, (-> casper.click '.habits a[data-direction="down"]') + casper.repeat 50, (-> casper.click '.habits a[data-direction="down"]') casper.then -> - u = helper.getUser() - @test.assertEquals u.stats.hp, 0, 'hp==0 (death by habits)' - @test.assertEquals u.stats.lvl, 0, 'lvl==0 (death by habits)' - @test.assert(@visible('#dead-modal'), 'Revive Modal Visible') + helpers.getModelDelayed (model) -> + casper.test.assertEquals model._user.stats.hp, 0, 'hp==0 (death by habits)' + casper.test.assertEquals model._user.stats.lvl, 0, 'lvl==0 (death by habits)' + casper.test.assertVisible '#dead-modal', 'Revive Modal Visible' # ---------- Run ------------ casper.run -> diff --git a/test/casper/helpers.coffee b/test/casper/helpers.coffee index bfca6b3d5d..138da7d03f 100644 --- a/test/casper/helpers.coffee +++ b/test/casper/helpers.coffee @@ -44,6 +44,10 @@ module.exports = -> casper.evaluate -> window.DERBY.app.reset() getModelDelayed: (cb) -> + # This time is needed for derby to have enough time to update all it's data. + # It still happens sometimes that the retrieved model does not contain any + # data. It might be worth to do some basic checks on the model here, and if + # it doesn't look OK, wait a bit longer and get it again. casper.wait SYNC_WAIT_TIME, -> cb(getModel()) @@ -82,9 +86,9 @@ module.exports = -> register: -> casper.fill 'form#derby-auth-register', username: random - email: random + '@gmail.com' - 'email-confirmation': random + '@gmail.com' + email: random + '@example.com' password: random + 'password-confirmation': random , true login: -> diff --git a/test/casper/items.casper.coffee b/test/casper/items.casper.coffee new file mode 100644 index 0000000000..853aa3f127 --- /dev/null +++ b/test/casper/items.casper.coffee @@ -0,0 +1,22 @@ +helpers = new require('./test/casper/helpers')() +casper = helpers.casper +utils = helpers.utils +url = helpers.playUrl + +casper.start url + +# ---------- Items (in-game rewards) ------------ +casper.then -> + helpers.reset() + helpers.addTasks(['habit'], 1) + +casper.then -> casper.test.assertDoesntExist 'ul.items', 'no items after reset' +casper.then -> casper.repeat 70, -> + casper.click '.habits a[data-direction="up"]' +casper.then -> + casper.test.assertVisible '.item-store-popover', 'store popover visible' + casper.test.assertExists 'ul.items', 'items appear after lvl up' + +# ---------- Run ------------ +casper.run -> + casper.test.renderResults true \ No newline at end of file diff --git a/test/casper/memoryleak.casper.coffee b/test/casper/memoryleak.casper.coffee index 3f71b725b7..320442e66e 100644 --- a/test/casper/memoryleak.casper.coffee +++ b/test/casper/memoryleak.casper.coffee @@ -1,12 +1,12 @@ -helper = new require('./test/casper/helpers')() -casper = helper.casper -utils = helper.utils -url = helper.url +helpers = new require('./test/casper/helpers')() +casper = helpers.casper +utils = helpers.utils +url = helpers.playUrl -casper.start url + '/?play=1' +casper.start url casper.repeat 50, -> casper.reload() casper.run -> - @test.renderResults true \ No newline at end of file + casper.test.renderResults true \ No newline at end of file diff --git a/test/casper/registration.casper.coffee b/test/casper/registration.casper.coffee index 005c04245f..e3e1a53574 100644 --- a/test/casper/registration.casper.coffee +++ b/test/casper/registration.casper.coffee @@ -1,35 +1,34 @@ -helper = new require('./test/casper/helpers')() -casper = helper.casper -utils = helper.utils -url = helper.url +helpers = new require('./test/casper/helpers')() +casper = helpers.casper +utils = helpers.utils +url = helpers.playUrl -casper.start url + '/?play=1' +casper.start url # ---------- Register ------------ -user = undefined -casper.then -> helper.register() -casper.then -> user = helper.getUser() +registeredUser = undefined +casper.then -> helpers.register() +casper.then -> + helpers.getModelDelayed (model) -> + registeredUser = model + casper.then -> casper.reload() casper.then -> - nowUser = helper.getUser() - casper.then -> - casper.test.assertEqual user.id, nowUser.id, 'user registered and maintained session' + helpers.getModelDelayed (nowModel) -> + casper.test.assertEqual registeredUser._userId, nowModel._userId, 'user registered and maintained session' # ---------- Log Out ------------ -casper.thenOpen helper.url + '/logout' -casper.thenOpen helper.url + '/?play=1' +casper.thenOpen helpers.baseUrl + '/logout' +casper.thenOpen helpers.playUrl casper.then -> - nowUser = helper.getUser() - casper.then -> - casper.test.assertNotEquals user.id, nowUser.id, 'user logged out' + helpers.getModelDelayed (nowModel) -> + casper.test.assertNotEquals registeredUser._userId, nowModel._userId, 'user logged out' # ---------- Login ------------ -casper.then -> helper.login() -casper.then -> utils.dump casper.debugHTML '#derby-auth-login' +casper.then -> helpers.login() casper.then -> - nowUser = helper.getUser() - casper.then -> - casper.test.assertEqual user.id, nowUser.id, 'user logged in' + helpers.getModelDelayed (nowModel) -> + casper.test.assertEqual registeredUser._userId, nowModel._userId, 'user logged out' # ---------- Run ------------ casper.run -> diff --git a/test/casper/rewards.casper.coffee b/test/casper/rewards.casper.coffee deleted file mode 100644 index 1082537cc0..0000000000 --- a/test/casper/rewards.casper.coffee +++ /dev/null @@ -1,19 +0,0 @@ -helper = new require('./test/casper/helpers')() -casper = helper.casper -utils = helper.utils -url = helper.url - -casper.start url + '/?play=1' - -# ---------- Rewardsj1 ------------ -casper.then -> - helper.reset() - helper.addTasks() - -casper.then -> @test.assertDoesntExist('ul.items') -casper.then -> @repeat 50, -> casper.click('.habits a[data-direction="up"]') -casper.then -> @test.assertExists('ul.items') - -# ---------- Run ------------ -casper.run -> - casper.test.renderResults true \ No newline at end of file From 24dbae3a4141a22b59f552702008aa23a54898ae Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 25 Feb 2013 18:34:11 -0500 Subject: [PATCH 02/14] update config.json.exampl for default mongo db --- config.json.example | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/config.json.example b/config.json.example index 0dafb95956..cc40d79701 100644 --- a/config.json.example +++ b/config.json.example @@ -1,16 +1,15 @@ { "PORT":3000, "IP":"0.0.0.0", - "BASE_URL": "http://localhost", + "BASE_URL":"http://localhost", "FACEBOOK_KEY":"123456789012345", "FACEBOOK_SECRET":"aaaabbbbccccddddeeeeffff00001111", - "NODE_DB_URI":"mongodb://user:pass@hostname:27017/db_name", + "NODE_DB_URI":"mongodb://localhost/habitrpg", "NODE_ENV":"development", "SESSION_SECRET":"YOUR SECRET HERE", "SMTP_USER":"user@domain.com", "SMTP_PASS":"password", "SMTP_SERVICE":"Gmail", "STRIPE_API_KEY":"aaaabbbbccccddddeeeeffff00001111", - "STRIPE_PUB_KEY":"22223333444455556666777788889999", - "NODETIME_KEY":"abcdefg" + "STRIPE_PUB_KEY":"22223333444455556666777788889999" } \ No newline at end of file From 06da720dfb1fc1cafd914433c1a2b6484dae9e99 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 25 Feb 2013 18:59:14 -0500 Subject: [PATCH 03/14] return next(err) for errors in subscription callbacks. Also, try reloading the page when userObj? is null (bustedSession victims. See the comments) --- src/app/index.coffee | 4 ++-- src/app/party.coffee | 14 ++++++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/app/index.coffee b/src/app/index.coffee index 2c2f3340ca..55ebf7cc2f 100644 --- a/src/app/index.coffee +++ b/src/app/index.coffee @@ -23,7 +23,7 @@ _ = require('underscore') # ========== ROUTES ========== -get '/', (page, model, next) -> +get '/', (page, model, params, next) -> return page.redirect '/' if page.params?.query?.play? # temporary view variables, so we don't call model.set() too fast @@ -34,7 +34,7 @@ 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 - party.partySubscribe model, -> + party.partySubscribe page, model, params, next, -> character.updateUser(model) items.server(model) model.set '_view', _view diff --git a/src/app/party.coffee b/src/app/party.coffee index 0284d2444d..268214bb47 100644 --- a/src/app/party.coffee +++ b/src/app/party.coffee @@ -17,7 +17,7 @@ partyUnsubscribe = (model, cb) -> 2) If in a an empty party, just subscribe to the user & party meta. 3) If full party, subscribe to everything. ### -module.exports.partySubscribe = partySubscribe = (model, cb) -> +module.exports.partySubscribe = partySubscribe = (page, model, params, next, cb) -> # unsubscribe from everything - we're starting over # partyUnsubscribe model, -> @@ -25,10 +25,15 @@ module.exports.partySubscribe = partySubscribe = (model, cb) -> # Restart subscription to the main user selfQ = model.query('users').withId model.get('_userId') #or model.session.userId # see http://goo.gl/TPYIt selfQ.subscribe (err, self) -> - throw err if err + return next(err) if err u = self.at(0) uObj = u.get() + # Attempted handling for 'party of undefined' error, which is caused by bustedSession (see derby-auth). + # Theoretically simply reloading the page should restore model.at('_userId') and the second load should work just fine + # bustedSession victims might hit a redirection loop if I'm wrong :/ + return page.redirect('/') unless uObj + ## (1) User is solo, just return that subscription unless uObj.party?.current? model.ref '_user', u @@ -47,13 +52,14 @@ module.exports.partySubscribe = partySubscribe = (model, cb) -> finished = -> # model.unsubscribe selfQ, -> selfQ.subscribe (err, self) -> + return next(err) if err model.ref '_user', self.at(0) cb() # User in a party partiesQ = model.query('parties').withId(uObj.party.current) partiesQ.fetch (err, res) -> - throw err if err + return next(err) if err p = res.at(0) model.ref '_party', p ids = p.get('members') @@ -73,7 +79,7 @@ module.exports.partySubscribe = partySubscribe = (model, cb) -> ## (3) Party has members, subscribe to those users too membersQ = model.query('users').party(ids) membersQ.fetch (err, members) -> - throw err if err + return next(err) if err model.ref '_partyMembers', members finished() From 7c480124cfadcfb101900889a1aff0e550f678b9 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Tue, 26 Feb 2013 12:10:23 -0500 Subject: [PATCH 04/14] show infinite stack trace in development --- src/server/index.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/server/index.coffee b/src/server/index.coffee index 1fb7d7fd4d..05beb1f7ec 100644 --- a/src/server/index.coffee +++ b/src/server/index.coffee @@ -21,6 +21,9 @@ racer.set('bundleTimeout', 40000) # racer.use(racer.logPlugin) # derby.use(derby.logPlugin) +# Infinite stack trace +Error.stackTraceLimit = Infinity if process.env.NODE_ENV is 'development' + ## SERVER CONFIGURATION ## expressApp = express() From 0a3fc9f71573af1d00a395fee89289f73ab12e3f Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Tue, 26 Feb 2013 12:42:00 -0500 Subject: [PATCH 05/14] instructions on running node-webkit-agent for debugging memory leaks --- package.json | 3 ++- server.js | 11 +++++++++++ test/casper/memoryleak.casper.coffee | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 3590ffea94..367c9641de 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "nconf": "*", "icalendar": "git://github.com/lefnire/node-icalendar#master", "resolve": "~0.2.3", - "browserify": "1.17.3" + "browserify": "1.17.3", + "webkit-devtools-agent": "*" }, "private": true, "subdomain": "habitrpg", diff --git a/server.js b/server.js index db73ddd361..51db9480cf 100644 --- a/server.js +++ b/server.js @@ -22,6 +22,17 @@ process.env.SMTP_SERVICE = conf.get("SMTP_SERVICE"); process.env.STRIPE_API_KEY = conf.get("STRIPE_API_KEY"); process.env.STRIPE_PUB_KEY = conf.get("STRIPE_PUB_KEY"); +var agent; +if (process.env.NODE_ENV === 'development') { + // Follow these instructions for profiling / debugging leaks + // * https://developers.google.com/chrome-developer-tools/docs/heap-profiling + // * https://developers.google.com/chrome-developer-tools/docs/memory-analysis-101 + agent = require('webkit-devtools-agent'); + console.log("To debug memory leaks:" + + "\n\t(1) Run `kill -SIGUSR2 " + process.pid + "`" + + "\n\t(2) open http://c4milo.github.com/node-webkit-agent/21.0.1180.57/inspector.html?host=localhost:1337&page=0"); +} + process.on('uncaughtException', function (error) { function sendEmail(mailData) { diff --git a/test/casper/memoryleak.casper.coffee b/test/casper/memoryleak.casper.coffee index 320442e66e..ff5357a90f 100644 --- a/test/casper/memoryleak.casper.coffee +++ b/test/casper/memoryleak.casper.coffee @@ -5,7 +5,7 @@ url = helpers.playUrl casper.start url -casper.repeat 50, -> +casper.repeat 100, -> casper.reload() casper.run -> From 63392d644f2da3b8e227e4d4311eaf56bd4db333 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Tue, 26 Feb 2013 16:37:47 -0500 Subject: [PATCH 06/14] use derby-auth new findOne() motif spec (aka, no more res.at(0).get()). Also, try tripple subscription on parties instead of multiple individual subs --- src/app/party.coffee | 85 +++++++++++++++++------------------------ src/server/api.coffee | 4 +- src/server/store.coffee | 3 +- 3 files changed, 39 insertions(+), 53 deletions(-) diff --git a/src/app/party.coffee b/src/app/party.coffee index 268214bb47..5d931f84a4 100644 --- a/src/app/party.coffee +++ b/src/app/party.coffee @@ -16,6 +16,15 @@ partyUnsubscribe = (model, cb) -> 1) If the user is solo, just subscribe to the user. 2) If in a an empty party, just subscribe to the user & party meta. 3) If full party, subscribe to everything. + + Note a strange hack - we subscribe to queries incrementally. First self, then party, then party members. + Party members come with limited fields, so you can't hack their stuff. Strangely, subscribing to the members after + already subscribing to self limits self's fields to the fields which members are limited to. As a result, we have + to re-subscribe to self to get all the fields (otherwise everything breaks). Weirdly, this last subscription doesn't + do the opposite - granting all the fields back to members. I dont' know what's going on here + + Another issue: `model.unsubscribe(selfQ)` would seem to mitigate the above, so we at least don't have a stray + subscription floating around - but alas, it doesn't seem to work (or at least never calls the callback) ### module.exports.partySubscribe = partySubscribe = (page, model, params, next, cb) -> @@ -23,65 +32,41 @@ module.exports.partySubscribe = partySubscribe = (page, model, params, next, cb) # partyUnsubscribe model, -> # Restart subscription to the main user - selfQ = model.query('users').withId model.get('_userId') #or model.session.userId # see http://goo.gl/TPYIt - selfQ.subscribe (err, self) -> + selfQ = model.query('users').withId (model.get('_userId') or model.session.userId) # see http://goo.gl/TPYIt + selfQ.fetch (err, user) -> return next(err) if err - u = self.at(0) - uObj = u.get() + return next("User not found - this shouldn't be happening!") unless user.get() + + finished = (descriptors, paths) -> + model.subscribe.apply model, descriptors.concat -> + [err, refs] = [arguments[0], arguments] + return next(err) if err + _.each paths, (path, idx) -> model.ref path, refs[idx+1] + cb() + # Attempted handling for 'party of undefined' error, which is caused by bustedSession (see derby-auth). # Theoretically simply reloading the page should restore model.at('_userId') and the second load should work just fine # bustedSession victims might hit a redirection loop if I'm wrong :/ - return page.redirect('/') unless uObj +# return page.redirect('/') unless uObj - ## (1) User is solo, just return that subscription - unless uObj.party?.current? - model.ref '_user', u - return cb() + partyId = user.get('party.current') - ### - Note this strange hack - we subscribe to queries incrementally. First self, then party, then party members. - Party members come with limited fields, so you can't hack their stuff. Strangely, subscribing to the members after - already subscribing to self limits self's fields to the fields which members are limited to. As a result, we have - to re-subscribe to self to get all the fields (otherwise everything breaks). Weirdly, this last subscription doesn't - do the opposite - granting all the fields back to members. I dont' know what's going on here - - Another issue: `model.unsubscribe(selfQ)` would seem to mitigate the above, so we at least don't have a stray - subscription floating around - but alas, it doesn't seem to work (or at least never calls the callback) - ### - finished = -> - # model.unsubscribe selfQ, -> - selfQ.subscribe (err, self) -> - return next(err) if err - model.ref '_user', self.at(0) - cb() + # (1) Solo player + return finished([selfQ], ['_user']) #unless partyId # User in a party - partiesQ = model.query('parties').withId(uObj.party.current) - partiesQ.fetch (err, res) -> + partyQ = model.query('parties').withId(partyId) + partyQ.fetch (err, party) -> return next(err) if err - p = res.at(0) - model.ref '_party', p - ids = p.get('members') - - # 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") - # debugger - # membersSubscribe model, ids + members = party.get('members') ## (2) Party has no members, just subscribe to the party itself - if _.isEmpty(ids) - return finished() + return finished([partyQ, selfQ], ['_party', '_user']) if _.isEmpty(members) - else - ## (3) Party has members, subscribe to those users too - membersQ = model.query('users').party(ids) - membersQ.fetch (err, members) -> - return next(err) if err - model.ref '_partyMembers', members - finished() + ## (3) Party has members, subscribe to those users too + membersQ = model.query('users').party(members) + return finished [partyQ, membersQ, selfQ], ['_party', '_partyMembers', '_user'] module.exports.app = (appExports, model) -> user = model.at('_user') @@ -121,7 +106,7 @@ module.exports.app = (appExports, model) -> query = model.query('users').party([id]) model.fetch query, (err, res) -> throw err if err - u = res.at(0).get() + u = res.get() if !u?.id? model.set "_view.partyError", "User with id #{id} not found." return @@ -143,9 +128,9 @@ module.exports.app = (appExports, model) -> user.set 'party.current', partyId # model.push "parties.#{partyId}.members", user.get('id'), -> #FIXME why this not working? model.query('parties').withId(partyId).fetch (err, p) -> - members = p.at(0).get('members') + members = p.get('members') members.push user.get('id') - p.at(0).set 'members', members, -> + p.set 'members', members, -> window.location.reload true # partySubscribe model, -> # p = model.at('_party') @@ -174,5 +159,5 @@ module.exports.app = (appExports, model) -> # partyUnsubscribe model, -> # selfQ = model.query('users').withId model.get('_userId') #or model.session.userId # see http://goo.gl/TPYIt # selfQ.subscribe (err, u) -> -# model.ref '_user', u.at(0) +# model.ref '_user', u # browser.resetDom model \ No newline at end of file diff --git a/src/server/api.coffee b/src/server/api.coffee index 84531744f3..043dfc37d8 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -28,7 +28,7 @@ router.post '/users/:uid/tasks/:taskId/:direction', (req, res) -> model = req.getModel() model.fetch model.query('users').withIdAndToken(uid, apiToken), (err, result) -> return res.send(500, err) if err - user = result.at(0) + user = result userObj = user.get() if _.isEmpty(userObj) return res.send(500, "User with uid=#{uid}, token=#{apiToken} not found. Make sure you're not using your username, but your User Id") @@ -63,7 +63,7 @@ router.get '/users/:uid/calendar.ics', (req, res) -> query = model.query('users').withIdAndToken(uid, apiToken) query.fetch (err, result) -> return res.send(500, err) if err - tasks = result.at(0).get('tasks') + tasks = result.get('tasks') # tasks = result[0].tasks tasksWithDates = _.filter tasks, (task) -> !!task.date return res.send(500, "No events found") if _.isEmpty(tasksWithDates) diff --git a/src/server/store.coffee b/src/server/store.coffee index cebd70e5cd..3ee0f45b24 100644 --- a/src/server/store.coffee +++ b/src/server/store.coffee @@ -69,7 +69,7 @@ REST = (store) -> store.query.expose "users", "withIdAndToken", (uid, token) -> @byId(uid) .where('apiToken').equals(token) - .one + .findOne() store.queryAccess "users", "withIdAndToken", (uid, token, accept, err) -> return accept(true) if uid && token @@ -96,6 +96,7 @@ partySystem = (store) -> store.query.expose "parties", "withId", (id) -> @where("id").equals(id) + .findOne() store.queryAccess "parties", "withId", (id, accept, err) -> # return err(derbyAuth.SESSION_INVALIDATED_ERROR) if derbyAuth.bustedSession(@) return accept(false) if derbyAuth.bustedSession(@) From 9b20d8ceba8e279f6f5f6fc71f6fb6fefab453ea Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Tue, 26 Feb 2013 16:51:00 -0500 Subject: [PATCH 07/14] move scoring to its own class (attempt to remove model from hanging around in closure on server, which is what's causing our memory leak - see http://goo.gl/7Gt0M) --- src/app/index.coffee | 4 +- src/app/scoring.coffee | 508 ++++++++++++++++++++--------------------- src/app/tasks.coffee | 7 +- src/server/api.coffee | 4 +- 4 files changed, 260 insertions(+), 263 deletions(-) diff --git a/src/app/index.coffee b/src/app/index.coffee index 55ebf7cc2f..b25e52cb9d 100644 --- a/src/app/index.coffee +++ b/src/app/index.coffee @@ -45,13 +45,13 @@ get '/', (page, model, params, next) -> ready (model) -> user = model.at('_user') - scoring.setModel(model) + score = new scoring.Scoring(model) #set cron immediately lastCron = user.get('lastCron') user.set('lastCron', +new Date) if (!lastCron? or lastCron == 'new') - scoring.cron() + score.cron() character.app(exports, model) tasks.app(exports, model) diff --git a/src/app/scoring.coffee b/src/app/scoring.coffee index 2eb2065c60..4f1b78db70 100644 --- a/src/app/scoring.coffee +++ b/src/app/scoring.coffee @@ -5,273 +5,269 @@ helpers = require './helpers' browser = require './browser' character = require './character' items = require './items' -MODIFIER = .02 # each new level, armor, weapon add 2% modifier (this number may change) -user = undefined -model = undefined -# This is required by all the functions, make sure it's set before anythign else is called -setModel = (m) -> - model = m - user = model.at('_user') +module.exports.Scoring = (model) -> -### - Calculates Exp & GP modification based on weapon & lvl - {value} task.value for gain - {modifiers} may manually pass in stats as {weapon, exp}. This is used for testing -### -expModifier = (value, modifiers = {}) -> - weapon = modifiers.weapon || user.get('items.weapon') - lvl = modifiers.lvl || user.get('stats.lvl') - dmg = items.items.weapon[weapon].modifier # each new weapon increases exp gain - dmg += (lvl-1) * MODIFIER # same for lvls - modified = value + (value * dmg) - return modified + MODIFIER = .02 # each new level, armor, weapon add 2% modifier (this mechanism will change) + user = model.at '_user' -### - Calculates HP-loss modification based on armor & lvl - {value} task.value which is hurting us - {modifiers} may manually pass in modifier as {armor, lvl}. This is used for testing -### -hpModifier = (value, modifiers = {}) -> - armor = modifiers.armor || user.get('items.armor') - head = modifiers.head || user.get('items.head') - shield = modifiers.shield || user.get('items.shield') - lvl = modifiers.lvl || user.get('stats.lvl') - ac = items.items.armor[armor].modifier + items.items.head[head].modifier + items.items.shield[shield].modifier # each new armor decreases HP loss - ac += (lvl-1) * MODIFIER # same for lvls - modified = value - (value * ac) - return modified - -### - Calculates the next task.value based on direction - For negative values, use a line: something like y=-.1x+1 - For positibe values, taper off with inverse log: y=.9^x - Would love to use inverse log for the whole thing, but after 13 fails it hits infinity. Revisit this formula later - {currentValue} the current value of the task, determines it's next value - {direction} 'up' or 'down' -### -taskDeltaFormula = (currentValue, direction) -> - sign = if (direction == "up") then 1 else -1 - delta = if (currentValue < 0) then (( -0.1 * currentValue + 1 ) * sign) else (( Math.pow(0.9,currentValue) ) * sign) - return delta + ### + Calculates Exp & GP modification based on weapon & lvl + {value} task.value for gain + {modifiers} may manually pass in stats as {weapon, exp}. This is used for testing + ### + expModifier = (value, modifiers = {}) -> + weapon = modifiers.weapon || user.get('items.weapon') + lvl = modifiers.lvl || user.get('stats.lvl') + dmg = items.items.weapon[weapon].modifier # each new weapon increases exp gain + dmg += (lvl-1) * MODIFIER # same for lvls + modified = value + (value * dmg) + return modified -### - 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, batch) -> - obj = batch.obj() + ### + Calculates HP-loss modification based on armor & lvl + {value} task.value which is hurting us + {modifiers} may manually pass in modifier as {armor, lvl}. This is used for testing + ### + hpModifier = (value, modifiers = {}) -> + armor = modifiers.armor || user.get('items.armor') + head = modifiers.head || user.get('items.head') + shield = modifiers.shield || user.get('items.shield') + lvl = modifiers.lvl || user.get('stats.lvl') + ac = items.items.armor[armor].modifier + items.items.head[head].modifier + items.items.shield[shield].modifier # each new armor decreases HP loss + ac += (lvl-1) * MODIFIER # same for lvls + modified = value - (value * ac) + return modified - # if user is dead, dont do anything - return if obj.stats.lvl == 0 + ### + Calculates the next task.value based on direction + For negative values, use a line: something like y=-.1x+1 + For positibe values, taper off with inverse log: y=.9^x + Would love to use inverse log for the whole thing, but after 13 fails it hits infinity. Revisit this formula later + {currentValue} the current value of the task, determines it's next value + {direction} 'up' or 'down' + ### + taskDeltaFormula = (currentValue, direction) -> + sign = if (direction == "up") then 1 else -1 + delta = if (currentValue < 0) then (( -0.1 * currentValue + 1 ) * sign) else (( Math.pow(0.9,currentValue) ) * sign) + return delta - if newStats.hp? - # Game Over - if newStats.hp <= 0 - obj.stats.lvl = 0 # signifies dead - obj.stats.hp = 0 - return - else - obj.stats.hp = newStats.hp - - if newStats.exp? - # level up & carry-over exp - tnl = model.get '_tnl' - if newStats.exp >= tnl - newStats.exp -= tnl - obj.stats.lvl++ - obj.stats.hp = 50 - - obj.stats.exp = newStats.exp - - # Set flags when they unlock features - if !obj.flags.customizationsNotification and (obj.stats.exp > 10 or obj.stats.lvl > 1) - batch.set 'flags.customizationsNotification', true - obj.flags.customizationsNotification = true - 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 '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 - if !obj.flags.petsEnabled and obj.stats.lvl >= 4 - batch.set 'flags.petsEnabled', true - obj.flags.petsEnabled = true - - 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' -# {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, batch, cron) -> - - commit = false - unless batch? - commit = true - batch = new character.BatchUpdate(model) - batch.startTransaction() - obj = batch.obj() - - {gp, hp, exp, lvl} = obj.stats - - taskPath = "tasks.#{taskId}" - taskObj = obj.tasks[taskId] - {type, value} = taskObj - - # If they're trying to purhcase a too-expensive reward, confirm they want to take a hit for it - if taskObj.value > obj.stats.gp and taskObj.type is 'reward' - r = confirm "Not enough GP to purchase this reward, buy anyway and lose HP? (Punishment for taking a reward you didn't earn)." - unless r - batch.commit() - return - - delta = 0 - times ?= 1 - calculateDelta = (adjustvalue=true) -> - # If multiple days have passed, multiply times days missed - _.times times, (n) -> - # Each iteration calculate the delta (nextDelta), which is then accumulated in delta - # (aka, the total delta). This weirdness won't be necessary when calculating mathematically - # rather than iteratively - nextDelta = taskDeltaFormula(value, direction) - value += nextDelta if adjustvalue - delta += nextDelta - - addPoints = -> - modified = expModifier(delta) - exp += modified - gp += modified - - subtractPoints = -> - modified = hpModifier(delta) - hp += modified - - switch type - when 'habit' - # Don't adjust values for habits that don't have both + and - - adjustvalue = if (taskObj.up==false or taskObj.down==false) then false else true - calculateDelta(adjustvalue) - # Add habit value to habit-history (if different) - if (delta > 0) then addPoints() else subtractPoints() - 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 cron? # cron - subtractPoints() - else - addPoints() # obviously for delta>0, but also a trick to undo accidental checkboxes - - when 'todo' - calculateDelta() - unless cron? # don't touch stats on cron - addPoints() # obviously for delta>0, but also a trick to undo accidental checkboxes - - when 'reward' - # Don't adjust values for rewards - calculateDelta(false) - # purchase item - gp -= Math.abs(taskObj.value) - num = parseFloat(taskObj.value).toFixed(2) - # 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, 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 - _.each Object.keys(origStats), (key) -> obj.stats[key] = origStats[key] - batch.setStats(newStats) -# batch.setStats() - batch.commit() - return delta - -### - At end of day, add value to all incomplete Daily & Todo tasks (further incentive) - For incomplete Dailys, deduct experience -### -cron = () -> - today = +new Date - daysPassed = helpers.daysBetween(today, user.get('lastCron')) - if daysPassed > 0 - batch = new character.BatchUpdate(model) - batch.startTransaction() - batch.set 'lastCron', today + ### + 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, batch) -> obj = batch.obj() - hpBefore = obj.stats.hp #we'll use this later so we can animate hp loss - # Tally each task - todoTally = 0 - _.each obj.tasks, (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 + # if user is dead, dont do anything + return if obj.stats.lvl == 0 + + if newStats.hp? + # Game Over + if newStats.hp <= 0 + obj.stats.lvl = 0 # signifies dead + obj.stats.hp = 0 + return + else + obj.stats.hp = newStats.hp + + if newStats.exp? + # level up & carry-over exp + tnl = model.get '_tnl' + if newStats.exp >= tnl + newStats.exp -= tnl + obj.stats.lvl++ + obj.stats.hp = 50 + + obj.stats.exp = newStats.exp + + # Set flags when they unlock features + if !obj.flags.customizationsNotification and (obj.stats.exp > 10 or obj.stats.lvl > 1) + batch.set 'flags.customizationsNotification', true + obj.flags.customizationsNotification = true + 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 '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 + if !obj.flags.petsEnabled and obj.stats.lvl >= 4 + batch.set 'flags.petsEnabled', true + obj.flags.petsEnabled = true + + 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' + # {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, batch, cron) -> + + commit = false + unless batch? + commit = true + batch = new character.BatchUpdate(model) + batch.startTransaction() + obj = batch.obj() + + {gp, hp, exp, lvl} = obj.stats + + taskPath = "tasks.#{taskId}" + taskObj = obj.tasks[taskId] + {type, value} = taskObj + + # If they're trying to purhcase a too-expensive reward, confirm they want to take a hit for it + if taskObj.value > obj.stats.gp and taskObj.type is 'reward' + r = confirm "Not enough GP to purchase this reward, buy anyway and lose HP? (Punishment for taking a reward you didn't earn)." + unless r + batch.commit() + return + + delta = 0 + times ?= 1 + calculateDelta = (adjustvalue=true) -> + # If multiple days have passed, multiply times days missed + _.times times, (n) -> + # Each iteration calculate the delta (nextDelta), which is then accumulated in delta + # (aka, the total delta). This weirdness won't be necessary when calculating mathematically + # rather than iteratively + nextDelta = taskDeltaFormula(value, direction) + value += nextDelta if adjustvalue + delta += nextDelta + + addPoints = -> + modified = expModifier(delta) + exp += modified + gp += modified + + subtractPoints = -> + modified = hpModifier(delta) + hp += modified + + switch type + when 'habit' + # Don't adjust values for habits that don't have both + and - + adjustvalue = if (taskObj.up==false or taskObj.down==false) then false else true + calculateDelta(adjustvalue) + # Add habit value to habit-history (if different) + if (delta > 0) then addPoints() else subtractPoints() + 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 cron? # cron + subtractPoints() else - value = obj.tasks[taskObj.id].value #get updated value - absVal = if (completed) then Math.abs(value) else value - todoTally += absVal + addPoints() # obviously for delta>0, but also a trick to undo accidental checkboxes - # Finished tallying - obj.history ?= {}; obj.history.todos ?= []; obj.history.exp ?= [] - obj.history.todos.push { date: today, value: todoTally } - # tally experience - expTally = obj.stats.exp - lvl = 0 #iterator - while lvl < (obj.stats.lvl-1) - lvl++ - expTally += (lvl*100)/5 - obj.history.exp.push { date: today, value: expTally } + when 'todo' + calculateDelta() + unless cron? # don't touch stats on cron + addPoints() # obviously for delta>0, but also a trick to undo accidental checkboxes - # Set the new user specs, and animate HP loss - [hpAfter, obj.stats.hp] = [obj.stats.hp, hpBefore] - batch.setStats() - batch.set('history', obj.history) - batch.commit() - browser.resetDom(model) - setTimeout (-> user.set 'stats.hp', hpAfter), 1000 # animate hp loss + when 'reward' + # Don't adjust values for rewards + calculateDelta(false) + # purchase item + gp -= Math.abs(taskObj.value) + num = parseFloat(taskObj.value).toFixed(2) + # 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, 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 + _.each Object.keys(origStats), (key) -> obj.stats[key] = origStats[key] + batch.setStats(newStats) + # batch.setStats() + batch.commit() + return delta + + ### + At end of day, add value to all incomplete Daily & Todo tasks (further incentive) + For incomplete Dailys, deduct experience + ### + cron = () -> + today = +new Date + daysPassed = helpers.daysBetween(today, user.get('lastCron')) + if daysPassed > 0 + batch = new character.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 obj.tasks, (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 + obj.history ?= {}; obj.history.todos ?= []; obj.history.exp ?= [] + obj.history.todos.push { date: today, value: todoTally } + # tally experience + expTally = obj.stats.exp + lvl = 0 #iterator + while lvl < (obj.stats.lvl-1) + lvl++ + expTally += (lvl*100)/5 + obj.history.exp.push { date: today, value: expTally } + + # Set the new user specs, and animate HP loss + [hpAfter, obj.stats.hp] = [obj.stats.hp, hpBefore] + batch.setStats() + batch.set('history', obj.history) + batch.commit() + browser.resetDom(model) + setTimeout (-> user.set 'stats.hp', hpAfter), 1000 # animate hp loss -module.exports = { - setModel: setModel - MODIFIER: MODIFIER - score: score - cron: cron + return { + MODIFIER: MODIFIER + score: score + cron: cron - # testing stuff - expModifier: expModifier - hpModifier: hpModifier - taskDeltaFormula: taskDeltaFormula -} + # testing stuff + expModifier: expModifier + hpModifier: hpModifier + taskDeltaFormula: taskDeltaFormula + } diff --git a/src/app/tasks.coffee b/src/app/tasks.coffee index 8f14840e9f..22995d868c 100644 --- a/src/app/tasks.coffee +++ b/src/app/tasks.coffee @@ -26,6 +26,7 @@ module.exports.view = (view) -> module.exports.app = (appExports, model) -> user = model.at('_user') + score = new scoring.Scoring(model) user.on 'set', 'tasks.*.completed', (i, completed, previous, isLocal, passed) -> return if passed? && passed.cron # Don't do this stuff on cron @@ -36,7 +37,7 @@ module.exports.app = (appExports, model) -> # Score the user based on todo task task = user.at("tasks.#{i}") - scoring.score(i, direction()) + score.score(i, direction()) appExports.addTask = (e, el, next) -> type = $(el).attr('data-task-type') @@ -81,7 +82,7 @@ module.exports.app = (appExports, model) -> return # Cancel. Don't delete, don't hurt user else task.set('type','habit') # hack to make sure it hits HP, instead of performing "undo checkbox" - scoring.score(id, direction:'down') + score.score(id, direction:'down') # prevent accidently deleting long-standing tasks else @@ -171,4 +172,4 @@ module.exports.app = (appExports, model) -> direction = 'up' if direction == 'true/' direction = 'down' if direction == 'false/' task = model.at $(el).parents('li')[0] - scoring.score(task.get('id'), direction) + score.score(task.get('id'), direction) diff --git a/src/server/api.coffee b/src/server/api.coffee index 043dfc37d8..ef18e8d233 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -48,8 +48,8 @@ router.post '/users/:uid/tasks/:taskId/:direction', (req, res) -> 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." - scoring.setModel(model) - delta = scoring.score(taskId, direction) + score = scoring.Scoring(model) + delta = score.score(taskId, direction) result = model.get ('_user.stats') result.delta = delta res.send(result) From 7c0abe4a4742d2908ef3fd46a963e1ae7fad524d Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Tue, 26 Feb 2013 16:54:31 -0500 Subject: [PATCH 08/14] remove "testing party bug" --- src/app/party.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/party.coffee b/src/app/party.coffee index 5d931f84a4..f1dcd2890e 100644 --- a/src/app/party.coffee +++ b/src/app/party.coffee @@ -53,7 +53,7 @@ module.exports.partySubscribe = partySubscribe = (page, model, params, next, cb) partyId = user.get('party.current') # (1) Solo player - return finished([selfQ], ['_user']) #unless partyId + return finished([selfQ], ['_user']) unless partyId # User in a party partyQ = model.query('parties').withId(partyId) From 203e0a124a3ebc02228ac16d8e47ba2844f57f2b Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Tue, 26 Feb 2013 17:41:51 -0500 Subject: [PATCH 09/14] proper stripe ajax formatting --- src/server/private.coffee | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/server/private.coffee b/src/server/private.coffee index f882bac83b..3f7bc8e5cc 100644 --- a/src/server/private.coffee +++ b/src/server/private.coffee @@ -12,13 +12,13 @@ module.exports.app = (appExports, model) -> token = (res) -> console.log(res); $.ajax({ - type:"POST", - url:"/charge", - data:res - }).success -> - window.location.href = "/" - .error (err) -> - alert err.responseText + type: "POST", + url: "/charge", + data: res + }).success -> + window.location.href = "/" + .error (err) -> + alert err.responseText StripeCheckout.open key: model.get('_stripePubKey') From 01182b377d2303d67a79669fc2d4e8376538f0b8 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 27 Feb 2013 09:07:51 -0500 Subject: [PATCH 10/14] add a self-serve restore modal for users who want to restore their stats --- src/app/character.coffee | 8 ++++++ views/app/settings.html | 53 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/src/app/character.coffee b/src/app/character.coffee index 901e3cb518..7ace61dbf1 100644 --- a/src/app/character.coffee +++ b/src/app/character.coffee @@ -72,6 +72,14 @@ module.exports.app = (appExports, model) -> appExports.customizeArmorSet = (e, el) -> user.set 'preferences.armorSet', $(el).attr('data-value') + appExports.restoreSave = (e, el) -> + batch = new BatchUpdate(model) + batch.startTransaction() + obj = batch.obj() + _.each obj.stats, (stat, key) -> batch.set("stats.#{key}", parseInt(stat)) + _.each obj.items, (item, key) -> batch.set("items.#{key}", parseInt(item)) + batch.commit() + user.on 'set', 'flags.customizationsNotification', (captures, args) -> return unless captures == true $('.main-avatar').popover('destroy') #remove previous popovers diff --git a/views/app/settings.html b/views/app/settings.html index 5f564d221e..57354e3bb9 100644 --- a/views/app/settings.html +++ b/views/app/settings.html @@ -10,7 +10,8 @@
{_user.apiToken}

- Reset + Reset +  Restore <@footer> @@ -25,6 +26,56 @@ + + +

HabitRPG is quite Beta-quality at present, and many find they need to restore character attributes as a result. Enter your numbers here and it will be applied automatically to your character. This will be removed once Habit is more stable.

+ + {#with _user} +
+

Stats

+
+ HP + +
+
+ Exp + +
+
+ GP + +
+
+ Level + +
+ +

Items

+
+ Weapon + +
+
+ Armor + +
+
+ Helm + +
+
+ Shield + +
+ +
+ {/} + + <@footer> + + +
+ {{else}} Login / Register With Facebook From 811d6de107a7d73699e672bcd4f92026ab2daa58 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 27 Feb 2013 09:15:06 -0500 Subject: [PATCH 11/14] fix to previous - don't do it dynamically, do it on save --- src/app/character.coffee | 5 ++--- views/app/settings.html | 18 +++++++++--------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/app/character.coffee b/src/app/character.coffee index 7ace61dbf1..07b0996dcb 100644 --- a/src/app/character.coffee +++ b/src/app/character.coffee @@ -75,9 +75,8 @@ module.exports.app = (appExports, model) -> appExports.restoreSave = (e, el) -> batch = new BatchUpdate(model) batch.startTransaction() - obj = batch.obj() - _.each obj.stats, (stat, key) -> batch.set("stats.#{key}", parseInt(stat)) - _.each obj.items, (item, key) -> batch.set("items.#{key}", parseInt(item)) + $('#restore-form input').each -> + batch.set $(this).attr('data-for'), parseInt($(this).val()) batch.commit() user.on 'set', 'flags.customizationsNotification', (captures, args) -> diff --git a/views/app/settings.html b/views/app/settings.html index 57354e3bb9..6d053bc3df 100644 --- a/views/app/settings.html +++ b/views/app/settings.html @@ -31,41 +31,41 @@

HabitRPG is quite Beta-quality at present, and many find they need to restore character attributes as a result. Enter your numbers here and it will be applied automatically to your character. This will be removed once Habit is more stable.

{#with _user} -
+

Stats

HP - +
Exp - +
GP - +
Level - +

Items

Weapon - +
Armor - +
Helm - +
Shield - +
From 9a0ac021023972d10e1da3f70b10abdcde09dfc8 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 27 Feb 2013 16:21:06 -0500 Subject: [PATCH 12/14] fixes to party bugs --- src/app/party.coffee | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/app/party.coffee b/src/app/party.coffee index f1dcd2890e..bf6518bd4a 100644 --- a/src/app/party.coffee +++ b/src/app/party.coffee @@ -89,7 +89,8 @@ module.exports.app = (appExports, model) -> content: html $('.main-avatar').popover 'show' - model.on 'set', '_user.party.invitation', -> partySubscribe(model) + #TODO implement this when we have unsubscribe working properly + #model.on 'set', '_user.party.invitation', -> partySubscribe(null, model, null, null, null) appExports.partyCreate = -> newParty = model.get("_newParty") @@ -102,12 +103,10 @@ module.exports.app = (appExports, model) -> 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) -> + model.query('users').party([id]).fetch (err, res) -> throw err if err - u = res.get() - if !u?.id? + u = res.at(0).get() + if !u? model.set "_view.partyError", "User with id #{id} not found." return else if u.party.current? or u.party.invitation? @@ -115,23 +114,24 @@ module.exports.app = (appExports, model) -> return else p = model.at '_party' - p.push "invites", id - model.set "users.#{id}.party.invitation", p.get('id') + #p.push "invites", id $.bootstrapGrowl "Invitation Sent." $('#party-modal').modal('hide') - model.set '_newPartyMember', '', -> window.location.reload true + model.set "users.#{id}.party.invitation", p.get('id'), -> window.location.reload(true) + #model.set '_newPartyMember', '' #partySubscribe model appExports.partyAccept = -> partyId = user.get('party.invitation') user.set 'party.invitation', null user.set 'party.current', partyId -# model.push "parties.#{partyId}.members", user.get('id'), -> #FIXME why this not working? - model.query('parties').withId(partyId).fetch (err, p) -> - members = p.get('members') - members.push user.get('id') - p.set 'members', members, -> - window.location.reload true + model.at("parties.#{partyId}.members").push user.get('id'), -> window.location.reload(true) +# model.query('parties').withId(partyId).fetch (err, p) -> +# members = p.get('members') +# members.push user.get('id') +# p.set 'members', members, -> +# window.location.reload true + # partySubscribe model, -> # p = model.at('_party') # p.push 'members', user.get('id') From c9a73300023fdd01abac72c0b02311e788ec15e1 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 27 Feb 2013 16:40:17 -0500 Subject: [PATCH 13/14] unlock parties for users who get invited, regardless of level --- src/app/party.coffee | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/app/party.coffee b/src/app/party.coffee index bf6518bd4a..fe5f795277 100644 --- a/src/app/party.coffee +++ b/src/app/party.coffee @@ -90,7 +90,12 @@ module.exports.app = (appExports, model) -> $('.main-avatar').popover 'show' #TODO implement this when we have unsubscribe working properly - #model.on 'set', '_user.party.invitation', -> partySubscribe(null, model, null, null, null) + model.on 'set', '_user.party.invitation', (after, before) -> + if !before? and after? # they just got invited + # if they haven't unlocked parties yet, unlock it for them + user.set('flags.partyEnabled',true) unless user.get('flags.partyEnabled') + window.setTimeout (-> window.location.reload true), 1000 + #partySubscribe(null, model, null, null, null) appExports.partyCreate = -> newParty = model.get("_newParty") From 18c0d4e6aa3fe30911073ad9922cf6a3fd9cf188 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 27 Feb 2013 17:06:39 -0500 Subject: [PATCH 14/14] pin coffeescript --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 367c9641de..258bab8e95 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "stripe": "*", "async": "*", "lodash": "*", - "coffee-script": "*", + "coffee-script": "1.4.x", "underscore": "*", "mongoskin": "*", "nconf": "*",