|
|
@ -1,6 +1,3 @@
|
|||
language: node_js
|
||||
node_js:
|
||||
- "0.10"
|
||||
branches:
|
||||
only:
|
||||
- rewrite
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ Shared resources useful for the multiple HabitRPG repositories, that way all the
|
|||
* `algos.score(user, task, direction)`, etc
|
||||
* TODO document all the functions
|
||||
|
||||
##Tests
|
||||
* `npm install`
|
||||
|
||||
##CSS
|
||||
Shared CSS between the website and the mobile app is a fuzzy area. Spritesheets definitely go in habitrpg-shared (since mobile
|
||||
uses them too). Other things, like customizer buttons, *may* want to go here? As you find sharable components, (1) move them
|
||||
|
|
|
|||
|
|
@ -1,43 +1,64 @@
|
|||
/* FIXME - what's going on with these negative margin requirements? */
|
||||
.weapon_7, .armor_6, .head_6, .shield_6 {
|
||||
/*background: url("../img/sprites/backer-only/BackerOnly-SpriteSheet.png") no-repeat*/
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
|
||||
/*FIXME - not he best way of handling the alignment issues*/
|
||||
margin-left: -3px;
|
||||
margin-top: -18px;
|
||||
/*background: url("../img/sprites/backer-only/BackerOnly-SpriteSheet.png") no-repeat*/
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
margin-left: -3px;
|
||||
margin-top: -18px;
|
||||
}
|
||||
.weapon_8, .armor_7, .head_7, .shield_7 {
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
margin-left: -9px;
|
||||
margin-top: -15px;
|
||||
}
|
||||
|
||||
.head_6 {
|
||||
background: url("../img/sprites/backer-only/BackerOnly-Equip-ShadeHelmet.gif") no-repeat;
|
||||
background: url("../img/sprites/backer-only/BackerOnly-Equip-ShadeHelmet.gif") no-repeat;
|
||||
}
|
||||
.head_7 {
|
||||
background: url("../img/sprites/backer-only/ContributorOnly-Equip-CrystalHelmet.gif") no-repeat;
|
||||
}
|
||||
|
||||
.armor_6 {
|
||||
background: url("../img/sprites/backer-only/BackerOnly-Equip-ShadeArmor.gif") no-repeat;
|
||||
background: url("../img/sprites/backer-only/BackerOnly-Equip-ShadeArmor.gif") no-repeat;
|
||||
}
|
||||
.armor_7 {
|
||||
background: url("../img/sprites/backer-only/ContributorOnly-Equip-CrystalArmor.gif") no-repeat;
|
||||
}
|
||||
|
||||
.shield_6 {
|
||||
background: url("../img/sprites/backer-only/BackerOnly-Shield-TormentedSkull.gif") no-repeat;
|
||||
background: url("../img/sprites/backer-only/BackerOnly-Shield-TormentedSkull.gif") no-repeat;
|
||||
}
|
||||
.shield_7 {
|
||||
background: url("../img/sprites/backer-only/ContributorOnly-Shield-Crystal.png") no-repeat;
|
||||
}
|
||||
|
||||
.weapon_7 {
|
||||
background: url("../img/sprites/backer-only/BackerOnly-Weapon-DarkSoulsBlade.gif") no-repeat;
|
||||
background: url("../img/sprites/backer-only/BackerOnly-Weapon-DarkSoulsBlade.gif") no-repeat;
|
||||
}
|
||||
.weapon_8 {
|
||||
background: url("../img/sprites/backer-only/ContributorOnly-Weapon-CrystalBlade.png") no-repeat;
|
||||
}
|
||||
|
||||
.Pet-Wolf-Cerberus {
|
||||
width: 105px;
|
||||
height: 72px;
|
||||
background: url("../img/sprites/backer-only/BackerOnly-Pet-CerberusPup.gif") no-repeat;
|
||||
width: 105px;
|
||||
height: 72px;
|
||||
background: url("../img/sprites/backer-only/BackerOnly-Pet-CerberusPup.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Pet-Dragon-Hydra {
|
||||
width: 106px;
|
||||
height: 89px;
|
||||
background: url("../img/sprites/backer-only/ContributorOnly-Pet-Hydra.png") no-repeat;
|
||||
width: 106px;
|
||||
height: 89px;
|
||||
background: url("../img/sprites/backer-only/ContributorOnly-Pet-Hydra.png") no-repeat;
|
||||
}
|
||||
|
||||
.shop_shield_6 {background-position: -1040px 0}
|
||||
.shop_weapon_7 {background-position: -1080px 0}
|
||||
.shop_armor_6 {background-position: -1160px 0}
|
||||
.shop_head_6 {background-position: -1120px 0}
|
||||
.shop_armor_6 {background-position: -1160px 0}
|
||||
|
||||
.shop_shield_7 {background-position: -1318px 0; width: 35px;} /* something got busted here, @Shaners can you take a peak? */
|
||||
.shop_weapon_8 {background-position: -1352px 0; width: 45px;}
|
||||
.shop_head_7 {background-position: -1398px 0}
|
||||
.shop_armor_7 {background-position: -1438px 0}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
.customize-menu .f_hair_brown {width: 60px; height: 60px; background-position: -2099px -8px;}
|
||||
.customize-menu .f_hair_black {width: 60px; height: 60px; background-position: -2189px -8px;}
|
||||
.customize-menu .f_hair_blond {width: 60px; height: 60px; background-position: -2279px -8px;}
|
||||
.customize-menu .f_hair_red {width: 60px; height: 60px; background-position: -4979px -8px;}
|
||||
|
||||
.customize-menu .f_skin_dead {width: 60px; height: 60px; background-position: -2997px -20px;}
|
||||
.customize-menu .f_skin_orc {width: 60px; height: 60px; background-position: -3087px -20px;}
|
||||
|
|
@ -40,6 +41,7 @@
|
|||
.customize-menu .m_hair_black {width: 60px; height: 60px; background-position: -1737px -6px;}
|
||||
.customize-menu .m_hair_brown {width: 60px; height: 60px; background-position: -1827px -6px;}
|
||||
.customize-menu .m_hair_white {width: 60px; height: 60px; background-position: -1917px -6px;}
|
||||
.customize-menu .m_hair_red {width: 60px; height: 60px; background-position: -4528px -6px;}
|
||||
|
||||
.customize-menu .m_skin_dead {width: 60px; height: 60px; background-position: -2547px -20px;}
|
||||
.customize-menu .m_skin_orc {width: 60px; height: 60px; background-position: -2637px -20px;}
|
||||
|
|
|
|||
4245
dist/habitrpg-shared.js
vendored
2
dist/spritesheets.css
vendored
|
Before Width: | Height: | Size: 912 B After Width: | Height: | Size: 912 B |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
BIN
img/sprites/pet-hatching/Wolf-CottonCandyBlue.gif
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
img/sprites/pet-hatching/Wolf-CottonCandyPink.gif
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
img/sprites/pet-hatching/Wolf-Desert.gif
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
img/sprites/pet-hatching/Wolf-Golden.gif
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
img/sprites/pet-hatching/Wolf-Red.gif
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
img/sprites/pet-hatching/Wolf-Shade.gif
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
img/sprites/pet-hatching/Wolf-Skeleton.gif
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
img/sprites/pet-hatching/Wolf-White.gif
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
img/sprites/pet-hatching/Wolf-Zombie.gif
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
img/sprites/pet-hatching/Wolf.gif
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
|
|
@ -2,11 +2,11 @@
|
|||
"name": "habitrpg-shared",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"browserify": "~2.34.3",
|
||||
"browserify": "~2.35",
|
||||
"coffeeify": "0.4.0",
|
||||
"coffee-script": "1.6.2",
|
||||
"relative-date": "~1.1.1",
|
||||
"lodash": "~1.3.1",
|
||||
"lodash": "~2.2.1",
|
||||
"moment": "~2.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -383,6 +383,11 @@ obj.cron = (user, options={}) ->
|
|||
|
||||
user.lastCron = now; paths['lastCron'] = true
|
||||
|
||||
# Reset the lastDrop count to zero
|
||||
if user.items.lastDrop.count > 0
|
||||
user.items.lastDrop.count = 0
|
||||
paths['items.lastDrop'] = true
|
||||
|
||||
# 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
|
||||
return if user.flags.rest is true
|
||||
|
|
@ -431,4 +436,56 @@ obj.cron = (user, options={}) ->
|
|||
expTally += obj.tnl(lvl)
|
||||
(user.history.exp ?= []).push { date: now, value: expTally }
|
||||
paths["history"] = true
|
||||
obj.preenUserHistory(user) # we can probably start removing paths[...] stuff, no longer used by our app
|
||||
user
|
||||
|
||||
###
|
||||
Preen history for users with > 7 history entries
|
||||
This takes an infinite array of single day entries [day day day day day...], and turns it into a condensed array
|
||||
of averages, condensing more the further back in time we go. Eg, 7 entries each for last 7 days; 1 entry each week
|
||||
of this month; 1 entry for each month of this year; 1 entry per previous year: [day*7 week*4 month*12 year*infinite]
|
||||
###
|
||||
preenHistory = (history) ->
|
||||
history = _.filter(history, (h) -> !!h) # discard nulls (corrupted somehow)
|
||||
newHistory = []
|
||||
preen = (amount, groupBy) ->
|
||||
groups = _.chain(history)
|
||||
.groupBy((h) -> moment(h.date).format groupBy) # get date groupings to average against
|
||||
.sortBy((h, k) -> k) # sort by date
|
||||
.value() # turn into an array
|
||||
groups = groups.slice(-amount)
|
||||
groups.pop() # get rid of "this week", "this month", etc (except for case of days)
|
||||
_.each groups, (group) ->
|
||||
newHistory.push
|
||||
date: moment(group[0].date).toDate()
|
||||
#date: moment(group[0].date).format('MM/DD/YYYY') # Use this one when testing
|
||||
value: _.reduce(group, ((m, obj) -> m + obj.value), 0) / group.length # average
|
||||
true
|
||||
|
||||
# Keep the last:
|
||||
preen 50, "YYYY" # 50 years (habit will toootally be around that long!)
|
||||
preen moment().format('MM'), "YYYYMM" # last MM months (eg, if today is 05, keep the last 5 months)
|
||||
|
||||
# Then keep all days of this month. Note, the extra logic is to account for Habits, which can be counted multiple times per day
|
||||
# FIXME I'd rather keep 1-entry/week of this month, then last 'd' days in this week. However, I'm having issues where the 1st starts mid week
|
||||
thisMonth = moment().format('YYYYMM')
|
||||
newHistory = newHistory.concat _.filter(history, (h)-> moment(h.date).format('YYYYMM') is thisMonth)
|
||||
#preen Math.ceil(moment().format('D')/7), "YYYYww" # last __ weeks (# weeks so far this month)
|
||||
#newHistory = newHistory.concat(history.slice -moment().format('D')) # each day of this week
|
||||
|
||||
newHistory
|
||||
|
||||
# Registered users with some history
|
||||
obj.preenUserHistory = (user) ->
|
||||
minHistLen = 7
|
||||
_.each user.habits.concat(user.dailys), (task) ->
|
||||
task.history = preenHistory(task.history) if task.history?.length > minHistLen
|
||||
true
|
||||
|
||||
_.defaults user.history, {todos:[], exp: []}
|
||||
user.history.exp = preenHistory(user.history.exp) if user.history.exp.length > minHistLen
|
||||
user.history.todos = preenHistory(user.history.todos) if user.history.todos.length > minHistLen
|
||||
#user.markModified('history')
|
||||
#user.markModified('habits')
|
||||
#user.markModified('dailys')
|
||||
|
||||
|
|
|
|||
|
|
@ -59,24 +59,29 @@ module.exports =
|
|||
self = @
|
||||
defaults =
|
||||
id: self.uuid()
|
||||
text: ''
|
||||
up: true
|
||||
down: true
|
||||
type: 'habit'
|
||||
completed: false
|
||||
repeat: {su:true,m:true,t:true,w:true,th:true,f:true,s:true}
|
||||
text: ''
|
||||
notes: ''
|
||||
priority: '!'
|
||||
challenge: {}
|
||||
tags: _.transform(filters, (m,v,k) -> m[k]=v if v)
|
||||
_.defaults task, defaults
|
||||
_.defaults(task, {up:true,down:true}) if task.type is 'habit'
|
||||
_.defaults(task, {history: []}) if task.type in ['habit', 'daily']
|
||||
_.defaults(task, {completed:false}) if task.type in ['daily', 'todo']
|
||||
_.defaults(task, {streak:0, repeat: {su:1,m:1,t:1,w:1,th:1,f:1,s:1}}) if task.type is 'daily'
|
||||
task._id = task.id # may need this for TaskSchema if we go back to using it, see http://goo.gl/a5irq4
|
||||
task.value ?= if task.type is 'reward' then 10 else 0
|
||||
task
|
||||
|
||||
newUser: (isDerby=false) ->
|
||||
# FIXME - should we remove this completely, since all defaults are accounted for in mongoose?
|
||||
# Or should we keep it so mobile can create an offline new user in the future?
|
||||
newUser: () ->
|
||||
userSchema =
|
||||
# _id / id handled by Racer
|
||||
stats: { gp: 0, exp: 0, lvl: 1, hp: 50 }
|
||||
invitations: {party:null, guilds: []}
|
||||
items: { weapon: 0, armor: 0, head: 0, shield: 0 }
|
||||
items: { weapon: 0, armor: 0, head: 0, shield: 0, lastDrop: { date: +new Date, count: 0 } }
|
||||
preferences: { gender: 'm', skin: 'white', hair: 'blond', armorSet: 'v1', dayStart:0, showHelm: true }
|
||||
apiToken: uuid() # set in newUserObject below
|
||||
lastCron: +new Date #this will be replaced with `+new Date` on first run
|
||||
|
|
@ -87,17 +92,10 @@ module.exports =
|
|||
ads: 'show'
|
||||
tags: []
|
||||
|
||||
if isDerby
|
||||
userSchema.habitIds = []
|
||||
userSchema.dailyIds = []
|
||||
userSchema.todoIds = []
|
||||
userSchema.rewardIds = []
|
||||
userSchema.tasks = {}
|
||||
else
|
||||
userSchema.habits = []
|
||||
userSchema.dailys = []
|
||||
userSchema.todos = []
|
||||
userSchema.rewards = []
|
||||
userSchema.habits = []
|
||||
userSchema.dailys = []
|
||||
userSchema.todos = []
|
||||
userSchema.rewards = []
|
||||
|
||||
# deep clone, else further new users get duplicate objects
|
||||
newUser = _.cloneDeep userSchema
|
||||
|
|
@ -126,11 +124,7 @@ module.exports =
|
|||
|
||||
for task in defaultTasks
|
||||
guid = task.id = uuid()
|
||||
if isDerby
|
||||
newUser.tasks[guid] = task
|
||||
newUser["#{task.type}Ids"].push guid
|
||||
else
|
||||
newUser["#{task.type}s"].push task
|
||||
newUser["#{task.type}s"].push task
|
||||
|
||||
for tag in defaultTags
|
||||
tag.id = uuid()
|
||||
|
|
@ -216,44 +210,7 @@ module.exports =
|
|||
loc = window?.location.host or process?.env?.BASE_URL or ''
|
||||
encodeURIComponent "http://#{loc}/v1/users/#{uid}/calendar.ics?apiToken=#{apiToken}"
|
||||
|
||||
###
|
||||
User's currently equiped item
|
||||
###
|
||||
equipped: (type, item=0, preferences={gender:'m', armorSet:'v1'}, backerTier=0) ->
|
||||
{gender, armorSet} = preferences
|
||||
item = ~~item
|
||||
backerTier = ~~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}"
|
||||
equipped: items.equipped
|
||||
|
||||
###
|
||||
Gold amount from their money
|
||||
|
|
@ -276,7 +233,7 @@ module.exports =
|
|||
###
|
||||
Task classes given everything about the class
|
||||
###
|
||||
taskClasses: (task, filters, dayStart, lastCron, showCompleted=false, main) ->
|
||||
taskClasses: (task, filters=[], dayStart=0, lastCron=+new Date, showCompleted=false, main=false) ->
|
||||
return unless task
|
||||
{type, completed, value, repeat} = task
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ items = module.exports.items =
|
|||
{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}
|
||||
{index: 7, text: "Dark Souls Blade", classes:'weapon_7', notes:'Increases experience gain by 21%.', strength: 21, value:150, canOwn: ((u)-> +u.backer?.tier >= 70)}
|
||||
{index: 8, text: "Crystal Blade", classes:'weapon_8', notes:'Increases experience gain by 24%.', strength: 24, value:170, canOwn: ((u)-> +u.contributor?.level >= 4)}
|
||||
]
|
||||
armor: [
|
||||
{index: 0, text: "Cloth Armor", classes: 'armor_0', notes:'Training armor.', defense: 0, value:0}
|
||||
|
|
@ -18,7 +19,8 @@ items = module.exports.items =
|
|||
{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}
|
||||
{index: 6, text: "Shade Armor", classes: 'armor_6', notes:'Decreases HP loss by 12%.', defense: 12, value:150, canOwn: ((u)-> +u.backer?.tier >= 45)}
|
||||
{index: 7, text: "Crystal Armor", classes: 'armor_7', notes:'Decreases HP loss by 14%.', defense: 14, value:170, canOwn: ((u)-> +u.contributor?.level >= 2)}
|
||||
]
|
||||
head: [
|
||||
{index: 0, text: "No Helm", classes: 'head_0', notes:'Training helm.', defense: 0, value:0}
|
||||
|
|
@ -27,7 +29,8 @@ items = module.exports.items =
|
|||
{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}
|
||||
{index: 6, text: "Shade Helm", classes: 'head_6', notes:'Decreases HP loss by 7%.', defense: 7, value:100, canOwn: ((u)-> +u.backer?.tier >= 45)}
|
||||
{index: 7, text: "Crystal Helm", classes: 'head_7', notes:'Decreases HP loss by 8%.', defense: 8, value:120, canOwn: ((u)-> +u.contributor?.level >= 3)}
|
||||
]
|
||||
shield: [
|
||||
{index: 0, text: "No Shield", classes: 'shield_0', notes:'No Shield.', defense: 0, value:0}
|
||||
|
|
@ -36,7 +39,8 @@ items = module.exports.items =
|
|||
{index: 3, text: "Reinforced 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}
|
||||
{index: 6, text: "Tormented Skull", classes: 'shield_6', notes:'Decreases HP loss by 9%.', defense: 9, value:120, canOwn: ((u)-> +u.backer?.tier >= 45)}
|
||||
{index: 7, text: "Crystal Shield", classes: 'shield_7', notes:'Decreases HP loss by 10%.', defense: 10, value:150, canOwn: ((u)-> +u.contributor?.level >= 5)}
|
||||
]
|
||||
potion: {type: 'potion', text: "Potion", notes: "Recover 15 HP (Instant Use)", 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 }
|
||||
|
|
@ -67,30 +71,32 @@ items = module.exports.items =
|
|||
{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
|
||||
# we somtimes want item arrays above in reverse order, for backward lookups (you'll see later in the code)
|
||||
reversed = {}
|
||||
|
||||
_.each ['weapon', 'armor', 'head', 'shield'], (type) ->
|
||||
reversed[type] = items[type].slice().reverse()
|
||||
# add "type" to each item, so we can reference that as "weapon" or "armor" in the html
|
||||
# Also add canOwn(), which we use when comparing if user is a backer or contributor - but defaulted to `return true`
|
||||
_.each items[type], (item) -> _.defaults(item, {type, canOwn: ->true})
|
||||
|
||||
_.each items.pets, (pet) -> pet.notes = 'Find a hatching potion to pour on this egg, and 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."
|
||||
|
||||
module.exports.buyItem = (user, type, options={}) ->
|
||||
_.defaults options, {paths: {}}
|
||||
|
||||
module.exports.buyItem = (user, type) ->
|
||||
nextItem =
|
||||
if type is 'potion' then items.potion
|
||||
else module.exports.getItem type, ((~~user.items[type] or 0) + 1)
|
||||
if type is 'potion' then items.potion
|
||||
else _.find items[type].slice(~~user.items[type] + 1), ((i) -> i.canOwn user)
|
||||
|
||||
return false if +user.stats.gp < +nextItem.value
|
||||
if nextItem.type is 'potion'
|
||||
user.stats.hp += 15;
|
||||
user.stats.hp = 50 if user.stats.hp > 50
|
||||
options.paths['stats.hp'] = true
|
||||
else
|
||||
user.items[type] = ~~nextItem.index
|
||||
options.paths["items.#{type}"] = true
|
||||
if +user.items.weapon >= 6 and +user.items.armor >= 5 and +user.items.head >= 5 and +user.items.shield >= 5
|
||||
user.achievements.ultimateGear = true;
|
||||
user.stats.gp -= +nextItem.value
|
||||
options.paths['stats.gp'] = true
|
||||
true
|
||||
|
||||
###
|
||||
|
|
@ -99,23 +105,37 @@ module.exports.buyItem = (user, type, options={}) ->
|
|||
module.exports.updateStore = (user) ->
|
||||
changes = {}
|
||||
_.each ['weapon', 'armor', 'shield', 'head'], (type) ->
|
||||
i = ~~(user.items?[type] or 0) + 1
|
||||
showNext = true
|
||||
if i is items[type].length - 1
|
||||
if (type in ['armor', 'shield', 'head'])
|
||||
showNext = user.backer?.tier and user.backer.tier >= 45 # backer armor
|
||||
else
|
||||
showNext = user.backer?.tier and user.backer.tier >= 70 # backer weapon
|
||||
else if i is items[type].length
|
||||
showNext = false
|
||||
changes[type] = if showNext then items[type][i] else {hide:true}
|
||||
# Find the first item that fits the "next" bill, from between current item and end of the list
|
||||
curr = user.items?[type] or 0
|
||||
changes[type] = _.find(items[type].slice(curr+1), ((item) -> item.canOwn user)) or {hide:true}
|
||||
changes.potion = items.potion
|
||||
changes.reroll = items.reroll
|
||||
changes
|
||||
|
||||
###
|
||||
Gets an itme, and caps max to the last item in its array
|
||||
Gets an item, and caps max to the last item in its array
|
||||
###
|
||||
module.exports.getItem = (type, index=0) ->
|
||||
i = if (~~index > items[type].length - 1) then items[type].length - 1 else ~~index
|
||||
# if they set their gear manually to something over what they can own, just set to 0 to avoid errors
|
||||
i = if (~~index > items[type].length - 1) then 0 else ~~index
|
||||
items[type][i]
|
||||
|
||||
###
|
||||
User's currently equiped item
|
||||
TODO this function is ugly, find a more elegant solution
|
||||
###
|
||||
module.exports.equipped = (type, item=0, pref={gender:'m', armorSet:'v1'}, backer={}, contributor={}) ->
|
||||
lastStandardItem = items[type].length - 3 # -1 for 0-based index, -2 for backer + contrib
|
||||
if (item > lastStandardItem) and !items[type][item]?.canOwn({backer,contributor})
|
||||
# They entered a # above what's legal in the restore dialog, find first legit item. (we should do checking on the restore-save instead)
|
||||
item = _.find(reversed[type], ((i) -> i.canOwn({backer, contributor}))).index
|
||||
|
||||
# backer / contrib (they don't have gender)
|
||||
return "#{type}_#{item}" if (item > lastStandardItem)
|
||||
|
||||
# Females have some special thing going on for their armor / helms
|
||||
if (type in ['armor', 'head'] and pref.gender is 'f')
|
||||
return "f_armor_#{item}_#{pref.armorSet}" if (item is 0 and type is 'armor')
|
||||
return "f_head_#{item}_#{pref.armorSet}" if (item > 1 and type is 'head')
|
||||
|
||||
return "#{pref.gender}_#{type}_#{item}"
|
||||
|
|
@ -9,10 +9,10 @@ helpers = require '../script/helpers.coffee'
|
|||
items = require '../script/items.coffee'
|
||||
|
||||
### Helper Functions ####
|
||||
|
||||
expectStrings = (obj, paths) ->
|
||||
_.each paths, (path) -> expect(obj[path]).to.be.ok()
|
||||
|
||||
# options.daysAgo: days ago when the last cron was executed
|
||||
beforeAfter = (options={}) ->
|
||||
user = helpers.newUser()
|
||||
[before, after] = [_.cloneDeep(user), _.cloneDeep(user)]
|
||||
|
|
@ -30,8 +30,8 @@ beforeAfter = (options={}) ->
|
|||
expectLostPoints = (before, after, taskType) ->
|
||||
if taskType in ['daily','habit']
|
||||
expect(after.stats.hp).to.be.lessThan before.stats.hp
|
||||
expect(_.size(after["#{taskType}s"][0].history)).to.be(1)
|
||||
else expect(_.size(after.history.todos)).to.be(1)
|
||||
expect(after["#{taskType}s"][0].history).to.have.length(1)
|
||||
else expect(after.history.todos).to.have.length(1)
|
||||
expect(after.stats.exp).to.be 0
|
||||
expect(after.stats.gp).to.be 0
|
||||
expect(after["#{taskType}s"][0].value).to.be.lessThan before["#{taskType}s"][0].value
|
||||
|
|
@ -41,7 +41,7 @@ expectGainedPoints = (before, after, taskType) ->
|
|||
expect(after.stats.exp).to.be.greaterThan before.stats.exp
|
||||
expect(after.stats.gp).to.be.greaterThan before.stats.gp
|
||||
expect(after["#{taskType}s"][0].value).to.be.greaterThan before["#{taskType}s"][0].value
|
||||
expect(_.size(after["#{taskType}s"][0].history)).to.be(1) if taskType is 'habit'
|
||||
expect(after["#{taskType}s"][0].history).to.have.length(1) if taskType is 'habit'
|
||||
# daily & todo histories handled on cron
|
||||
|
||||
expectNoChange = (before,after) -> expect(before).to.eql after
|
||||
|
|
@ -52,11 +52,11 @@ expectDayResetNoDamage = (b,a) ->
|
|||
expect(task.completed).to.be false
|
||||
expect(before.dailys[i].value).to.be task.value
|
||||
expect(before.dailys[i].streak).to.be task.streak
|
||||
expect(_.size(task.history)).to.be(1)
|
||||
expect(task.history).to.have.length(1)
|
||||
_.each after.todos, (task,i) ->
|
||||
expect(task.completed).to.be false
|
||||
expect(before.todos[i].value).to.be.greaterThan task.value
|
||||
expect(_.size(after.history.todos)).to.be(1)
|
||||
expect(after.history.todos).to.have.length(1)
|
||||
# hack so we can compare user before/after obj equality sans effected paths
|
||||
_.each ['dailys','todos','history','lastCron'], (path) ->
|
||||
_.each [before,after], (obj) -> delete obj[path]
|
||||
|
|
@ -69,13 +69,25 @@ cycle = (array)->
|
|||
n++
|
||||
return array[n % array.length]
|
||||
|
||||
repeatWithoutLastWeekday = ()->
|
||||
repeat = {su:1,m:1,t:1,w:1,th:1,f:1,s:1}
|
||||
if helpers.startOfWeek(moment().zone(0)).isoWeekday() == 1 # Monday
|
||||
repeat.su = false
|
||||
else
|
||||
repeat.s = false
|
||||
{repeat: repeat}
|
||||
|
||||
###### Specs ######
|
||||
|
||||
describe 'User', ->
|
||||
it 'sets correct user defaults', ->
|
||||
user = helpers.newUser()
|
||||
expect(user.stats).to.eql { gp: 0, exp: 0, lvl: 1, hp: 50 }
|
||||
expect(user.items).to.eql { weapon: 0, armor: 0, head: 0, shield: 0 }
|
||||
expect(user.items.weapon).to.eql 0
|
||||
expect(user.items.armor).to.eql 0
|
||||
expect(user.items.head).to.eql 0
|
||||
expect(user.items.shield).to.eql 0
|
||||
expect(user.items.lastDrop.count).to.eql 0
|
||||
expect(user.preferences).to.eql { gender: 'm', skin: 'white', hair: 'blond', armorSet: 'v1', dayStart:0, showHelm: true }
|
||||
expect(user.balance).to.eql 0
|
||||
expect(user.lastCron).to.be.greaterThan 0
|
||||
|
|
@ -95,7 +107,10 @@ describe 'User', ->
|
|||
user.weapon = 1
|
||||
algos.revive user
|
||||
expect(user.stats).to.eql { gp: 0, exp: 0, lvl: 1, hp: 50 }
|
||||
expect(user.items).to.eql { weapon: 0, armor: 0, head: 0, shield: 0 }
|
||||
expect(user.items.weapon).to.eql 0
|
||||
expect(user.items.armor).to.eql 0
|
||||
expect(user.items.head).to.eql 0
|
||||
expect(user.items.shield).to.eql 0
|
||||
|
||||
describe 'store', ->
|
||||
it 'recovers hp buying potions', ->
|
||||
|
|
@ -206,6 +221,60 @@ describe 'Cron', ->
|
|||
afterTasks = after.habits.concat(after.dailys).concat(after.todos).concat(after.rewards)
|
||||
expect(beforeTasks).to.eql afterTasks
|
||||
|
||||
it.only 'should preen user history', ->
|
||||
{before,after} = beforeAfter({daysAgo:1})
|
||||
history = [
|
||||
# Last year should be condensed to one entry, avg: 1
|
||||
{date:'09/01/2012', value: 0}
|
||||
{date:'10/01/2012', value: 0}
|
||||
{date:'11/01/2012', value: 2}
|
||||
{date:'12/01/2012', value: 2}
|
||||
|
||||
# Each month of this year should be condensed to 1/mo, averages follow
|
||||
{date:'01/01/2013', value: 1} #2
|
||||
{date:'01/15/2013', value: 3}
|
||||
|
||||
{date:'02/01/2013', value: 2} #3
|
||||
{date:'02/15/2013', value: 4}
|
||||
|
||||
{date:'03/01/2013', value: 3} #4
|
||||
{date:'03/15/2013', value: 5}
|
||||
|
||||
{date:'04/01/2013', value: 4} #5
|
||||
{date:'04/15/2013', value: 6}
|
||||
|
||||
{date:'05/01/2013', value: 5} #6
|
||||
{date:'05/15/2013', value: 7}
|
||||
|
||||
{date:'06/01/2013', value: 6} #7
|
||||
{date:'06/15/2013', value: 8}
|
||||
|
||||
{date:'07/01/2013', value: 7} #8
|
||||
{date:'07/15/2013', value: 9}
|
||||
|
||||
{date:'08/01/2013', value: 8} #9
|
||||
{date:'08/15/2013', value: 10}
|
||||
|
||||
{date:'09/01/2013', value: 9} #10
|
||||
{date:'09/15/2013', value: 11}
|
||||
|
||||
{date:'010/01/2013', value: 10} #11
|
||||
{date:'010/15/2013', value: 12}
|
||||
|
||||
# This month should condense each week
|
||||
{date:'011/01/2013', value: 12}
|
||||
{date:'011/02/2013', value: 13}
|
||||
{date:'011/03/2013', value: 14}
|
||||
{date:'011/04/2013', value: 15}
|
||||
]
|
||||
after.history = {exp: _.cloneDeep(history), todos: _.cloneDeep(history)}
|
||||
after.habits[0].history = _.cloneDeep(history)
|
||||
algos.cron(after)
|
||||
console.log after.habits[0].history
|
||||
# _.each [after.history.exp, after.history.todos, after.habits[0].history], (arr) ->
|
||||
# expect(arr.length).to.be 15
|
||||
# expect(arr).to.eql [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
|
||||
|
||||
describe 'Todos', ->
|
||||
it '1 day missed', ->
|
||||
{before,after} = beforeAfter({daysAgo:1})
|
||||
|
|
@ -219,7 +288,7 @@ describe 'Cron', ->
|
|||
|
||||
# but they devalue
|
||||
expect(after.todos[0].value).to.be.lessThan before.todos[0].value
|
||||
expect(_.size(after.history.todos)).to.be 1
|
||||
expect(after.history.todos).to.have.length 1
|
||||
|
||||
describe 'dailies', ->
|
||||
|
||||
|
|
@ -286,7 +355,7 @@ describe 'Cron', ->
|
|||
'unchecked': {checked:false, expect: 'losePoints'}
|
||||
|
||||
'not due yesterday':
|
||||
defaults: {repeat:{su:false,m:1,t:1,w:1,th:1,f:1,s:1}}
|
||||
defaults: repeatWithoutLastWeekday()
|
||||
steps:
|
||||
'(simple)': {expect:'noDamage'}
|
||||
'post-dayStart': {currentHour:5,dayStart:4, expect:'noDamage'}
|
||||
|
|
@ -332,3 +401,15 @@ describe 'Helper', ->
|
|||
expect(algos.tnl 10).to.eql 260
|
||||
expect(algos.tnl 99).to.eql 3580
|
||||
expect(algos.tnl 100).to.eql 0
|
||||
|
||||
it 'calculates priority values', ->
|
||||
expect(algos.priorityValue()).to.eql 1
|
||||
expect(algos.priorityValue '!').to.eql 1
|
||||
expect(algos.priorityValue '!!').to.eql 1.5
|
||||
expect(algos.priorityValue '!!!').to.eql 2
|
||||
expect(algos.priorityValue '!!!!').to.eql 1
|
||||
|
||||
it 'calculates the start of the day', ->
|
||||
expect(helpers.startOfDay({now: new Date(2013, 0, 1, 0)}).format('YYYY-MM-DD HH:mm')).to.eql '2013-01-01 00:00'
|
||||
expect(helpers.startOfDay({now: new Date(2013, 0, 1, 5)}).format('YYYY-MM-DD HH:mm')).to.eql '2013-01-01 00:00'
|
||||
expect(helpers.startOfDay({now: new Date(2013, 0, 1, 23, 59, 59)}).format('YYYY-MM-DD HH:mm')).to.eql '2013-01-01 00:00'
|
||||
|
|
|
|||