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)
|
||||
model.fn '_tnl', '_user.stats.lvl', (lvl) ->
|
||||
# see https://github.com/lefnire/habitrpg/issues/4
|
||||
# also update in scoring.coffee. TODO create a function accessible in both locations
|
||||
(lvl*100)/5
|
||||
|
||||
# Subscribe to friends
|
||||
if !_.isEmpty(user.get('party'))
|
||||
model.subscribe model.query('users').party(user.get('party')), (err, party) ->
|
||||
model.ref '_party', party
|
||||
|
||||
page.render()
|
||||
if obj.party?.current?
|
||||
party.partySubscribe model, obj.party.current, (p) -> page.render()
|
||||
else
|
||||
page.render()
|
||||
|
||||
# ========== CONTROLLER FUNCTIONS ==========
|
||||
|
||||
resetDom = (model) ->
|
||||
window.DERBY.app.dom.clear()
|
||||
view.render(model)
|
||||
setupModelFns(model)
|
||||
|
||||
ready (model) ->
|
||||
user = model.at('_user')
|
||||
scoring.setModel(model)
|
||||
|
|
@ -87,7 +78,7 @@ ready (model) ->
|
|||
user.set('lastCron', +new Date) if (!lastCron? or lastCron == 'new')
|
||||
|
||||
# Setup model in scoring functions
|
||||
scoring.cron(resetDom)
|
||||
scoring.cron()
|
||||
|
||||
# Load all the jQuery, Growl, Tour, etc
|
||||
browser.loadJavaScripts(model)
|
||||
|
|
@ -96,8 +87,12 @@ ready (model) ->
|
|||
browser.setupTour(model)
|
||||
browser.setupGrowlNotifications(model) unless model.get('_view.mobileDevice')
|
||||
|
||||
party.app(exports, model)
|
||||
|
||||
require('../server/private').app(exports, model)
|
||||
|
||||
require('./debug').app(exports, model)
|
||||
|
||||
user.on 'set', 'tasks.*.completed', (i, completed, previous, isLocal, passed) ->
|
||||
return if passed? && passed.cron # Don't do this stuff on cron
|
||||
direction = () ->
|
||||
|
|
@ -168,14 +163,14 @@ ready (model) ->
|
|||
# fix when query subscriptions implemented properly
|
||||
$('[rel=tooltip]').tooltip('hide')
|
||||
|
||||
ids = user.get("#{type}Ids")
|
||||
ids = user.get("idLists.#{type}")
|
||||
ids.splice(ids.indexOf(id),1)
|
||||
user.del('tasks.'+id)
|
||||
user.set("#{type}Ids", ids)
|
||||
user.set("idLists.#{type}", ids)
|
||||
|
||||
|
||||
exports.clearCompleted = (e, el) ->
|
||||
todoIds = user.get('todoIds')
|
||||
todoIds = user.get('idLists.todo')
|
||||
removed = false
|
||||
_.each model.get('_todoList'), (task) ->
|
||||
if task.completed
|
||||
|
|
@ -183,7 +178,7 @@ ready (model) ->
|
|||
user.del('tasks.'+task.id)
|
||||
todoIds.splice(todoIds.indexOf(task.id), 1)
|
||||
if removed
|
||||
user.set('todoIds', todoIds)
|
||||
user.set('idLists.todo', todoIds)
|
||||
|
||||
exports.toggleDay = (e, el) ->
|
||||
task = model.at(e.target)
|
||||
|
|
@ -226,11 +221,11 @@ ready (model) ->
|
|||
#TODO: this should be working but it's not. so instead, i'm passing all needed values as data-attrs
|
||||
# item = model.at(e.target)
|
||||
|
||||
money = user.get 'stats.money'
|
||||
gp = user.get 'stats.gp'
|
||||
[type, value, index] = [ $(el).attr('data-type'), $(el).attr('data-value'), $(el).attr('data-index') ]
|
||||
|
||||
return if money < value
|
||||
user.set 'stats.money', money - value
|
||||
return if gp < value
|
||||
user.set 'stats.gp', gp - value
|
||||
if type == 'armor'
|
||||
user.set 'items.armor', index
|
||||
model.set '_view.items.armor', content.items.armor[parseInt(index) + 1]
|
||||
|
|
@ -254,7 +249,7 @@ ready (model) ->
|
|||
# Reset stats
|
||||
batch.set 'stats.hp', 50
|
||||
batch.set 'stats.lvl', 1
|
||||
batch.set 'stats.money', 0
|
||||
batch.set 'stats.gp', 0
|
||||
batch.set 'stats.exp', 0
|
||||
|
||||
# Reset items
|
||||
|
|
@ -276,40 +271,15 @@ ready (model) ->
|
|||
batch.startTransaction()
|
||||
taskTypes = ['habit', 'daily', 'todo', 'reward']
|
||||
batch.set 'tasks', {}
|
||||
_.each taskTypes, (type) -> batch.set "#{type}Ids", []
|
||||
_.each taskTypes, (type) -> batch.set "idLists.#{type}", []
|
||||
batch.set 'balance', 2 if user.get('balance') < 2 #only if they haven't manually bought tokens
|
||||
revive(batch, true)
|
||||
revive(batch)
|
||||
batch.commit()
|
||||
resetDom(model)
|
||||
browser.resetDom(model)
|
||||
|
||||
exports.closeKickstarterNofitication = (e, el) ->
|
||||
user.set('notifications.kickstarter', 'hide')
|
||||
exports.closeKickstarterNofitication = (e, el) -> user.set('flags.kickstarter', 'hide')
|
||||
|
||||
exports.setMale = -> user.set('preferences.gender', 'm')
|
||||
exports.setFemale = -> user.set('preferences.gender', 'f')
|
||||
exports.setArmorsetV1 = -> user.set('preferences.armorSet', 'v1')
|
||||
exports.setArmorsetV2 = -> user.set('preferences.armorSet', 'v2')
|
||||
|
||||
exports.addParty = ->
|
||||
id = model.get('_newPartyMember').replace(/[\s"]/g, '')
|
||||
debugger
|
||||
return if _.isEmpty(id)
|
||||
if user.get('party').indexOf(id) != -1
|
||||
model.set "_view.addPartyError", "#{id} already in party."
|
||||
return
|
||||
query = model.query('users').party([id])
|
||||
model.fetch query, (err, users) ->
|
||||
partyMember = users.at(0).get()
|
||||
if partyMember?.id?
|
||||
user.push('party', id)
|
||||
$('#add-party-modal').modal('hide')
|
||||
window.location.reload() #TODO break old subscription, setup new subscript, remove this reload
|
||||
model.set '_newPartyMember', ''
|
||||
else
|
||||
model.set "_view.addPartyError", "User with id #{id} not found."
|
||||
|
||||
exports.emulateNextDay = ->
|
||||
yesterday = +moment().subtract('days', 1).toDate()
|
||||
user.set 'lastCron', yesterday
|
||||
window.location.reload()
|
||||
|
||||
|
|
|
|||
150
src/app/party.coffee
Normal file
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)
|
||||
|
||||
###
|
||||
Get user with API token
|
||||
###
|
||||
store.query.expose "users", "withIdAndToken", (id, api_token) ->
|
||||
|
||||
###
|
||||
REST
|
||||
Get user with API token
|
||||
###
|
||||
REST = (store) ->
|
||||
store.query.expose "users", "withIdAndToken", (id, apiToken) ->
|
||||
@where("id").equals(id)
|
||||
.where('preferences.api_token').equals(api_token)
|
||||
.where('apiToken').equals(apiToken)
|
||||
.limit(1)
|
||||
|
||||
store.queryAccess "users", "withIdAndToken", (id, token, next) ->
|
||||
return next(false) unless @session and @session.userId # https://github.com/codeparty/racer/issues/37
|
||||
isServer = not @req.socket
|
||||
next(isServer)
|
||||
store.queryAccess "users", "withIdAndToken", (id, apiToken, next) ->
|
||||
#return next(false) unless @session and @session.userId # https://github.com/codeparty/racer/issues/37
|
||||
next(true) # only user has id & token
|
||||
|
||||
###
|
||||
Party permissions
|
||||
###
|
||||
|
||||
###
|
||||
Party permissions
|
||||
###
|
||||
partySystem = (store) ->
|
||||
store.query.expose "users", "party", (ids) ->
|
||||
@where("id").within(ids)
|
||||
.only('stats', 'preferences.gender', 'preferences.armorSet', 'items', 'auth.local.username', 'auth.facebook.displayName')
|
||||
.only('stats',
|
||||
'items',
|
||||
'party',
|
||||
'preferences.gender',
|
||||
'preferences.armorSet',
|
||||
'auth.local.username',
|
||||
'auth.facebook.displayName')
|
||||
|
||||
store.queryAccess "users", "party", (ids, next) ->
|
||||
next(true) # no harm in public user stats
|
||||
|
||||
store.query.expose "parties", "withId", (id) ->
|
||||
@where("id").equals(id)
|
||||
store.queryAccess "parties", "withId", (id, next) ->
|
||||
next(true)
|
||||
|
||||
store.readPathAccess "parties.*", ->
|
||||
next = arguments[arguments.length-1]
|
||||
next(true)
|
||||
|
||||
store.writeAccess "*", "parties.*", ->
|
||||
next = arguments[arguments.length-1]
|
||||
next(true)
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
<pre class=prettyprint>{_user.id}</pre>
|
||||
|
||||
<h6>API Token</h6>
|
||||
<pre class=prettyprint>{_user.preferences.api_token}</pre>
|
||||
<pre class=prettyprint>{_user.apiToken}</pre>
|
||||
|
||||
<hr/>
|
||||
<h4>Gender</h4>
|
||||
|
|
@ -40,6 +40,9 @@
|
|||
</label>
|
||||
{/}
|
||||
|
||||
<hr/>
|
||||
<a class='btn btn-danger' data-target="#reset-modal" data-toggle="modal">Reset</a>
|
||||
|
||||
<@footer>
|
||||
<button class="btn" data-dismiss="modal" aria-hidden="true">Close</button>
|
||||
</@footer>
|
||||
|
|
@ -49,9 +52,9 @@
|
|||
<p>This resets your entire account - your tasks will be deleted and your character will start over.</p>
|
||||
<p>This is highly discouraged because you'll lose historical data, which is useful for graphing your progress over time. However, some people find it useful in the beginning after playing with the app for a while.</p>
|
||||
<@footer>
|
||||
<button class="btn" data-dismiss="modal" aria-hidden="true">Close</button>
|
||||
<button data-dismiss="modal" x-bind=click:reset class="btn btn-danger btn-large">Reset</button>
|
||||
</@footer>
|
||||
<button class="btn" data-dismiss="modal" aria-hidden="true">Close</button>
|
||||
<button data-dismiss="modal" x-bind=click:reset class="btn btn-danger btn-large">Reset</button>
|
||||
</@footer>
|
||||
</app:myModal>
|
||||
|
||||
<app:myModal modalId="why-ads-modal" header="Why Ads?">
|
||||
|
|
@ -100,7 +103,7 @@
|
|||
<app:userTokens/>
|
||||
<p>Highly discouraged because red tasks provide good incentive to improve (<a target="_blank" href="https://github.com/lefnire/habitrpg#all-my-tasks-are-red-im-dying-too-fast">read more</a>). However, this becomes necessary after long bouts of bad habits.</p>
|
||||
<@footer>
|
||||
{#if lessThan(_user.balance,1)}
|
||||
{#if lt(_user.balance,1)}
|
||||
<a data-dismiss="modal" x-bind="click:showStripe" class="btn btn-danger btn-large">Buy More Tokens</a><span class='token-cost'>Not enough tokens</span>
|
||||
{else}
|
||||
<a data-dismiss="modal" x-bind=click:buyReroll class="btn btn-danger btn-large">Re-Roll</a><span class='token-cost'>4 Tokens</span>
|
||||
|
|
@ -108,14 +111,46 @@
|
|||
</@footer>
|
||||
</app:myModal>
|
||||
|
||||
<app:myModal modalId="add-party-modal" header="Add Party Member">
|
||||
<form x-bind="submit: addParty">
|
||||
{#if _view.addPartyError}
|
||||
<div class='alert alert-danger'>{_view.addPartyError}</div>
|
||||
<app:myModal modalId="party-modal">
|
||||
|
||||
{#if _user.party.current}
|
||||
<h2>{_party.name}</h2>
|
||||
<table class="table table-striped">
|
||||
{#each _partyMembers as :member}
|
||||
<tr><td>{username(:member.auth)}</td><td>({:member.id})</td></tr>
|
||||
{/}
|
||||
</table>
|
||||
<form class=form-inline x-bind="submit: partyInvite">
|
||||
{#if _view.partyError}
|
||||
<div class='alert alert-danger'>{_view.partyError}</div>
|
||||
{/}
|
||||
<input type="text" class="input-medium search-query" value="{_newPartyMember}">
|
||||
<input type="submit" class="btn" value="Add" />
|
||||
</form>
|
||||
<div class='control-group'>
|
||||
<input type="text" class="input-medium" placeholder="User Id" value="{_newPartyMember}">
|
||||
<input type="submit" class="btn" value="Invite" />
|
||||
</div>
|
||||
</form>
|
||||
<a class='btn btn-danger' x-bind="click: partyLeave">Leave</a>
|
||||
|
||||
{else if _user.party.invitation}
|
||||
<!-- TODO show by whom -->
|
||||
<h2>You're Invited To {_party.name}</h2>
|
||||
<a class='btn btn-success' x-bind="click: partyAccept">Accept</a>
|
||||
<a class='btn btn-danger' x-bind="click: partyReject">Reject</a>
|
||||
|
||||
{else}
|
||||
<h2>Create A Party</h2>
|
||||
<!-- Not in a party , no invites - create a new one -->
|
||||
<form class=form-inline x-bind="submit: partyCreate">
|
||||
{#if _view.partyError}
|
||||
<div class='alert alert-danger'>{_view.partyError}</div>
|
||||
{/}
|
||||
<div class=control-group>
|
||||
<input type="text" class="input-medium" placeholder="Party Name" value="{_newParty}">
|
||||
<input type="submit" class="btn" value="Create" />
|
||||
</div>
|
||||
<form>
|
||||
{/}
|
||||
|
||||
</app:myModal>
|
||||
|
||||
<alerts:>
|
||||
|
|
@ -125,7 +160,7 @@
|
|||
</ul>
|
||||
{/}
|
||||
|
||||
{#if equal(_user.notifications.kickstarter,'show')}
|
||||
{#if equal(_user.flags.kickstarter,'show')}
|
||||
<div class='alert alert-success'>
|
||||
<a x-bind="click:closeKickstarterNofitication" class='pull-right'>Dismiss</a>
|
||||
Help Habit by backing the <strong><a href="http://kck.st/XoA3Yg">Kickstarter</a></strong>! Funds iPhone & Android apps, <a href="https://github.com/lefnire/habitrpg/issues?labels=critical&page=1&state=open">bug fixes</a>, and the <a href="https://github.com/lefnire/habitrpg/issues/58">Groups feature</a>.
|
||||
|
|
@ -142,13 +177,20 @@
|
|||
<a href="#" class="btn btn-small btn-info" data-target="#login-modal" data-toggle="modal">Login / Register</a>
|
||||
{else}
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-small">{username(_user.auth)}</button>
|
||||
<button class="btn btn-small">
|
||||
{#if _user.party.invitation}<span class="badge badge-success">1</span>{/}
|
||||
{username(_user.auth)}
|
||||
</button>
|
||||
<button class="btn btn-small dropdown-toggle" data-toggle="dropdown">
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="#" data-target="#settings-modal" data-toggle="modal">Settings</a></li>
|
||||
<li><a href="#" data-target="#reset-modal" data-toggle="modal">Reset</a></li>
|
||||
{#if _user.flags.partyEnabled}
|
||||
<li><a href="#" data-target="#party-modal" data-toggle="modal">
|
||||
Party{#if _user.party.invitation}<span class="badge badge-success">1</span>{/}
|
||||
</a></li>
|
||||
{/}
|
||||
<li><a href='/logout'>Logout</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
@ -158,7 +200,7 @@
|
|||
<div class='container-fluid'>
|
||||
|
||||
<div class='row-fluid'>
|
||||
<div id=character class='{#if _party}span9{else}span5{/}'>
|
||||
<div id=character class='{#if gt(_partyMembers.length,1)}span9{else}span5{/}'>
|
||||
<table>
|
||||
<tr>
|
||||
|
||||
|
|
@ -172,26 +214,26 @@
|
|||
</td>
|
||||
|
||||
<!-- Progress Bars -->
|
||||
<td id="bars" style="width:{#if _party}70%{else}90%{/};">
|
||||
<td id="bars" style="width:{#if gt(_partyMembers.length,1)}80%{else}90%{/};">
|
||||
<div class="progress progress-danger" rel=tooltip data-placement=bottom title="Health">
|
||||
<div class="bar" style="width: {percent(_user.stats.hp, 50)}%;"></div>
|
||||
<span class="progress-text"><i class=icon-heart></i> {round(_user.stats.hp)} / 50</span>
|
||||
</div>
|
||||
|
||||
<div class="progress progress-warning" rel=tooltip data-placement=bottom title="Experience">
|
||||
<div class="bar" style="width: {percent(_user.stats.exp,_user._tnl)}%;"></div>
|
||||
<div class="bar" style="width: {percent(_user.stats.exp,_tnl)}%;"></div>
|
||||
<span class="progress-text">
|
||||
{#if _user.history.exp}
|
||||
<a x-bind=click:toggleChart data-toggle-id="exp-chart" data-history-path="_user.history.exp" rel=tooltip title="Progress"><i class=icon-signal></i></a>
|
||||
{/}
|
||||
<i class=icon-star></i> {round(_user.stats.exp)} / {_user._tnl}
|
||||
<i class=icon-star></i> {round(_user.stats.exp)} / {_tnl}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Party -->
|
||||
{#if _user.flags.partyEnabled}
|
||||
{#each _party as :member}
|
||||
{#each _partyMembers as :member}
|
||||
{#unless equal(:member.id, _userId)}
|
||||
<td class="avatar party-avatar" rel="tooltip" title="{username(:member.auth)}" data-placement="bottom" >
|
||||
<div class='avatar-sprites'>
|
||||
<img class='weapon weapon-{:member.items.weapon}' src="/img/BrowserQuest/habitrpg_mods/weapon{:member.items.weapon}.png" />
|
||||
|
|
@ -199,8 +241,7 @@
|
|||
</div>
|
||||
<div id="lvl"><span class="badge badge-info">Lvl {:member.stats.lvl}</span></div>
|
||||
</td>
|
||||
{/}
|
||||
<td><a class="btn" id="add-party-button" data-target="#add-party-modal" data-toggle="modal"><i class="icon-user"></i></a></td>
|
||||
{/}
|
||||
{/}
|
||||
|
||||
</tr>
|
||||
|
|
@ -281,7 +322,7 @@
|
|||
<!--Title -->
|
||||
<div class="row-fluid">
|
||||
<div class="span6"><h2>Rewards</h2></div>
|
||||
<div class="span6" id="money">{gold(_user.stats.money)} <img src='/img/coin_single_gold.png'/> {silver(_user.stats.money)} <img src='/img/coin_single_silver.png'/></div>
|
||||
<div class="span6" id="money">{gold(_user.stats.gp)} <img src='/img/coin_single_gold.png'/> {silver(_user.stats.gp)} <img src='/img/coin_single_silver.png'/></div>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
|
|
@ -289,7 +330,7 @@
|
|||
{#each _rewardList as :task}<app:task />{/}
|
||||
</ul>
|
||||
|
||||
{#if _user.items.itemsEnabled}
|
||||
{#if _user.flags.itemsEnabled}
|
||||
<ul class='items'>
|
||||
{#with _view.items.armor as :item}<app:item />{/}
|
||||
{#with _view.items.weapon as :item}<app:item />{/}
|
||||
|
|
@ -308,9 +349,10 @@
|
|||
<!-- Footer -->
|
||||
<footer class=footer>
|
||||
<div class=container>
|
||||
<!--<div class='pull-right'>
|
||||
<div class='pull-right'>
|
||||
<button class='btn' x-bind="click:emulateNextDay">Emulate Next Day</button>
|
||||
</div>-->
|
||||
<button class='btn' x-bind="click:cheat">Add GP & Exp</button>
|
||||
</div>
|
||||
<div>
|
||||
<ul>
|
||||
<li>Copyright © 2012 OCDevel LLC</li>
|
||||
|
|
|
|||
Loading…
Reference in a new issue