Merge branch 'develop' into challenges-tmp

Conflicts:
	src/app/helpers.coffee
	src/app/index.coffee
This commit is contained in:
Tyler Renelle 2013-05-26 15:29:54 +01:00
commit 35049f7752
33 changed files with 265 additions and 1223 deletions

View file

@ -12,17 +12,16 @@
*/
var un_registered = {
"auth.local": {$exists: false},
"auth.facebook": {$exists: false}
};
var registered = {
$or: [
{ 'auth.local': { $exists: true }},
{ 'auth.facebook': { $exists: true }}
]
};
var today = +(new Date);
"auth.local": {$exists: false},
"auth.facebook": {$exists: false}
},
registered = {
$or: [
{ 'auth.local': { $exists: true }},
{ 'auth.facebook': { $exists: true }}
]
},
today = +new Date;
// isValidDate = (d) ->
// return false if Object::toString.call(d) isnt "[object Date]"
@ -30,12 +29,9 @@ var today = +(new Date);
db.users.find(un_registered).forEach(function(user) {
var diff, lastCron;
if (!user) return;
if (!!user.lastCron) {
lastCron = new Date(user.lastCron);
diff = Math.abs(moment(today).startOf('day').diff(moment(lastCron).startOf('day'), "days"));
if (diff > 3) {
if (Math.abs(moment(today).diff(user.lastCron, 'days')) > 7) {
return db.users.remove({_id:user._id});
}
} else {
@ -49,8 +45,8 @@ db.users.find(un_registered).forEach(function(user) {
* revisit if needs be
*/
/*db.sessions.find().forEach(function(sess){
var uid = JSON.parse(sess.session).userId;
if (!uid || db.users.count({_id:uid}) === 0) {
db.sessions.remove({_id:sess._id});
}
});*/
var uid = JSON.parse(sess.session).userId;
if (!uid || db.users.count({_id:uid}) === 0) {
db.sessions.remove({_id:sess._id});
}
});*/

View file

@ -1,4 +1,4 @@
// mongo habitrpg ./node_modules/underscore/underscore.js ./migrations/find_unique_user.js
// mongo habitrpg ./node_modules/lodash/index.js ./migrations/find_unique_user.js
/**
* There are some rare instances of lost user accounts, due to a corrupt user auth variable (see https://github.com/lefnire/habitrpg/wiki/User-ID)
@ -6,6 +6,6 @@
*/
db.users.find().forEach(function(user){
var found = _.findWhere(user.tasks, {text: "Replace Me"})
var found = _.any(user.tasks, {text: "Replace Me"})
if (found) printjson({id:user._id, auth:user.auth});
})

View file

@ -4,6 +4,7 @@
"version": "0.0.0-151",
"main": "./server.js",
"dependencies": {
"habitrpg-shared": "git://github.com/HabitRPG/habitrpg-shared#master",
"derby": "git://github.com/lefnire/derby#habitrpg",
"racer": "git://github.com/lefnire/racer#habitrpg",
"racer-db-mongo": "git://github.com/lefnire/racer-db-mongo#habitrpg",
@ -17,7 +18,6 @@
"moment": "*",
"stripe": "*",
"coffee-script": "1.4.x",
"underscore": "*",
"mongoskin": "*",
"nconf": "*",
"icalendar": "git://github.com/lefnire/node-icalendar#master",

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

View file

@ -1,74 +0,0 @@
XP = 15
HP = 2
priorityValue = module.exports.priorityValue = (priority='!') ->
switch priority
when '!' then 1
when '!!' then 1.5
when '!!!' then 2
else 1
module.exports.tnl = (level) ->
if level >= 100
value = 0
else
value = Math.round(((Math.pow(level,2)*0.25)+(10 * level) + 139.75)/10)*10 # round to nearest 10
return value
###
Calculates Exp modificaiton based on level and weapon strength
{value} task.value for exp gain
{weaponStrength) weapon strength
{level} current user level
{priority} user-defined priority multiplier
###
module.exports.expModifier = (value, weaponStr, level, priority='!') ->
str = (level-1) / 2 # ultimately get this from user
totalStr = (str + weaponStr) / 100
strMod = 1 + totalStr
exp = value * XP * strMod * priorityValue(priority)
return Math.round(exp)
###
Calculates HP modification based on level and armor defence
{value} task.value for hp loss
{armorDefense} defense from armor
{helmDefense} defense from helm
{level} current user level
{priority} user-defined priority multiplier
###
module.exports.hpModifier = (value, armorDef, helmDef, shieldDef, level, priority='!') ->
def = (level-1) / 2 # ultimately get this from user?
totalDef = (def + armorDef + helmDef + shieldDef) / 100 #ultimate get this from user
defMod = 1 - totalDef
hp = value * HP * defMod * priorityValue(priority)
return Math.round(hp * 10)/10 # round to 1dp
###
Future use
{priority} user-defined priority multiplier
###
module.exports.gpModifier = (value, modifier, priority='!', streak, model) ->
val = value * modifier * priorityValue(priority)
if streak and model
streakBonus = streak / 100 + 1 # eg, 1-day streak is 1.1, 2-day is 1.2, etc
afterStreak = val * streakBonus
model.set('_streakBonus', afterStreak - val) if (val > 0) # can we do this without model? just global emit?
return afterStreak
else
return val
###
Calculates the next task.value based on direction
Uses a capped inverse log y=.95^x, y>= -5
{currentValue} the current value of the task
{direction} up or down
###
module.exports.taskDeltaFormula = (currentValue, direction) ->
if currentValue < -47.27 then currentValue = -47.27
else if currentValue > 21.27 then currentValue = 21.27
delta = Math.pow(0.9747,currentValue)
return delta if direction is 'up'
return -delta

View file

@ -1,4 +1,4 @@
_ = require 'underscore'
_ = require 'lodash'
moment = require 'moment'
###
@ -35,7 +35,7 @@ loadJavaScripts = (model) ->
###
setupSortable = (model) ->
unless (model.get('_mobileDevice') is true) #don't do sortable on mobile
_.each ['habit', 'daily', 'todo', 'reward'], (type) ->
['habit', 'daily', 'todo', 'reward'].forEach (type) ->
$("ul.#{type}s").sortable
dropOnEmpty: false
cursor: "move"
@ -109,8 +109,7 @@ setupTour = (model) ->
$('.main-herobox').popover('destroy') #remove previous popovers
tour = new Tour()
_.each tourSteps, (step) ->
tour.addStep _.defaults step, {html:true}
tourSteps.forEach (step) -> tour.addStep _.defaults step, {html:true}
tour._current = 0 if isNaN(tour._current) #bootstrap-tour bug
tour.start()

View file

@ -1,10 +1,9 @@
_ = require 'underscore'
lodash = require 'lodash'
helpers = require 'habitrpg-shared/script/helpers'
module.exports.app = (appExports, model) ->
browser = require './browser'
helpers = require './helpers'
user = model.at '_user'
appExports.challengeCreate = ->

View file

@ -1,10 +1,9 @@
browser = require './browser'
items = require './items'
algos = require './algos'
algos = require 'habitrpg-shared/script/algos'
moment = require 'moment'
_ = require 'underscore'
lodash = require 'lodash'
_ = require 'lodash'
derby = require 'derby'
module.exports.app = (appExports, model) ->
@ -22,7 +21,6 @@ module.exports.app = (appExports, model) ->
owned = user.get('items')
# unless they're already at 0-everything
if parseInt(owned.armor)>0 or parseInt(owned.head)>0 or parseInt(owned.shield)>0 or parseInt(owned.weapon)>0
console.log 'test'
# find a random item to lose
until loseThisItem
#candidate = {0:'items.armor', 1:'items.head', 2:'items.shield', 3:'items.weapon', 4:'stats.gp'}[Math.random()*5|0]
@ -37,8 +35,8 @@ module.exports.app = (appExports, model) ->
batch.startTransaction()
taskTypes = ['habit', 'daily', 'todo', 'reward']
batch.set 'tasks', {}
_.each taskTypes, (type) -> batch.set "#{type}Ids", []
batch.set 'balance', 1 if user.get('balance') < 1 #only if they haven't manually bought gems
taskTypes.forEach (type) -> batch.set "#{type}Ids", []
#batch.set 'balance', 1 if user.get('balance') < 1 #only if they haven't manually bought gems
# Reset stats
batch.set 'stats.hp', 50
@ -84,68 +82,6 @@ module.exports.app = (appExports, model) ->
model.del "users.#{user.get('id')}", ->
window.location.href = "/logout"
userSchema =
# _id
stats: { gp: 0, exp: 0, lvl: 1, hp: 50 }
party: { current: null, invitation: null }
items: { weapon: 0, armor: 0, head: 0, shield: 0 }
preferences: { gender: 'm', skin: 'white', hair: 'blond', armorSet: 'v1', dayStart:0, showHelm: true }
habitIds: []
dailyIds: []
todoIds: []
rewardIds: []
apiToken: null # set in newUserObject below
lastCron: 'new' #this will be replaced with `+new Date` on first run
balance: 0
tasks: {}
flags:
partyEnabled: false
itemsEnabled: false
tags: []
# ads: 'show' # added on registration
module.exports.newUserObject = ->
# deep clone, else further new users get duplicate objects
newUser = lodash.cloneDeep userSchema
newUser.apiToken = derby.uuid()
repeat = {m:true,t:true,w:true,th:true,f:true,s:true,su:true}
defaultTasks = [
{type: 'habit', text: '1h Productive Work', notes: '-- Habits: Constantly Track --\nFor some habits, it only makes sense to *gain* points (like this one).', value: 0, up: true, down: false }
{type: 'habit', text: 'Eat Junk Food', notes: 'For others, it only makes sense to *lose* points', value: 0, up: false, down: true}
{type: 'habit', text: 'Take The Stairs', notes: 'For the rest, both + and - make sense (stairs = gain, elevator = lose)', value: 0, up: true, down: true}
{type: 'daily', text: '1h Personal Project', notes: '-- Dailies: Complete Once a Day --\nAt the end of each day, non-completed Dailies dock you points.', value: 0, completed: false, repeat: repeat }
{type: 'daily', text: 'Exercise', notes: "If you are doing well, they turn green and are less valuable (experience, gold) and less damaging (HP). This means you can ease up on them for a bit.", value: 3, completed: false, repeat: repeat }
{type: 'daily', text: '45m Reading', notes: 'But if you are doing poorly, they turn red. The worse you do, the more valuable (exp, gold) and more damaging (HP) these goals become. This encourages you to focus on your shortcomings, the reds.', value: -10, completed: false, repeat: repeat }
{type: 'todo', text: 'Call Mom', notes: "-- Todos: Complete Eventually --\nNon-completed Todos won't hurt you, but they will become more valuable over time. This will encourage you to wrap up stale Todos.", value: -3, completed: false }
{type: 'reward', text: '1 Episode of Game of Thrones', notes: '-- Rewards: Treat Yourself! --\nAs you complete goals, you earn gold to buy rewards. Buy them liberally - rewards are integral in forming good habits.', value: 20 }
{type: 'reward', text: 'Cake', notes: 'But only buy if you have enough gold - you lose HP otherwise.', value: 10 }
]
defaultTags = [
{name: 'morning'}
{name: 'afternoon'}
{name: 'evening'}
]
for task in defaultTasks
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
for tag in defaultTags
tag.id = derby.uuid()
newUser.tags.push tag
return newUser
module.exports.BatchUpdate = BatchUpdate = (model) ->
user = model.at("_user")
transactionInProgress = false
@ -181,7 +117,7 @@ module.exports.BatchUpdate = BatchUpdate = (model) ->
setStats: (stats) ->
stats ?= obj.stats
that = @
_.each Object.keys(stats), (key) -> that.set "stats.#{key}", stats[key]
_.each Object.keys(stats), (key) -> that.set "stats.#{key}", stats[key]; true
# queue: (path, val) ->
# # Special function for setting object properties by string dot-notation. See http://stackoverflow.com/a/6394168/362790

View file

@ -1,5 +1,5 @@
moment = require 'moment'
algos = require './algos'
algos = require 'habitrpg-shared/script/algos'
module.exports.app = (appExports, model) ->
user = model.at('_user')

View file

@ -1,4 +1,4 @@
_ = require 'underscore'
_ = require 'lodash'
browser = require './browser'
module.exports.app = (appExports, model) ->
@ -37,5 +37,5 @@ module.exports.app = (appExports, model) ->
tag.remove()
# remove tag from all tasks
_.each user.get("tasks"), (task) -> user.del "tasks.#{task.id}.tags.#{tagId}"
_.each user.get("tasks"), (task) -> user.del "tasks.#{task.id}.tags.#{tagId}"; true

View file

@ -1,225 +0,0 @@
moment = require 'moment'
_ = require 'underscore'
relative = require 'relative-date'
algos = require './algos'
items = require('./items').items
sod = (timestamp, dayStart=0) ->
#sanity-check reset-time (is it 24h time?)
dayStart = 0 unless (dayStart = parseInt(dayStart)) and (0 <= dayStart <= 24)
moment(timestamp).startOf('day').add('h', dayStart)
# Absolute diff between two dates
daysBetween = (yesterday, now, dayStart) -> Math.abs sod(yesterday, dayStart).diff(now, 'days')
dayMapping = {0:'su',1:'m',2:'t',3:'w',4:'th',5:'f',6:'s'}
shouldDo = (day, repeat, dayStart=0) ->
return false unless repeat
now = +new Date
selected = repeat[dayMapping[sod(day, dayStart).day()]]
return selected unless moment(day).isSame(now,'d')
if dayStart <= moment(now).hour() # we're past the dayStart mark, is it due today?
return selected
else # we're not past dayStart mark, check if it was due "yesterday"
yesterday = moment(now).subtract(1,'d').day()
return repeat[dayMapping[yesterday]]
# http://stackoverflow.com/questions/2532218/pick-random-property-from-a-javascript-object
# obj: object
# returns random property (the value)
randomVal = (obj) ->
result = undefined
count = 0
for key, val of obj
result = val if Math.random() < (1 / ++count)
result
removeWhitespace = (str) ->
return '' unless str
str.replace /\s/g, ''
username = (auth, override) ->
#some people define custom profile name in Avatar -> Profile
return override if override
if auth?.facebook?.displayName?
auth.facebook.displayName
else if auth?.facebook?
fb = auth.facebook
if fb._raw then "#{fb.name.givenName} #{fb.name.familyName}" else fb.name
else if auth?.local?
auth.local.username
else
'Anonymous'
viewHelpers = (view) ->
view.fn "percent", (x, y) ->
x=1 if x==0
Math.round(x/y*100)
view.fn "round", Math.round
view.fn "floor", Math.floor
view.fn "ceil", Math.ceil
view.fn "lt", (a, b) -> a < b
view.fn 'gt', (a, b) -> a > b
view.fn "mod", (a, b) -> parseInt(a) % parseInt(b) == 0
view.fn 'removeWhitespace', removeWhitespace
view.fn "notEqual", (a, b) -> (a != b)
view.fn "and", -> _.reduce arguments, (cumm, curr) -> cumm && curr
view.fn "or", -> _.reduce arguments, (cumm, curr) -> cumm || curr
view.fn "truarr", (num) -> num-1
view.fn 'count', (arr) -> arr?.length or 0
view.fn "gems", (gp) -> return gp/0.25
view.fn "encodeiCalLink", (uid, apiToken) ->
loc = window?.location.host or process.env.BASE_URL
encodeURIComponent "http://#{loc}/v1/users/#{uid}/calendar.ics?apiToken=#{apiToken}"
###
User
###
view.fn "username", (auth, override) -> username(auth, override)
view.fn "tnl", algos.tnl
###
Items
###
view.fn 'equipped', (type, item=0, preferences={gender:'m', armorSet:'v1'}, backerTier=0) ->
{gender, armorSet} = preferences
item = parseInt(item)
backerTier = parseInt(backerTier)
switch type
when'armor'
if item > 5
return 'armor_6' if backerTier >= 45
item = 5 # set them back if they're trying to cheat
if gender is 'f'
return if (item is 0) then "f_armor_#{item}_#{armorSet}" else "f_armor_#{item}"
else
return "m_armor_#{item}"
when 'head'
if item > 5
return 'head_6' if backerTier >= 45
item = 5
if gender is 'f'
return if (item > 1) then "f_head_#{item}_#{armorSet}" else "f_head_#{item}"
else
return "m_head_#{item}"
when 'shield'
if item > 5
return 'shield_6' if backerTier >= 45
item = 5
return "#{preferences.gender}_shield_#{item}"
when 'weapon'
if item > 6
return 'weapon_7' if backerTier >= 70
item = 6
return "#{preferences.gender}_weapon_#{item}"
view.fn "gold", (num) ->
if num
return (num).toFixed(1).split('.')[0]
else
return "0"
view.fn "silver", (num) ->
if num
(num).toFixed(2).split('.')[1]
else
return "00"
###
Tasks
###
view.fn 'taskClasses', (task, filters, dayStart, lastCron, showCompleted=false, main) ->
return unless task
{type, completed, value, repeat} = task
# completed / remaining toggle
return 'hidden' if (type is 'todo') and (completed != showCompleted)
# Filters
if main # only show when on your own list
for filter, enabled of filters
if enabled and not task.tags?[filter]
# All the other classes don't matter
return 'hidden'
classes = type
# show as completed if completed (naturally) or not required for today
if type in ['todo', 'daily']
if completed or (type is 'daily' and !shouldDo(+new Date, task.repeat, dayStart))
classes += " completed"
else
classes += " uncompleted"
else if type is 'habit'
classes += ' habit-wide' if task.down and task.up
if value < -20
classes += ' color-worst'
else if value < -10
classes += ' color-worse'
else if value < -1
classes += ' color-bad'
else if value < 1
classes += ' color-neutral'
else if value < 5
classes += ' color-good'
else if value < 10
classes += ' color-better'
else
classes += ' color-best'
return classes
view.fn 'ownsPet', (pet, userPets) -> _.isArray(userPets) and userPets.indexOf(pet) != -1
view.fn 'friendlyTimestamp', (timestamp) -> moment(timestamp).format('MM/DD h:mm:ss a')
view.fn 'newChatMessages', (messages, lastMessageSeen) ->
return false unless messages?.length > 0
messages?[0] and (messages[0].id != lastMessageSeen)
view.fn 'indexOf', (str1, str2) ->
return false unless str1 && str2
str1.indexOf(str2) != -1
view.fn 'relativeDate', relative
view.fn 'noTags', (tags) ->
_.isEmpty(tags) or _.isEmpty(_.filter( tags, (t) -> t ) )
view.fn 'appliedTags', (userTags, taskTags) ->
arr = []
_.each userTags, (t) ->
return unless t?
arr.push(t.name) if taskTags?[t.id]
arr.join(', ')
view.fn 'userStr', (level) ->
str = (level-1) / 2
view.fn 'totalStr', (level, weapon=0) ->
str = (level-1) / 2
totalStr = (str + items.weapon[weapon].strength)
view.fn 'userDef', (level) ->
def = (level-1) / 2
view.fn 'totalDef', (level, armor=0, helm=0, shield=0) ->
def = (level-1) / 2
totalDef = (def + items.armor[armor].defense + items.head[helm].defense + items.shield[shield].defense)
view.fn 'itemText', (type, item=0) -> items[type][parseInt(item)].text
view.fn 'itemStat', (type, item=0) -> if type is 'weapon' then items[type][parseInt(item)].strength else items[type][parseInt(item)].defense
# view.fn 'activeFilters', (filters) ->
# debugger
# (_.find filters, (f) -> f)?
module.exports = { viewHelpers, removeWhitespace, randomVal, daysBetween, shouldDo, username }

View file

@ -17,10 +17,12 @@ i18n.localize app,
urlScheme: false
checkHeader: true
helpers = require './helpers'
helpers.viewHelpers view
misc = require('./misc')
misc.viewHelpers view
_ = require('underscore')
_ = require('lodash')
algos = require 'habitrpg-shared/script/algos'
helpers = require 'habitrpg-shared/script/helpers'
###
Cleanup task-corruption (null tasks, rogue/invisible tasks, etc)
@ -36,11 +38,12 @@ cleanupCorruptTasks = (model) ->
unless task?.id? and task?.type?
user.del("tasks.#{key}")
delete tasks[key]
true
batch = null
## Task List Cleanup
_.each ['habit','daily','todo','reward'], (type) ->
['habit','daily','todo','reward'].forEach (type) ->
# 1. remove duplicates
# 2. restore missing zombie tasks back into list
@ -58,6 +61,7 @@ cleanupCorruptTasks = (model) ->
batch.startTransaction()
batch.set("#{type}Ids", preened)
console.error user.get('id') + "'s #{type}s were corrupt."
true
batch.commit() if batch?
@ -69,17 +73,16 @@ setupSubscriptions = (page, model, params, next, cb) ->
selfQ = model.query('users').withId(uuid) #keep this for later
groupsQ = model.query('groups').withMember(uuid)
groupsQ.fetch (err, groups, extra) ->
groupsQ.fetch (err, groups) ->
return next(err) if err
finished = (descriptors, paths) ->
model.subscribe.apply model, descriptors.concat ->
[err, refs] = [arguments[0], arguments]
return next(err) if err
_.each paths, (path, idx) -> model.ref path, refs[idx+1]
_.each paths, (path, idx) -> model.ref path, refs[idx+1]; true
unless model.get('_user')
console.error "User not found - this shouldn't be happening!"
return page.redirect('/logout') #delete model.session.userId
extra(arguments) if extra
return cb()
groupsObj = groups.get()
@ -88,6 +91,7 @@ setupSubscriptions = (page, model, params, next, cb) ->
return finished([selfQ, "groups.habitrpg"], ['_user', '_habitRPG']) if _.isEmpty(groupsObj)
## (2) Party or Guild has members, fetch those users too
# Subscribe to the groups themselves. We separate them by _party, _guilds, and _habitRPG (the "global" guild).
groupsInfo = _.reduce groupsObj, ((m,g)->
if g.type is 'guild' then m.guildIds.push(g.id) else m.partyId = g.id
m.members = m.members.concat(g.members)
@ -101,7 +105,6 @@ setupSubscriptions = (page, model, params, next, cb) ->
mObj = members.get()
model.set "_members", _.object(_.pluck(mObj,'id'), mObj)
## Then subscribe to the groups themselves. We separate them by _party, _guilds, and _habitRPG (the "global" guild).
# Note - selfQ *must* come after membersQ in subscribe, otherwise _user will only get the fields restricted by party-members in store.coffee. Strang bug, but easy to get around
partyQ = model.query('groups').withIds(groupsInfo.partyId)
if _.isEmpty(groupsInfo.guildIds)
@ -124,6 +127,7 @@ get '/', (page, model, params, next) ->
#refLists
_.each ['habit', 'daily', 'todo', 'reward'], (type) ->
model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids"
true
page.render()
@ -131,9 +135,7 @@ get '/', (page, model, params, next) ->
ready (model) ->
user = model.at('_user')
model.setNull '_user.apiToken', derby.uuid()
require('./scoring').cron(model)
browser = require './browser'
require('./character').app(exports, model)
require('./tasks').app(exports, model)
@ -143,7 +145,22 @@ ready (model) ->
require('./pets').app(exports, model)
require('../server/private').app(exports, model)
require('./debug').app(exports, model) if model.flags.nodeEnv != 'production'
require('./browser').app(exports, model, app)
browser.app(exports, model, app)
require('./unlock').app(exports, model)
require('./filters').app(exports, model)
require('./challenges').app(exports, model)
uObj = misc.hydrate(user.get())
cron = ->
#return setTimeout(cron, 1) if model._txnQueue.length > 0
# habitrpg-shared/algos requires uObj.habits, uObj.dailys etc instead of uObj.tasks
_.each ['habit','daily','todo','reward'], (type) ->
uObj["#{type}s"] = _.where(uObj.tasks, {type:type}); true
paths = {}
algos.cron(uObj, {paths:paths})
lostHp = delete paths['stats.hp'] # we'll set this manually so we can get a cool animation
_.each paths, (v,k) -> user.pass({cron:true}).set(k,helpers.dotGet(k, uObj)); true
if lostHp
browser.resetDom(model)
setTimeout (-> user.set('stats.hp', uObj.stats.hp)), 750
cron() if algos.shouldCron(uObj)

View file

@ -1,84 +1,11 @@
_ = require 'underscore'
items = module.exports.items =
weapon: [
{index: 0, text: "Training Sword", classes: "weapon_0", notes:'Training weapon.', strength: 0, value:0}
{index: 1, text: "Sword", classes:'weapon_1', notes:'Increases experience gain by 3%.', strength: 3, value:20}
{index: 2, text: "Axe", classes:'weapon_2', notes:'Increases experience gain by 6%.', strength: 6, value:30}
{index: 3, text: "Morningstar", classes:'weapon_3', notes:'Increases experience gain by 9%.', strength: 9, value:45}
{index: 4, text: "Blue Sword", classes:'weapon_4', notes:'Increases experience gain by 12%.', strength: 12, value:65}
{index: 5, text: "Red Sword", classes:'weapon_5', notes:'Increases experience gain by 15%.', strength: 15, value:90}
{index: 6, text: "Golden Sword", classes:'weapon_6', notes:'Increases experience gain by 18%.', strength: 18, value:120}
{index: 7, text: "Dark Souls Blade", classes:'weapon_7', notes:'Increases experience gain by 21%.', strength: 21, value:150}
]
armor: [
{index: 0, text: "Cloth Armor", classes: 'armor_0', notes:'Training armor.', defense: 0, value:0}
{index: 1, text: "Leather Armor", classes: 'armor_1', notes:'Decreases HP loss by 4%.', defense: 4, value:30}
{index: 2, text: "Chain Mail", classes: 'armor_2', notes:'Decreases HP loss by 6%.', defense: 6, value:45}
{index: 3, text: "Plate Mail", classes: 'armor_3', notes:'Decreases HP loss by 7%.', defense: 7, value:65}
{index: 4, text: "Red Armor", classes: 'armor_4', notes:'Decreases HP loss by 8%.', defense: 8, value:90}
{index: 5, text: "Golden Armor", classes: 'armor_5', notes:'Decreases HP loss by 10%.', defense: 10, value:120}
{index: 6, text: "Shade Armor", classes: 'armor_6', notes:'Decreases HP loss by 12%.', defense: 12, value:150}
]
head: [
{index: 0, text: "No Helm", classes: 'head_0', notes:'Training helm.', defense: 0, value:0}
{index: 1, text: "Leather Helm", classes: 'head_1', notes:'Decreases HP loss by 2%.', defense: 2, value:15}
{index: 2, text: "Chain Coif", classes: 'head_2', notes:'Decreases HP loss by 3%.', defense: 3, value:25}
{index: 3, text: "Plate Helm", classes: 'head_3', notes:'Decreases HP loss by 4%.', defense: 4, value:45}
{index: 4, text: "Red Helm", classes: 'head_4', notes:'Decreases HP loss by 5%.', defense: 5, value:60}
{index: 5, text: "Golden Helm", classes: 'head_5', notes:'Decreases HP loss by 6%.', defense: 6, value:80}
{index: 6, text: "Shade Helm", classes: 'head_6', notes:'Decreases HP loss by 7%.', defense: 7, value:100}
]
shield: [
{index: 0, text: "No Shield", classes: 'shield_0', notes:'No Shield.', defense: 0, value:0}
{index: 1, text: "Wooden Shield", classes: 'shield_1', notes:'Decreases HP loss by 3%', defense: 3, value:20}
{index: 2, text: "Buckler", classes: 'shield_2', notes:'Decreases HP loss by 4%.', defense: 4, value:35}
{index: 3, text: "Enforced Shield", classes: 'shield_3', notes:'Decreases HP loss by 5%.', defense: 5, value:55}
{index: 4, text: "Red Shield", classes: 'shield_4', notes:'Decreases HP loss by 7%.', defense: 7, value:70}
{index: 5, text: "Golden Shield", classes: 'shield_5', notes:'Decreases HP loss by 8%.', defense: 8, value:90}
{index: 6, text: "Tormented Skull", classes: 'shield_6', notes:'Decreases HP loss by 9%.', defense: 9, value:120}
]
potion: {type: 'potion', text: "Potion", notes: "Recover 15 HP", value: 25, classes: 'potion'}
reroll: {type: 'reroll', text: "Re-Roll", classes: 'reroll', notes: "Resets your task values back to 0 (yellow). Useful when everything's red and it's hard to stay alive.", value:0 }
pets: [
{text: 'Wolf', name: 'Wolf', value: 3}
{text: 'Tiger Cub', name: 'TigerCub', value: 3}
#{text: 'Polar Bear Cub', name: 'PolarBearCub', value: 3} #commented out because there are no polarbear modifiers yet, special drop?
{text: 'Panda Cub', name: 'PandaCub', value: 3}
{text: 'Lion Cub', name: 'LionCub', value: 3}
{text: 'Fox', name: 'Fox', value: 3}
{text: 'Flying Pig', name: 'FlyingPig', value: 3}
{text: 'Dragon', name: 'Dragon', value: 3}
{text: 'Cactus', name: 'Cactus', value: 3}
{text: 'Bear Cub', name: 'BearCub', value: 3}
]
hatchingPotions: [
{text: 'Base', name: 'Base', notes: "Hatches your pet in it's base form.", value: 1}
{text: 'White', name: 'White', notes: 'Turns your animal into a White pet.', value: 2}
{text: 'Desert', name: 'Desert', notes: 'Turns your animal into a Desert pet.', value: 2}
{text: 'Red', name: 'Red', notes: 'Turns your animal into a Red pet.', value: 3}
{text: 'Shade', name: 'Shade', notes: 'Turns your animal into a Shade pet.', value: 3}
{text: 'Skeleton', name: 'Skeleton', notes: 'Turns your animal into a Skeleton.', value: 3}
{text: 'Zombie', name: 'Zombie', notes: 'Turns your animal into a Zombie.', value: 4}
{text: 'Cotton Candy Pink', name: 'CottonCandyPink', notes: 'Turns your animal into a Cotton Candy Pink pet.', value: 4}
{text: 'Cotton Candy Blue', name: 'CottonCandyBlue', notes: 'Turns your animal into a Cotton Candy Blue pet.', value: 4}
{text: 'Golden', name: 'Golden', notes: 'Turns your animal into a Golden pet.', value: 5}
]
# add "type" to each item, so we can reference that as "weapon" or "armor" in the html
_.each ['weapon', 'armor', 'head', 'shield'], (key) ->
_.each items[key], (item) -> item.type = key
_.each items.pets, (pet) -> pet.notes = 'Find a hatching potion to pour on this egg, and one day it will hatch into a loyal pet.'
_.each items.hatchingPotions, (hatchingPotion) -> hatchingPotion.notes = "Pour this on an egg, and it will hatch as a #{hatchingPotion.text} pet."
items = require 'habitrpg-shared/script/items'
_ = require 'lodash'
###
server exports
###
module.exports.server = (model) ->
model.set '_items', items
model.set '_items', items.items
updateStore(model)
###
@ -87,35 +14,11 @@ module.exports.server = (model) ->
module.exports.app = (appExports, model) ->
user = model.at '_user'
appExports.buyItem = (e, el, next) ->
user = model.at '_user'
#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)
gp = user.get 'stats.gp'
appExports.buyItem = (e, el) ->
[type, value, index] = [ $(el).attr('data-type'), $(el).attr('data-value'), $(el).attr('data-index') ]
return if gp < value
# make sure deduction doesn't happen unless purchase was successful, see https://github.com/lefnire/habitrpg/issues/233
deductGP = -> user.set 'stats.gp', gp - value
if type == 'weapon'
user.set 'items.weapon', index, deductGP
updateStore model
else if type == 'armor'
user.set 'items.armor', index, deductGP
updateStore model
else if type == 'head'
user.set 'items.head', index, deductGP
updateStore model
else if type == 'shield'
user.set 'items.shield', index, deductGP
updateStore model
else if type == 'potion'
hp = user.get 'stats.hp'
hp += 15
hp = 50 if hp > 50
user.set 'stats.hp', hp, deductGP
if changes = items.buyItem(user.get(), type, value, index)
_.each changes, (v,k) -> user.set k,v; true
updateStore(model)
appExports.activateRewardsTab = ->
model.set '_activeTabRewards', true
@ -124,26 +27,11 @@ module.exports.app = (appExports, model) ->
model.set '_activeTabPets', true
model.set '_activeTabRewards', false
###
update store
###
module.exports.updateStore = updateStore = (model) ->
model.setNull '_items.next', {}
user = model.at('_user')
equipped = user.get('items')
_.each ['weapon', 'armor', 'shield', 'head'], (type) ->
i = parseInt(equipped?[type] || 0) + 1
showNext = true
if i is items[type].length - 1
if (type in ['armor', 'shield', 'head'])
showNext = user.get('backer.tier') >= 45 # backer armor
else
showNext = user.get('backer.tier') >= 70 # backer weapon
else if i is items[type].length
showNext = false
model.set "_items.next.#{type}", if showNext then items[type][i] else {hide:true}
nextItems = items.updateStore(model.get('_user'))
_.each nextItems, (v,k) -> model.set("_items.next.#{k}",v); true

104
src/app/misc.coffee Normal file
View file

@ -0,0 +1,104 @@
_ = require 'lodash'
algos = require 'habitrpg-shared/script/algos'
items = require('habitrpg-shared/script/items').items
helpers = require('habitrpg-shared/script/helpers')
###
algos.score wrapper for habitrpg-helpers to work in Derby. We need to do model.set() instead of simply setting the
object properties, and it's very difficult to diff the two objects and find dot-separated paths to set. So we to first
clone our user object (if we don't do that, it screws with model.on() listeners, ping Tyler for an explaination),
perform the updates while tracking paths, then all the values at those paths
###
module.exports.score = (model, taskId, direction, allowUndo=false) ->
#return setTimeout( (-> score(taskId, direction)), 500) if model._txnQueue.length > 0
user = model.at("_user")
uObj = hydrate(user.get()) # see https://github.com/codeparty/racer/issues/116
tObj = uObj.tasks[taskId]
# Stuff for undo
if allowUndo
tObjBefore = _.cloneDeep tObj
tObjBefore.completed = !tObjBefore.completed if tObjBefore.type in ['daily', 'todo']
previousUndo = model.get('_undo')
clearTimeout(previousUndo.timeoutId) if previousUndo?.timeoutId
timeoutId = setTimeout (-> model.del('_undo')), 20000
model.set '_undo', {stats:_.cloneDeep(uObj.stats), task:tObjBefore, timeoutId: timeoutId}
paths = {}
delta = algos.score(uObj, tObj, direction, {paths})
_.each paths, (v,k) -> user.set(k,helpers.dotGet(k, uObj)); true
model.set('_streakBonus', uObj._tmp.streakBonus) if uObj._tmp?.streakBonus
if uObj._tmp?.drop and $?
model.set '_drop', uObj._tmp.drop
$('#item-dropped-modal').modal 'show'
delta
###
Make sure model.get() returns all properties, see https://github.com/codeparty/racer/issues/116
###
module.exports.hydrate = hydrate = (spec) ->
if _.isObject(spec) and !_.isArray(spec)
hydrated = {}
keys = _.keys(spec).concat(_.keys(spec.__proto__))
keys.forEach (k) -> hydrated[k] = hydrate(spec[k])
hydrated
else spec
module.exports.viewHelpers = (view) ->
#misc
view.fn "percent", (x, y) ->
x=1 if x==0
Math.round(x/y*100)
view.fn 'indexOf', (str1, str2) ->
return false unless str1 && str2
str1.indexOf(str2) != -1
view.fn "round", Math.round
view.fn "floor", Math.floor
view.fn "ceil", Math.ceil
view.fn "lt", (a, b) -> a < b
view.fn 'gt', (a, b) -> a > b
view.fn "mod", (a, b) -> parseInt(a) % parseInt(b) == 0
view.fn "notEqual", (a, b) -> (a != b)
view.fn "and", -> _.reduce arguments, (cumm, curr) -> cumm && curr
view.fn "or", -> _.reduce arguments, (cumm, curr) -> cumm || curr
view.fn "truarr", (num) -> num-1
view.fn 'count', (arr) -> arr?.length or 0
view.fn 'int',
get: (num) -> num
set: (num) -> [parseInt(num)]
#iCal
view.fn "encodeiCalLink", helpers.encodeiCalLink
#User
view.fn "gems", (balance) -> return balance/0.25
view.fn "username", helpers.username
view.fn "tnl", algos.tnl
view.fn 'equipped', helpers.equipped
view.fn "gold", helpers.gold
view.fn "silver", helpers.silver
#Stats
view.fn 'userStr', helpers.userStr
view.fn 'totalStr', helpers.totalStr
view.fn 'userDef', helpers.userDef
view.fn 'totalDef', helpers.totalDef
view.fn 'itemText', helpers.itemText
view.fn 'itemStat', helpers.itemStat
#Pets
view.fn 'ownsPet', helpers.ownsPet
#Tasks
view.fn 'taskClasses', helpers.taskClasses
#Chat
view.fn 'friendlyTimestamp',helpers.friendlyTimestamp
view.fn 'newChatMessages', helpers.newChatMessages
view.fn 'relativeDate', helpers.relativeDate
#Tags
view.fn 'noTags', helpers.noTags
view.fn 'appliedTags', helpers.appliedTags

View file

@ -1,10 +1,9 @@
_ = require('underscore')
helpers = require './helpers'
_ = require('lodash')
helpers = require('habitrpg-shared/script/helpers')
module.exports.app = (appExports, model, app) ->
character = require './character'
browser = require './browser'
helpers = require './helpers'
_currentTime = model.at '_currentTime'
@ -134,7 +133,9 @@ module.exports.app = (appExports, model, app) ->
return next() unless e.keyCode is 13
appExports.tavernSendChat()
appExports.deleteChatMessage = (e) -> e.at().remove() #requires the {#with}
appExports.deleteChatMessage = (e) ->
if confirm("Delete chat message?") is true
e.at().remove() #requires the {#with}
app.on 'render', (ctx) ->
$('#party-tab-link').on 'shown', (e) ->

View file

@ -1,6 +1,6 @@
_ = require 'underscore'
{ randomVal } = require './helpers'
{ pets, hatchingPotions } = require('./items').items
_ = require 'lodash'
{ randomVal } = require 'habitrpg-shared/script/helpers'
{ pets, hatchingPotions } = require('habitrpg-shared/script/items').items
###
app exports
@ -46,14 +46,14 @@ module.exports.app = (appExports, model) ->
return user.set 'items.currentPet', {} if user.get('items.currentPet.str') is petStr
[name, modifier] = petStr.split('-')
pet = _.findWhere pets, name: name
pet = _.find pets, {name: name}
pet.modifier = modifier
pet.str = petStr
user.set 'items.currentPet', pet
appExports.buyHatchingPotion = (e, el) ->
name = $(el).attr 'data-hatchingPotion'
newHatchingPotion = _.findWhere hatchingPotions, name: name
newHatchingPotion = _.find hatchingPotions, {name: name}
gems = user.get('balance') * 4
if gems >= newHatchingPotion.value
if confirm "Buy this hatching potion with #{newHatchingPotion.value} of your #{gems} Gems?"
@ -64,7 +64,7 @@ module.exports.app = (appExports, model) ->
appExports.buyEgg = (e, el) ->
name = $(el).attr 'data-egg'
newEgg = _.findWhere pets, name: name
newEgg = _.find pets, {name: name}
gems = user.get('balance') * 4
if gems >= newEgg.value
if confirm "Buy this egg with #{newEgg.value} of your #{gems} Gems?"

View file

@ -1,6 +1,6 @@
character = require './character'
browser = require './browser'
helpers = require './helpers'
helpers = require 'habitrpg-shared/script/helpers'
module.exports.app = (appExports, model) ->
user = model.at('_user')

View file

@ -1,360 +0,0 @@
moment = require 'moment'
_ = require 'underscore'
{ randomVal } = helpers = require './helpers'
browser = require './browser'
character = require './character'
items = require './items'
{ pets, hatchingPotions } = items.items
algos = require './algos'
MODIFIER = algos.MODIFIER # each new level, armor, weapon add 2% modifier (this mechanism will change)
###
Drop System
###
randomDrop = (model, delta, priority, streak=0) ->
user = model.at('_user')
# limit drops to 2 / day
user.setNull 'items.lastDrop',
date: +moment().subtract('d',1) # trick - set it to yesterday on first run, that way they can get drops today
count: 0
reachedDropLimit = (helpers.daysBetween(user.get('items.lastDrop.date'), +new Date, user.get('preferences.dayStart')) is 0) and user.get('items.lastDrop.count') >= 2
return if reachedDropLimit
# % chance of getting a pet or meat
chanceMultiplier = Math.abs(delta)
chanceMultiplier *= algos.priorityValue(priority) # multiply chance by reddness
chanceMultiplier += streak # streak bonus
if user.get('flags.dropsEnabled') and Math.random() < (.05 * chanceMultiplier)
# current breakdown - 3% (adjustable) chance on drop
# If they got a drop: 50% chance of egg, 50% Hatching Potion. If hatchingPotion, broken down further even further
rarity = Math.random()
# Egg, 40% chance
if rarity > .6
drop = randomVal(pets)
user.push 'items.eggs', drop
drop.type = 'Egg'
drop.dialog = "You've found a #{drop.text} Egg! #{drop.notes}"
# Hatching Potion, 60% chance - break down by rarity even more. FIXME this may not be the best method, so revisit
else
acceptableDrops = []
# Tier 5 (Blue Moon Rare)
if rarity < .1
acceptableDrops = ['Base', 'White', 'Desert', 'Red', 'Shade', 'Skeleton', 'Zombie', 'CottonCandyPink', 'CottonCandyBlue', 'Golden']
# Tier 4 (Very Rare)
else if rarity < .2
acceptableDrops = ['Base', 'White', 'Desert', 'Red', 'Shade', 'Skeleton', 'Zombie', 'CottonCandyPink', 'CottonCandyBlue']
# Tier 3 (Rare)
else if rarity < .3
acceptableDrops = ['Base', 'White', 'Desert', 'Red', 'Shade', 'Skeleton']
# Tier 2 (Scarce)
else if rarity < .4
acceptableDrops = ['Base', 'White', 'Desert']
# Tier 1 (Common)
else
acceptableDrops = ['Base']
acceptableDrops = _.filter(hatchingPotions, (hatchingPotion) -> hatchingPotion.name in acceptableDrops)
drop = randomVal acceptableDrops
user.push 'items.hatchingPotions', drop.name
drop.type = 'HatchingPotion'
drop.dialog = "You've found a #{drop.text} Hatching Potion! #{drop.notes}"
model.set '_drop', drop
$('#item-dropped-modal').modal 'show'
user.set 'items.lastDrop.date', +new Date
user.incr 'items.lastDrop.count'
# {taskId} task you want to score
# {direction} 'up' or 'down'
# {times} # times to call score on this task (1 unless cron, usually)
# {update} if we're running updates en-mass (eg, cron on server) pass in userObj
score = (model, taskId, direction, times, batch, cron) ->
user = model.at '_user'
commit = false
unless batch?
commit = true
batch = new character.BatchUpdate(model)
batch.startTransaction()
obj = batch.obj()
{gp, hp, exp, lvl} = obj.stats
taskPath = "tasks.#{taskId}"
taskObj = obj.tasks[taskId]
{type, value, streak} = taskObj
priority = taskObj.priority or '!'
# If they're trying to purhcase a too-expensive reward, confirm they want to take a hit for it
if taskObj.value > obj.stats.gp and taskObj.type is 'reward'
r = confirm "Not enough GP to purchase this reward, buy anyway and lose HP? (Punishment for taking a reward you didn't earn)."
unless r
batch.commit()
return
delta = 0
times ?= 1
calculateDelta = (adjustvalue=true) ->
# If multiple days have passed, multiply times days missed
_.times times, (n) ->
# Each iteration calculate the delta (nextDelta), which is then accumulated in delta
# (aka, the total delta). This weirdness won't be necessary when calculating mathematically
# rather than iteratively
nextDelta = algos.taskDeltaFormula(value, direction)
value += nextDelta if adjustvalue
delta += nextDelta
addPoints = ->
level = user.get('stats.lvl')
weaponStrength = items.items.weapon[user.get('items.weapon')].strength
exp += algos.expModifier(delta,weaponStrength,level, priority) / 2 # / 2 hack for now bcause people leveling too fast
if streak
gp += algos.gpModifier(delta, 1, priority, streak, model)
else
gp += algos.gpModifier(delta, 1, priority)
subtractPoints = ->
level = user.get('stats.lvl')
armorDefense = items.items.armor[user.get('items.armor')].defense
helmDefense = items.items.head[user.get('items.head')].defense
shieldDefense = items.items.shield[user.get('items.shield')].defense
hp += algos.hpModifier(delta,armorDefense,helmDefense,shieldDefense,level, priority)
switch type
when 'habit'
calculateDelta()
# Add habit value to habit-history (if different)
if (delta > 0) then addPoints() else subtractPoints()
taskObj.history ?= []
if taskObj.value != value
historyEntry = { date: +new Date, value: value }
taskObj.history.push historyEntry
batch.set "#{taskPath}.history", taskObj.history
when 'daily'
if cron? # cron
calculateDelta()
subtractPoints()
batch.set "#{taskPath}.streak", 0
else
calculateDelta(false)
if delta != 0
addPoints() # obviously for delta>0, but also a trick to undo accidental checkboxes
if direction is 'up'
streak = if streak then streak + 1 else 1
else
streak = if streak then streak - 1 else 0
batch.set "#{taskPath}.streak", streak
taskObj.streak = streak
when 'todo'
if cron? #cron
calculateDelta()
#don't touch stats on cron
else
calculateDelta()
addPoints() # obviously for delta>0, but also a trick to undo accidental checkboxes
when 'reward'
# Don't adjust values for rewards
calculateDelta(false)
# purchase item
gp -= Math.abs(taskObj.value)
num = parseFloat(taskObj.value).toFixed(2)
# if too expensive, reduce health & zero gp
if gp < 0
hp += gp # hp - gp difference
gp = 0
taskObj.value = value
batch.set "#{taskPath}.value", taskObj.value
origStats = _.clone obj.stats
updateStats model, { hp, exp, gp }, batch
# Commit
if commit
# newStats / origStats is a glorious hack to trick Derby into seeing the change in model.on(*)
newStats = _.clone batch.obj().stats
_.each Object.keys(origStats), (key) -> obj.stats[key] = origStats[key]
batch.setStats(newStats)
# batch.setStats()
batch.commit()
# Drop system
randomDrop(model, delta, priority, streak) if direction is 'up'
return delta
###
Updates user stats with new stats. Handles death, leveling up, etc
{stats} new stats
{update} if aggregated changes, pass in userObj as update. otherwise commits will be made immediately
###
updateStats = (model, newStats, batch) ->
user = model.at '_user'
obj = batch.obj()
# if user is dead, dont do anything
return if obj.stats.hp <= 0
if newStats.hp?
# Game Over
if newStats.hp <= 0
obj.stats.hp = 0 # signifies dead
return
else
obj.stats.hp = newStats.hp
if newStats.exp?
tnl = algos.tnl(obj.stats.lvl)
#silent = false
# if we're at level 100, turn xp to gold
if obj.stats.lvl >= 100
newStats.gp += newStats.exp / 15
newStats.exp = 0
obj.stats.lvl = 100
else
# level up & carry-over exp
if newStats.exp >= tnl
#silent = true # push through the negative xp silently
user.set('stats.exp', newStats.exp) # push normal + notification
while newStats.exp >= tnl and obj.stats.lvl < 100 # keep levelling up
newStats.exp -= tnl
obj.stats.lvl++
tnl = algos.tnl(obj.stats.lvl)
if obj.stats.lvl== 100
newStats.exp = 0
obj.stats.hp = 50
obj.stats.exp = newStats.exp
#if silent
#console.log("pushing silent :" + obj.stats.exp)
#user.pass(true).set('stats.exp', obj.stats.exp)
# Set flags when they unlock features
# NOTE we have to first model.set() the flag to true, then AFTER that obj.flags.flag = true
# The reason is model.on() listeners still track object references, so if obj.flags.flags = true and then we
# model.set(), the second argument of .on() listeners will be true (in otherwords, before/after tests will fail)
if !obj.flags.customizationsNotification and (obj.stats.exp > 10 or obj.stats.lvl > 1)
batch.set 'flags.customizationsNotification', true
obj.flags.customizationsNotification = true
if !obj.flags.itemsEnabled and obj.stats.lvl >= 2
# Set to object, then also send to browser right away to get model.on() subscription notification
batch.set 'flags.itemsEnabled', true
obj.flags.itemsEnabled = true
if !obj.flags.partyEnabled and obj.stats.lvl >= 3
batch.set 'flags.partyEnabled', true
obj.flags.partyEnabled = true
if !obj.flags.dropsEnabled and obj.stats.lvl >= 4
batch.set 'flags.dropsEnabled', true
obj.flags.dropsEnabled = true
if newStats.gp?
#FIXME what was I doing here? I can't remember, gp isn't defined
gp = 0.0 if (!gp? or gp<0)
obj.stats.gp = newStats.gp
###
At end of day, add value to all incomplete Daily & Todo tasks (further incentive)
For incomplete Dailys, deduct experience
###
cron = (model) ->
user = model.at '_user'
today = +new Date
lastCron = user.get('lastCron')
# New user (!lastCron, lastCron==new) or it got busted somehow, maybe they went to a different timezone
if !lastCron? or lastCron is 'new' or moment(lastCron).isAfter(today)
user.set('lastCron', +new Date)
return
daysMissed = helpers.daysBetween(user.get('lastCron'), today, user.get('preferences.dayStart'))
if daysMissed > 0
# User is resting at the inn. Used to be we un-checked each daily without performing calculation (see commits before fb29e35)
# but to prevent abusing the inn (http://goo.gl/GDb9x) we now do *not* calculate dailies, and simply set lastCron to today
if user.get('flags.rest') is true
return user.set('lastCron', today)
batch = new character.BatchUpdate(model)
batch.startTransaction()
obj = batch.obj()
batch.set 'lastCron', today
hpBefore = obj.stats.hp #we'll use this later so we can animate hp loss
# Tally each task
todoTally = 0
_.each obj.tasks, (taskObj) ->
{id, type, completed, repeat} = taskObj
if type in ['todo', 'daily']
# Deduct experience for missed Daily tasks, but not for Todos (just increase todo's value)
unless completed
scheduleMisses = daysMissed
# for dailys which have repeat dates, need to calculate how many they've missed according to their own schedule
if (type is 'daily') and repeat
scheduleMisses = 0
_.times daysMissed, (n) ->
thatDay = moment(today).subtract('days', n+1)
scheduleMisses++ if helpers.shouldDo(thatDay, repeat, obj.preferences?.dayStart) is true
score(model, id, 'down', scheduleMisses, batch, true) if scheduleMisses > 0
if type == 'daily'
if completed #set OHV for completed dailies
newValue = taskObj.value + algos.taskDeltaFormula(taskObj.value,'up')
batch.set "tasks.#{taskObj.id}.value", newValue
taskObj.history ?= []
taskObj.history.push { date: +new Date, value: taskObj.value }
batch.set "tasks.#{taskObj.id}.history", taskObj.history
batch.set "tasks.#{taskObj.id}.completed", false
else
value = obj.tasks[taskObj.id].value #get updated value
absVal = if (completed) then Math.abs(value) else value
todoTally += absVal
else if type is 'habit' # slowly reset 'onlies' value to 0
if taskObj.up==false or taskObj.down==false
if Math.abs(taskObj.value) < 0.1
batch.set "tasks.#{taskObj.id}.value", 0
else
batch.set "tasks.#{taskObj.id}.value", taskObj.value / 2
# Finished tallying
obj.history ?= {}; obj.history.todos ?= []; obj.history.exp ?= []
obj.history.todos.push { date: today, value: todoTally }
# tally experience
expTally = obj.stats.exp
lvl = 0 #iterator
while lvl < (obj.stats.lvl-1)
lvl++
expTally += algos.tnl(lvl)
obj.history.exp.push { date: today, value: expTally }
# Set the new user specs, and animate HP loss
[hpAfter, obj.stats.hp] = [obj.stats.hp, hpBefore]
batch.setStats()
batch.set('history', obj.history)
batch.commit()
browser.resetDom(model)
setTimeout (-> user.set 'stats.hp', hpAfter), 1000 # animate hp loss
module.exports = {
score: score
cron: cron
# testing stuff
expModifier: algos.expModifier
hpModifier: algos.hpModifier
taskDeltaFormula: algos.taskDeltaFormula
}

View file

@ -1,10 +1,15 @@
scoring = require './scoring'
helpers = require './helpers'
_ = require 'underscore'
algos = require 'habitrpg-shared/script/algos'
helpers = require 'habitrpg-shared/script/helpers'
_ = require 'lodash'
moment = require 'moment'
character = require './character'
misc = require './misc'
###
Make scoring functionality available to the app
###
module.exports.app = (appExports, model) ->
character = require './character'
user = model.at('_user')
appExports.addTask = (e, el) ->
@ -30,26 +35,24 @@ module.exports.app = (appExports, model) ->
e.at().unshift newTask # e.at() in this case is the list, which was scoped here using {#with @list}...{/}
newModel.set ''
appExports.del = (e, el) ->
appExports.del = (e) ->
# Derby extends model.at to support creation from DOM nodes
task = e.at()
id = task.get('id')
history = task.get('history')
if history and history.length>2
if history and history.length > 2
# prevent delete-and-recreate hack on red tasks
if task.get('value') < 0
result = confirm("Are you sure? Deleting this task will hurt you (to prevent deleting, then re-creating red tasks).")
if result != true
return # Cancel. Don't delete, don't hurt user
else
if confirm("Are you sure? Deleting this task will hurt you (to prevent deleting, then re-creating red tasks).") is true
task.set('type','habit') # hack to make sure it hits HP, instead of performing "undo checkbox"
scoring.score(model, id, direction:'down')
misc.score(model, id, 'down', true)
else
return # Cancel. Don't delete, don't hurt user
# prevent accidently deleting long-standing tasks
else
result = confirm("Are you sure you want to delete this task?")
return if result != true
return unless confirm("Are you sure you want to delete this task?") is true
#TODO bug where I have to delete from _users.tasks AND _{type}List,
# fix when query subscriptions implemented properly
@ -63,7 +66,7 @@ module.exports.app = (appExports, model) ->
completedIds = _.pluck( _.where(model.get('_todoList'), {completed:true}), 'id')
todoIds = user.get('todoIds')
_.each completedIds, (id) -> user.del "tasks.#{id}"
_.each completedIds, (id) -> user.del "tasks.#{id}"; true
user.set 'todoIds', _.difference(todoIds, completedIds)
appExports.toggleDay = (e, el) ->
@ -104,40 +107,22 @@ module.exports.app = (appExports, model) ->
appExports.todosShowRemaining = -> model.set '_showCompleted', false
appExports.todosShowCompleted = -> model.set '_showCompleted', true
setUndo = (stats, task) ->
previousUndo = model.get('_undo')
clearTimeout(previousUndo.timeoutId) if previousUndo?.timeoutId
timeoutId = setTimeout (-> model.del('_undo')), 10000
model.set '_undo', {stats:stats, task:task, timeoutId: timeoutId}
###
Call scoring functions for habits & rewards (todos & dailies handled below)
###
appExports.score = (e, el) ->
task= model.at $(el).parents('li')[0]
taskObj = task.get()
id = $(el).parents('li').attr('data-id')
direction = $(el).attr('data-direction')
# set previous state for undo
setUndo _.clone(user.get('stats')), _.clone(taskObj)
scoring.score(model, taskObj.id, direction)
misc.score(model, id, direction, true)
###
This is how we handle appExports.score for todos & dailies. Due to Derby's special handling of `checked={:task.completd}`,
the above function doesn't work so we need a listener here
###
user.on 'set', 'tasks.*.completed', (i, completed, previous, isLocal, passed) ->
return if passed? && passed.cron # Don't do this stuff on cron
return if passed?.cron # Don't do this stuff on cron
direction = if completed then 'up' else 'down'
# set previous state for undo
taskObj = _.clone user.get("tasks.#{i}")
taskObj.completed = previous
setUndo _.clone(user.get('stats')), taskObj
scoring.score(model, i, direction)
misc.score(model, i, direction, true)
###
Undo
@ -148,14 +133,15 @@ module.exports.app = (appExports, model) ->
batch = character.BatchUpdate(model)
batch.startTransaction()
model.del '_undo'
_.each undo.stats, (val, key) -> batch.set "stats.#{key}", val
_.each undo.stats, (val, key) -> batch.set "stats.#{key}", val; true
taskPath = "tasks.#{undo.task.id}"
_.each undo.task, (val, key) ->
return if key in ['id', 'type'] # strange bugs in this world: https://workflowy.com/shared/a53582ea-43d6-bcce-c719-e134f9bf71fd/
return true if key in ['id', 'type'] # strange bugs in this world: https://workflowy.com/shared/a53582ea-43d6-bcce-c719-e134f9bf71fd/
if key is 'completed'
user.pass({cron:true}).set("#{taskPath}.completed",val)
else
batch.set "#{taskPath}.#{key}", val
true
batch.commit()
appExports.tasksToggleAdvanced = (e, el) ->

View file

@ -1,6 +1,6 @@
_ = require 'underscore'
{ randomVal } = require './helpers'
{ pets, hatchingPotions } = require('./items').items
_ = require 'lodash'
{ randomVal } = require 'habitrpg-shared/script/helpers'
{ pets, hatchingPotions } = require('habitrpg-shared/script/items').items
###
Listeners to enabled flags, set notifications to the user when they've unlocked features

View file

@ -1,12 +1,13 @@
express = require 'express'
router = new express.Router()
scoring = require '../app/scoring'
_ = require 'underscore'
{ tnl } = require '../app/algos'
_ = require 'lodash'
algos = require 'habitrpg-shared/script/algos'
helpers = require 'habitrpg-shared/script/helpers'
validator = require 'derby-auth/node_modules/validator'
check = validator.check
sanitize = validator.sanitize
misc = require '../app/misc'
NO_TOKEN_OR_UID = err: "You must include a token and uid (user id) in your request"
NO_USER_FOUND = err: "No user found."
@ -51,7 +52,7 @@ auth = (req, res, next) ->
router.get '/user', auth, (req, res) ->
user = req.userObj
user.stats.toNextLevel = tnl user.stats.lvl
user.stats.toNextLevel = algos.tnl user.stats.lvl
user.stats.maxHealth = 50
delete user.apiToken
@ -83,7 +84,7 @@ router.put '/user', auth, (req, res) ->
acceptableAttrs = ['flags', 'history', 'items', 'preferences', 'profile', 'stats']
user.set 'lastCron', partialUser.lastCron if partialUser.lastCron?
_.each acceptableAttrs, (attr) ->
_.each partialUser[attr], (val, key) -> user.set("#{attr}.#{key}", val)
_.each partialUser[attr], (val, key) -> user.set("#{attr}.#{key}", val);true
updateTasks partialUser.tasks, req.user, req.getModel() if partialUser.tasks?
@ -255,7 +256,8 @@ scoreTask = (req, res, next) ->
model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids"
model.at("_#{type}List").push task
delta = scoring.score(model, taskId, direction)
#FIXME
delta = misc.score(model, taskId, direction)
result = model.get '_user.stats'
result.delta = delta
res.json result

View file

@ -1,8 +1,7 @@
express = require 'express'
router = new express.Router()
scoring = require '../app/scoring'
_ = require 'underscore'
_ = require 'lodash'
icalendar = require('icalendar')
api = require './api'
@ -45,6 +44,7 @@ router.get '/v1/users/:uid/calendar.ics', (req, res) ->
d.date_only = true
event.setDate d
ical.addComponent event
true
res.type('text/calendar')
formattedIcal = ical.toString().replace(/DTSTART\:/g, 'DTSTART;VALUE=DATE:')
res.send(200, formattedIcal)

View file

@ -12,6 +12,8 @@ priv = require './private'
habitrpgStore = require './store'
middleware = require './middleware'
helpers = require("habitrpg-shared/script/helpers")
## RACER CONFIGURATION ##
#racer.io.set('transports', ['xhr-polling'])
@ -50,7 +52,7 @@ strategies =
options =
domain: process.env.BASE_URL || 'http://localhost:3000'
allowPurl: true
schema: require('../app/character').newUserObject()
schema: helpers.newUser(true)
# This has to happen before our middleware stuff
auth.store(store, habitrpgStore.customAccessControl)

View file

@ -1,4 +1,4 @@
_ = require 'underscore'
_ = require 'lodash'
character = require "../app/character"
module.exports.middleware = (req, res, next) ->
@ -37,7 +37,7 @@ module.exports.app = (appExports, model) ->
batch = new character.BatchUpdate(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'
_.each obj.tasks, (task) -> batch.set("tasks.#{task.id}.value", 0) unless task.type is 'reward';true
batch.commit()
module.exports.routes = (expressApp) ->

View file

@ -34,3 +34,6 @@
background-position: -1200px 0px
width: 40px
height: 40px
.buttonList li
margin: 5px;

View file

@ -1,4 +1,4 @@
_ = require 'underscore'
_ = require 'lodash'
request = require 'superagent'
expect = require 'expect.js'
require 'coffee-script'
@ -22,7 +22,7 @@ expect.Assertion::be = expect.Assertion::equal = (obj) ->
origBe.call this, obj
# Custom modules
character = require '../src/app/character'
helpers = require 'habitrpg-shared/script/helpers'
###### Helpers & Variables ######
@ -48,7 +48,7 @@ describe 'API', ->
#store.flush()
model = store.createModel()
model.set '_userId', uid = model.id()
user = character.newUserObject()
user = helpers.newUser(true)
user.apiToken = model.id()
model.session = {userId:uid}
model.set "users.#{uid}", user
@ -373,13 +373,13 @@ describe 'API', ->
expect(res.statusCode).to.be 201
tasks = res.body.tasks
expect(_.findWhere(tasks,{id:habitId})).to.eql {id: habitId,text: 'hello2',notes: 'note2'}
expect(_.find(tasks,{id:habitId})).to.eql {id: habitId,text: 'hello2',notes: 'note2'}
foundNewTask = _.findWhere(tasks,{text:'new task2'})
foundNewTask = _.find(tasks,{text:'new task2'})
expect(foundNewTask.text).to.be 'new task2'
expect(foundNewTask.notes).to.be 'notes2'
found = _.findWhere(res.body.tasks, {id:dailyId})
found = _.find(res.body.tasks, {id:dailyId})
expect(found).to.not.be.ok()
query.fetch (err, user) ->

View file

@ -1,253 +0,0 @@
{expect} = require 'derby/node_modules/racer/test/util'
{BrowserModel: Model} = require 'derby/node_modules/racer/test/util/model'
derby = require 'derby'
lodash = require 'lodash'
_ = require 'underscore'
moment = require 'moment'
# Custom modules
scoring = require '../src/app/scoring'
schema = require '../src/app/character'
helpers = require '../src/app/helpers'
###### Helpers & Variables ######
model = null
uuid = null
taskPath = null
## Helper which clones the content at a path so tests can compare before/after values
# Otherwise, using model.get(path) will give the same object before as after
pathSnapshots = (paths) ->
if _.isString(paths)
return lodash.cloneDeep(model.get(paths))
_.map paths, (path) -> lodash.cloneDeep(model.get(path))
statsTask = -> pathSnapshots(['_user.stats', taskPath]) # quick snapshot of user.stats & task
cleanUserObj = ->
userObj = schema.newUserObject()
userObj.tasks = {}
userObj.habitIds = []
userObj.dailyIds = []
userObj.todoIds = []
userObj.rewardIds = []
return userObj
resetUser = -> model.set '_user', cleanUserObj()
freshTask = (taskObj) ->
resetUser()
# create a test task
uuid = derby.uuid()
taskPath = "_user.tasks.#{uuid}"
{type} = taskObj
model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids"
[taskObj.id, taskObj.value] = [uuid, 0]
model.at("_#{type}List").push taskObj
###
Helper function to determine if stats updates are numerically correct based on scoring
@direction: 'up' or 'down'
@options: The user stats modifiers and times to run, defaults to {times:1, modifiers:{lvl:1, weapon:0, armor:0}}
###
modificationsLookup = (direction, options = {}) ->
merged = _.defaults options, {times:1, lvl:1, weapon:0, armor:0}
{times, lvl, armor, weapon} = merged
userObj = cleanUserObj()
value = 0
_.times times, (n) ->
delta = scoring.taskDeltaFormula(value, direction)
value += delta
if direction=='up'
gain = scoring.expModifier(delta, options)
userObj.stats.exp += gain
userObj.stats.money += gain
else
loss = scoring.hpModifier(delta, options)
userObj.stats.hp += loss
return {user:userObj, value:value}
###### Specs ######
describe 'Cron', ->
it 'calculates day differences with dayStart properly', ->
dayStart = 4
yesterday = moment().subtract('d', 1).add('h', dayStart)
now = moment().startOf('day').add('h', dayStart-1) #today
console.log {yesterday: yesterday.format('MM/DD HH:00'), now: now.format('MM/DD HH:00')}
console.log {diff: Math.abs(moment(yesterday).diff(moment(now), 'days'))}
expect(helpers.daysBetween(yesterday, now, dayStart)).to.eql 0
now = moment().startOf('day').add('h', dayStart)
console.log {now: now.format('MM/DD HH:00')}
expect(helpers.daysBetween(yesterday, now, dayStart)).to.eql 1
describe 'User', ->
model = null
before ->
model = new Model
model.set '_user', schema.newUserObject()
scoring.setModel model
it 'sets correct user defaults', ->
user = model.get '_user'
expect(user.stats).to.eql { money: 0, exp: 0, lvl: 1, hp: 50 }
expect(user.items).to.eql { itemsEnabled: false, armor: 0, weapon: 0 }
expect(user.balance).to.eql 2
expect(_.size(user.tasks)).to.eql 9
expect(_.size(user.habitIds)).to.eql 3
expect(_.size(user.dailyIds)).to.eql 3
expect(_.size(user.todoIds)).to.eql 1
expect(_.size(user.rewardIds)).to.eql 2
##### Habits #####
describe 'Tasks', ->
beforeEach ->
resetUser()
describe 'Habits', ->
beforeEach ->
freshTask {type: 'habit', text: 'Habit', up: true, down: true}
it 'created the habit', ->
task = model.get(taskPath)
expect(task.text).to.eql 'Habit'
expect(task.value).to.eql 0
it 'test a few scoring numbers (this will change if constants / formulae change)', ->
{user} = modificationsLookup('down')
expect(user.stats.hp).to.eql 49
{user} = modificationsLookup('down', {times:5})
expect(user.stats.hp).to.be.within(42,44)
{user} = modificationsLookup('up')
expect(user.stats.exp).to.eql 1
expect(user.stats.money).to.eql 1
{user} = modificationsLookup('up', {times:5})
expect(user.stats.exp).to.be.within(4,5)
it 'made proper modifications when down-scored', ->
## Trial 1
shouldBe = modificationsLookup('down')
scoring.score(uuid,'down')
[stats, task] = statsTask()
expect(stats.hp).to.be.eql shouldBe.user.stats.hp
expect(task.value).to.eql shouldBe.value
## Trial 2
freshTask {type: 'habit', text: 'Habit', completed: false}
shouldBe = modificationsLookup('down', {times:10})
scoring.score(uuid,'down', {times:10})
[stats, task] = statsTask()
expect(stats.hp).to.be.eql shouldBe.user.stats.hp
expect(task.value).to.eql shouldBe.value
it 'made proper modifications when up-scored', ->
# Up-score the habit
[statsBefore, taskBefore] = statsTask()
scoring.score(uuid, 'up')
[statsAfter, taskAfter] = statsTask()
# User should have gained Exp, GP
expect(statsAfter.exp).to.be.greaterThan statsBefore.exp
expect(statsAfter.money).to.be.greaterThan statsBefore.money
# HP should not change
expect(statsAfter.hp).to.eql statsBefore.hp
# Task should have lost value
expect(taskBefore.value).to.eql 0
expect(taskAfter.value).to.be.greaterThan taskBefore.value
## Trial 2
taskBefore = pathSnapshots(taskPath)
scoring.score(uuid, 'up')
taskAfter = pathSnapshots(taskPath)
# Should have lost in value
expect(taskAfter.value).to.be > taskBefore.value
# And lost more than trial 1
diff = Math.abs(taskAfter.value) - Math.abs(taskBefore.value)
expect(diff).to.be.lessThan 1
it 'makes history entry for habit'
it 'makes proper modifications each time when clicking + / - in rapid succession'
# saw an issue here once, so test that it wasn't a fluke
it 'should not modify certain attributes given certain conditions'
# non up+down habits
# what else?
it 'should show "undo" notification if user unchecks completed daily'
describe 'Lvl & Items', ->
beforeEach ->
freshTask {type: 'habit', text: 'Habit', up: true, down: true}
it 'modified damage based on lvl & armor'
it 'modified exp/gp based on lvl & weapon'
it 'always decreases hp with damage, regardless of stats/items'
it 'always increases exp/gp with gain, regardless of stats/items'
describe 'Dailies', ->
beforeEach ->
freshTask {type: 'daily', text: 'Daily', completed: false}
it 'created the daily', ->
task = model.get(taskPath)
expect(task.text).to.eql 'Daily'
expect(task.value).to.eql 0
it 'does proper calculations when daily is complete'
it 'calculates dailys properly when they have repeat dates'
runCron = (times, pass=1) ->
# Set lastCron to days ago
today = new moment()
ago = new moment().subtract('days',times)
model.set '_user.lastCron', ago.toDate()
# Run run
scoring.cron()
[stats, task] = statsTask()
# Should have updated cron to today
lastCron = moment(model.get('_user.lastCron'))
expect(today.diff(lastCron, 'days')).to.eql 0
shouldBe = modificationsLookup('down', {times:times*pass})
# Should have updated points properly
expect(Math.round(stats.hp)).to.be.eql Math.round(shouldBe.user.stats.hp)
expect(Math.round(task.value)).to.eql Math.round(shouldBe.value)
it 'calculates user.stats & task.value properly on cron', ->
runCron(10)
it 'runs cron multiple times properly', ->
runCron(5)
runCron(5, 2)
#TODO clicking repeat dates on newly-created item doesn't refresh until you refresh the page
#TODO dates on dailies is having issues, possibility: date cusps? my saturday exempts were set to exempt at 8pm friday
describe 'Todos', ->
describe 'Cron', ->
it 'calls cron asyncronously'
it 'should calculate user.stats & task.value properly on cron'
it 'should calculate cron based on difference between start-of-days, and not run in the middle of the day'
it 'should only run set operations once per task, even when daysPassed > 1'
# pass in daysPassed to score, multiply modification values by daysPassed before running set
it 'should only push a history point for lastCron, not each day in between'
# stop passing in tallyFor, let moment().sod().toDate() be handled in scoring.score()
it 'should defer saving user modifications until, save as aggregate values'
# pass in commit parameter to scoring func, if true save right away, otherwise return aggregated array so can save in the end (so total hp loss, etc)
describe 'Rewards', ->
#### Require.js stuff, might be necessary to place in casper.coffee
it "doesn't setup dependent functions until their modules are loaded, require.js callback"
# sortable, stripe, etc
#TODO refactor as user->habits, user->dailys, user->todos, user->rewards

View file

@ -14,6 +14,10 @@
<hr/>
<p>
<h4>5/25/2013</h4>
<ul>
<li>Code logic migrated to <a target="_blank" href="https://github.com/habitrpg/habitrpg-shared">habitrpg-shared</a>. See <a target="_blank" href="https://github.com/lefnire/habitrpg/issues/1039">details here</a>, but two takeaways: (1) keep an eye out and <a href="http://community.habitrpg.com/content/submitting-bugs" target="_blank">report a problem</a> if you experience any issues, (2) this is going to allow for much less buggy code (read previous link for reasoning).</li>
</ul>
<h4>5/12/2013</h4>
<ul>

View file

@ -105,7 +105,7 @@
<li><h4><a target="_blank" href="http://community.habitrpg.com/forums/lfg">LFG Posts</a></h4></li>
<li><h4><a target="_blank" href="http://www.youtube.com/watch?feature=player_embedded&v=cT5ghzZFfao">Tutorial</a></h4></li>
<li><h4><a target="_blank" href="http://community.habitrpg.com/faq-page">FAQ</a></h4></li>
<li><h4><a target="_blank" href="https://github.com/lefnire/habitrpg/issues?state=open">Submit a Bug</a></h4></li>
<li><h4><a target="_blank" href="https://github.com/lefnire/habitrpg/issues?state=open">Report a Problem</a></h4></li>
<li><h4><a target="_blank" href="https://trello.com/board/habitrpg/50e5d3684fe3a7266b0036d6">Request a Feature</a></h4></li>
<li><h4><a target="_blank" href="http://community.habitrpg.com/forum">Community Forum</a></h4></li>
</ul>
@ -114,10 +114,22 @@
</div>
<div class='span8'>
<h3>Tavern Talk & LFG</h3>
<form x-bind='submit:tavernSendChat'>
<textarea class="span6" rows="3"x-bind='keyup:tavernMessageKeyup'>{_tavernMessage}</textarea><br/>
<input class=btn type=submit value=Submit />
</form>
<div class='row-fluid'>
<div class='span3'>
<ul class='unstyled buttonList'>
<li><a class='btn btn-info' style='width:100%' target="_blank" href="http://community.habitrpg.com/faq-page">FAQ</a></li>
<li><a class='btn btn-info' style='width:100%' target="_blank" href="https://github.com/lefnire/habitrpg/issues?state=open">Report a Problem</a></li>
<li><a class='btn btn-info' style='width:100%' target="_blank" href="https://trello.com/board/habitrpg/50e5d3684fe3a7266b0036d6">Request a Feature</a></li>
</ul>
</div>
<div class=span9>
<form x-bind='submit:tavernSendChat'>
<textarea class="span6" rows="3" x-bind='keyup:tavernMessageKeyup'>{_tavernMessage}</textarea><br/>
<input class=btn type=submit value="Send Chat" />
</form>
</div>
</div>
<ul class='unstyled tavern-chat'>
{#each _tavern.chat.messages as :message}
<app:party:chat-message message={{:message}} />

View file

@ -63,6 +63,6 @@
class="label {{#if @message.npc}}label-success{{else if @message.contributor}}label-inverse{{else if equal(@message.uuid,_user.id)}}label-info{{/}} chat-message"
rel='tooltip' title="{{@message.contributor}}{{@message.npc}}">
{{@message.user}}</span> {{@message.text}} - <span class='muted time'>{relativeDate(@message.timestamp, _currentTime)}
{{#if equal(@message.uuid,_user.id)}}{{#with @message}}<a x-bind="click:deleteChatMessage"><i rel=tooltip title=Delete class=icon-remove></i></a>{{/}}{{/}}
{{#if or(_user.backer.admin,equal(@message.uuid,_user.id))}}{{#with @message}}<a x-bind="click:deleteChatMessage"><i rel=tooltip title=Delete class=icon-remove></i></a>{{/}}{{/}}
</span>
</li>

View file

@ -309,6 +309,11 @@
<button type="button" class="task-action-btn tile {#if equal(:task.priority,'!!')}active{/}" data-priority='!!' x-bind=click:tasksSetPriority>Medium</button>
<button type="button" class="task-action-btn tile {#if equal(:task.priority,'!!!')}active{/}" data-priority='!!!' x-bind=click:tasksSetPriority>Hard</button>
</div>
{{#if equal(:task.type,'daily')}}
<legend class="option-title">Restore Streak</legend>
<input class='option-content' type=number value="{int(:task.streak)}" />
{{/}}
</fieldset>
{/}