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