feat(customize): Animal skins and ears

Implements a base-pet-themed set of avatar skins and head accessories, the latter of which has additional handling for the user to purchase them from the Avatar Customization page.
This commit is contained in:
Sabe Jones
2015-05-13 11:19:08 -05:00
parent 4050e9ad0c
commit 29109051d7
41 changed files with 88 additions and 32 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 524 B

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: 520 B

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: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 B

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.3 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.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 913 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 B

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.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@@ -138,5 +138,6 @@
"displayNameDescription3": "and scroll down to the Registration section to change your login name.", "displayNameDescription3": "and scroll down to the Registration section to change your login name.",
"unequipBattleGear": "Unequip Battle Gear", "unequipBattleGear": "Unequip Battle Gear",
"unequipCostume": "Unequip Costume", "unequipCostume": "Unequip Costume",
"unequipPetMountBackground": "Unequip Pet, Mount, Background" "unequipPetMountBackground": "Unequip Pet, Mount, Background",
"animalSkins": "Animal Skins"
} }

View File

@@ -529,6 +529,8 @@
"bodySpecialSummerHealerNotes": "A stylish collar of live coral! Confers no benefit. Limited Edition 2014 Summer Gear.", "bodySpecialSummerHealerNotes": "A stylish collar of live coral! Confers no benefit. Limited Edition 2014 Summer Gear.",
"headAccessory": "head accessory", "headAccessory": "head accessory",
"accessories": "Accessories",
"animalEars": "Animal Ears",
"headAccessoryBase0Text": "No Head Accessory", "headAccessoryBase0Text": "No Head Accessory",
"headAccessoryBase0Notes": "No Head Accessory.", "headAccessoryBase0Notes": "No Head Accessory.",
@@ -551,6 +553,23 @@
"headAccessorySpecialSpring2015HealerText": "Green Kitty Ears", "headAccessorySpecialSpring2015HealerText": "Green Kitty Ears",
"headAccessorySpecialSpring2015HealerNotes": "These cute kitty ears will make others green with envy. Confers no benefit. Limited Edition 2015 Spring Gear.", "headAccessorySpecialSpring2015HealerNotes": "These cute kitty ears will make others green with envy. Confers no benefit. Limited Edition 2015 Spring Gear.",
"headAccessoryBearEarsText": "Bear Ears",
"headAccessoryBearEarsNotes": "These ears make you look like a cuddly bear! Confers no benefit.",
"headAccessoryCactusEarsText": "Cactus Ears",
"headAccessoryCactusEarsNotes": "These ears make you look like a prickly cactus! Confers no benefit.",
"headAccessoryFoxEarsText": "Fox Ears",
"headAccessoryFoxEarsNotes": "These ears make you look like a wily fox! Confers no benefit.",
"headAccessoryLionEarsText": "Lion Ears",
"headAccessoryLionEarsNotes": "These ears make you look like a regal lion! Confers no benefit.",
"headAccessoryPandaEarsText": "Panda Ears",
"headAccessoryPandaEarsNotes": "These ears make you look like a gentle panda! Confers no benefit.",
"headAccessoryPigEarsText": "Pig Ears",
"headAccessoryPigEarsNotes": "These ears make you look like a whimsical pig! Confers no benefit.",
"headAccessoryTigerEarsText": "Tiger Ears",
"headAccessoryTigerEarsNotes": "These ears make you look like a fierce tiger! Confers no benefit.",
"headAccessoryWolfEarsText": "Wolf Ears",
"headAccessoryWolfEarsNotes": "These ears make you look like a loyal wolf! Confers no benefit.",
"headAccessoryMystery201403Text": "Forest Walker Antlers", "headAccessoryMystery201403Text": "Forest Walker Antlers",
"headAccessoryMystery201403Notes": "These antlers shimmer with moss and lichen. Confers no benefit. March 2014 Subscriber Item.", "headAccessoryMystery201403Notes": "These antlers shimmer with moss and lichen. Confers no benefit. March 2014 Subscriber Item.",
"headAccessoryMystery201404Text": "Twilight Butterfly Antennae", "headAccessoryMystery201404Text": "Twilight Butterfly Antennae",

View File

@@ -400,7 +400,15 @@ gear =
spring2015Warrior: event: events.spring2015, specialClass: 'warrior', text: t('headAccessorySpecialSpring2015WarriorText'), notes: t('headAccessorySpecialSpring2015WarriorNotes'), value: 20 spring2015Warrior: event: events.spring2015, specialClass: 'warrior', text: t('headAccessorySpecialSpring2015WarriorText'), notes: t('headAccessorySpecialSpring2015WarriorNotes'), value: 20
spring2015Mage: event: events.spring2015, specialClass: 'wizard', text: t('headAccessorySpecialSpring2015MageText'), notes: t('headAccessorySpecialSpring2015MageNotes'), value: 20 spring2015Mage: event: events.spring2015, specialClass: 'wizard', text: t('headAccessorySpecialSpring2015MageText'), notes: t('headAccessorySpecialSpring2015MageNotes'), value: 20
spring2015Healer: event: events.spring2015, specialClass: 'healer', text: t('headAccessorySpecialSpring2015HealerText'), notes: t('headAccessorySpecialSpring2015HealerNotes'), value: 20 spring2015Healer: event: events.spring2015, specialClass: 'healer', text: t('headAccessorySpecialSpring2015HealerText'), notes: t('headAccessorySpecialSpring2015HealerNotes'), value: 20
# Animal ears
bearEars: gearSet: 'animal', text: t('headAccessoryBearEarsText'), notes: t('headAccessoryBearEarsNotes'), value: 20, canOwn: ((u)-> u.items.gear.owned.headAccessory_animalEars_bearEars?)
cactusEars: gearSet: 'animal', text: t('headAccessoryCactusEarsText'), notes: t('headAccessoryCactusEarsNotes'), value: 20, canOwn: ((u)-> u.items.gear.owned.headAccessory_animalEars_cactusEars?)
foxEars: gearSet: 'animal', text: t('headAccessoryFoxEarsText'), notes: t('headAccessoryFoxEarsNotes'), value: 20, canOwn: ((u)-> u.items.gear.owned.headAccessory_animalEars_foxEars?)
lionEars: gearSet: 'animal', text: t('headAccessoryLionEarsText'), notes: t('headAccessoryLionEarsNotes'), value: 20, canOwn: ((u)-> u.items.gear.owned.headAccessory_animalEars_lionEars?)
pandaEars: gearSet: 'animal', text: t('headAccessoryPandaEarsText'), notes: t('headAccessoryPandaEarsNotes'), value: 20, canOwn: ((u)-> u.items.gear.owned.headAccessory_animalEars_pandaEars?)
pigEars: gearSet: 'animal', text: t('headAccessoryPigEarsText'), notes: t('headAccessoryPigEarsNotes'), value: 20, canOwn: ((u)-> u.items.gear.owned.headAccessory_animalEars_pigEars?)
tigerEars: gearSet: 'animal', text: t('headAccessoryTigerEarsText'), notes: t('headAccessoryTigerEarsNotes'), value: 20, canOwn: ((u)-> u.items.gear.owned.headAccessory_animalEars_tigerEars?)
wolfEars: gearSet: 'animal', text: t('headAccessoryWolfEarsText'), notes: t('headAccessoryWolfEarsNotes'), value: 20, canOwn: ((u)-> u.items.gear.owned.headAccessory_animalEars_wolfEars?)
mystery: mystery:
201403: text: t('headAccessoryMystery201403Text'), notes: t('headAccessoryMystery201403Notes'), mystery:'201403', value: 0 201403: text: t('headAccessoryMystery201403Text'), notes: t('headAccessoryMystery201403Notes'), mystery:'201403', value: 0
201404: text: t('headAccessoryMystery201404Text'), notes: t('headAccessoryMystery201404Notes'), mystery:'201404', value: 0 201404: text: t('headAccessoryMystery201404Text'), notes: t('headAccessoryMystery201404Notes'), mystery:'201404', value: 0

View File

@@ -745,7 +745,7 @@ api.wrap = (user, main=true) ->
if type is 'gear' if type is 'gear'
item = content.gear.flat[key] item = content.gear.flat[key]
return cb?({code:401, message: i18n.t('alreadyHave', req.language)}) if user.items.gear.owned[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 price = (if item.twoHanded or item.gearSet is 'animal' then 2 else 1) / 4
else else
item = content[type][key] item = content[type][key]
price = item.value / 4 price = item.value / 4
@@ -895,6 +895,9 @@ api.wrap = (user, main=true) ->
return cb?({code:401, message: i18n.t('notEnoughGems', req.language)}) if user.balance < cost and !alreadyOwns return cb?({code:401, message: i18n.t('notEnoughGems', req.language)}) if user.balance < cost and !alreadyOwns
if fullSet if fullSet
_.each path.split(","), (p) -> _.each path.split(","), (p) ->
if ~path.indexOf('gear.')
user.fns.dotSet("#{p}", true);true
else
user.fns.dotSet("purchased.#{p}", true);true user.fns.dotSet("purchased.#{p}", true);true
else else
if alreadyOwns if alreadyOwns
@@ -904,8 +907,8 @@ api.wrap = (user, main=true) ->
return cb? null, req return cb? null, req
user.fns.dotSet "purchased." + path, true user.fns.dotSet "purchased." + path, true
user.balance -= cost user.balance -= cost
user.markModified? 'purchased' if ~path.indexOf('gear.') then user.markModified? 'gear.owned' else user.markModified? 'purchased'
cb? null, _.pick(user,$w 'purchased preferences') cb? null, _.pick(user,$w 'purchased preferences items')
ga?.event('behavior', 'gems', path).send() ga?.event('behavior', 'gems', path).send()
# ------ # ------

View File

@@ -103,26 +103,6 @@ habitrpg.controller("InventoryCtrl",
} }
} }
$scope.purchase = function(type, item){
if (type == 'special') return User.user.ops.buySpecialSpell({params:{key:item.key}});
var gems = User.user.balance * 4;
var string = (type == 'weapon') ? window.env.t('weapon') : (type == 'armor') ? window.env.t('armor') : (type == 'head') ? window.env.t('headgear') : (type == 'shield') ? window.env.t('offhand') : (type == 'headAccessory') ? window.env.t('headAccessory') : (type == 'hatchingPotions') ? window.env.t('hatchingPotion') : (type == 'eggs') ? window.env.t('eggSingular') : (type == 'quests') ? window.env.t('quest') : (item.key == 'Saddle') ? window.env.t('foodSaddleText').toLowerCase() : type; // this is ugly but temporary, once the purchase modal is done this will be removed
if (type == 'weapon' || type == 'armor' || type == 'head' || type == 'shield' || type == 'headAccessory') {
if (gems < ((item.specialClass == "wizard") && (item.type == "weapon")) + 1) return $rootScope.openModal('buyGems');
var message = window.env.t('buyThis', {text: string, price: ((item.specialClass == "wizard") && (item.type == "weapon")) + 1, gems: gems})
if($window.confirm(message))
User.user.ops.purchase({params:{type:"gear",key:item.key}});
} else {
if(gems < item.value) return $rootScope.openModal('buyGems');
var message = window.env.t('buyThis', {text: string, price: item.value, gems: gems})
if($window.confirm(message))
User.user.ops.purchase({params:{type:type,key:item.key}});
}
}
$scope.choosePet = function(egg, potion){ $scope.choosePet = function(egg, potion){
var petDisplayName = env.t('petName', { var petDisplayName = env.t('petName', {
potion: Content.hatchingPotions[potion] ? Content.hatchingPotions[potion].text() : potion, potion: Content.hatchingPotions[potion] ? Content.hatchingPotions[potion].text() : potion,

View File

@@ -3,8 +3,8 @@
/* Make user and settings available for everyone through root scope. /* Make user and settings available for everyone through root scope.
*/ */
habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$http', '$state', '$stateParams', 'Notification', 'Groups', 'Shared', 'Content', '$modal', '$timeout', 'ApiUrl', 'Payments','$sce', habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$http', '$state', '$stateParams', 'Notification', 'Groups', 'Shared', 'Content', '$modal', '$timeout', 'ApiUrl', 'Payments','$sce','$window',
function($scope, $rootScope, $location, User, $http, $state, $stateParams, Notification, Groups, Shared, Content, $modal, $timeout, ApiUrl, Payments,$sce) { function($scope, $rootScope, $location, User, $http, $state, $stateParams, Notification, Groups, Shared, Content, $modal, $timeout, ApiUrl, Payments, $sce, $window) {
var user = User.user; var user = User.user;
var initSticky = _.once(function(){ var initSticky = _.once(function(){
@@ -201,7 +201,40 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$
chart.draw(data, options); chart.draw(data, options);
}; };
$rootScope.getGearArray = function(set){
var flatGearArray = _.toArray(Content.gear.flat);
var filteredArray = _.where(flatGearArray, {gearSet: set});
return filteredArray;
}
$rootScope.purchase = function(type, item){
if (type == 'special') return User.user.ops.buySpecialSpell({params:{key:item.key}});
var gems = User.user.balance * 4;
var string = (type == 'weapon') ? window.env.t('weapon') : (type == 'armor') ? window.env.t('armor') : (type == 'head') ? window.env.t('headgear') : (type == 'shield') ? window.env.t('offhand') : (type == 'headAccessory') ? window.env.t('headAccessory') : (type == 'hatchingPotions') ? window.env.t('hatchingPotion') : (type == 'eggs') ? window.env.t('eggSingular') : (type == 'quests') ? window.env.t('quest') : (item.key == 'Saddle') ? window.env.t('foodSaddleText').toLowerCase() : type; // FIXME this is ugly but temporary, once the purchase modal is done this will be removed
var price = ((((item.specialClass == "wizard") && (item.type == "weapon")) || item.gearSet == "animal") + 1);
if (type == 'weapon' || type == 'armor' || type == 'head' || type == 'shield' || type == 'headAccessory') {
if (User.user.items.gear.owned[item.key]) {
if (User.user.preferences.costume) return User.user.ops.equip({params:{type: 'costume', key: item.key}});
else {
return User.user.ops.equip({params:{type: 'equipped', key: item.key}})
}
}
if (gems < price) return $rootScope.openModal('buyGems');
var message = window.env.t('buyThis', {text: string, price: price, gems: gems})
if($window.confirm(message))
User.user.ops.purchase({params:{type:"gear",key:item.key}});
} else {
if(gems < item.value) return $rootScope.openModal('buyGems');
var message = window.env.t('buyThis', {text: string, price: item.value, gems: gems})
if($window.confirm(message))
User.user.ops.purchase({params:{type:type,key:item.key}});
}
}
/* /*
------------------------ ------------------------

View File

@@ -198,7 +198,8 @@ module.exports.locals = function(req, res, next) {
Content: shared.content, Content: shared.content,
mods: require('./models/user').mods, mods: require('./models/user').mods,
tavern: tavern, // for world boss tavern: tavern, // for world boss
worldDmg: (tavern && tavern.quest && tavern.quest.extra && tavern.quest.extra.worldDmg) || {} worldDmg: (tavern && tavern.quest && tavern.quest.extra && tavern.quest.extra.worldDmg) || {},
_: _
}); });
// Put query-string party (& guild but use partyInvite for backward compatibility) // Put query-string party (& guild but use partyInvite for backward compatibility)

View File

@@ -4,6 +4,7 @@ mixin gemCost(cost)
= ' ' + env.t('locked') = ' ' + env.t('locked')
block block
-var gearGroup = function(grouping) { return env._(env.Content.gear.flat).where({gearSet:grouping}).pluck('key') }
-var showPath = function(path, items, joiner) { return path+'["'+items.join('"] '+joiner+' '+path+'["')+'"]'; } -var showPath = function(path, items, joiner) { return path+'["'+items.join('"] '+joiner+' '+path+'["')+'"]'; }
-var unlockPath = function(path, items) { return 'unlock("'+path+'.'+items.join(','+path+'.')+'")'; } -var unlockPath = function(path, items) { return 'unlock("'+path+'.'+items.join(','+path+'.')+'")'; }
@@ -45,6 +46,15 @@ mixin customizeProfile(mobile)
each shirt in specialShirts each shirt in specialShirts
button.customize-option(type='button', class='{{user.preferences.size}}_shirt_'+shirt, ng-class='{locked: !user.purchased.shirt.'+shirt+'}', ng-click='unlock("shirt.'+shirt+'")') button.customize-option(type='button', class='{{user.preferences.size}}_shirt_'+shirt, ng-class='{locked: !user.purchased.shirt.'+shirt+'}', ng-click='unlock("shirt.'+shirt+'")')
h3(class=mobile?'item item-divider':'')=env.t('accessories')
menu(type='list')
li.customize-menu
menu(label=env.t('animalEars'))
span(ng-hide='#{showPath("user.items.gear.owned", gearGroup("animal"), "&&")}')
+gemCost(2)
button.btn.btn-xs(ng-click='#{unlockPath("items.gear.owned", gearGroup("animal"))}')!= env.t('unlockSet', {cost: 5}) + ' <span class="Pet_Currency_Gem1x inline-gems"/>'
button.customize-option(ng-repeat='item in ::getGearArray("animal")' ng-class="{locked: user.items.gear.owned[item.key] == undefined}", popover='{{::item.notes()}}', popover-title='{{::item.text()}}', popover-trigger='mouseenter', popover-placement='right', popover-append-to-body='true', ng-click='purchase(item.type,item)', class='shop_{{::item.key}}')
.col-md-4 .col-md-4
h3(class=mobile?'item item-divider':'')=env.t('bodyHead') h3(class=mobile?'item item-divider':'')=env.t('bodyHead')
@@ -132,11 +142,12 @@ mixin customizeProfile(mobile)
each color in ['ddc994','f5a76e','ea8349','c06534','98461a','915533','c3e1dc','6bd049'] each color in ['ddc994','f5a76e','ea8349','c06534','98461a','915533','c3e1dc','6bd049']
button.customize-option(type='button', class='skin_#{color}', ng-click='set({"preferences.skin":"#{color}"})') button.customize-option(type='button', class='skin_#{color}', ng-click='set({"preferences.skin":"#{color}"})')
// Rainbow Skin // Always-available premium skins
+buyPref('skin', ['eb052b','f69922','f5d70f','0ff591','2b43f6','d7a9f7','800ed0','rainbow'], 'rainbowSkins') +buyPref('skin', ['eb052b','f69922','f5d70f','0ff591','2b43f6','d7a9f7','800ed0','rainbow'], 'rainbowSkins')
+buyPref('skin', ['pastelPink','pastelOrange','pastelYellow','pastelGreen','pastelBlue','pastelPurple','pastelRainbowChevron','pastelRainbowDiagonal'], 'pastelSkins', 'disabled') +buyPref('skin', ['bear','cactus','fox','lion','panda','pig','tiger','wolf'], 'animalSkins')
// Special Events // Seasonal event skins. Note that Spooky Skins are a legacy set and should always be disabled for purchase
+buyPref('skin', ['pastelPink','pastelOrange','pastelYellow','pastelGreen','pastelBlue','pastelPurple','pastelRainbowChevron','pastelRainbowDiagonal'], 'pastelSkins', 'disabled')
+buyPref('skin', ['monster','pumpkin','skeleton','zombie','ghost','shadow'], 'spookySkins', 'disabled') +buyPref('skin', ['monster','pumpkin','skeleton','zombie','ghost','shadow'], 'spookySkins', 'disabled')
+buyPref('skin', ['candycorn','ogre','pumpkin2','reptile','shadow2','skeleton2','transparent','zombie2'], 'supernaturalSkins', 'disabled') +buyPref('skin', ['candycorn','ogre','pumpkin2','reptile','shadow2','skeleton2','transparent','zombie2'], 'supernaturalSkins', 'disabled')