Merge remote-tracking branch 'upstream/develop' into todos-dated-tab-2014-11-01

Conflicts:
	dist/habitrpg-shared.js
This commit is contained in:
Alice Harris
2015-01-11 14:18:37 +10:00
740 changed files with 25023 additions and 9042 deletions
+165 -40
View File
@@ -8,6 +8,7 @@ api.i18n = i18n
# little helper for large arrays of strings. %w"this that another" equivalent from Rails, I really miss that function
$w = api.$w = (s)->s.split(' ')
api.dotSet = (obj,path,val)->
arr = path.split('.')
_.reduce arr, (curr, next, index) =>
@@ -18,6 +19,20 @@ api.dotSet = (obj,path,val)->
api.dotGet = (obj,path)->
_.reduce path.split('.'), ((curr, next) => curr?[next]), obj
###
Reflists are arrays, but stored as objects. Mongoose has a helluvatime working with arrays (the main problem for our
syncing issues) - so the goal is to move away from arrays to objects, since mongoose can reference elements by ID
no problem. To maintain sorting, we use these helper functions:
###
api.refPush = (reflist, item, prune=0) ->
item.sort = if _.isEmpty(reflist) then 0 else _.max(reflist,'sort').sort+1
item.id = api.uuid() unless item.id and !reflist[item.id]
reflist[item.id] = item
api.planGemLimits =
convRate: 20 #how much does a gem cost?
convCap: 25 #how many gems can be converted / month?
###
------------------------------------------------------
Time / Day
@@ -42,7 +57,10 @@ api.startOfWeek = api.startOfWeek = (options={}) ->
api.startOfDay = (options={}) ->
o = sanitizeOptions(options)
moment(o.now).startOf('day').add({hours:o.dayStart})
dayStart = moment(o.now).startOf('day').add({hours:o.dayStart})
if moment(o.now).hour() < o.dayStart
dayStart.subtract({days:1})
dayStart
api.dayMapping = {0:'su',1:'m',2:'t',3:'w',4:'th',5:'f',6:'s'}
@@ -51,7 +69,7 @@ api.dayMapping = {0:'su',1:'m',2:'t',3:'w',4:'th',5:'f',6:'s'}
###
api.daysSince = (yesterday, options = {}) ->
o = sanitizeOptions options
Math.abs api.startOfDay(_.defaults {now:yesterday}, o).diff(o.now, 'days')
Math.abs api.startOfDay(_.defaults {now:yesterday}, o).diff(api.startOfDay(_.defaults {now:o.now}, o), 'days')
###
Should the user do this taks on this date, given the task's repeat options and user.preferences.dayStart?
@@ -60,13 +78,7 @@ api.shouldDo = (day, repeat, options={}) ->
return false unless repeat
o = sanitizeOptions options
selected = repeat[api.dayMapping[api.startOfDay(_.defaults {now:day}, o).day()]]
return selected unless moment(day).zone(o.timezoneOffset).isSame(o.now,'d')
if options.dayStart <= o.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(o.now).subtract({days:1}).day() # have to wrap o.now so as not to modify original
return repeat[api.dayMapping[yesterday]] # FIXME is this correct?? Do I need to do any timezone calcaulation here?
return selected
###
------------------------------------------------------
@@ -263,6 +275,7 @@ api.taskClasses = (task, filters=[], dayStart=0, lastCron=+new Date, main=false)
classes += " uncompleted"
else if type is 'habit'
classes += ' habit-wide' if task.down and task.up
classes += ' habit-narrow' if !task.down and !task.up
if value < -20
classes += ' color-worst'
@@ -381,7 +394,9 @@ api.wrap = (user, main=true) ->
cb? null, {}
revive: (req, cb) ->
# Reset stats
return cb?({code:400, message: "Cannot revive if not dead"}) unless user.stats.hp <= 0
# Reset stats after death
_.merge user.stats, {hp:50, exp:0, gp:0}
user.stats.lvl-- if user.stats.lvl > 1
@@ -390,10 +405,18 @@ api.wrap = (user, main=true) ->
user.stats[lostStat]-- if lostStat
# Lose a gear piece
# Note, they can actually lose item weapon_*_0 - it's 0 to buy back, no big deal
# Free items (value:0) cannot be lost to avoid "pay to win". Subscribers have more free (Mystery) items and so would have a higher chance of losing a free one. The only exception is that the weapon_warrior_0 free item can be lost so that a new player who dies before buying any gear does experience equipment loss.
# Note ""+k string-casting. Without this, when run on the server Mongoose returns funny objects
lostItem = user.fns.randomVal _.reduce(user.items.gear.owned, ((m,v,k)->m[''+k]=''+k if v;m), {})
cl = user.stats.class
gearOwned = user.items.gear.owned.toObject?() or user.items.gear.owned
losableItems = {}
_.each gearOwned, (v,k) ->
if v
itm = content.gear.flat[''+k]
if itm
if (itm.value > 0 || k == 'weapon_warrior_0') && ( itm.klass == cl || ( itm.klass == 'special' && (! itm.specialClass || itm.specialClass == cl) ) )
losableItems[''+k]=''+k
lostItem = user.fns.randomVal losableItems
if item = content.gear.flat[lostItem]
user.items.gear.owned[lostItem] = false
user.items.gear.equipped[item.type] = "#{item.type}_base_0" if user.items.gear.equipped[item.type] is lostItem
@@ -589,6 +612,40 @@ api.wrap = (user, main=true) ->
user.markModified? type
cb? null, user.tags
# ------
# Webhooks
# ------
addWebhook: (req, cb) ->
wh = user.preferences.webhooks
api.refPush(wh, {url:req.body.url, enabled: req.body.enabled or true, id:req.body.id})
user.markModified? 'preferences.webhooks'
cb? null, user.preferences.webhooks
updateWebhook: (req, cb) ->
_.merge(user.preferences.webhooks[req.params.id], req.body)
user.markModified? 'preferences.webhooks'
cb? null, user.preferences.webhooks
deleteWebhook: (req, cb) ->
delete user.preferences.webhooks[req.params.id]
user.markModified? 'preferences.webhooks'
cb? null, user.preferences.webhooks
# ------
# Inbox
# ------
clearPMs: (req, cb) ->
user.inbox.messages = {}
user.markModified? 'inbox.messages'
cb? null, user.inbox.messages
deletePM: (req, cb) ->
delete user.inbox.messages[req.params.id]
user.markModified? 'inbox.messages.'+req.params.id
cb? null, user.inbox.messages
blockUser: (req, cb) ->
i = user.inbox.blocks.indexOf(req.params.uuid)
if ~i then user.inbox.blocks.splice(i,1) else user.inbox.blocks.push(req.params.uuid)
user.markModified? 'inbox.blocks'
cb? null, user.inbox.blocks
# ------
# Inventory
# ------
@@ -626,26 +683,47 @@ api.wrap = (user, main=true) ->
user.items.food[food.key]--
cb? {code:200, message}, userPets[pet]
#FIXME stupid method of special-handling spookDust, since it can be purchased with gold and the system only accomodates gem-purchasable holiday spells
buySpookDust: (req,cb) ->
item = content.special.spookDust
buySpecialSpell: (req,cb) ->
{key} = req.params
item = content.special[key]
return cb?({code:401, message: i18n.t('messageNotEnoughGold', req.language)}) if user.stats.gp < item.value
user.stats.gp -= item.value
user.items.special.spookDust ?= 0
user.items.special.spookDust++
user.items.special[key] ?= 0
user.items.special[key]++
user.markModified? 'items.special'
cb? null, _.pick(user,$w 'items stats')
message = i18n.t('messageBought', {itemText: item.text(req.language)}, req.language)
cb? {code:200,message}, _.pick(user,$w 'items stats')
# buy is for gear, purchase is for gem-purchaseables (i know, I know...)
# buy is for using Gold, purchase is for Gems (I know, I know...)
purchase: (req, cb, ga) ->
{type,key} = req.params
return cb?({code:404,message:":type must be in [hatchingPotions,eggs,food,quests,special]"},req) unless type in ['eggs','hatchingPotions','food','quests','special']
item = content[type][key]
if type is 'gems' and key is 'gem'
{convRate, convCap} = api.planGemLimits
convCap += user.purchased.plan.consecutive.gemCapExtra
return cb?({code:401,message:"Must subscribe to purchase gems with GP"},req) unless user.purchased?.plan?.customerId
return cb?({code:401,message:"Not enough Gold"}) unless user.stats.gp >= convRate
return cb?({code:401,message:"You've reached the Gold=>Gem conversion cap (#{convCap}) for this month. We have this to prevent abuse / farming. The cap will reset within the first three days of next month."}) if user.purchased.plan.gemsBought >= convCap
user.balance += .25
user.purchased.plan.gemsBought++
user.stats.gp -= convRate
return cb? {code:200,message:"+1 Gems"}, _.pick(user,$w 'stats balance')
return cb?({code:404,message:":type must be in [eggs,hatchingPotions,food,quests,gear]"},req) unless type in ['eggs','hatchingPotions','food','quests','gear']
if type is 'gear'
item = content.gear.flat[key]
return cb?({code:401, message: i18n.t('alreadyHave', req.language)}) if user.items.gear.owned[key]
price = (if item.twoHanded then 2 else 1) / 4
else
item = content[type][key]
price = item.value / 4
return cb?({code:404,message:":key not found for Content.#{type}"},req) unless item
return cb?({code:401, message: i18n.t('notEnoughGems', req.language)}) if user.balance < (item.value / 4)
user.items[type][key] = 0 unless user.items[type][key] > 0
user.items[type][key]++
user.balance -= (item.value / 4)
return cb?({code:401, message: i18n.t('notEnoughGems', req.language)}) if user.balance < price
user.balance -= price
if type is 'gear' then user.items.gear.owned[key] = true
else
user.items[type][key] = 0 unless user.items[type][key] > 0
user.items[type][key]++
cb? null, _.pick(user,$w 'items balance')
ga?.event('purchase', key).send()
@@ -698,6 +776,16 @@ api.wrap = (user, main=true) ->
user.stats.gp -= item.value
cb? {code:200, message}, _.pick(user,$w 'items achievements stats')
buyMysterySet: (req, cb)->
return cb?({code:401, message:"You don't have enough Mystic Hourglasses"}) unless user.purchased.plan.consecutive.trinkets>0
mysterySet = content.timeTravelerStore(user.items.gear.owned)?[req.params.key]
if window?.confirm?
return unless window.confirm("Buy this full set of items for 1 Mystic Hourglass?")
return cb?({code:404, message:"Mystery set not found, or set already owned"}) unless mysterySet
_.each mysterySet.items, (i)->user.items.gear.owned[i.key]=true
user.purchased.plan.consecutive.trinkets--
cb? null, _.pick(user,$w 'items purchased.plan.consecutive')
sell: (req, cb) ->
{key, type} = req.params
return cb?({code:404,message:":type not found. Must bes in [eggs, hatchingPotions, food]"}) unless type in ['eggs','hatchingPotions', 'food']
@@ -828,6 +916,22 @@ api.wrap = (user, main=true) ->
user.markModified? 'items.special.valentineReceived'
cb? null, 'items.special'
openMysteryItem: (req,cb,ga) ->
item = user.purchased.plan?.mysteryItems?.shift()
return cb?(code:400,message:"Empty") unless item
item = content.gear.flat[item]
user.items.gear.owned[item.key] = true
user.markModified? 'purchased.plan.mysteryItems'
# Could show {code:200} message, but it's yellow with no icon. This is round-about, but prettier. FIXME
(user._tmp?={}).drop = {type: 'gear', dialog: "#{item.text(req.language)} inside!"} if typeof window != 'undefined'
#cb? {code:200, message:"#{item.text} inside!"}, user.items.gear.owned
cb? null, user.items.gear.owned
readNYE: (req,cb) ->
user.items.special.nyeReceived.shift()
user.markModified? 'items.special.nyeReceived'
cb? null, 'items.special'
# ------
# Score
# ------
@@ -1126,7 +1230,8 @@ api.wrap = (user, main=true) ->
user.markModified? 'party.quest.progress'
#console.log {progress:user.party.quest.progress}
return if (api.daysSince(user.items.lastDrop.date, user.preferences) is 0) and (user.items.lastDrop.count >= 5 + Math.floor(user._statsComputed.per / 25) + (user.contributor.level or 0))
dropMultiplier = if user.purchased?.plan?.customerId then 2 else 1
return if (api.daysSince(user.items.lastDrop.date, user.preferences) is 0) and (user.items.lastDrop.count >= dropMultiplier * (5 + Math.floor(user._statsComputed.per / 25) + (user.contributor.level or 0)))
if user.flags?.dropsEnabled and user.fns.predictableRandom(user.stats.exp) < chance
# current breakdown - 1% (adjustable) chance on drop
@@ -1210,7 +1315,7 @@ api.wrap = (user, main=true) ->
)()]++
updateStats: (stats, req) ->
# Game Over
# Game Over (death)
return user.stats.hp=0 if stats.hp <= 0
user.stats.hp = stats.hp
@@ -1251,7 +1356,7 @@ api.wrap = (user, main=true) ->
if !user.flags.classSelected and user.stats.lvl >= 10
user.flags.classSelected
# Level Drops
_.each {vice1:30, atom1:15, moonstone1:60}, (lvl,k)->
_.each {vice1:30, atom1:15, moonstone1:60, goldenknight1: 40}, (lvl,k)->
if !user.flags.levelDrops?[k] and user.stats.lvl >= lvl
user.items.quests[k] ?= 0
user.items.quests[k]++
@@ -1305,6 +1410,29 @@ api.wrap = (user, main=true) ->
perfect = true
clearBuffs = {str:0,int:0,per:0,con:0,stealth:0,streaks:false}
# end-of-month perks for subscribers
plan = user.purchased?.plan
if plan?.customerId
if moment(plan.dateUpdated).format('MMYYYY') != moment().format('MMYYYY')
plan.gemsBought = 0 # reset gem-cap
plan.dateUpdated = new Date()
# For every month, inc their "consecutive months" counter. Give perks based on consecutive blocks
# If they already got perks for those blocks (eg, 6mo subscription, subscription gifts, etc) - then dec the offset until it hits 0
# TODO use month diff instead of ++ / --?
_.defaults plan.consecutive, {count:0, offset:0, trinkets:0, gemCapExtra:0} #fixme see https://github.com/HabitRPG/habitrpg/issues/4317
plan.consecutive.count++
if plan.consecutive.offset > 0
plan.consecutive.offset--
else if plan.consecutive.count%3==0 # every 3 months
plan.consecutive.trinkets++
plan.consecutive.gemCapExtra+=5
plan.consecutive.gemCapExtra=25 if plan.consecutive.gemCapExtra>25 # cap it at 50 (hard 25 limit + extra 25)
# If user cancelled subscription, we give them until 30day's end until it terminates
if plan.dateTerminated && moment(plan.dateTerminated).isBefore(+new Date)
_.merge plan, {planId:null, customerId:null, paymentMethod:null}
_.merge plan.consecutive, {count:0, offset:0, gemCapExtra:0}
user.markModified? 'purchased.plan'
# 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.preferences.sleep is true
@@ -1363,9 +1491,14 @@ api.wrap = (user, main=true) ->
lvl++
expTally += api.tnl(lvl)
(user.history.exp ?= []).push { date: now, value: expTally }
user.fns.preenUserHistory()
user.markModified? 'history'
user.markModified? 'dailys' # covers dailys.*.history
# premium subscribers can keep their full history.
# TODO figure out something performance-wise
unless user.purchased?.plan?.customerId
user.fns.preenUserHistory()
user.markModified? 'history'
user.markModified? 'dailys' # covers dailys.*.history
user.stats.buffs =
if perfect
user.achievements.perfect ?= 0
@@ -1378,17 +1511,9 @@ api.wrap = (user, main=true) ->
else clearBuffs
# Add 10 MP, or 10% of max MP if that'd be more. Perform this after Perfect Day for maximum benefit
user.stats.mp += _.max([10,.1 * user._statsComputed.maxMP])
user.stats.mp = user._statsComputed.maxMP if user.stats.mp > user._statsComputed.maxMP
# If user cancelled subscription, we give them until 30day's end until it terminates
plan = user.purchased?.plan
if plan && plan.customerId && plan.dateTerminated && moment(plan.dateTerminated).isBefore(+new Date)
_.merge user.purchased.plan, {planId:null, customerId:null, paymentMethod:null}
user.markModified 'purchased.plan'
# After all is said and done, progress up user's effect on quest, return those values & reset the user's
progress = user.party.quest.progress; _progress = _.cloneDeep progress
_.merge progress, {down:0,up:0}