mirror of
https://github.com/sudoxnym/habitica-self-host.git
synced 2026-08-02 04:00:36 +00:00
Merge branch 'develop' into challenges-tmp
Conflicts: src/app/helpers.coffee src/app/index.coffee
This commit is contained in:
commit
35049f7752
33 changed files with 265 additions and 1223 deletions
|
|
@ -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});
|
||||
}
|
||||
});*/
|
||||
|
|
@ -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});
|
||||
})
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
BIN
public/img/coin_single_gold.png
Normal file
BIN
public/img/coin_single_gold.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
public/img/coin_single_silver.png
Normal file
BIN
public/img/coin_single_silver.png
Normal file
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 |
|
|
@ -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
|
||||
|
||||
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = ->
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
104
src/app/misc.coffee
Normal 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
|
||||
|
|
@ -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) ->
|
||||
|
|
|
|||
|
|
@ -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?"
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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) ->
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) ->
|
||||
|
|
|
|||
|
|
@ -34,3 +34,6 @@
|
|||
background-position: -1200px 0px
|
||||
width: 40px
|
||||
height: 40px
|
||||
|
||||
.buttonList li
|
||||
margin: 5px;
|
||||
|
|
@ -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) ->
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}} />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
{/}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue