diff --git a/common/locales/en/gear.json b/common/locales/en/gear.json index 37ca45998a..c9cc62f30c 100644 --- a/common/locales/en/gear.json +++ b/common/locales/en/gear.json @@ -1,4 +1,6 @@ { + "set": "Set", + "weapon": "weapon", "weaponBase0Text": "No Weapon", diff --git a/common/locales/en/generic.json b/common/locales/en/generic.json index 409e48e1f0..3c1e1bfb21 100644 --- a/common/locales/en/generic.json +++ b/common/locales/en/generic.json @@ -177,5 +177,6 @@ "hatchPetShare": "I hatched a new pet by completing my real-life tasks!", "raisePetShare": "I raised a pet into a mount by completing my real-life tasks!", "wonChallengeShare": "I won a challenge in Habitica!", - "achievementShare": "I earned a new achievement in Habitica!" + "achievementShare": "I earned a new achievement in Habitica!", + "orderBy": "Order By <%= item %>" } diff --git a/common/script/content/gear/index.js b/common/script/content/gear/index.js index cf288e89dc..d7013e9986 100644 --- a/common/script/content/gear/index.js +++ b/common/script/content/gear/index.js @@ -39,10 +39,12 @@ each(GEAR_TYPES, (type) => { each(allGearTypes, (klass) => { each(gear[type][klass], (item, index) => { let key = `${type}_${klass}_${index}`; + let set = `${klass}-${index}`; defaults(item, { type, key, + set, klass, index, str: 0, diff --git a/test/content/gear.js b/test/content/gear.js index 1e6a80ffe9..a3d0e95917 100644 --- a/test/content/gear.js +++ b/test/content/gear.js @@ -13,7 +13,7 @@ describe('Gear', () => { each(piece, (items, klass) => { context(`${klass} ${gearType}s`, () => { it('have a value of at least 0 for each stat', () => { - each(items, (gear, itemKey) => { + each(items, (gear) => { expect(gear.con).to.be.at.least(0); expect(gear.int).to.be.at.least(0); expect(gear.per).to.be.at.least(0); @@ -22,23 +22,29 @@ describe('Gear', () => { }); it('have a purchase value of at least 0', () => { - each(items, (gear, itemKey) => { + each(items, (gear) => { expect(gear.value).to.be.at.least(0); }); }); it('has a canBuy function', () => { - each(items, (gear, itemKey) => { + each(items, (gear) => { expect(gear.canBuy).to.be.a('function'); }); }); it('have valid translation strings for text and notes', () => { - each(items, (gear, itemKey) => { + each(items, (gear) => { expectValidTranslationString(gear.text); expectValidTranslationString(gear.notes); }); }); + + it('has a set attribue', () => { + each(items, (gear) => { + expect(gear.set).to.exist; + }); + }); }); }); }); diff --git a/test/spec/controllers/sortableInventoryCtrlSpec.js b/test/spec/controllers/sortableInventoryCtrlSpec.js new file mode 100644 index 0000000000..7bdb288c45 --- /dev/null +++ b/test/spec/controllers/sortableInventoryCtrlSpec.js @@ -0,0 +1,42 @@ +describe('Sortable Inventory Controller', () => { + let scope; + + beforeEach(inject(($rootScope, $controller) => { + scope = $rootScope.$new(); + $controller('SortableInventoryController', {$scope: scope}); + })); + + it('defaults scope.order to name', () => { + expect(scope.order).to.eql('text()') + }); + + describe('#setOrder', () => { + it('sets sort criteria for all standard attributes', () =>{ + let oldOrder = scope.order; + + let attrs = [ + 'constitution', + 'intelligence', + 'perception', + 'strength', + 'name', + 'set' + ]; + + attrs.forEach((attribute) => { + scope.setOrder(attribute); + expect(scope.order).to.exist; + expect(scope.order).to.not.eql(oldOrder); + oldOrder = scope.order; + }); + }); + + it('does nothing when missing sort criteria', () =>{ + scope.order = null; + + scope.setOrder('foooo'); + + expect(scope.order).to.not.exist; + }); + }); +}); diff --git a/website/public/js/controllers/sortableInventoryCtrl.js b/website/public/js/controllers/sortableInventoryCtrl.js new file mode 100644 index 0000000000..16512927ec --- /dev/null +++ b/website/public/js/controllers/sortableInventoryCtrl.js @@ -0,0 +1,21 @@ +habitrpg.controller('SortableInventoryController', ['$scope', + function ($scope) { + var attributeSort = { + constitution: ['-con', '-(con+int+per+str)'], + intelligence: ['-int', '-(con+int+per+str)'], + perception: ['-per', '-(con+int+per+str)'], + strength: ['-str', '-(con+int+per+str)'], + name: 'text()', + set: 'set' + } + + $scope.order = attributeSort.name; + $scope.orderChoice = 'name'; + + $scope.setOrder = function (order) { + $scope.orderChoice = order; + if (order in attributeSort) { + $scope.order = attributeSort[order]; + } + }; +}]); diff --git a/website/public/manifest.json b/website/public/manifest.json index accb695df3..c4d107c03e 100644 --- a/website/public/manifest.json +++ b/website/public/manifest.json @@ -91,6 +91,7 @@ "js/controllers/partyCtrl.js", "js/controllers/rootCtrl.js", "js/controllers/settingsCtrl.js", + "js/controllers/sortableInventoryCtrl.js", "js/controllers/tavernCtrl.js", "js/controllers/tasksCtrl.js", "js/controllers/userCtrl.js" diff --git a/website/views/options/inventory/equipment.jade b/website/views/options/inventory/equipment.jade index 8d77a25a8b..5fac343b79 100644 --- a/website/views/options/inventory/equipment.jade +++ b/website/views/options/inventory/equipment.jade @@ -1,10 +1,36 @@ +mixin orderByButton + mixin choice(attr) + li(ng-class="{ 'active': orderChoice === '#{attr}' }") + a(ng-click="setOrder('#{attr}')")=env.t(attr) + + .btn-group + button.btn.btn-default.dropdown-toggle(type='button', data-toggle='dropdown') + | {{env.t("orderBy", { item: env.t(orderChoice) })}} #[span.caret] + ul.dropdown-menu + - each attr in ["name", "set"] + +choice(attr) + li.divider(role="separator") + - each attr in ["constitution", "intelligence", "perception", "strength"] + +choice(attr) + +mixin equipmentList(type) + menu.pets-menu(label='{{::label}}', ng-show='gear[klass]', + 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")}') + div(ng-repeat='item in gear[klass] | orderBy: order') + button.customize-option(class='shop_{{::item.key}}', + ng-class='{selectableInventory: user.items.gear.#{type}[item.type] == item.key}', + ng-click='equip(item.key, "${type}")', + popover='{{::item.notes()}}', popover-title='{{::item.text()}}', + popover-trigger='mouseenter', popover-placement='right', + popover-append-to-body='true') + .container-fluid .row - .col-md-6.border-right + .col-md-6.border-right(ng-controller="SortableInventoryController") h3.equipment-title.hint(popover-trigger='mouseenter', popover-placement='right', popover-append-to-body='true', popover=env.t('battleGearText'))=env.t('battleGear') - + .checkbox.equipment-title label input(type="checkbox", ng-model="user.preferences.autoEquip", @@ -12,21 +38,15 @@ |   =env.t('autoEquipBattleGear') - div - button.btn.btn-default(type="button", ng-click='dequip("battleGear");') {{env.t("unequipBattleGear")}} + .btn-toolbar + .btn-group + button.btn.btn-default(type="button", ng-click='dequip("battleGear");') {{env.t("unequipBattleGear")}} + +orderByButton li.customize-menu.inventory-gear - menu.pets-menu(label='{{::label}}', ng-show='gear[klass]', - 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")}') - div(ng-repeat='item in gear[klass]') - button.customize-option(, class='shop_{{::item.key}}', - ng-class='{selectableInventory: user.items.gear.equipped[item.type] == item.key}', - ng-click='equip(item.key, "equipped")', - popover='{{::item.notes()}}', popover-title='{{::item.text()}}', - popover-trigger='mouseenter', popover-placement='right', - popover-append-to-body='true') + +equipmentList("equipped") - .col-md-6 + .col-md-6(ng-controller="SortableInventoryController") h3.equipment-title.hint(popover-trigger='mouseenter', popover-placement='right', popover-append-to-body='true', popover=env.t('costumeText'))=env.t('costume') @@ -38,9 +58,11 @@ |   =env.t('useCostume') - div - 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")}} + .btn-toolbar + .btn-group + 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")}} + +orderByButton li.customize-menu(ng-if='!user.preferences.costume') .well.use-costume-info @@ -49,12 +71,4 @@ p: a(ng-click='showUseCostumeInfo = !showUseCostumeInfo') {{!showUseCostumeInfo ? env.t('showMoreMore') : env.t('showMoreLess')}} li.customize-menu(ng-if='user.preferences.costume') - menu.pets-menu(label='{{::label}}', ng-show='gear[klass]', - 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")}') - div(ng-repeat='item in gear[klass]') - button.customize-option(class='shop_{{::item.key}}', - ng-class='{selectableInventory: user.items.gear.costume[item.type] == item.key}', - ng-click='equip(item.key, "costume")', - popover='{{::item.notes()}}', popover-title='{{::item.text()}}', - popover-trigger='mouseenter', popover-placement='right', - popover-append-to-body='true') + +equipmentList("costume")