mirror of
https://github.com/sudoxnym/habitica.git
synced 2026-08-05 03:52:14 +00:00
Merge branch 'parties'
Conflicts: src/app/helpers.coffee src/app/scoring.coffee
This commit is contained in:
commit
906932090f
15 changed files with 561 additions and 197 deletions
97
migrations/20230204_user_public_private_paths.js
Normal file
97
migrations/20230204_user_public_private_paths.js
Normal 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);
|
||||||
|
}
|
||||||
|
})
|
||||||
BIN
public/img/party-unlocked.png
Normal file
BIN
public/img/party-unlocked.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
37
server.js
37
server.js
|
|
@ -1,6 +1,37 @@
|
||||||
process.on('uncaughtException', function (exception) {
|
process.on('uncaughtException', function (error) {
|
||||||
console.error(exception);
|
|
||||||
// don't crash for now
|
function sendEmail(mailData) {
|
||||||
|
var nodemailer = require("derby-auth/node_modules/nodemailer");
|
||||||
|
|
||||||
|
// create reusable transport method (opens pool of SMTP connections)
|
||||||
|
// TODO derby-auth isn't currently configurable here, if you need customizations please send pull request
|
||||||
|
var smtpTransport = nodemailer.createTransport("SMTP",{
|
||||||
|
service: process.env.SMTP_SERVICE,
|
||||||
|
auth: {
|
||||||
|
user: process.env.SMTP_USER,
|
||||||
|
pass: process.env.SMTP_PASS
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// send mail with defined transport object
|
||||||
|
smtpTransport.sendMail(mailData, function(error, response){
|
||||||
|
if(error){
|
||||||
|
console.log(error);
|
||||||
|
}else{
|
||||||
|
console.log("Message sent: " + response.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
smtpTransport.close(); // shut down the connection pool, no more messages
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sendEmail({
|
||||||
|
from: "HabitRPG <admin@habitrpg.com>",
|
||||||
|
to: "tylerrenelle@gmail.com",
|
||||||
|
subject: "HabitRPG Error",
|
||||||
|
text: error.stack
|
||||||
|
});
|
||||||
|
console.log(error.stack);
|
||||||
});
|
});
|
||||||
|
|
||||||
require('coffee-script') // remove intermediate compilation requirement
|
require('coffee-script') // remove intermediate compilation requirement
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,10 @@
|
||||||
content = require('./content')
|
content = require('./content')
|
||||||
|
_ = require 'underscore'
|
||||||
|
|
||||||
|
module.exports.resetDom = (model) ->
|
||||||
|
window.DERBY.app.dom.clear()
|
||||||
|
window.DERBY.app.view.render(model)
|
||||||
|
model.fn '_tnl', '_user.stats.lvl', (lvl) -> (lvl*100)/5
|
||||||
|
|
||||||
###
|
###
|
||||||
Loads JavaScript files from (1) public/js/* and (2) external sources
|
Loads JavaScript files from (1) public/js/* and (2) external sources
|
||||||
|
|
@ -54,7 +60,7 @@ module.exports.setupSortable = (model) ->
|
||||||
# Also, note that refList index arguments can either be an index
|
# Also, note that refList index arguments can either be an index
|
||||||
# or the item's id property
|
# or the item's id property
|
||||||
model.at("_#{type}List").pass(ignore: domId).move {id}, to
|
model.at("_#{type}List").pass(ignore: domId).move {id}, to
|
||||||
setupSortable(type) for type in ['habit', 'daily', 'todo', 'reward']
|
_.each ['habit', 'daily', 'todo', 'reward'], (type) -> setupSortable(type)
|
||||||
|
|
||||||
module.exports.setupTooltips = (model) ->
|
module.exports.setupTooltips = (model) ->
|
||||||
$('[rel=tooltip]').tooltip()
|
$('[rel=tooltip]').tooltip()
|
||||||
|
|
@ -95,7 +101,7 @@ module.exports.setupGrowlNotifications = (model) ->
|
||||||
allow_dismiss: true
|
allow_dismiss: true
|
||||||
stackup_spacing: 10 # spacing between consecutive stacecked growls.
|
stackup_spacing: 10 # spacing between consecutive stacecked growls.
|
||||||
|
|
||||||
user.on 'set', 'items.itemsEnabled', (captures, args) ->
|
user.on 'set', 'flags.itemsEnabled', (captures, args) ->
|
||||||
return unless captures == true
|
return unless captures == true
|
||||||
message = "Congratulations, you have unlocked the Item Store! You can now buy weapons, armor, potions, etc. Read each item's comment for more information."
|
message = "Congratulations, you have unlocked the Item Store! You can now buy weapons, armor, potions, etc. Read each item's comment for more information."
|
||||||
$('ul.items').popover
|
$('ul.items').popover
|
||||||
|
|
@ -112,16 +118,16 @@ module.exports.setupGrowlNotifications = (model) ->
|
||||||
user.on 'set', 'flags.partyEnabled', (captures, args) ->
|
user.on 'set', 'flags.partyEnabled', (captures, args) ->
|
||||||
return unless captures == true
|
return unless captures == true
|
||||||
message = "Congratulations, you have unlocked the Party System! You can now group with your friends by adding their User Ids."
|
message = "Congratulations, you have unlocked the Party System! You can now group with your friends by adding their User Ids."
|
||||||
$('#add-party-button').popover
|
$('.main-avatar').popover
|
||||||
title: "Pary System Unlocked"
|
title: "Pary System Unlocked"
|
||||||
placement: 'bottom'
|
placement: 'bottom'
|
||||||
trigger: 'manual'
|
trigger: 'manual'
|
||||||
html: true
|
html: true
|
||||||
content: "<div class='party-system-popover'>
|
content: "<div class='party-system-popover'>
|
||||||
<img src='/img/BrowserQuest/favicon.png' />
|
<img src='/img/party-unlocked.png' style='float:right;padding:5px;' />
|
||||||
#{message} <a href='#' onClick=\"$('#add-party-button').popover('hide');return false;\">[Close]</a>
|
#{message} <a href='#' onClick=\"$('.main-avatar').popover('hide');return false;\">[Close]</a>
|
||||||
</div>"
|
</div>"
|
||||||
$('#add-party-button').popover 'show'
|
$('.main-avatar').popover 'show'
|
||||||
|
|
||||||
|
|
||||||
# Setup listeners which trigger notifications
|
# Setup listeners which trigger notifications
|
||||||
|
|
@ -131,14 +137,14 @@ module.exports.setupGrowlNotifications = (model) ->
|
||||||
if num < 0
|
if num < 0
|
||||||
statsNotification "<i class='icon-heart'></i>HP -#{rounded}", 'error' # lost hp from purchase
|
statsNotification "<i class='icon-heart'></i>HP -#{rounded}", 'error' # lost hp from purchase
|
||||||
|
|
||||||
user.on 'set', 'stats.money', (captures, args) ->
|
user.on 'set', 'stats.gp', (captures, args) ->
|
||||||
num = captures - args
|
num = captures - args
|
||||||
rounded = Math.abs(num.toFixed(1))
|
rounded = Math.abs(num.toFixed(1))
|
||||||
# made purchase
|
# made purchase
|
||||||
if num < 0
|
if num < 0
|
||||||
# FIXME use 'warning' when unchecking an accidently completed daily/todo, and notify of exp too
|
# FIXME use 'warning' when unchecking an accidently completed daily/todo, and notify of exp too
|
||||||
statsNotification "<i class='icon-star'></i>GP -#{rounded}", 'success'
|
statsNotification "<i class='icon-star'></i>GP -#{rounded}", 'success'
|
||||||
# gained money (and thereby exp)
|
# gained gp (and thereby exp)
|
||||||
else if num > 0
|
else if num > 0
|
||||||
num = Math.abs(num)
|
num = Math.abs(num)
|
||||||
statsNotification "<i class='icon-star'></i>Exp,GP +#{rounded}", 'success'
|
statsNotification "<i class='icon-star'></i>Exp,GP +#{rounded}", 'success'
|
||||||
|
|
|
||||||
13
src/app/debug.coffee
Normal file
13
src/app/debug.coffee
Normal 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
|
||||||
|
|
@ -44,17 +44,18 @@ module.exports.viewHelpers = (view) ->
|
||||||
else
|
else
|
||||||
return "0"
|
return "0"
|
||||||
|
|
||||||
view.fn "money", (num) ->
|
view.fn "gp", (num) ->
|
||||||
if num
|
if num
|
||||||
return num.toFixed(2)
|
return num.toFixed(2)
|
||||||
else
|
else
|
||||||
return "0.00"
|
return "0.00"
|
||||||
|
|
||||||
view.fn "lessThan", (a, b) ->
|
view.fn "lt", (a, b) ->
|
||||||
a < b
|
a < b
|
||||||
|
view.fn 'gt', (a, b) -> a > b
|
||||||
|
|
||||||
view.fn "tokens", (money) ->
|
view.fn "tokens", (gp) ->
|
||||||
return money/0.25
|
return gp/0.25
|
||||||
|
|
||||||
view.fn 'currentArmor', (user) ->
|
view.fn 'currentArmor', (user) ->
|
||||||
armor = user?.items?.armor || 0
|
armor = user?.items?.armor || 0
|
||||||
|
|
|
||||||
|
|
@ -10,23 +10,15 @@ content = require './content'
|
||||||
scoring = require './scoring'
|
scoring = require './scoring'
|
||||||
schema = require './schema'
|
schema = require './schema'
|
||||||
helpers = require './helpers'
|
helpers = require './helpers'
|
||||||
helpers.viewHelpers view
|
|
||||||
browser = require './browser'
|
browser = require './browser'
|
||||||
|
party = require './party'
|
||||||
|
helpers.viewHelpers view
|
||||||
_ = require('underscore')
|
_ = require('underscore')
|
||||||
|
|
||||||
setupListReferences = (model) ->
|
setupListReferences = (model) ->
|
||||||
taskTypes = ['habit', 'daily', 'todo', 'reward']
|
taskTypes = ['habit', 'daily', 'todo', 'reward']
|
||||||
_.each taskTypes, (type) -> model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids"
|
_.each taskTypes, (type) -> model.refList "_#{type}List", "_user.tasks", "_user.idLists.#{type}"
|
||||||
|
|
||||||
setupModelFns = (model) ->
|
|
||||||
model.fn '_user._tnl', '_user.stats.lvl', (lvl) ->
|
|
||||||
# see https://github.com/lefnire/habitrpg/issues/4
|
|
||||||
# also update in scoring.coffee. TODO create a function accessible in both locations
|
|
||||||
(lvl*100)/5
|
|
||||||
|
|
||||||
# model.fn '_party', '_user.party', (ids) ->
|
|
||||||
# model.fetch model.query('users').party(ids), (err, party) ->
|
|
||||||
# model.set '_view.party', party
|
|
||||||
|
|
||||||
# ========== ROUTES ==========
|
# ========== ROUTES ==========
|
||||||
|
|
||||||
|
|
@ -39,20 +31,23 @@ get '/', (page, model, next) ->
|
||||||
#if req.headers['x-forwarded-proto']!='https' and process.env.NODE_ENV=='production'
|
#if req.headers['x-forwarded-proto']!='https' and process.env.NODE_ENV=='production'
|
||||||
# return page.redirect 'https://' + req.headers.host + req.url
|
# return page.redirect 'https://' + req.headers.host + req.url
|
||||||
|
|
||||||
#FIXME subscribing to this query causes "Fatal Error: Unauuthorized" after conncetion for a time (racer/lib/accessControl/accessControl.Store.js)
|
# This used to be in party.server(model, cb), but was getting `TypeError: Object #<Model> has no method 'server'`
|
||||||
#q = model.query('users').withId(model.session.userId)
|
# on the second load for some reason
|
||||||
q = "users.#{model.session.userId}"
|
selfQ = model.query('users').withId(model.get('_userId') or model.session.userId)
|
||||||
model.subscribe q, (err, user) ->
|
model.subscribe selfQ, (err, users) ->
|
||||||
#user = result.at(0)
|
throw err if err
|
||||||
|
|
||||||
|
user = users.at(0)
|
||||||
model.ref '_user', user
|
model.ref '_user', user
|
||||||
|
|
||||||
batch = new schema.BatchUpdate(model)
|
batch = new schema.BatchUpdate(model)
|
||||||
batch.startTransaction()
|
batch.startTransaction()
|
||||||
|
obj = batch.obj()
|
||||||
|
|
||||||
# Setup Item Store
|
# Setup Item Store
|
||||||
items = user.get('items')
|
|
||||||
_view.items =
|
_view.items =
|
||||||
armor: content.items.armor[parseInt(items?.armor || 0) + 1]
|
armor: content.items.armor[parseInt(obj.items?.armor || 0) + 1]
|
||||||
weapon: content.items.weapon[parseInt(items?.weapon || 0) + 1]
|
weapon: content.items.weapon[parseInt(obj.items?.weapon || 0) + 1]
|
||||||
potion: content.items.potion
|
potion: content.items.potion
|
||||||
reroll: content.items.reroll
|
reroll: content.items.reroll
|
||||||
|
|
||||||
|
|
@ -62,22 +57,18 @@ get '/', (page, model, next) ->
|
||||||
batch.commit()
|
batch.commit()
|
||||||
|
|
||||||
setupListReferences(model)
|
setupListReferences(model)
|
||||||
setupModelFns(model)
|
model.fn '_tnl', '_user.stats.lvl', (lvl) ->
|
||||||
|
# see https://github.com/lefnire/habitrpg/issues/4
|
||||||
|
# also update in scoring.coffee. TODO create a function accessible in both locations
|
||||||
|
(lvl*100)/5
|
||||||
|
|
||||||
# Subscribe to friends
|
if obj.party?.current?
|
||||||
if !_.isEmpty(user.get('party'))
|
party.partySubscribe model, obj.party.current, (p) -> page.render()
|
||||||
model.subscribe model.query('users').party(user.get('party')), (err, party) ->
|
else
|
||||||
model.ref '_party', party
|
page.render()
|
||||||
|
|
||||||
page.render()
|
|
||||||
|
|
||||||
# ========== CONTROLLER FUNCTIONS ==========
|
# ========== CONTROLLER FUNCTIONS ==========
|
||||||
|
|
||||||
resetDom = (model) ->
|
|
||||||
window.DERBY.app.dom.clear()
|
|
||||||
view.render(model)
|
|
||||||
setupModelFns(model)
|
|
||||||
|
|
||||||
ready (model) ->
|
ready (model) ->
|
||||||
user = model.at('_user')
|
user = model.at('_user')
|
||||||
scoring.setModel(model)
|
scoring.setModel(model)
|
||||||
|
|
@ -87,7 +78,7 @@ ready (model) ->
|
||||||
user.set('lastCron', +new Date) if (!lastCron? or lastCron == 'new')
|
user.set('lastCron', +new Date) if (!lastCron? or lastCron == 'new')
|
||||||
|
|
||||||
# Setup model in scoring functions
|
# Setup model in scoring functions
|
||||||
scoring.cron(resetDom)
|
scoring.cron()
|
||||||
|
|
||||||
# Load all the jQuery, Growl, Tour, etc
|
# Load all the jQuery, Growl, Tour, etc
|
||||||
browser.loadJavaScripts(model)
|
browser.loadJavaScripts(model)
|
||||||
|
|
@ -96,8 +87,12 @@ ready (model) ->
|
||||||
browser.setupTour(model)
|
browser.setupTour(model)
|
||||||
browser.setupGrowlNotifications(model) unless model.get('_view.mobileDevice')
|
browser.setupGrowlNotifications(model) unless model.get('_view.mobileDevice')
|
||||||
|
|
||||||
|
party.app(exports, model)
|
||||||
|
|
||||||
require('../server/private').app(exports, model)
|
require('../server/private').app(exports, model)
|
||||||
|
|
||||||
|
require('./debug').app(exports, model)
|
||||||
|
|
||||||
user.on 'set', 'tasks.*.completed', (i, completed, previous, isLocal, passed) ->
|
user.on 'set', 'tasks.*.completed', (i, completed, previous, isLocal, passed) ->
|
||||||
return if passed? && passed.cron # Don't do this stuff on cron
|
return if passed? && passed.cron # Don't do this stuff on cron
|
||||||
direction = () ->
|
direction = () ->
|
||||||
|
|
@ -168,14 +163,14 @@ ready (model) ->
|
||||||
# fix when query subscriptions implemented properly
|
# fix when query subscriptions implemented properly
|
||||||
$('[rel=tooltip]').tooltip('hide')
|
$('[rel=tooltip]').tooltip('hide')
|
||||||
|
|
||||||
ids = user.get("#{type}Ids")
|
ids = user.get("idLists.#{type}")
|
||||||
ids.splice(ids.indexOf(id),1)
|
ids.splice(ids.indexOf(id),1)
|
||||||
user.del('tasks.'+id)
|
user.del('tasks.'+id)
|
||||||
user.set("#{type}Ids", ids)
|
user.set("idLists.#{type}", ids)
|
||||||
|
|
||||||
|
|
||||||
exports.clearCompleted = (e, el) ->
|
exports.clearCompleted = (e, el) ->
|
||||||
todoIds = user.get('todoIds')
|
todoIds = user.get('idLists.todo')
|
||||||
removed = false
|
removed = false
|
||||||
_.each model.get('_todoList'), (task) ->
|
_.each model.get('_todoList'), (task) ->
|
||||||
if task.completed
|
if task.completed
|
||||||
|
|
@ -183,7 +178,7 @@ ready (model) ->
|
||||||
user.del('tasks.'+task.id)
|
user.del('tasks.'+task.id)
|
||||||
todoIds.splice(todoIds.indexOf(task.id), 1)
|
todoIds.splice(todoIds.indexOf(task.id), 1)
|
||||||
if removed
|
if removed
|
||||||
user.set('todoIds', todoIds)
|
user.set('idLists.todo', todoIds)
|
||||||
|
|
||||||
exports.toggleDay = (e, el) ->
|
exports.toggleDay = (e, el) ->
|
||||||
task = model.at(e.target)
|
task = model.at(e.target)
|
||||||
|
|
@ -226,11 +221,11 @@ ready (model) ->
|
||||||
#TODO: this should be working but it's not. so instead, i'm passing all needed values as data-attrs
|
#TODO: this should be working but it's not. so instead, i'm passing all needed values as data-attrs
|
||||||
# item = model.at(e.target)
|
# item = model.at(e.target)
|
||||||
|
|
||||||
money = user.get 'stats.money'
|
gp = user.get 'stats.gp'
|
||||||
[type, value, index] = [ $(el).attr('data-type'), $(el).attr('data-value'), $(el).attr('data-index') ]
|
[type, value, index] = [ $(el).attr('data-type'), $(el).attr('data-value'), $(el).attr('data-index') ]
|
||||||
|
|
||||||
return if money < value
|
return if gp < value
|
||||||
user.set 'stats.money', money - value
|
user.set 'stats.gp', gp - value
|
||||||
if type == 'armor'
|
if type == 'armor'
|
||||||
user.set 'items.armor', index
|
user.set 'items.armor', index
|
||||||
model.set '_view.items.armor', content.items.armor[parseInt(index) + 1]
|
model.set '_view.items.armor', content.items.armor[parseInt(index) + 1]
|
||||||
|
|
@ -254,7 +249,7 @@ ready (model) ->
|
||||||
# Reset stats
|
# Reset stats
|
||||||
batch.set 'stats.hp', 50
|
batch.set 'stats.hp', 50
|
||||||
batch.set 'stats.lvl', 1
|
batch.set 'stats.lvl', 1
|
||||||
batch.set 'stats.money', 0
|
batch.set 'stats.gp', 0
|
||||||
batch.set 'stats.exp', 0
|
batch.set 'stats.exp', 0
|
||||||
|
|
||||||
# Reset items
|
# Reset items
|
||||||
|
|
@ -276,40 +271,15 @@ ready (model) ->
|
||||||
batch.startTransaction()
|
batch.startTransaction()
|
||||||
taskTypes = ['habit', 'daily', 'todo', 'reward']
|
taskTypes = ['habit', 'daily', 'todo', 'reward']
|
||||||
batch.set 'tasks', {}
|
batch.set 'tasks', {}
|
||||||
_.each taskTypes, (type) -> batch.set "#{type}Ids", []
|
_.each taskTypes, (type) -> batch.set "idLists.#{type}", []
|
||||||
batch.set 'balance', 2 if user.get('balance') < 2 #only if they haven't manually bought tokens
|
batch.set 'balance', 2 if user.get('balance') < 2 #only if they haven't manually bought tokens
|
||||||
revive(batch, true)
|
revive(batch)
|
||||||
batch.commit()
|
batch.commit()
|
||||||
resetDom(model)
|
browser.resetDom(model)
|
||||||
|
|
||||||
exports.closeKickstarterNofitication = (e, el) ->
|
exports.closeKickstarterNofitication = (e, el) -> user.set('flags.kickstarter', 'hide')
|
||||||
user.set('notifications.kickstarter', 'hide')
|
|
||||||
|
|
||||||
exports.setMale = -> user.set('preferences.gender', 'm')
|
exports.setMale = -> user.set('preferences.gender', 'm')
|
||||||
exports.setFemale = -> user.set('preferences.gender', 'f')
|
exports.setFemale = -> user.set('preferences.gender', 'f')
|
||||||
exports.setArmorsetV1 = -> user.set('preferences.armorSet', 'v1')
|
exports.setArmorsetV1 = -> user.set('preferences.armorSet', 'v1')
|
||||||
exports.setArmorsetV2 = -> user.set('preferences.armorSet', 'v2')
|
exports.setArmorsetV2 = -> user.set('preferences.armorSet', 'v2')
|
||||||
|
|
||||||
exports.addParty = ->
|
|
||||||
id = model.get('_newPartyMember').replace(/[\s"]/g, '')
|
|
||||||
debugger
|
|
||||||
return if _.isEmpty(id)
|
|
||||||
if user.get('party').indexOf(id) != -1
|
|
||||||
model.set "_view.addPartyError", "#{id} already in party."
|
|
||||||
return
|
|
||||||
query = model.query('users').party([id])
|
|
||||||
model.fetch query, (err, users) ->
|
|
||||||
partyMember = users.at(0).get()
|
|
||||||
if partyMember?.id?
|
|
||||||
user.push('party', id)
|
|
||||||
$('#add-party-modal').modal('hide')
|
|
||||||
window.location.reload() #TODO break old subscription, setup new subscript, remove this reload
|
|
||||||
model.set '_newPartyMember', ''
|
|
||||||
else
|
|
||||||
model.set "_view.addPartyError", "User with id #{id} not found."
|
|
||||||
|
|
||||||
exports.emulateNextDay = ->
|
|
||||||
yesterday = +moment().subtract('days', 1).toDate()
|
|
||||||
user.set 'lastCron', yesterday
|
|
||||||
window.location.reload()
|
|
||||||
|
|
||||||
|
|
|
||||||
150
src/app/party.coffee
Normal file
150
src/app/party.coffee
Normal 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 = ->
|
||||||
|
|
@ -5,62 +5,60 @@ lodash = require 'lodash'
|
||||||
derby = require 'derby'
|
derby = require 'derby'
|
||||||
|
|
||||||
userSchema =
|
userSchema =
|
||||||
|
# _id
|
||||||
|
stats: { gp: 0, exp: 0, lvl: 1, hp: 50 }
|
||||||
|
party: { current: null, invitation: null }
|
||||||
|
items: { armor: 0, weapon: 0 }
|
||||||
|
preferences: { gender: 'm', armorSet: 'v1' }
|
||||||
|
idLists:
|
||||||
|
habit: []
|
||||||
|
daily: []
|
||||||
|
todo: []
|
||||||
|
reward: []
|
||||||
|
apiToken: null # set in newUserObject below
|
||||||
lastCron: 'new' #this will be replaced with `+new Date` on first run
|
lastCron: 'new' #this will be replaced with `+new Date` on first run
|
||||||
balance: 2
|
balance: 2
|
||||||
stats: { money: 0, exp: 0, lvl: 1, hp: 50 }
|
|
||||||
items: { itemsEnabled: false, armor: 0, weapon: 0 }
|
|
||||||
notifications: { kickstarter: 'show' }
|
|
||||||
preferences: { gender: 'm', armorSet: 'v1' }
|
|
||||||
flags: { partyEnabled: false }
|
|
||||||
party: []
|
|
||||||
tasks: {}
|
tasks: {}
|
||||||
habitIds: []
|
flags:
|
||||||
dailyIds: []
|
partyEnabled: false
|
||||||
todoIds: []
|
itemsEnabled: false
|
||||||
rewardIds: []
|
kickstarter: 'show'
|
||||||
|
# ads: 'show' # added on registration
|
||||||
|
|
||||||
module.exports.newUserObject = ->
|
module.exports.newUserObject = ->
|
||||||
# deep clone, else further new users get duplicate objects
|
# deep clone, else further new users get duplicate objects
|
||||||
newUser = require('lodash').cloneDeep userSchema
|
newUser = lodash.cloneDeep userSchema
|
||||||
|
newUser.apiToken = derby.uuid()
|
||||||
for task in content.defaultTasks
|
for task in content.defaultTasks
|
||||||
guid = task.id = require('racer').uuid()
|
guid = task.id = derby.uuid()
|
||||||
newUser.tasks[guid] = task
|
newUser.tasks[guid] = task
|
||||||
switch task.type
|
switch task.type
|
||||||
when 'habit' then newUser.habitIds.push guid
|
when 'habit' then newUser.idLists.habit.push guid
|
||||||
when 'daily' then newUser.dailyIds.push guid
|
when 'daily' then newUser.idLists.daily.push guid
|
||||||
when 'todo' then newUser.todoIds.push guid
|
when 'todo' then newUser.idLists.todo.push guid
|
||||||
when 'reward' then newUser.rewardIds.push guid
|
when 'reward' then newUser.idLists.reward.push guid
|
||||||
return newUser
|
return newUser
|
||||||
|
|
||||||
module.exports.updateUser = (batch) ->
|
module.exports.updateUser = (batch) ->
|
||||||
user = batch.user
|
user = batch.user
|
||||||
|
obj = batch.obj()
|
||||||
|
|
||||||
batch.set('notifications.kickstarter', 'show') unless user.get('notifications.kickstarter')
|
batch.set('apiToken', derby.uuid()) unless obj.apiToken
|
||||||
batch.set('party', []) unless !_.isEmpty(user.get('party'))
|
|
||||||
|
|
||||||
# Preferences, including API key
|
|
||||||
# Some side-stepping to avoid unecessary set (one day, model.update... one day..)
|
|
||||||
currentPrefs = _.clone user.get('preferences')
|
|
||||||
mergedPrefs = _.defaults currentPrefs, { gender: 'm', armorSet: 'v1', api_token: derby.uuid() }
|
|
||||||
batch.set('preferences', mergedPrefs)
|
|
||||||
|
|
||||||
## Task List Cleanup
|
## Task List Cleanup
|
||||||
# FIXME temporary hack to fix lists (Need to figure out why these are happening)
|
# FIXME temporary hack to fix lists (Need to figure out why these are happening)
|
||||||
# FIXME consolidate these all under user.listIds so we can set them en-masse
|
tasks = obj.tasks
|
||||||
tasks = user.get('tasks')
|
|
||||||
_.each ['habit','daily','todo','reward'], (type) ->
|
_.each ['habit','daily','todo','reward'], (type) ->
|
||||||
path = "#{type}Ids"
|
|
||||||
|
|
||||||
# 1. remove duplicates
|
# 1. remove duplicates
|
||||||
# 2. restore missing zombie tasks back into list
|
# 2. restore missing zombie tasks back into list
|
||||||
taskIds = _.pluck( _.where(tasks, {type:type}), 'id')
|
taskIds = _.pluck( _.where(tasks, {type:type}), 'id')
|
||||||
union = _.union user.get(path), taskIds
|
union = _.union obj.idLists[type], taskIds
|
||||||
|
|
||||||
# 2. remove empty (grey) tasks
|
# 2. remove empty (grey) tasks
|
||||||
preened = _.filter(union, (val) -> _.contains(taskIds, val))
|
preened = _.filter(union, (val) -> _.contains(taskIds, val))
|
||||||
|
|
||||||
# There were indeed issues found, set the new list
|
# There were indeed issues found, set the new list
|
||||||
batch.set(path, preened) # if _.difference(preened, userObj[path]).length != 0
|
batch.set("idLists.#{type}", preened) # if _.difference(preened, userObj[path]).length != 0
|
||||||
|
|
||||||
module.exports.BatchUpdate = BatchUpdate = (model) ->
|
module.exports.BatchUpdate = BatchUpdate = (model) ->
|
||||||
user = model.at("_user")
|
user = model.at("_user")
|
||||||
|
|
|
||||||
|
|
@ -75,24 +75,24 @@ updateStats = (newStats, batch) ->
|
||||||
|
|
||||||
if newStats.exp?
|
if newStats.exp?
|
||||||
# level up & carry-over exp
|
# level up & carry-over exp
|
||||||
tnl = user.get '_tnl'
|
tnl = model.get '_tnl'
|
||||||
if newStats.exp >= tnl
|
if newStats.exp >= tnl
|
||||||
newStats.exp -= tnl
|
newStats.exp -= tnl
|
||||||
obj.stats.lvl++
|
obj.stats.lvl++
|
||||||
obj.stats.hp = 50
|
obj.stats.hp = 50
|
||||||
if !obj.items.itemsEnabled and obj.stats.lvl >= 2
|
if !obj.flags.itemsEnabled and obj.stats.lvl >= 2
|
||||||
# Set to object, then also send to browser right away to get model.on() subscription notification
|
# Set to object, then also send to browser right away to get model.on() subscription notification
|
||||||
batch.set 'items.itemsEnabled', true
|
batch.set 'flags.itemsEnabled', true
|
||||||
obj.items.itemsEnabled = true
|
obj.flags.itemsEnabled = true
|
||||||
# if !obj.flags.partyEnabled and obj.stats.lvl >= 3
|
if !obj.flags.partyEnabled and obj.stats.lvl >= 3
|
||||||
# batch.set 'flags.partyEnabled', true
|
batch.set 'flags.partyEnabled', true
|
||||||
# obj.flags.partyEnabled = true
|
obj.flags.partyEnabled = true
|
||||||
obj.stats.exp = newStats.exp
|
obj.stats.exp = newStats.exp
|
||||||
|
|
||||||
if newStats.money?
|
if newStats.gp?
|
||||||
#FIXME what was I doing here? I can't remember, money isn't defined
|
#FIXME what was I doing here? I can't remember, gp isn't defined
|
||||||
money = 0.0 if (!money? or money<0)
|
gp = 0.0 if (!gp? or gp<0)
|
||||||
obj.stats.money = newStats.money
|
obj.stats.gp = newStats.gp
|
||||||
|
|
||||||
# {taskId} task you want to score
|
# {taskId} task you want to score
|
||||||
# {direction} 'up' or 'down'
|
# {direction} 'up' or 'down'
|
||||||
|
|
@ -106,7 +106,7 @@ score = (taskId, direction, times, batch, cron) ->
|
||||||
batch.startTransaction()
|
batch.startTransaction()
|
||||||
obj = batch.obj()
|
obj = batch.obj()
|
||||||
|
|
||||||
{money, hp, exp, lvl} = obj.stats
|
{gp, hp, exp, lvl} = obj.stats
|
||||||
|
|
||||||
taskPath = "tasks.#{taskId}"
|
taskPath = "tasks.#{taskId}"
|
||||||
taskObj = obj.tasks[taskId]
|
taskObj = obj.tasks[taskId]
|
||||||
|
|
@ -127,7 +127,7 @@ score = (taskId, direction, times, batch, cron) ->
|
||||||
addPoints = ->
|
addPoints = ->
|
||||||
modified = expModifier(delta)
|
modified = expModifier(delta)
|
||||||
exp += modified
|
exp += modified
|
||||||
money += modified
|
gp += modified
|
||||||
|
|
||||||
subtractPoints = ->
|
subtractPoints = ->
|
||||||
modified = hpModifier(delta)
|
modified = hpModifier(delta)
|
||||||
|
|
@ -162,17 +162,17 @@ score = (taskId, direction, times, batch, cron) ->
|
||||||
# Don't adjust values for rewards
|
# Don't adjust values for rewards
|
||||||
calculateDelta(false)
|
calculateDelta(false)
|
||||||
# purchase item
|
# purchase item
|
||||||
money -= Math.abs(taskObj.value)
|
gp -= Math.abs(taskObj.value)
|
||||||
num = parseFloat(taskObj.value).toFixed(2)
|
num = parseFloat(taskObj.value).toFixed(2)
|
||||||
# if too expensive, reduce health & zero money
|
# if too expensive, reduce health & zero gp
|
||||||
if money < 0
|
if gp < 0
|
||||||
hp += money # hp - money difference
|
hp += gp # hp - gp difference
|
||||||
money = 0
|
gp = 0
|
||||||
|
|
||||||
taskObj.value = value
|
taskObj.value = value
|
||||||
batch.set "#{taskPath}.value", taskObj.value
|
batch.set "#{taskPath}.value", taskObj.value
|
||||||
origStats = _.clone obj.stats
|
origStats = _.clone obj.stats
|
||||||
updateStats {hp: hp, exp: exp, money: money}, batch
|
updateStats {hp: hp, exp: exp, gp: gp}, batch
|
||||||
if commit
|
if commit
|
||||||
# newStats / origStats is a glorious hack to trick Derby into seeing the change in model.on(*)
|
# newStats / origStats is a glorious hack to trick Derby into seeing the change in model.on(*)
|
||||||
newStats = _.clone batch.obj().stats
|
newStats = _.clone batch.obj().stats
|
||||||
|
|
@ -186,7 +186,7 @@ score = (taskId, direction, times, batch, cron) ->
|
||||||
At end of day, add value to all incomplete Daily & Todo tasks (further incentive)
|
At end of day, add value to all incomplete Daily & Todo tasks (further incentive)
|
||||||
For incomplete Dailys, deduct experience
|
For incomplete Dailys, deduct experience
|
||||||
###
|
###
|
||||||
cron = (resetDom_cb) ->
|
cron = () ->
|
||||||
today = +new Date
|
today = +new Date
|
||||||
daysPassed = helpers.daysBetween(today, user.get('lastCron'))
|
daysPassed = helpers.daysBetween(today, user.get('lastCron'))
|
||||||
if daysPassed > 0
|
if daysPassed > 0
|
||||||
|
|
@ -245,7 +245,7 @@ cron = (resetDom_cb) ->
|
||||||
batch.setStats()
|
batch.setStats()
|
||||||
batch.set('history', obj.history)
|
batch.set('history', obj.history)
|
||||||
batch.commit()
|
batch.commit()
|
||||||
resetDom_cb(model)
|
browser.resetDom(model)
|
||||||
setTimeout (-> user.set 'stats.hp', hpAfter), 1000 # animate hp loss
|
setTimeout (-> user.set 'stats.hp', hpAfter), 1000 # animate hp loss
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ serverError = require './serverError'
|
||||||
MongoStore = require('connect-mongo')(express)
|
MongoStore = require('connect-mongo')(express)
|
||||||
auth = require 'derby-auth'
|
auth = require 'derby-auth'
|
||||||
priv = require './private'
|
priv = require './private'
|
||||||
|
habitrpgStore = require('./store')
|
||||||
|
|
||||||
## Run server cron ##
|
## Run server cron ##
|
||||||
require('./cron').deleteStaleAccounts()
|
require('./cron').deleteStaleAccounts()
|
||||||
|
|
@ -31,7 +32,6 @@ derby.use(require 'racer-db-mongo')
|
||||||
store = derby.createStore
|
store = derby.createStore
|
||||||
db: {type: 'Mongo', uri: process.env.NODE_DB_URI, safe:true}
|
db: {type: 'Mongo', uri: process.env.NODE_DB_URI, safe:true}
|
||||||
listen: server
|
listen: server
|
||||||
require('./store')(store) #setup custom accessControl
|
|
||||||
|
|
||||||
ONE_YEAR = 1000 * 60 * 60 * 24 * 365
|
ONE_YEAR = 1000 * 60 * 60 * 24 * 365
|
||||||
root = path.dirname path.dirname __dirname
|
root = path.dirname path.dirname __dirname
|
||||||
|
|
@ -48,6 +48,7 @@ options =
|
||||||
domain: process.env.BASE_URL || 'http://localhost:3000'
|
domain: process.env.BASE_URL || 'http://localhost:3000'
|
||||||
allowPurl: true
|
allowPurl: true
|
||||||
schema: require('../app/schema').newUserObject()
|
schema: require('../app/schema').newUserObject()
|
||||||
|
customAccessControl: habitrpgStore.customAccessControl
|
||||||
|
|
||||||
mongo_store = new MongoStore {url: process.env.NODE_DB_URI}, ->
|
mongo_store = new MongoStore {url: process.env.NODE_DB_URI}, ->
|
||||||
expressApp
|
expressApp
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,6 @@ module.exports.app = (appExports, model) ->
|
||||||
obj = model.get('_user')
|
obj = model.get('_user')
|
||||||
batch.set 'balance', obj.balance-1
|
batch.set 'balance', obj.balance-1
|
||||||
_.each obj.tasks, (task) -> batch.set("tasks.#{task.id}.value", 0) unless task.type == 'reward'
|
_.each obj.tasks, (task) -> batch.set("tasks.#{task.id}.value", 0) unless task.type == 'reward'
|
||||||
console.log(obj)
|
|
||||||
batch.commit()
|
batch.commit()
|
||||||
|
|
||||||
module.exports.routes = (expressApp) ->
|
module.exports.routes = (expressApp) ->
|
||||||
|
|
@ -52,6 +51,7 @@ module.exports.routes = (expressApp) ->
|
||||||
else
|
else
|
||||||
model = req.getModel()
|
model = req.getModel()
|
||||||
userId = model.session.userId
|
userId = model.session.userId
|
||||||
|
req._isServer = true
|
||||||
model.fetch "users.#{userId}", (err, user) ->
|
model.fetch "users.#{userId}", (err, user) ->
|
||||||
model.ref '_user', "users.#{userId}"
|
model.ref '_user', "users.#{userId}"
|
||||||
model.set('_user.balance', model.get('_user.balance')+5)
|
model.set('_user.balance', model.get('_user.balance')+5)
|
||||||
|
|
|
||||||
|
|
@ -17,40 +17,40 @@ module.exports = (expressApp, root, derby) ->
|
||||||
deprecatedMessage = 'This API is no longer supported, see https://github.com/lefnire/habitrpg/wiki/API for new protocol'
|
deprecatedMessage = 'This API is no longer supported, see https://github.com/lefnire/habitrpg/wiki/API for new protocol'
|
||||||
expressApp.get '/:uid/up/:score?', (req, res) -> res.send(500, deprecatedMessage)
|
expressApp.get '/:uid/up/:score?', (req, res) -> res.send(500, deprecatedMessage)
|
||||||
expressApp.get '/:uid/down/:score?', (req, res) -> res.send(500, deprecatedMessage)
|
expressApp.get '/:uid/down/:score?', (req, res) -> res.send(500, deprecatedMessage)
|
||||||
expressApp.post '/v1/users/:uid/tasks/:taskId/:direction', (req, res) -> res.send(500, deprecatedMessage)
|
expressApp.post '/users/:uid/tasks/:taskId/:direction', (req, res) -> res.send(500, deprecatedMessage)
|
||||||
|
|
||||||
# ---------- v1 API ------------
|
# ---------- v1 API ------------
|
||||||
|
|
||||||
###
|
###
|
||||||
v1 API. Requires user-id and api_token, task-id, direction. Test with:
|
v1 API. Requires user-id and apiToken, task-id, direction. Test with:
|
||||||
curl -X POST -H "Content-Type:application/json" -d '{"api_token":"{TOKEN}"}' localhost:3000/v1/users/{UID}/tasks/productivity/up
|
curl -X POST -H "Content-Type:application/json" -d '{"apiToken":"{TOKEN}"}' localhost:3000/v1/users/{UID}/tasks/productivity/up
|
||||||
###
|
###
|
||||||
# TODO /v1/..
|
expressApp.post '/v1/users/:uid/tasks/:taskId/:direction', (req, res) ->
|
||||||
expressApp.post '/users/:uid/tasks/:taskId/:direction', (req, res) ->
|
|
||||||
{uid, taskId, direction} = req.params
|
{uid, taskId, direction} = req.params
|
||||||
{api_token, title, service, icon} = req.body
|
{apiToken, title, service, icon} = req.body
|
||||||
console.log {params:req.params, body:req.body} if process.env.NODE_ENV == 'development'
|
console.log {params:req.params, body:req.body} if process.env.NODE_ENV == 'development'
|
||||||
|
|
||||||
# Send error responses for improper API call
|
# Send error responses for improper API call
|
||||||
return res.send(500, 'request body "api_token" required') unless api_token
|
return res.send(500, 'request body "apiToken" required') unless apiToken
|
||||||
return res.send(500, ':uid required') unless uid
|
return res.send(500, ':uid required') unless uid
|
||||||
return res.send(500, ':taskId required') unless taskId
|
return res.send(500, ':taskId required') unless taskId
|
||||||
return res.send(500, ":direction must be 'up' or 'down'") unless direction in ['up','down']
|
return res.send(500, ":direction must be 'up' or 'down'") unless direction in ['up','down']
|
||||||
|
|
||||||
model = req.getModel()
|
model = req.getModel()
|
||||||
model.fetch model.query('users').withIdAndToken(uid, api_token), (err, result) ->
|
req._isServer = true
|
||||||
|
model.fetch model.query('users').withIdAndToken(uid, apiToken), (err, result) ->
|
||||||
return res.send(500, err) if err
|
return res.send(500, err) if err
|
||||||
user = result.at(0)
|
user = result.at(0)
|
||||||
userObj = user.get()
|
userObj = user.get()
|
||||||
if _.isEmpty(userObj)
|
if _.isEmpty(userObj)
|
||||||
return res.send(500, "User with uid=#{uid}, token=#{api_token} not found. Make sure you're not using your username, but your User Id")
|
return res.send(500, "User with uid=#{uid}, token=#{apiToken} not found. Make sure you're not using your username, but your User Id")
|
||||||
|
|
||||||
model.ref('_user', user)
|
model.ref('_user', user)
|
||||||
|
|
||||||
# Create task if doesn't exist
|
# Create task if doesn't exist
|
||||||
# TODO add service & icon to task
|
# TODO add service & icon to task
|
||||||
unless model.get("_user.tasks.#{taskId}")
|
unless model.get("_user.tasks.#{taskId}")
|
||||||
model.refList "_habitList", "_user.tasks", "_user.habitIds"
|
model.refList "_habitList", "_user.tasks", "_user.idLists.habit"
|
||||||
model.at('_habitList').push {
|
model.at('_habitList').push {
|
||||||
id: taskId
|
id: taskId
|
||||||
type: 'habit'
|
type: 'habit'
|
||||||
|
|
@ -59,7 +59,9 @@ module.exports = (expressApp, root, derby) ->
|
||||||
up: true
|
up: true
|
||||||
down: true
|
down: true
|
||||||
notes: "This task was created by a third-party service. Feel free to edit, it won't harm the connection to that service. Additionally, multiple services may piggy-back off this task."
|
notes: "This task was created by a third-party service. Feel free to edit, it won't harm the connection to that service. Additionally, multiple services may piggy-back off this task."
|
||||||
}
|
}, (a,b,c) ->
|
||||||
|
console.log {a:a,b:b,c:c}
|
||||||
|
|
||||||
|
|
||||||
scoring.setModel(model)
|
scoring.setModel(model)
|
||||||
delta = scoring.score(taskId, direction)
|
delta = scoring.score(taskId, direction)
|
||||||
|
|
|
||||||
|
|
@ -2,39 +2,92 @@
|
||||||
Setup read / write access
|
Setup read / write access
|
||||||
@param store
|
@param store
|
||||||
###
|
###
|
||||||
module.exports = (store) ->
|
|
||||||
|
|
||||||
# store.writeAccess "*", "users.*.balance", (id, newBalance, next) ->
|
module.exports.customAccessControl = (store) ->
|
||||||
# return unless @session and @session.userId # https://github.com/codeparty/racer/issues/37
|
userAccess(store)
|
||||||
# purchasingSomethingOnClient = newBalance < this.session.req._racerModel.get("users.#{id}.balance")
|
partySystem(store)
|
||||||
# isServer = not @req.socket
|
REST(store)
|
||||||
# next(purchasingSomethingOnClient or isServer)
|
|
||||||
|
###
|
||||||
|
General user access
|
||||||
|
###
|
||||||
|
userAccess = (store) ->
|
||||||
|
|
||||||
|
store.readPathAccess "users.*", -> # captures, next) ->
|
||||||
|
#return unless @session and @session.userId # https://github.com/codeparty/racer/issues/37
|
||||||
|
uid = arguments[0]
|
||||||
|
next = arguments[arguments.length - 1]
|
||||||
|
next (uid is @session.userId) or @session.req?._isServer
|
||||||
|
|
||||||
|
store.writeAccess "*", "users.*", -> # captures, value, next) ->
|
||||||
|
#return unless @session and @session.userId # https://github.com/codeparty/racer/issues/37
|
||||||
|
[captures, next] = [arguments[0].split('.'), arguments[arguments.length-1]]
|
||||||
|
uid = captures.shift()
|
||||||
|
attrPath = captures.join('.') # new array shifted left, after shift() was run
|
||||||
|
|
||||||
|
# public access to users.*.party.invitation (TODO, lock down a bit more)
|
||||||
|
if (attrPath == 'party.invitation')
|
||||||
|
return next(true)
|
||||||
|
|
||||||
|
# Same session (user.id = this.session.userId)
|
||||||
|
if (uid is @session.userId) or @session.req?._isServer
|
||||||
|
return next(true)
|
||||||
|
|
||||||
|
next(false)
|
||||||
|
|
||||||
|
store.writeAccess "*", "users.*.balance", (id, newBalance, next) ->
|
||||||
|
#return unless @session and @session.userId # https://github.com/codeparty/racer/issues/37
|
||||||
|
oldBalance = @session.req._racerModel?.get("users.#{id}.balance") || 0
|
||||||
|
purchasingSomethingOnClient = newBalance < oldBalance
|
||||||
|
next(purchasingSomethingOnClient or @session.req?._isServer)
|
||||||
|
|
||||||
store.writeAccess "*", "users.*.flags.ads", -> # captures, value, next ->
|
store.writeAccess "*", "users.*.flags.ads", -> # captures, value, next ->
|
||||||
return unless @session and @session.userId # https://github.com/codeparty/racer/issues/37
|
#return unless @session and @session.userId # https://github.com/codeparty/racer/issues/37
|
||||||
next = arguments[arguments.length - 1]
|
next = arguments[arguments.length - 1]
|
||||||
isServer = not @req.socket
|
next(@session.req?._isServer)
|
||||||
next(isServer)
|
|
||||||
|
|
||||||
###
|
|
||||||
Get user with API token
|
###
|
||||||
###
|
REST
|
||||||
store.query.expose "users", "withIdAndToken", (id, api_token) ->
|
Get user with API token
|
||||||
|
###
|
||||||
|
REST = (store) ->
|
||||||
|
store.query.expose "users", "withIdAndToken", (id, apiToken) ->
|
||||||
@where("id").equals(id)
|
@where("id").equals(id)
|
||||||
.where('preferences.api_token').equals(api_token)
|
.where('apiToken').equals(apiToken)
|
||||||
.limit(1)
|
.limit(1)
|
||||||
|
|
||||||
store.queryAccess "users", "withIdAndToken", (id, token, next) ->
|
store.queryAccess "users", "withIdAndToken", (id, apiToken, next) ->
|
||||||
return next(false) unless @session and @session.userId # https://github.com/codeparty/racer/issues/37
|
#return next(false) unless @session and @session.userId # https://github.com/codeparty/racer/issues/37
|
||||||
isServer = not @req.socket
|
next(true) # only user has id & token
|
||||||
next(isServer)
|
|
||||||
|
|
||||||
###
|
|
||||||
Party permissions
|
###
|
||||||
###
|
Party permissions
|
||||||
|
###
|
||||||
|
partySystem = (store) ->
|
||||||
store.query.expose "users", "party", (ids) ->
|
store.query.expose "users", "party", (ids) ->
|
||||||
@where("id").within(ids)
|
@where("id").within(ids)
|
||||||
.only('stats', 'preferences.gender', 'preferences.armorSet', 'items', 'auth.local.username', 'auth.facebook.displayName')
|
.only('stats',
|
||||||
|
'items',
|
||||||
|
'party',
|
||||||
|
'preferences.gender',
|
||||||
|
'preferences.armorSet',
|
||||||
|
'auth.local.username',
|
||||||
|
'auth.facebook.displayName')
|
||||||
|
|
||||||
store.queryAccess "users", "party", (ids, next) ->
|
store.queryAccess "users", "party", (ids, next) ->
|
||||||
next(true) # no harm in public user stats
|
next(true) # no harm in public user stats
|
||||||
|
|
||||||
|
store.query.expose "parties", "withId", (id) ->
|
||||||
|
@where("id").equals(id)
|
||||||
|
store.queryAccess "parties", "withId", (id, next) ->
|
||||||
|
next(true)
|
||||||
|
|
||||||
|
store.readPathAccess "parties.*", ->
|
||||||
|
next = arguments[arguments.length-1]
|
||||||
|
next(true)
|
||||||
|
|
||||||
|
store.writeAccess "*", "parties.*", ->
|
||||||
|
next = arguments[arguments.length-1]
|
||||||
|
next(true)
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
<pre class=prettyprint>{_user.id}</pre>
|
<pre class=prettyprint>{_user.id}</pre>
|
||||||
|
|
||||||
<h6>API Token</h6>
|
<h6>API Token</h6>
|
||||||
<pre class=prettyprint>{_user.preferences.api_token}</pre>
|
<pre class=prettyprint>{_user.apiToken}</pre>
|
||||||
|
|
||||||
<hr/>
|
<hr/>
|
||||||
<h4>Gender</h4>
|
<h4>Gender</h4>
|
||||||
|
|
@ -40,6 +40,9 @@
|
||||||
</label>
|
</label>
|
||||||
{/}
|
{/}
|
||||||
|
|
||||||
|
<hr/>
|
||||||
|
<a class='btn btn-danger' data-target="#reset-modal" data-toggle="modal">Reset</a>
|
||||||
|
|
||||||
<@footer>
|
<@footer>
|
||||||
<button class="btn" data-dismiss="modal" aria-hidden="true">Close</button>
|
<button class="btn" data-dismiss="modal" aria-hidden="true">Close</button>
|
||||||
</@footer>
|
</@footer>
|
||||||
|
|
@ -49,9 +52,9 @@
|
||||||
<p>This resets your entire account - your tasks will be deleted and your character will start over.</p>
|
<p>This resets your entire account - your tasks will be deleted and your character will start over.</p>
|
||||||
<p>This is highly discouraged because you'll lose historical data, which is useful for graphing your progress over time. However, some people find it useful in the beginning after playing with the app for a while.</p>
|
<p>This is highly discouraged because you'll lose historical data, which is useful for graphing your progress over time. However, some people find it useful in the beginning after playing with the app for a while.</p>
|
||||||
<@footer>
|
<@footer>
|
||||||
<button class="btn" data-dismiss="modal" aria-hidden="true">Close</button>
|
<button class="btn" data-dismiss="modal" aria-hidden="true">Close</button>
|
||||||
<button data-dismiss="modal" x-bind=click:reset class="btn btn-danger btn-large">Reset</button>
|
<button data-dismiss="modal" x-bind=click:reset class="btn btn-danger btn-large">Reset</button>
|
||||||
</@footer>
|
</@footer>
|
||||||
</app:myModal>
|
</app:myModal>
|
||||||
|
|
||||||
<app:myModal modalId="why-ads-modal" header="Why Ads?">
|
<app:myModal modalId="why-ads-modal" header="Why Ads?">
|
||||||
|
|
@ -100,7 +103,7 @@
|
||||||
<app:userTokens/>
|
<app:userTokens/>
|
||||||
<p>Highly discouraged because red tasks provide good incentive to improve (<a target="_blank" href="https://github.com/lefnire/habitrpg#all-my-tasks-are-red-im-dying-too-fast">read more</a>). However, this becomes necessary after long bouts of bad habits.</p>
|
<p>Highly discouraged because red tasks provide good incentive to improve (<a target="_blank" href="https://github.com/lefnire/habitrpg#all-my-tasks-are-red-im-dying-too-fast">read more</a>). However, this becomes necessary after long bouts of bad habits.</p>
|
||||||
<@footer>
|
<@footer>
|
||||||
{#if lessThan(_user.balance,1)}
|
{#if lt(_user.balance,1)}
|
||||||
<a data-dismiss="modal" x-bind="click:showStripe" class="btn btn-danger btn-large">Buy More Tokens</a><span class='token-cost'>Not enough tokens</span>
|
<a data-dismiss="modal" x-bind="click:showStripe" class="btn btn-danger btn-large">Buy More Tokens</a><span class='token-cost'>Not enough tokens</span>
|
||||||
{else}
|
{else}
|
||||||
<a data-dismiss="modal" x-bind=click:buyReroll class="btn btn-danger btn-large">Re-Roll</a><span class='token-cost'>4 Tokens</span>
|
<a data-dismiss="modal" x-bind=click:buyReroll class="btn btn-danger btn-large">Re-Roll</a><span class='token-cost'>4 Tokens</span>
|
||||||
|
|
@ -108,14 +111,46 @@
|
||||||
</@footer>
|
</@footer>
|
||||||
</app:myModal>
|
</app:myModal>
|
||||||
|
|
||||||
<app:myModal modalId="add-party-modal" header="Add Party Member">
|
<app:myModal modalId="party-modal">
|
||||||
<form x-bind="submit: addParty">
|
|
||||||
{#if _view.addPartyError}
|
{#if _user.party.current}
|
||||||
<div class='alert alert-danger'>{_view.addPartyError}</div>
|
<h2>{_party.name}</h2>
|
||||||
|
<table class="table table-striped">
|
||||||
|
{#each _partyMembers as :member}
|
||||||
|
<tr><td>{username(:member.auth)}</td><td>({:member.id})</td></tr>
|
||||||
|
{/}
|
||||||
|
</table>
|
||||||
|
<form class=form-inline x-bind="submit: partyInvite">
|
||||||
|
{#if _view.partyError}
|
||||||
|
<div class='alert alert-danger'>{_view.partyError}</div>
|
||||||
{/}
|
{/}
|
||||||
<input type="text" class="input-medium search-query" value="{_newPartyMember}">
|
<div class='control-group'>
|
||||||
<input type="submit" class="btn" value="Add" />
|
<input type="text" class="input-medium" placeholder="User Id" value="{_newPartyMember}">
|
||||||
</form>
|
<input type="submit" class="btn" value="Invite" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<a class='btn btn-danger' x-bind="click: partyLeave">Leave</a>
|
||||||
|
|
||||||
|
{else if _user.party.invitation}
|
||||||
|
<!-- TODO show by whom -->
|
||||||
|
<h2>You're Invited To {_party.name}</h2>
|
||||||
|
<a class='btn btn-success' x-bind="click: partyAccept">Accept</a>
|
||||||
|
<a class='btn btn-danger' x-bind="click: partyReject">Reject</a>
|
||||||
|
|
||||||
|
{else}
|
||||||
|
<h2>Create A Party</h2>
|
||||||
|
<!-- Not in a party , no invites - create a new one -->
|
||||||
|
<form class=form-inline x-bind="submit: partyCreate">
|
||||||
|
{#if _view.partyError}
|
||||||
|
<div class='alert alert-danger'>{_view.partyError}</div>
|
||||||
|
{/}
|
||||||
|
<div class=control-group>
|
||||||
|
<input type="text" class="input-medium" placeholder="Party Name" value="{_newParty}">
|
||||||
|
<input type="submit" class="btn" value="Create" />
|
||||||
|
</div>
|
||||||
|
<form>
|
||||||
|
{/}
|
||||||
|
|
||||||
</app:myModal>
|
</app:myModal>
|
||||||
|
|
||||||
<alerts:>
|
<alerts:>
|
||||||
|
|
@ -125,7 +160,7 @@
|
||||||
</ul>
|
</ul>
|
||||||
{/}
|
{/}
|
||||||
|
|
||||||
{#if equal(_user.notifications.kickstarter,'show')}
|
{#if equal(_user.flags.kickstarter,'show')}
|
||||||
<div class='alert alert-success'>
|
<div class='alert alert-success'>
|
||||||
<a x-bind="click:closeKickstarterNofitication" class='pull-right'>Dismiss</a>
|
<a x-bind="click:closeKickstarterNofitication" class='pull-right'>Dismiss</a>
|
||||||
Help Habit by backing the <strong><a href="http://kck.st/XoA3Yg">Kickstarter</a></strong>! Funds iPhone & Android apps, <a href="https://github.com/lefnire/habitrpg/issues?labels=critical&page=1&state=open">bug fixes</a>, and the <a href="https://github.com/lefnire/habitrpg/issues/58">Groups feature</a>.
|
Help Habit by backing the <strong><a href="http://kck.st/XoA3Yg">Kickstarter</a></strong>! Funds iPhone & Android apps, <a href="https://github.com/lefnire/habitrpg/issues?labels=critical&page=1&state=open">bug fixes</a>, and the <a href="https://github.com/lefnire/habitrpg/issues/58">Groups feature</a>.
|
||||||
|
|
@ -142,13 +177,20 @@
|
||||||
<a href="#" class="btn btn-small btn-info" data-target="#login-modal" data-toggle="modal">Login / Register</a>
|
<a href="#" class="btn btn-small btn-info" data-target="#login-modal" data-toggle="modal">Login / Register</a>
|
||||||
{else}
|
{else}
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button class="btn btn-small">{username(_user.auth)}</button>
|
<button class="btn btn-small">
|
||||||
|
{#if _user.party.invitation}<span class="badge badge-success">1</span>{/}
|
||||||
|
{username(_user.auth)}
|
||||||
|
</button>
|
||||||
<button class="btn btn-small dropdown-toggle" data-toggle="dropdown">
|
<button class="btn btn-small dropdown-toggle" data-toggle="dropdown">
|
||||||
<span class="caret"></span>
|
<span class="caret"></span>
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li><a href="#" data-target="#settings-modal" data-toggle="modal">Settings</a></li>
|
<li><a href="#" data-target="#settings-modal" data-toggle="modal">Settings</a></li>
|
||||||
<li><a href="#" data-target="#reset-modal" data-toggle="modal">Reset</a></li>
|
{#if _user.flags.partyEnabled}
|
||||||
|
<li><a href="#" data-target="#party-modal" data-toggle="modal">
|
||||||
|
Party{#if _user.party.invitation}<span class="badge badge-success">1</span>{/}
|
||||||
|
</a></li>
|
||||||
|
{/}
|
||||||
<li><a href='/logout'>Logout</a></li>
|
<li><a href='/logout'>Logout</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -158,7 +200,7 @@
|
||||||
<div class='container-fluid'>
|
<div class='container-fluid'>
|
||||||
|
|
||||||
<div class='row-fluid'>
|
<div class='row-fluid'>
|
||||||
<div id=character class='{#if _party}span9{else}span5{/}'>
|
<div id=character class='{#if gt(_partyMembers.length,1)}span9{else}span5{/}'>
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
||||||
|
|
@ -172,26 +214,26 @@
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<!-- Progress Bars -->
|
<!-- Progress Bars -->
|
||||||
<td id="bars" style="width:{#if _party}70%{else}90%{/};">
|
<td id="bars" style="width:{#if gt(_partyMembers.length,1)}80%{else}90%{/};">
|
||||||
<div class="progress progress-danger" rel=tooltip data-placement=bottom title="Health">
|
<div class="progress progress-danger" rel=tooltip data-placement=bottom title="Health">
|
||||||
<div class="bar" style="width: {percent(_user.stats.hp, 50)}%;"></div>
|
<div class="bar" style="width: {percent(_user.stats.hp, 50)}%;"></div>
|
||||||
<span class="progress-text"><i class=icon-heart></i> {round(_user.stats.hp)} / 50</span>
|
<span class="progress-text"><i class=icon-heart></i> {round(_user.stats.hp)} / 50</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="progress progress-warning" rel=tooltip data-placement=bottom title="Experience">
|
<div class="progress progress-warning" rel=tooltip data-placement=bottom title="Experience">
|
||||||
<div class="bar" style="width: {percent(_user.stats.exp,_user._tnl)}%;"></div>
|
<div class="bar" style="width: {percent(_user.stats.exp,_tnl)}%;"></div>
|
||||||
<span class="progress-text">
|
<span class="progress-text">
|
||||||
{#if _user.history.exp}
|
{#if _user.history.exp}
|
||||||
<a x-bind=click:toggleChart data-toggle-id="exp-chart" data-history-path="_user.history.exp" rel=tooltip title="Progress"><i class=icon-signal></i></a>
|
<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>
|
||||||
{/}
|
{/}
|
||||||
<i class=icon-star></i> {round(_user.stats.exp)} / {_user._tnl}
|
<i class=icon-star></i> {round(_user.stats.exp)} / {_tnl}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<!-- Party -->
|
<!-- Party -->
|
||||||
{#if _user.flags.partyEnabled}
|
{#each _partyMembers as :member}
|
||||||
{#each _party as :member}
|
{#unless equal(:member.id, _userId)}
|
||||||
<td class="avatar party-avatar" rel="tooltip" title="{username(:member.auth)}" data-placement="bottom" >
|
<td class="avatar party-avatar" rel="tooltip" title="{username(:member.auth)}" data-placement="bottom" >
|
||||||
<div class='avatar-sprites'>
|
<div class='avatar-sprites'>
|
||||||
<img class='weapon weapon-{:member.items.weapon}' src="/img/BrowserQuest/habitrpg_mods/weapon{:member.items.weapon}.png" />
|
<img class='weapon weapon-{:member.items.weapon}' src="/img/BrowserQuest/habitrpg_mods/weapon{:member.items.weapon}.png" />
|
||||||
|
|
@ -199,8 +241,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div id="lvl"><span class="badge badge-info">Lvl {:member.stats.lvl}</span></div>
|
<div id="lvl"><span class="badge badge-info">Lvl {:member.stats.lvl}</span></div>
|
||||||
</td>
|
</td>
|
||||||
{/}
|
{/}
|
||||||
<td><a class="btn" id="add-party-button" data-target="#add-party-modal" data-toggle="modal"><i class="icon-user"></i></a></td>
|
|
||||||
{/}
|
{/}
|
||||||
|
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -281,7 +322,7 @@
|
||||||
<!--Title -->
|
<!--Title -->
|
||||||
<div class="row-fluid">
|
<div class="row-fluid">
|
||||||
<div class="span6"><h2>Rewards</h2></div>
|
<div class="span6"><h2>Rewards</h2></div>
|
||||||
<div class="span6" id="money">{gold(_user.stats.money)} <img src='/img/coin_single_gold.png'/> {silver(_user.stats.money)} <img src='/img/coin_single_silver.png'/></div>
|
<div class="span6" id="money">{gold(_user.stats.gp)} <img src='/img/coin_single_gold.png'/> {silver(_user.stats.gp)} <img src='/img/coin_single_silver.png'/></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
|
|
@ -289,7 +330,7 @@
|
||||||
{#each _rewardList as :task}<app:task />{/}
|
{#each _rewardList as :task}<app:task />{/}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{#if _user.items.itemsEnabled}
|
{#if _user.flags.itemsEnabled}
|
||||||
<ul class='items'>
|
<ul class='items'>
|
||||||
{#with _view.items.armor as :item}<app:item />{/}
|
{#with _view.items.armor as :item}<app:item />{/}
|
||||||
{#with _view.items.weapon as :item}<app:item />{/}
|
{#with _view.items.weapon as :item}<app:item />{/}
|
||||||
|
|
@ -308,9 +349,10 @@
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<footer class=footer>
|
<footer class=footer>
|
||||||
<div class=container>
|
<div class=container>
|
||||||
<!--<div class='pull-right'>
|
<div class='pull-right'>
|
||||||
<button class='btn' x-bind="click:emulateNextDay">Emulate Next Day</button>
|
<button class='btn' x-bind="click:emulateNextDay">Emulate Next Day</button>
|
||||||
</div>-->
|
<button class='btn' x-bind="click:cheat">Add GP & Exp</button>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Copyright © 2012 OCDevel LLC</li>
|
<li>Copyright © 2012 OCDevel LLC</li>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue