Merge pull request #5351 from HabitRPG/armoire

Enchanted Armoire
This commit is contained in:
Sabe Jones 2015-06-05 11:58:59 -07:00
commit cfa6efce85
65 changed files with 8669 additions and 8135 deletions

View file

@ -37,6 +37,12 @@
margin-right: 10px;
}
.multi-achievement {
margin: auto;
padding-left: 0.5em;
padding-right: 0.5em;
}
[class*="Mount_Head_"], [class*="Mount_Body_"]{
margin-top:18px; /* Sprite accommodates 105x123 box */
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 302 KiB

After

Width:  |  Height:  |  Size: 304 KiB

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 77 KiB

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 153 KiB

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 153 KiB

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 672 KiB

After

Width:  |  Height:  |  Size: 681 KiB

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 354 KiB

After

Width:  |  Height:  |  Size: 357 KiB

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 298 KiB

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View file

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

View file

@ -52,9 +52,11 @@
"costume": "Costume",
"costumeText": "If you prefer the look of other gear to what you have equipped, check the \"Use Costume\" box to visually don a costume while wearing your battle gear underneath.",
"useCostume": "Use Costume",
"gearAchievement": "You have earned the \"Ultimate Gear\" Achievement for upgrading to the maximum gear set!",
"gearAchievement": "You have earned the \"Ultimate Gear\" Achievement for upgrading to the maximum gear set for a class! You have attained the following complete sets:",
"moreGearAchievements": "To attain more Ultimate Gear badges, change classes on <a href='/#/options/profile/stats' target='_blank'>your stats page</a> and buy up your new class's gear!",
"armoireUnlocked": "You've also unlocked the <strong>Enchanted Armoire!</strong> Click on the Enchanted Armoire Reward for a random chance at special Equipment! It may also give you random XP or food items.",
"ultimGearName": "Ultimate Gear",
"ultimGearText": "Has upgraded to the maximum weapon and armor set",
"ultimGearText": "Has upgraded to the maximum weapon and armor set for the following classes:",
"level": "Level",
"levelUp": "Level Up!",
"mana": "Mana",

View file

@ -2,6 +2,11 @@
"potionText": "Health Potion",
"potionNotes": "Recover 15 Health (Instant Use)",
"armoireText": "Enchanted Armoire",
"armoireNotesFull": "Open the Armoire to randomly receive special Equipment, Experience, or food! Equipment pieces remaining: ",
"armoireLastItem": "You've found the last piece of rare Equipment in the Enchanted Armoire.",
"armoireNotesEmpty": "The Armoire will have new Equipment every month. Until then, keep clicking for Experience and Food!",
"dropEggWolfText": "Wolf",
"dropEggWolfAdjective": "loyal",

View file

@ -138,6 +138,11 @@
"weaponMystery301404Text": "Steampunk Cane",
"weaponMystery301404Notes": "Excellent for taking a turn about town. March 3015 Subscriber Item. Confers no benefit.",
"weaponArmoireBasicCrossbowText": "Basic Crossbow",
"weaponArmoireBasicCrossbowNotes": "This crossbow can pierce a task's armor from very far away! Increases Strength by <%= str %>, Perception by <%= per %>, and Constitution by <%= con %>. Enchanted Armoire: Independent Item.",
"weaponArmoireLunarSceptreText": "Soothing Lunar Sceptre",
"weaponArmoireLunarSceptreNotes": "The healing power of this wand waxes and wanes. Increases Constitution by <%= con %> and Intelligence by <%= int %>. Enchanted Armoire: Soothing Lunar Set (Item 3 of 3).",
"armor": "armor",
"armorBase0Text": "Plain Clothing",
@ -282,6 +287,11 @@
"armorMystery301404Text": "Steampunk Suit",
"armorMystery301404Notes": "Dapper and dashing, wot! Confers no benefit. February 3015 Subscriber Item.",
"armorArmoireLunarArmorText": "Soothing Lunar Armor",
"armorArmoireLunarArmorNotes": "The light of the moon will make you strong and savvy. Increases Strength by <%= str %> and Intelligence by <%= int %>. Enchanted Armoire: Soothing Lunar Set (Item 2 of 3).",
"armorArmoireGladiatorArmorText": "Gladiator Armor",
"armorArmoireGladiatorArmorNotes": "To be a gladiator you must be not only cunning... but strong. Increases Perception by <%= per %> and Strength by <%= str %>. Enchanted Armoire: Gladiator Set (Item 2 of 3).",
"headgear": "headgear",
"headBase0Text": "No Helm",
@ -422,6 +432,17 @@
"headMystery301405Text": "Basic Top Hat",
"headMystery301405Notes": "A basic top hat, just begging to be paired with some fancy head accessories. Confers no benefit. May 3015 Subscriber Item.",
"headArmoireLunarCrownText": "Soothing Lunar Crown",
"headArmoireLunarCrownNotes": "This crown strengthens health and sharpens senses, especially when the moon is full. Increases Constitution by <%= con %> and Perception by <%= per %>. Enchanted Armoire: Soothing Lunar Set (Item 1 of 3).",
"headArmoireRedHairbowText": "Red Hairbow",
"headArmoireRedHairbowNotes": "Become strong, tough and smart while wearing this beautiful Red Hairbow! Increases Strength by <%= str %>, Constitution by <%= con %>, and Intelligence by <%= int %>. Enchanted Armoire: Independent Item.",
"headArmoireVioletFloppyHatText": "Violet Floppy Hat",
"headArmoireVioletFloppyHatNotes": "Many spells have been sewn into this simple hat, giving it a pleasing purple color. Increases Perception by <%= per %>, Intelligence by <%= int %>, and Constitution by <%= con %>. Enchanted Armoire: Independent Item.",
"headArmoireGladiatorHelmText": "Gladiator Helm",
"headArmoireGladiatorHelmNotes": "To be a gladiator you must be not only strong.... but cunning. Increases Intelligence by <%= int %> and Perception by <%= per %>. Enchanted Armoire: Gladiator Set (Item 1 of 3).",
"headArmoireRancherHatText": "Rancher Hat",
"headArmoireRancherHatNotes": "Round up your pets and wrangle your mounts while wearing this magical Rancher Hat! Increases Strength by <%= str %>, Perception by <%= per %>, and Intelligence by <%= int %>. Enchanted Armoire: Independent Item.",
"offhand": "shield-hand item",
"shieldBase0Text": "No Shield-Hand Equipment",
@ -500,6 +521,9 @@
"shieldMystery301405Text": "Clock Shield",
"shieldMystery301405Notes": "Time is on your side with this towering clock shield! Confers no benefit. June 3015 Subscriber Item.",
"shieldArmoireGladiatorShieldText": "Gladiator Shield",
"shieldArmoireGladiatorShieldNotes": "To be a gladiator you must.... eh, whatever, just bash them with your shield. Increases Constitution by <%= con %> and Strength by <%= str %>. Enchanted Armoire: Gladiator Set (Item 3 of 3).",
"backBase0Text": "No Back Accessory",
"backBase0Notes": "No Back Accessory.",

View file

@ -39,7 +39,7 @@
"close": "Close",
"saveAndClose": "Save & Close",
"cancel": "Cancel",
"ok": "Ok",
"ok": "OK",
"add": "Add",
"undo": "Undo",
"continue": "Continue",

View file

@ -20,5 +20,8 @@
"messageDropFood": "You've found <%= dropArticle %><%= dropText %>! <%= dropNotes %>",
"messageDropEgg": "You've found a <%= dropText %> Egg! <%= dropNotes %>",
"messageDropPotion": "You've found a <%= dropText %> Hatching Potion! <%= dropNotes %>",
"messageFoundQuest": "You've found the quest \"<%= questText %>\"!"
"messageFoundQuest": "You've found the quest \"<%= questText %>\"!",
"armoireEquipment": "<%= image %> You found a piece of rare Equipment in the Armoire: <%= dropText %>! Awesome!",
"armoireFood": "<%= image %> You rummage in the Armoire and find <%= dropArticle %><%= dropText %>. What's that doing in here?",
"armoireExp": "You wrestle with the Armoire and gain Experience. Take that!"
}

View file

@ -140,6 +140,9 @@ gear =
201502: text: t('weaponMystery201502Text'), notes: t('weaponMystery201502Notes'), mystery:'201502', value: 0
201505: text: t('weaponMystery201505Text'), notes: t('weaponMystery201505Notes'), mystery:'201505', value: 0
301404: text: t('weaponMystery301404Text'), notes: t('weaponMystery301404Notes'), mystery:'301404', value: 0
armoire:
basicCrossbow: text: t('weaponArmoireBasicCrossbowText'), notes: t('weaponArmoireBasicCrossbowNotes', {str: 5, per: 5, con: 5}), value: 100, str: 5, per: 5, con: 5, canOwn: ((u)-> u.items.gear.owned.weapon_armoire_basicCrossbow?)
lunarSceptre: text: t('weaponArmoireLunarSceptreText'), notes: t('weaponArmoireLunarSceptreNotes', {con: 7, int: 7}), value: 100, con: 7, int: 7, set: 'soothing', canOwn: ((u)-> u.items.gear.owned.weapon_armoire_lunarSceptre?)
armor:
base:
@ -224,6 +227,9 @@ gear =
201503: text: t('armorMystery201503Text'), notes: t('armorMystery201503Notes'), mystery:'201503', value: 0
201504: text: t('armorMystery201504Text'), notes: t('armorMystery201504Notes'), mystery:'201504', value: 0
301404: text: t('armorMystery301404Text'), notes: t('armorMystery301404Notes'), mystery:'301404', value: 0
armoire:
lunarArmor: text: t('armorArmoireLunarArmorText'), notes: t('armorArmoireLunarArmorNotes', {str: 7, int: 7}), value: 100, str: 7, int: 7, set: 'soothing', canOwn: ((u)-> u.items.gear.owned.armor_armoire_lunarArmor?)
gladiatorArmor: text: t('armorArmoireGladiatorArmorText'), notes: t('armorArmoireGladiatorArmorNotes', {str: 7, per: 7}), value: 100, str: 7, per: 7, set: 'gladiator', canOwn: ((u)-> u.items.gear.owned.armor_armoire_gladiatorArmor?)
head:
base:
@ -306,6 +312,12 @@ gear =
201505: text: t('headMystery201505Text'), notes: t('headMystery201505Notes'), mystery:'201505', value: 0
301404: text: t('headMystery301404Text'), notes: t('headMystery301404Notes'), mystery:'301404', value: 0
301405: text: t('headMystery301405Text'), notes: t('headMystery301405Notes'), mystery:'301405', value: 0
armoire:
lunarCrown: text: t('headArmoireLunarCrownText'), notes: t('headArmoireLunarCrownNotes', {con: 7, per: 7}), value: 100, con: 7, per: 7, set: 'soothing', canOwn: ((u)-> u.items.gear.owned.head_armoire_lunarCrown?)
redHairbow: text: t('headArmoireRedHairbowText'), notes: t('headArmoireRedHairbowNotes', {str: 5, int: 5, con: 5}), value: 100, str: 5, int: 5, con: 5, canOwn: ((u)-> u.items.gear.owned.head_armoire_redHairbow?)
violetFloppyHat: text: t('headArmoireVioletFloppyHatText'), notes: t('headArmoireVioletFloppyHatNotes', {per: 5, int: 5, con: 5}), value: 100, per: 5, int: 5, con: 5, canOwn: ((u)-> u.items.gear.owned.head_armoire_violetFloppyHat?)
gladiatorHelm: text: t('headArmoireGladiatorHelmText'), notes: t('headArmoireGladiatorHelmNotes', {per: 7, int: 7}), value: 100, per: 7, int: 7, set: 'gladiator', canOwn: ((u)-> u.items.gear.owned.head_armoire_gladiatorHelm?)
rancherHat: text: t('headArmoireRancherHatText'), notes: t('headArmoireRancherHatNotes', {str: 5, per: 5, int: 5}), value: 100, str: 5, per: 5, int: 5, canOwn: ((u)-> u.items.gear.owned.head_armoire_rancherHat?)
shield:
base:
@ -365,6 +377,8 @@ gear =
spring2015Healer: event: events.spring2015, specialClass: 'healer', text: t('shieldSpecialSpring2015HealerText'), notes: t('shieldSpecialSpring2015HealerNotes', {con: 9}), value: 70, con: 9
mystery:
301405: text: t('shieldMystery301405Text'), notes: t('shieldMystery301405Notes'), mystery:'301405', value: 0
armoire:
gladiatorShield: text: t('shieldArmoireGladiatorShieldText'), notes: t('shieldArmoireGladiatorShieldNotes', {con: 5, str: 5}), value: 100, con: 5, str: 5, set: 'gladiator', canOwn: ((u)-> u.items.gear.owned.shield_armoire_gladiatorShield?)
back:
base:
@ -442,7 +456,7 @@ api.gear =
flat: {}
_.each gearTypes, (type) ->
_.each classes.concat(['base', 'special', 'mystery']), (klass) ->
_.each classes.concat(['base', 'special', 'mystery', 'armoire']), (klass) ->
# add "type" to each item, so we can reference that as "weapon" or "armor" in the html
_.each gear[type][klass], (item, i) ->
key = "#{type}_#{klass}_#{i}"
@ -475,11 +489,12 @@ api.timeTravelerStore = (owned) ->
###
---------------------------------------------------------------
Potion
Unique Rewards: Potion and Armoire
---------------------------------------------------------------
###
api.potion = type: 'potion', text: t('potionText'), notes: t('potionNotes'), value: 25, key: 'potion'
api.armoire = type: 'armoire', text: t('armoireText'), notes: t('armoireNotesEmpty'), value: 100, key: 'armoire', canOwn: ((u)-> _.contains(u.achievements.ultimateGearSets, true))
###
---------------------------------------------------------------

View file

@ -164,8 +164,9 @@ api.updateStore = (user) ->
true
# Add special items (contrib gear, backer gear, etc)
changes = changes.concat _.filter content.gear.flat, (v) ->
v.klass in ['special','mystery'] and !user.items.gear.owned[v.key] and v.canOwn?(user)
v.klass in ['special','mystery','armoire'] and !user.items.gear.owned[v.key] and v.canOwn?(user)
changes.push content.potion
if user.flags.armoireEnabled then changes.push content.armoire
# Return sorted store (array)
_.sortBy changes, (c)->sortOrder[c.type]
@ -361,6 +362,10 @@ api.countTriad = (pets) ->
if pets[egg + "-" + potion] > 0 then count3++
count3
api.countArmoire = (gear) ->
count = _.size(_.filter(content.gear.flat, ((i)->i.klass is 'armoire' and !gear[i.key])))
count
###
------------------------------------------------------
User (prototype wrapper to give it ops, helper funcs, and virtuals
@ -441,7 +446,7 @@ api.wrap = (user, main=true) ->
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) ) )
if (itm.value > 0 || k == 'weapon_warrior_0') && ( itm.klass == cl || ( itm.klass == 'special' && (! itm.specialClass || itm.specialClass == cl) ) || itm.klass == 'armoire' )
losableItems[''+k]=''+k
lostItem = user.fns.randomVal losableItems
if item = content.gear.flat[lostItem]
@ -835,23 +840,41 @@ api.wrap = (user, main=true) ->
buy: (req, cb) ->
{key} = req.params
item = if key is 'potion' then content.potion else content.gear.flat[key]
item = if key is 'potion' then content.potion
else if key is 'armoire' then content.armoire
else content.gear.flat[key]
return cb?({code:404, message:"Item '#{key} not found (see https://github.com/HabitRPG/habitrpg-shared/blob/develop/script/content.coffee)"}) unless item
return cb?({code:401, message: i18n.t('messageNotEnoughGold', req.language)}) if user.stats.gp < item.value
return cb?({code:401, message: "You can't own this item"}) if item.canOwn? and !item.canOwn(user)
return cb?({code:401, message: "You can't buy this item"}) if item.canOwn? and !item.canOwn(user)
if item.key is 'potion'
user.stats.hp += 15
user.stats.hp = 50 if user.stats.hp > 50
else if item.key is 'armoire'
armoireResult = user.fns.predictableRandom()
eligibleEquipment = _.filter(content.gear.flat, ((i)->i.klass is 'armoire' and !user.items.gear.owned[i.key]))
if !_.isEmpty(eligibleEquipment) and (armoireResult < .6 or !user.flags.armoireOpened)
drop = user.fns.randomVal(eligibleEquipment)
user.items.gear.owned[drop.key] = true
user.flags.armoireOpened = true
message = i18n.t('armoireEquipment', {image: '<span class="shop_'+drop.key+' pull-left"></span>', dropText: drop.text(req.language)}, req.language)
if api.countArmoire(user.items.gear.owned) is 0 then user.flags.armoireEmpty = true
else if (!_.isEmpty(eligibleEquipment) and armoireResult < .8) or armoireResult < .5
drop = user.fns.randomVal _.where(content.food, {canDrop:true})
user.items.food[drop.key] ?= 0
user.items.food[drop.key] += 1
message = i18n.t('armoireFood', {image: '<span class="Pet_Food_'+drop.key+' pull-left"></span>', dropArticle: drop.article, dropText: drop.text(req.language)}, req.language)
else
user.stats.exp += Math.floor(user.fns.predictableRandom(user.stats.exp) * 40 + 10)
message = i18n.t('armoireExp', req.language)
else
user.items.gear.equipped[item.type] = item.key
user.items.gear.owned[item.key] = true
message = user.fns.handleTwoHanded(item, null, req)
message ?= i18n.t('messageBought', {itemText: item.text(req.language)}, req.language)
if not user.achievements.ultimateGear and item.last
user.fns.ultimateGear()
if item.last then user.fns.ultimateGear()
user.stats.gp -= item.value
mixpanel?.track("Acquire Item",{'itemName':key,'acquireMethod':'Gold','goldCost':item.value})
cb? {code:200, message}, _.pick(user,$w 'items achievements stats')
cb? {code:200, message}, _.pick(user,$w 'items achievements stats flags')
buyMysterySet: (req, cb)->
return cb?({code:401, message:"You don't have enough Mystic Hourglasses"}) unless user.purchased.plan.consecutive.trinkets>0
@ -1660,32 +1683,19 @@ api.wrap = (user, main=true) ->
# ----------------------------------------------------------------------
# Achievements
# ----------------------------------------------------------------------
ultimateGear: () ->
# on the server this is a LoDash transform, on the client its an object
gear = if window? then user.items.gear.owned else user.items.gear.owned.toObject()
ownedLastGear = _.chain(content.gear.flat)
.pick(_.keys gear)
.values()
.filter (gear) -> gear.last
lastGearClassTypeMatrix = {}
_.each content.classes, (klass) ->
lastGearClassTypeMatrix[klass] = {}
#_.each content.gearTypes, (type) ->
_.each ['armor', 'weapon', 'shield', 'head'], (type) ->
lastGearClassTypeMatrix[klass][type] = false
return true # false exits the each loop early
ownedLastGear.each (gear) ->
lastGearClassTypeMatrix[gear.klass]["shield"] = true if gear.twoHanded
lastGearClassTypeMatrix[gear.klass][gear.type] = true
shouldGrant = _(lastGearClassTypeMatrix)
.values()
.reduce(((ans, klass) -> ans or _(klass).values().reduce(((ans, gearType) -> ans and gearType), true)), false)
.valueOf()
user.achievements.ultimateGear = shouldGrant
ultimateGear: ->
# on the server this is a Lodash transform, on the client its an object
owned = if window? then user.items.gear.owned else user.items.gear.owned.toObject()
user.achievements.ultimateGearSets ?= {healer: false, wizard: false, rogue: false, warrior: false}
content.classes.forEach (klass) ->
user.achievements.ultimateGearSets[klass] = _.reduce ['armor', 'shield', 'head', 'weapon'], (soFarGood, type) ->
found = _.find content.gear.tree[type][klass], {last:true}
soFarGood and (!found or owned[found.key]==true) #!found only true when weapon is two-handed (mages)
, true # start with true, else `and` will fail right away
user.markModified? 'achievements.ultimateGearSets'
if _.contains(user.achievements.ultimateGearSets, true) and user.flags.armoireEnabled != true
user.flags.armoireEnabled = true
user.markModified? 'flags'
nullify: ->
user.ops = null

View file

@ -0,0 +1,86 @@
/**
* Created by Sabe on 6/3/2015.
*/
var migrationName = '20150604_ultimateGearSets';
var authorName = process.env.AUTHOR_NAME || 'Sabe'; // in case script author needs to know when their ...
var authorUuid = process.env.AUTHOR_UUID || '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
/*
* grant the new ultimateGearSets achievement for existing users' collected equipment
*/
var dbserver = process.env.DB_SERVER || 'localhost:27017'; // CHANGE THIS FOR PRODUCTION DATABASE
var dbname = process.env.DB_NAME || 'habitrpg';
var mongo = require('mongoskin');
var _ = require('lodash');
var dbUsers = mongo.db(dbserver + '/' + dbname + '?auto_reconnect').collection('users');
var fields = {
'achievements.ultimateGearSets':1
};
var query = {
'items.gear.owned.weapon_wizard_6': {$exists: true},
'items.gear.owned.armor_wizard_5': {$exists: true},
'items.gear.owned.head_wizard_5': {$exists: true}
};
/* var query = {
'items.gear.owned.weapon_warrior_6': {$exists: true},
'items.gear.owned.armor_warrior_5': {$exists: true},
'items.gear.owned.head_warrior_5': {$exists: true},
'items.gear.owned.shield_warrior_5': {$exists: true}
}; */
/* var query = {
'items.gear.owned.weapon_healer_6': {$exists: true},
'items.gear.owned.armor_healer_5': {$exists: true},
'items.gear.owned.head_healer_5': {$exists: true},
'items.gear.owned.shield_healer_5': {$exists: true}
}; */
/* var query = {
'items.gear.owned.weapon_rogue_6': {$exists: true},
'items.gear.owned.armor_rogue_5': {$exists: true},
'items.gear.owned.head_rogue_5': {$exists: true},
'items.gear.owned.shield_rogue_6': {$exists: true}
}; */
console.warn('Updating users...');
var progressCount = 1000;
var count = 0;
dbUsers.findEach(query, fields, {batchSize:250}, function(err, user) {
if (err) { return exiting(1, 'ERROR! ' + err); }
if (!user) {
console.warn('All appropriate users found and modified.');
return displayData();
}
count++;
var set = {'migration':migrationName, 'achievements.ultimateGearSets.wizard':true}; // Change per class
dbUsers.update({_id:user._id}, {$set:set});
if (count%progressCount == 0) console.warn(count + ' ' + user._id);
if (user._id == authorUuid) console.warn(authorName + ' processed');
if (user._id == '9' ) console.warn('lefnire' + ' processed');
});
function displayData() {
console.warn('\n' + count + ' users processed\n');
return exiting(0);
}
function exiting(code, msg) {
code = code || 0; // 0 = success
if (code && !msg) { msg = 'ERROR!'; }
if (msg) {
if (code) { console.error(msg); }
else { console.log( msg); }
}
process.exit(code);
}

View file

@ -33,7 +33,8 @@ newUser = (addTasks=true)->
todos: []
rewards: []
flags: {}
achievements: {}
achievements:
ultimateGearSets: {}
contributor:
level: 2
@ -432,7 +433,6 @@ describe 'User', ->
expect(spell.lvl).to.be.above(0)
expect(spell.cast).to.be.a('function')
describe 'drop system', ->
user = null
@ -481,6 +481,88 @@ describe 'User', ->
user.fns.randomVal.restore()
user.fns.predictableRandom.restore()
describe 'Enchanted Armoire', ->
user = newUser()
fullArmoire = {'weapon_warrior_0': true, 'armor_armoire_gladiatorArmor':true,'armor_armoire_lunarArmor':true,'head_armoire_gladiatorHelm':true,'head_armoire_lunarCrown':true,'head_armoire_rancherHat':true,'head_armoire_redHairbow':true,'head_armoire_violetFloppyHat':true,'shield_armoire_gladiatorShield':true,'weapon_armoire_basicCrossbow':true,'weapon_armoire_lunarSceptre':true}
beforeEach ->
# too many predictableRandom calls to stub, let's return the last element
sinon.stub(user.fns, 'randomVal', (obj)->
result = undefined
for key, val of obj
result = val
result
)
it 'counts all available equipment before any are claimed', ->
sinon.stub(user.fns, 'predictableRandom').returns 0
expect(shared.countArmoire(user.items.gear.owned)).to.eql (_.size(fullArmoire) - 1)
it 'does not open without paying', ->
sinon.stub(user.fns, 'predictableRandom').returns 0
user.ops.buy({params: {key: 'armoire'}})
expect(user.items.gear.owned).to.eql {'weapon_warrior_0': true}
expect(user.items.food).to.eql {}
expect(user.stats.exp).to.eql 0
it 'does not open without Ultimate Gear achievement', ->
sinon.stub(user.fns, 'predictableRandom').returns 0
user.stats.gp = 500
user.ops.buy({params: {key: 'armoire'}})
user.achievements.ultimateGearSets = {'healer':false,'wizard':false,'rogue':false,'warrior':false}
user.ops.buy({params: {key: 'armoire'}})
expect(user.items.gear.owned).to.eql {'weapon_warrior_0': true}
expect(user.items.food).to.eql {}
expect(user.stats.exp).to.eql 0
it 'always drops equipment the first time', ->
sinon.stub(user.fns, 'predictableRandom', cycle [.9,.5])
user.achievements.ultimateGearSets = {'healer':false,'wizard':false,'rogue':true,'warrior':false}
user.ops.buy({params: {key: 'armoire'}})
expect(user.items.gear.owned).to.eql {'weapon_warrior_0': true, 'shield_armoire_gladiatorShield':true}
expect(shared.countArmoire(user.items.gear.owned)).to.eql (_.size(fullArmoire) - 2)
expect(user.items.food).to.eql {}
expect(user.stats.exp).to.eql 0
expect(user.stats.gp).to.eql 400
it 'gives Experience', ->
sinon.stub(user.fns, 'predictableRandom', cycle [.9,.5])
user.ops.buy({params: {key: 'armoire'}})
expect(user.items.gear.owned).to.eql {'weapon_warrior_0': true, 'shield_armoire_gladiatorShield':true}
expect(user.items.food).to.eql {}
expect(user.stats.exp).to.eql 30
expect(user.stats.gp).to.eql 300
it 'gives food', ->
sinon.stub(user.fns, 'predictableRandom', cycle [.7,.5])
user.ops.buy({params: {key: 'armoire'}})
expect(user.items.gear.owned).to.eql {'weapon_warrior_0': true, 'shield_armoire_gladiatorShield':true}
expect(user.items.food).to.eql {'Honey': 1}
expect(user.stats.exp).to.eql 30
expect(user.stats.gp).to.eql 200
it 'gives more equipment', ->
sinon.stub(user.fns, 'predictableRandom', cycle [.5,.5])
user.ops.buy({params: {key: 'armoire'}})
expect(user.items.gear.owned).to.eql {'weapon_warrior_0': true, 'shield_armoire_gladiatorShield':true,'head_armoire_rancherHat':true}
expect(shared.countArmoire(user.items.gear.owned)).to.eql (_.size(fullArmoire) - 3)
expect(user.items.food).to.eql {'Honey': 1}
expect(user.stats.exp).to.eql 30
expect(user.stats.gp).to.eql 100
it 'does not give equipment if all equipment has been found', ->
sinon.stub(user.fns, 'predictableRandom', cycle [.5,.5])
user.items.gear.owned = fullArmoire
user.ops.buy({params: {key: 'armoire'}})
expect(user.items.gear.owned).to.eql fullArmoire
expect(shared.countArmoire(user.items.gear.owned)).to.eql 0
expect(user.items.food).to.eql {'Honey': 1}
expect(user.stats.exp).to.eql 60
expect(user.stats.gp).to.eql 0
afterEach ->
user.fns.randomVal.restore()
user.fns.predictableRandom.restore()
describe 'Quests', ->
_.each shared.content.quests, (quest)->
@ -510,11 +592,11 @@ describe 'User', ->
_.each [1..5], (i) ->
user.ops.buy {params:'#{type}_#{klass}_#{i}'}
it 'does not get ultimateGear ' + klass, ->
expect(user.achievements.ultimateGear).to.not.be.ok()
expect(user.achievements.ultimateGearSets[klass]).to.not.be.ok()
_.each shared.content.gearTypes, (type) ->
user.ops.buy {params:'#{type}_#{klass}_6'}
xit 'gets ultimateGear ' + klass, ->
expect(user.achievements.ultimateGear).to.be.ok()
expect(user.achievements.ultimateGearSets[klass]).to.be.ok()
it 'does not get beastMaster if user has less than 90 drop pets', ->
user = newUser()

View file

@ -0,0 +1,74 @@
'use strict'
TEST_DB = process.env.DB_NAME = 'habitrpg_migration_test'
process.env.NODE_DB_URI = 'mongodb://localhost/' + TEST_DB
app = require('../../website/src/server')
sh = require('shelljs')
runMigration = ->
sh.exec 'node ./migrations/20150604_ultimateGearSets.js'
describe 'Backfill for granting ultimate gear sets achievement', ->
before (done) ->
sh.exec "mongo \"#{TEST_DB}\" --eval \"db.dropDatabase()\""
done()
context 'User without any purchased equipment', ->
before (done) ->
registerNewUser done, true
it 'does not update user', (done)->
user_gear = user.items.gear.owned
expect(user_gear.weapon_wizard_6).to.not.exist
expect(user.achievements.ultimateGearSets).to.not.exist
runMigration()
User.findById user._id, (err, _user) ->
user = _user
expect(user.achievements.ultimateGearSets).to.not.exist
done()
context 'User with all but one needed piece of equipment', ->
before (done) ->
registerNewUser ->
items = {
weapon_wizard_6: true
armor_wizard_5: true
}
User.findByIdAndUpdate user._id, {'items.gear.owned': items}, (err, _user) ->
user = _user
done()
, true
it 'does not update user', (done)->
runMigration()
User.findById user._id, (err, _user) ->
user = _user
expect(user.achievements.ultimateGearSets).to.not.exist
done()
context 'User with all necessary equipment', ->
before (done) ->
registerNewUser ->
items = {
weapon_wizard_6: true
armor_wizard_5: true
head_wizard_5: true
}
User.findByIdAndUpdate user._id, {'items.gear.owned': items}, (err, _user) ->
user = _user
done()
, true
it 'grants user ultimate gear', (done)->
runMigration()
User.findById user._id, (err, _user) ->
user = _user
expect(user.achievements.ultimateGearSets.wizard).to.exist
done()

View file

@ -10,8 +10,12 @@ describe('Inventory Controller', function() {
inject(function($rootScope, $controller, Shared){
user = specHelper.newUser();
user.balance = 4,
user.items = {eggs: {Cactus: 1}, hatchingPotions: {Base: 1}, food: {Meat: 1}, pets: {}, mounts: {}};
user.balance = 4;
user.items.eggs = {Cactus: 1};
user.items.hatchingPotions = {Base: 1};
user.items.food = {Meat: 1};
user.items.pets = {}
user.items.mounts = {};
Shared.wrap(user);
var mockWindow = {
confirm: function(msg){

View file

@ -13,7 +13,7 @@ specHelper = {
food: {},
pets: {},
mounts: {},
gear: {equipped: {}, costume: {}},
gear: {equipped: {}, costume: {}, owned: {}},
},
party: {
quest: {

View file

@ -101,9 +101,14 @@ habitrpg.controller('NotificationCtrl',
}
});
$rootScope.$watch('user.achievements.ultimateGear', function(after, before){
if (after === before || after !== true) return;
$rootScope.$watch('user.achievements.ultimateGearSets', function(after, before){
if (_.isEqual(after,before) || !_.contains(User.user.achievements.ultimateGearSets, true)) return;
$rootScope.openModal('achievements/ultimateGear');
}, true);
$rootScope.$watch('user.flags.armoireEmpty', function(after,before){
if (before == undefined || after == before || after == false) return;
$rootScope.openModal('armoireEmpty');
});
$rootScope.$watch('user.achievements.rebirths', function(after, before){

View file

@ -4,6 +4,9 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','N
function($scope, $rootScope, $location, User, Notification, $http, ApiUrl, $timeout, Shared, Guide) {
$scope.obj = User.user; // used for task-lists
$scope.user = User.user;
$scope.armoireCount = function(gear) {
return Shared.countArmoire(gear);
};
$scope.score = function(task, direction) {
switch (task.type) {
@ -193,7 +196,7 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','N
------------------------
*/
$scope.$watch('user.items.gear.equipped', function(){
$scope.$watchGroup(['user.items.gear.owned', 'user.flags.armoireEnabled'], function(){
$scope.itemStore = Shared.updateStore(User.user);
},true);

View file

@ -34,7 +34,8 @@ var UserSchema = new Schema({
originalUser: Boolean,
helpedHabit: Boolean, //TODO: Deprecate this. Superseded by habitSurveys
habitSurveys: Number,
ultimateGear: Boolean,
ultimateGear: Boolean, //TODO: Deprecate this. Superseded by ultimateGearSets
ultimateGearSets: Schema.Types.Mixed,
beastMaster: Boolean,
beastMasterCount: Number,
mountMaster: Boolean,
@ -156,7 +157,10 @@ var UserSchema = new Schema({
lastWeeklyRecap: {type: Date, 'default': Date.now},
communityGuidelinesAccepted: {type: Boolean, 'default': false},
cronCount: {type:Number, 'default':0},
welcomed: {type: Boolean, 'default': false}
welcomed: {type: Boolean, 'default': false},
armoireEnabled: {type: Boolean, 'default': false},
armoireOpened: {type: Boolean, 'default': false},
armoireEmpty: {type: Boolean, 'default': false}
},
history: {
exp: Array, // [{date: Date, value: Number}], // big peformance issues if these are defined

View file

@ -6,7 +6,7 @@ script(type='text/ng-template', id='partials/options.inventory.equipment.html')
div
button.btn.btn-default(type="button", ng-click='dequip("battleGear");') {{env.t("unequipBattleGear")}}
li.customize-menu.inventory-gear
menu.pets-menu(label='{{::label}}', ng-repeat='(klass,label) in {warrior:env.t("warrior"), wizard:env.t("mage"), rogue:env.t("rogue"), healer:env.t("healer"), special:env.t("special"), mystery:env.t("mystery")}', ng-show='gear[klass]')
menu.pets-menu(label='{{::label}}', ng-repeat='(klass,label) in {warrior:env.t("warrior"), wizard:env.t("mage"), rogue:env.t("rogue"), healer:env.t("healer"), special:env.t("special"), mystery:env.t("mystery"), armoire:env.t("armoireText")}', ng-show='gear[klass]')
div(ng-repeat='item in gear[klass]')
button.customize-option(popover='{{::item.notes()}}', popover-title='{{::item.text()}}', popover-trigger='mouseenter', popover-placement='right', popover-append-to-body='true', ng-click='user.ops.equip({params:{key:item.key}})', class='shop_{{::item.key}}', ng-class='{selectableInventory: user.items.gear.equipped[item.type] == item.key}')
.col-md-6
@ -20,7 +20,7 @@ script(type='text/ng-template', id='partials/options.inventory.equipment.html')
button.btn.btn-default(type="button", ng-click='dequip("costume");') {{env.t("unequipCostume")}}
button.btn.btn-default(type="button", ng-click='dequip("petMountBackground");') {{env.t("unequipPetMountBackground")}}
li.customize-menu(ng-if='user.preferences.costume')
menu.pets-menu(label='{{::label}}', ng-repeat='(klass,label) in {warrior:env.t("warrior"), wizard:env.t("mage"), rogue:env.t("rogue"), healer:env.t("healer"), special:env.t("special"), mystery:env.t("mystery")}', ng-show='gear[klass]')
menu.pets-menu(label='{{::label}}', ng-repeat='(klass,label) in {warrior:env.t("warrior"), wizard:env.t("mage"), rogue:env.t("rogue"), healer:env.t("healer"), special:env.t("special"), mystery:env.t("mystery"), armoire:env.t("armoireText")}', ng-show='gear[klass]')
div(ng-repeat='item in gear[klass]')
button.customize-option(popover='{{::item.notes()}}', popover-title='{{::item.text()}}', popover-trigger='mouseenter', popover-placement='right', popover-append-to-body='true', ng-click='user.ops.equip({params:{type:"costume", key:item.key}})', class='shop_{{::item.key}}', ng-class='{selectableInventory: user.items.gear.costume[item.type] == item.key}')

View file

@ -14,7 +14,27 @@ script(id='modals/achievements/ultimateGear.html', type='text/ng-template')
h4=env.t('modalAchievement')
.modal-body
.achievement.achievement-armor
=env.t('gearAchievement')
p=env.t('gearAchievement')
br
table.multi-achievement
tr
td(ng-if='::user.achievements.ultimateGearSets.healer').multi-achievement
.achievement-ultimate-healer.multi-achievement
=env.t('healer')
td(ng-if='::user.achievements.ultimateGearSets.wizard').multi-achievement
.achievement-ultimate-mage.multi-achievement
=env.t('mage')
td(ng-if='::user.achievements.ultimateGearSets.rogue').multi-achievement
.achievement-ultimate-rogue.multi-achievement
=env.t('rogue')
td(ng-if='::user.achievements.ultimateGearSets.warrior').multi-achievement
.achievement-ultimate-warrior.multi-achievement
=env.t('warrior')
br
p(ng-if='!(user.achievements.ultimateGearSets.healer && user.achievements.ultimateGearSets.wizard && user.achievements.ultimateGearSets.rogue && user.achievements.ultimateGearSets.warrior)')!=env.t('moreGearAchievements')
br
.shop_armoire.pull-right
p!=env.t("armoireUnlocked")
.modal-footer
button.btn.btn-default(ng-click='$close()')=env.t('ok')

View file

@ -51,3 +51,14 @@ script(type='text/ng-template', id='modals/pet-key.html')
span(ng-if='!user.achievements.triadBingo')
| : 6&nbsp;
span.Pet_Currency_Gem1x.inline-gems
script(type='text/ng-template', id='modals/armoireEmpty.html')
.modal-header
.shop_armoire.pull-right
h4=env.t('armoireText')
.modal-body
p=env.t('armoireLastItem')
br
p=env.t('armoireNotesEmpty')
.modal-footer
button.btn.btn-default(ng-click='$close()')=env.t('close')

View file

@ -1,5 +1,16 @@
h5 6/1/2015 - JUNE BACKGROUNDS AND NEW MOUNT POSITIONING!
h5 6/1/2015 - NEW EQUIPMENT: THE ENCHANTED ARMOIRE, JUNE BACKGROUNDS, AND NEW MOUNT POSITIONING!
hr
tr
td
.promo_enchanted_armoire.pull-right
h5 New Equipment: The Enchanted Armoire!
p Now after you achieve Ultimate Gear, you'll unlock a new Reward: THE ENCHANTED ARMOIRE!
br
p Click on the Enchanted Armoire, a 100 GP Reward in the Rewards Column, for a random chance at special Equipment! It may also give you random XP or food items. We'll be adding new equipment to it every month, but even when you've exhausted the current supply, you can keep clicking for a chance at food and XP.
br
p Now go spend all that accumulated Gold! May the Random Number Generator smile upon you...
p.small.muted by Lemoness and SabreCat
p.small.muted Art by Kiwibot, Starsystemic, UncommonCriminal, Zoebeagle, and Andrews38
tr
td
.background_island_waterfalls.pull-right
@ -17,7 +28,7 @@ a(href='/static/old-news', target='_blank') Read older news
mixin oldNews
h5 6/1/2015 - JUNE MYSTERY ITEM AND NEW MOUNT POSITIONING!
h5 6/1/2015 - JUNE MYSTERY ITEM!
tr
td
.inventory_present_06.pull-right

View file

@ -78,18 +78,25 @@ div(ng-if='::profile.achievements.perfect || user._id == profile._id')
small(ng-if='::profile.achievements.perfect == 1')=env.t('perfectSingularText')
hr
//-div(ng-if='::profile.achievements.ultimateGear || user._id == profile._id')
.achievement.achievement-armor(ng-if='::profile.achievements.ultimateGear')
div(ng-class='::{muted: !profile.achievements.ultimateGear}')
div(ng-if='::profile.achievements.ultimateGearSets || user._id == profile._id')
.achievement.achievement-armor(ng-if='::profile.achievements.ultimateGearSets')
div(ng-class='::{muted: !profile.achievements.ultimateGearSets}')
h5=env.t('ultimGearName')
small=env.t('ultimGearText')
hr
// Remove the following when ultimate gear is fixed (https://github.com/HabitRPG/habitrpg/issues/2232):
div(ng-if='::user._id == profile._id')
div.muted
h5=env.t('ultimGearName')
small
+aLink('https://github.com/HabitRPG/habitrpg/issues/2232', 'Returning soon')
table.multi-achievement
tr
td(ng-if='::profile.achievements.ultimateGearSets.healer').multi-achievement
.achievement-ultimate-healer.multi-achievement
=env.t('healer')
td(ng-if='::profile.achievements.ultimateGearSets.wizard').multi-achievement
.achievement-ultimate-mage.multi-achievement
=env.t('mage')
td(ng-if='::profile.achievements.ultimateGearSets.rogue').multi-achievement
.achievement-ultimate-rogue.multi-achievement
=env.t('rogue')
td(ng-if='::profile.achievements.ultimateGearSets.warrior').multi-achievement
.achievement-ultimate-warrior.multi-achievement
=env.t('warrior')
hr
div(ng-if='::profile.achievements.beastMaster || user._id == profile._id')

View file

@ -126,7 +126,7 @@ script(id='templates/habitrpg-tasks.html', type="text/ng-template")
// Static Rewards
ul.items.rewards(ng-if='main && list.type=="reward"')
li.task.reward-item(ng-repeat='item in itemStore',popover-trigger='mouseenter', popover-placement='top', popover='{{item.notes()}}')
li.task.reward-item(ng-repeat='item in itemStore',popover-trigger='mouseenter', popover-placement='top', popover='{{item.key == "armoire" && !user.flags.armoireEmpty ? env.t("armoireNotesFull") + armoireCount(user.items.gear.owned) : item.notes()}}')
// right-hand side control buttons
.task-meta-controls
span.task-notes