diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..a38c5d0 --- /dev/null +++ b/TODO.md @@ -0,0 +1,6 @@ +- [ ] Prevent reducing talent points on a row when it is a dependency for points already spent in the next row +- [ ] Prevent reducing talent points on a talent that is a requirement for another talent with points in it +- [ ] Talent tooltips +- [ ] Change class +- [ ] Generate URL for chosen talents +- [ ] Responsive on mobile \ No newline at end of file diff --git a/package.json b/package.json index ecfdbe4..0ab1668 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "@types/node": "12.6.2", "@types/react": "16.8.23", "@types/react-dom": "16.8.4", + "classnames": "^2.2.6", "immutable": "^4.0.0-rc.12", "node-sass": "^4.12.0", "react": "^16.8.6", @@ -34,5 +35,8 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "devDependencies": { + "@welldone-software/why-did-you-render": "^3.2.1" } } diff --git a/public/index.html b/public/index.html index dd1ccfd..f468546 100644 --- a/public/index.html +++ b/public/index.html @@ -19,7 +19,7 @@ work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> - React App + Talent Calculator diff --git a/src/App.scss b/src/App.scss index dbe0f84..da97fae 100644 --- a/src/App.scss +++ b/src/App.scss @@ -2,44 +2,18 @@ text-align: center; } -.tree { - position: relative; - width: 400px; - height: 500px; - border: 1px solid black; +.calculator { } -.talent { - position: absolute; - width: 40px; - height: 40px; +.trees { + display: flex; +} + +.tree { + position: relative; + min-width: 300px; + height: 600px; border: 1px solid black; - border-radius: 2px; - - small { - font-size: 8px; - line-height: 1em; - } - - &[data-row="0"] { top: 70px; } - &[data-row="1"] { top: 140px; } - &[data-row="2"] { top: 210px; } - &[data-row="3"] { top: 280px; } - &[data-row="4"] { top: 400px; } - - &[data-col="0"] { left: 10px; } - &[data-col="1"] { left: 60px; } - &[data-col="2"] { left: 110px; } - &[data-col="3"] { left: 160px; } - - &__rank { - position: absolute; - padding: 1px 2px; - bottom: -2px; - right: -2px; - color: white; - font-size: 12px; - background: black; - border-radius: 2px; - } -} \ No newline at end of file + background-size: cover; + background-position: center; +} diff --git a/src/components/Calculator.tsx b/src/components/Calculator.tsx index 818cf74..353fe27 100644 --- a/src/components/Calculator.tsx +++ b/src/components/Calculator.tsx @@ -1,137 +1,51 @@ import React, { useState } from 'react'; -import { List, Map, fromJS } from 'immutable' +import { Map } from 'immutable' import { TalentTree } from './TalentTree'; -import { modifyPointsInTree, modifyKnownTalents } from '../lib/tree'; -import { talentsBySpec, specNames } from '../data/talents'; +import { + modifyTalentPoint, + calcAvailablePoints +} from '../lib/tree'; +import { talentsBySpec } from '../data/talents'; import { classByName } from '../data/classes'; -import { number } from 'prop-types'; - -const createTalent = (name: string, row: number, column: number, ranks: string | string[], type: Talent['type'] = 'talent'): Talent => { - return { - name, - row, - column, - ranks: typeof ranks === 'string' ? [ranks] : ranks, - type - } -} - -/** - * Max rows: 7 - * Max cols: 4 - * - * verify: no talent on same [row, column] combination - * potentially: sort talents based on row and column so order doesn't matter - */ - -const warlockTalents: TalentTree[] = [ - // Affliction - { - id: 164, - name: 'Affliction', - icon: 'https://wow.zamimg.com/images/wow/icons/small/spell_shadow_deathcoil.jpg', - talents: [ - // Row 1 - createTalent('Suppression', 0, 1, [ - 'Reduces the chance for enemies to resist your Affliction spells by 2%.', - 'Reduces the chance for enemies to resist your Affliction spells by 4%.', - 'Reduces the chance for enemies to resist your Affliction spells by 6%.', - 'Reduces the chance for enemies to resist your Affliction spells by 8%.', - 'Reduces the chance for enemies to resist your Affliction spells by 10%.', - ]), - createTalent('Improved Corruption', 0, 2, [ - 'Reduces the casting time of your corruption spell by 0.4 sec.', - 'Reduces the casting time of your corruption spell by 0.8 sec.', - 'Reduces the casting time of your corruption spell by 1.2 sec.', - 'Reduces the casting time of your corruption spell by 1.6 sec.', - 'Reduces the casting time of your corruption spell by 2 sec.', - ]), - - // Row 2 - createTalent('Improved Curse of Weakness', 1, 0, [ - 'Increases the effect of your Curse of Weakness by 6%.', - 'Increases the effect of your Curse of Weakness by 13%.', - 'Increases the effect of your Curse of Weakness by 20%.' - ]), - createTalent('Improved Drain Soul', 1, 1, [ - 'Gives you a 50% chance to get a 100% increase to your Mana regeneration for 10 sec if the target is killed by you while you drain its soul. In addition your Mana may continue to regenerate while casting at 50% of normal.', - 'Gives you a 100% chance to get a 100% increase to your Mana regeneration for 10 sec if the target is killed by you while you drain its soul. In addition your Mana may continue to regenerate while casting at 50% of normal.', - ]), - createTalent('Improved Life Tap', 1, 2, [ - 'Increases the amount of Mana awarded by your Life Tap spell by 10%.', - 'Increases the amount of Mana awarded by your Life Tap spell by 20%.', - ]), - createTalent('Improved Drain Life', 1, 3, [ - 'Increases the Health drained by your Drain Life spell by 2%.', - 'Increases the Health drained by your Drain Life spell by 4%.', - 'Increases the Health drained by your Drain Life spell by 6%.', - 'Increases the Health drained by your Drain Life spell by 8%.', - 'Increases the Health drained by your Drain Life spell by 10%.', - ]), - - // Row 3 - createTalent('Improved Curse of Agony', 2, 0, [ - 'Increases the damage done by your Curse of Agony by 2%.', - 'Increases the damage done by your Curse of Agony by 4%.', - 'Increases the damage done by your Curse of Agony by 6%.', - ]), - createTalent('Fel Concentration', 2, 1, [ - 'Gives you a 14% chance to avoid interruption caused by damage while channeling the Drain Life, Drain Mana, or Drain Soul spell.', - 'Gives you a 28% chance to avoid interruption caused by damage while channeling the Drain Life, Drain Mana, or Drain Soul spell.', - 'Gives you a 42% chance to avoid interruption caused by damage while channeling the Drain Life, Drain Mana, or Drain Soul spell.', - 'Gives you a 56% chance to avoid interruption caused by damage while channeling the Drain Life, Drain Mana, or Drain Soul spell.', - 'Gives you a 70% chance to avoid interruption caused by damage while channeling the Drain Life, Drain Mana, or Drain Soul spell.', - ]), - createTalent('Amplify Curse', 2, 2, [ - 'Increases the effect of your next Curse of Weakness or Curse of Agony by 50%, or your next Curse of Exhaustion by 20%. Lasts 30 sec.', - ]), - ] - } - // Demonoloy - - // Destruction -] interface Props { forClass: string pointString?: string // e.g. 2305302300--001 } -const initialSpentPoints: List> = fromJS([ - [], [], [] -]) - const initMap = Map() export const Calculator: React.FC = ({ forClass = 'warlock', pointString = '' }) => { const [knownTalents, setKnownTalents] = useState(initMap) - const [spentPoints, setSpentPoints] = useState(initialSpentPoints) - const selectedClass = classByName[forClass] - - console.log(knownTalents) + const availablePoints = calcAvailablePoints(knownTalents) const handleTalentPress = (specId: number, talentId: number, modifier: 1 | -1) => { - console.log('onTalentPress', { specId, talentId, modifier }) - const talent = talentsBySpec[specId][talentId] - - - setKnownTalents(modifyKnownTalents(knownTalents, talent, modifier)) - + setKnownTalents( + modifyTalentPoint(knownTalents, talent, modifier) + ) } return (
- {selectedClass.specs.map((specId, specIndex) => ( - - ))} +
+ {selectedClass.specs.map((specId, specIndex) => ( + + ))} +
+ +
+ Points: {availablePoints} +
) -} \ No newline at end of file +} + +(Calculator as any).whyDidYouRender = true \ No newline at end of file diff --git a/src/components/Icon.scss b/src/components/Icon.scss index 2104752..7758797 100644 --- a/src/components/Icon.scss +++ b/src/components/Icon.scss @@ -1,4 +1,5 @@ .icon { + background-position: center; background-repeat: no-repeat; background-size: cover; diff --git a/src/components/TalentSlot.scss b/src/components/TalentSlot.scss index e69de29..773f106 100644 --- a/src/components/TalentSlot.scss +++ b/src/components/TalentSlot.scss @@ -0,0 +1,64 @@ + +.talent { + position: absolute; + width: 40px; + height: 40px; + border: 1px solid black; + border-radius: 2px; + + &:after { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 10; + } + + small { + font-size: 8px; + line-height: 1em; + } + + &--disabled { + filter: grayscale(100%) + } + + &:not(&--disabled) { + cursor: pointer; + } + + &--maxed { + + } + + $row-distance: 70px; + + &[data-row="0"] { top: $row-distance; } + &[data-row="1"] { top: $row-distance * 2; } + &[data-row="2"] { top: $row-distance * 3; } + &[data-row="3"] { top: $row-distance * 4; } + &[data-row="4"] { top: $row-distance * 5; } + &[data-row="5"] { top: $row-distance * 6; } + &[data-row="6"] { top: $row-distance * 7; } + + $col-distance-offset: 30px; + $col-distance: 50px; + + &[data-col="0"] { left: $col-distance-offset; } + &[data-col="1"] { left: $col-distance-offset + ($col-distance * 1); } + &[data-col="2"] { left: $col-distance-offset + ($col-distance * 2); } + &[data-col="3"] { left: $col-distance-offset + ($col-distance * 3); } + + &__points { + position: absolute; + padding: 1px 2px; + bottom: -2px; + right: -2px; + color: white; + font-size: 12px; + background: black; + border-radius: 2px; + } +} \ No newline at end of file diff --git a/src/components/TalentSlot.tsx b/src/components/TalentSlot.tsx index a83f0c8..14125a4 100644 --- a/src/components/TalentSlot.tsx +++ b/src/components/TalentSlot.tsx @@ -1,30 +1,65 @@ -import React, { FC } from 'react' import './TalentSlot.scss' +import React, { FC } from 'react' import { Icon } from './Icon' +import classNames from 'classnames' import { spells } from '../data/spells' +import { Map } from 'immutable'; +import { getPointsInSpec, calcMeetsRequirements } from '../lib/tree'; interface Props { - key: number talent: TalentData - /** Points spent */ - points: number + specId: number + availablePoints: number + /** All spent talents */ + knownTalents: Map + /** Disabled override */ + disabled?: boolean onClick?: (e: any) => void + onRightClick?: (e: any) => void +} + +const isAvailable = (talent: TalentData, specId: number, knownTalents: Map): boolean => { + // Dependent on other talents? + if (!calcMeetsRequirements(talent, knownTalents)) { + return false + } + const pointsInSpec = getPointsInSpec(specId, knownTalents) + return talent.row * 5 <= pointsInSpec } export const TalentSlot: FC = (props) => { - const { talent, points } = props - const requiredPointsSpent = talent.row * 5 + const { talent, specId, knownTalents, availablePoints } = props + const points = knownTalents.get(talent.id, 0) + const showPoints = points > 0 || availablePoints > 0 + const disabled = false // props.disabled || !showPoints || !isAvailable(talent, specId, knownTalents) + + const cn = classNames('talent', { + 'talent--disabled': !!disabled, + 'talent--maxed': points >= talent.ranks.length + }) + + const handleContextMenu = (e) => { + if (props.onRightClick) props.onRightClick(e) + e.preventDefault() + return false + } return (
{}} + onContextMenu={handleContextMenu} > -
{points}/{talent.ranks.length}
+ + {showPoints && +
{points}/{talent.ranks.length}
+ }
) -} \ No newline at end of file +} + +(TalentSlot as any).whyDidYouRender = true \ No newline at end of file diff --git a/src/components/TalentTree.tsx b/src/components/TalentTree.tsx index 21948da..c2e007d 100644 --- a/src/components/TalentTree.tsx +++ b/src/components/TalentTree.tsx @@ -1,38 +1,45 @@ -import React, { MouseEvent } from 'react' -import { List, Map } from 'immutable' +import React, { MouseEvent, useCallback } from 'react' +import { Map } from 'immutable' import { TalentSlot } from './TalentSlot'; -import { getTreePointCount } from '../lib/tree'; +import { getPointsInSpec } from '../lib/tree'; import { talentsBySpec, specNames } from '../data/talents'; interface Props { specId: number - spentPoints: List + availablePoints: number knownTalents: Map onTalentPress: TalentClickHandler } -export const TalentTree: React.FC = ({ specId, spentPoints, knownTalents, onTalentPress }) => { +export const TalentTree: React.FC = ({ specId, knownTalents, availablePoints, onTalentPress }) => { const talents = Object.values(talentsBySpec[specId]) - const handleTalentPress = (talentId: number) => { + const handleTalentPress = useCallback((talentId: number, modifier: 1 | -1) => { return (e: MouseEvent) => { - onTalentPress(specId, talentId, e.shiftKey ? -1 : 1) + onTalentPress(specId, talentId, modifier) } + }, [specId, onTalentPress]) + + const style = { + backgroundImage: `url("https://wow.zamimg.com/images/wow/talents/backgrounds/classic/${specId}.jpg")` } return ( -
-

{specNames[specId]}

+
+

{specNames[specId]} ({getPointsInSpec(specId, knownTalents)})

{talents.map((talent, index) => )} - - Spent: {getTreePointCount(spentPoints)}
) -} \ No newline at end of file +} + +(TalentTree as any).whyDidYouRender = true \ No newline at end of file diff --git a/src/data/talents.ts b/src/data/talents.ts index a0d4c22..96207f2 100644 --- a/src/data/talents.ts +++ b/src/data/talents.ts @@ -3742,4 +3742,13 @@ export const talentsBySpec: Root = { }] } } -} \ No newline at end of file +} + +export const talentToSpec: {[key: number]: number} = {} +for (let specId in talentsBySpec) { + for (let talentId in talentsBySpec[specId]) { + talentToSpec[talentId] = parseInt(specId, 10) + } +} + +console.log(talentToSpec) \ No newline at end of file diff --git a/src/index.tsx b/src/index.tsx index 87d1be5..272e93e 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -4,6 +4,11 @@ import './index.css'; import App from './App'; import * as serviceWorker from './serviceWorker'; +if (process.env.NODE_ENV !== 'production') { + const whyDidYouRender = require('@welldone-software/why-did-you-render') + whyDidYouRender(React) +} + ReactDOM.render(, document.getElementById('root')); // If you want your app to work offline and load faster, you can change diff --git a/src/lib/tree.test.ts b/src/lib/tree.test.ts index fb6834a..a556689 100644 --- a/src/lib/tree.test.ts +++ b/src/lib/tree.test.ts @@ -1,40 +1,40 @@ import im from 'immutable' import { - setTalentPointsInTree, - getTreePointCount + // setTalentPointsInTree, + getPointsInSpec } from './tree' -describe('setTalentPointsInTree', () => { - it('sets points on an empty tree', () => { - const tree = im.List() - expect(setTalentPointsInTree(tree, 2, 5).toJS()).toEqual([0, 0, 5]) - }) +// describe('setTalentPointsInTree', () => { +// it('sets points on an empty tree', () => { +// const tree = im.List() +// expect(setTalentPointsInTree(tree, 2, 5).toJS()).toEqual([0, 0, 5]) +// }) - it('sets points in the end of the current range', () => { - const tree = im.List([0, 1]) - expect(setTalentPointsInTree(tree, 2, 5).toJS()).toEqual([0, 1, 5]) - }) +// it('sets points in the end of the current range', () => { +// const tree = im.List([0, 1]) +// expect(setTalentPointsInTree(tree, 2, 5).toJS()).toEqual([0, 1, 5]) +// }) - it('sets points in the middle of the current range', () => { - const tree = im.List([0, 0, 0, 0, 0, 0, 5]) - expect(setTalentPointsInTree(tree, 2, 5).toJS()).toEqual([0, 0, 5, 0, 0, 0, 5]) - }) +// it('sets points in the middle of the current range', () => { +// const tree = im.List([0, 0, 0, 0, 0, 0, 5]) +// expect(setTalentPointsInTree(tree, 2, 5).toJS()).toEqual([0, 0, 5, 0, 0, 0, 5]) +// }) - it('does not mutate the tree for points already set', () => { - const tree = im.List([0, 3, 2, 0, 5]) - expect(setTalentPointsInTree(tree, 1, 3)).toStrictEqual(tree) - }) -}) +// it('does not mutate the tree for points already set', () => { +// const tree = im.List([0, 3, 2, 0, 5]) +// expect(setTalentPointsInTree(tree, 1, 3)).toStrictEqual(tree) +// }) +// }) -describe('getTreePointCount', () => { - it('returns proper count', () => { - const result = getTreePointCount(im.List([0, 0, 4, 5, 3, 0, 0])) - expect(result).toBe(12) - }) +// describe('getTreePointCount', () => { +// it('returns proper count', () => { +// const result = getPointsInSpec(im.List([0, 0, 4, 5, 3, 0, 0])) +// expect(result).toBe(12) +// }) - it('returns 0 for empty list', () => { - const result = getTreePointCount(im.List()) - expect(result).toBe(0) - }) -}) \ No newline at end of file +// it('returns 0 for empty list', () => { +// const result = getPointsInSpec(im.List()) +// expect(result).toBe(0) +// }) +// }) \ No newline at end of file diff --git a/src/lib/tree.ts b/src/lib/tree.ts index 1686420..e520f1e 100644 --- a/src/lib/tree.ts +++ b/src/lib/tree.ts @@ -1,91 +1,102 @@ import { List, Map, fromJS } from 'immutable' +import { talentsBySpec, talentToSpec } from '../data/talents'; -/** - * Sets proper values for a tree, filling in 0s in between. - */ -export function setTalentPointsInTree(tree: List, talentIndex: number, points: number): List { - // Ensure all values until `index` are set, otherwise set to 0 - for (let i = tree.size; i < talentIndex; i++) { - tree = tree.set(i, 0) - } - return tree.set(talentIndex, points) -} +export const MAX_POINTS = 51 /** * Returns the overall points spent in the tree. */ -export function getTreePointCount(tree: List): number { - return tree.reduce((reduction, value) => value + reduction, 0) +export function getPointsInSpec(specId: number, known: Map): number { + // TODO: Hard to test this method when referencing talents from a file. Improve this. + return Object.values(talentsBySpec[specId]).reduce((prev: number, current: TalentData) => { + return prev + known.get(current.id, 0) + }, 0) } -export function canRemovePoint() { - +export function calcAvailablePoints(known: Map): number { + return Math.max(0, MAX_POINTS - known.reduce((prev, current) => prev + current, 0)) } -export function canSetPoint() { +/** + * Returns whether a talent's other talent requirements are met. + */ +export function calcMeetsRequirements(talent: TalentData, known: Map): boolean { + if (talent.requires.length === 0) { + return true + } + return talent.requires.reduce((prev, current) => { + if (!prev) return false + return known.get(current.id, 0) >= current.qty + }, true) +} + +/** + * Adds a single talent point to the Map, if possible. + */ +export const addTalentPoint = (known: Map, talent: TalentData): Map => { + const currentPoints = known.get(talent.id, 0) -} - -export const modifyPointsInTree = (tree: List, talent: TalentData, talentIndex: number, modifier: 1 | -1): List => { - const currentPoints = tree.get(talentIndex, 0) - - // TODO: We should prevent reducing talent points on a row when it is a dependency for points already spent in the next row. + // Support for specific Talent dependency requirement. + if (talent.requires.length > 0 && !calcMeetsRequirements(talent, known)) { + return known + } + + // Spend a maximum of 51 points + if (calcAvailablePoints(known) === 0) { + return known + } - // TODO: Support for specific Talent dependency requirement. - - // TODO: Spend a maximum of 51 points - // Check we have the required amount of points spent in the tree for this talent const requiredPoints = talent.row * 5 - const spentPointCount = getTreePointCount(tree) - if (requiredPoints > spentPointCount) return tree - - let newPoints = currentPoints - if (modifier === 1) { - if (currentPoints === talent.ranks.length) return tree - newPoints = currentPoints + 1 - } else if (modifier === -1) { - if (currentPoints === 0) return tree - newPoints = currentPoints - 1 + const pointsInSpec = getPointsInSpec(talentToSpec[talent.id], known) + if (requiredPoints > pointsInSpec) { + return known } - - return setTalentPointsInTree(tree, talentIndex, newPoints) -} - -export const modifyKnownTalents = (known: Map, talent: TalentData, modifier: 1 | -1): Map => { - const currentPoints = known.get(talent.id, 0) - - // TODO: We should prevent reducing talent points on a row when it is a dependency for points already spent in the next row. - // TODO: Support for specific Talent dependency requirement. - - // TODO: Spend a maximum of 51 points - - // TODO: Check we have the required amount of points spent in the tree for this talent - const requiredPoints = talent.row * 5 - // const spentPointCount = known.get(talent.id, 0) - // if (requiredPoints > spentPointCount) return tree - - let newPoints = currentPoints - if (modifier === 1) { - if (currentPoints === talent.ranks.length) return known - newPoints = currentPoints + 1 - } else if (modifier === -1) { - if (currentPoints === 0) return known - newPoints = currentPoints - 1 + // Reached the max rank? + if (currentPoints >= talent.ranks.length) { + return known } - return newPoints === 0 - ? known.remove(talent.id) - : known.set(talent.id, newPoints) + return known.set(talent.id, currentPoints + 1) } +/** + * Removes a single talent point from the Map, if possible. + */ +export const removeTalentPoint = (known: Map, talent: TalentData): Map => { + const currentPoints = known.get(talent.id, 0) + + // TODO: We should prevent reducing talent points on a row when it is a dependency for points already spent in the next row. + + // Already no points for this talent + if (currentPoints === 0) { + return known + } + + return currentPoints === 1 + ? known.remove(talent.id) + : known.set(talent.id, currentPoints - 1) +} + +/** + * Either adds or removes a talent point based on the modifier. + */ +export const modifyTalentPoint = (known: Map, talent: TalentData, modifier: 1 | -1): Map => { + if (modifier === 1) { + return addTalentPoint(known, talent) + } else { + return removeTalentPoint(known, talent) + } +} + +// TODO export function parsePointString(str: string): List> { const list: Array = [] const trees = str.split('-') trees.map((stringForTree, index) => { - const points = stringForTree.split('').map(a => parseInt(a, 2)) + const points = stringForTree.split('').map(a => parseInt(a, 10)) list[index] = points }) diff --git a/yarn.lock b/yarn.lock index 630df66..a7dc46d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1505,6 +1505,13 @@ "@webassemblyjs/wast-parser" "1.8.5" "@xtuc/long" "4.2.2" +"@welldone-software/why-did-you-render@^3.2.1": + version "3.2.1" + resolved "http://npm.soundtrackyourbrand.com/@welldone-software%2fwhy-did-you-render/-/why-did-you-render-3.2.1.tgz#9dc6fd8f8cb1640fbd386694290dbf9244a3a354" + integrity sha512-7rCVpFyE5Pnm0qyO8ByWfiFAKONvq6GAUUFuGjdJiOXnsAokdotu5EJ6VDBraV1I7UiVj9+TQRbwvrfsFKU0sw== + dependencies: + lodash "^4" + "@xtuc/ieee754@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" @@ -2498,6 +2505,11 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" +classnames@^2.2.6: + version "2.2.6" + resolved "http://npm.soundtrackyourbrand.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" + integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q== + clean-css@4.2.x: version "4.2.1" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.1.tgz#2d411ef76b8569b6d0c84068dabe85b0aa5e5c17" @@ -6070,7 +6082,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== -lodash@^4.0.0, lodash@~4.17.10: +lodash@^4, lodash@^4.0.0, lodash@~4.17.10: version "4.17.14" resolved "http://npm.soundtrackyourbrand.com/lodash/-/lodash-4.17.14.tgz#9ce487ae66c96254fe20b599f21b6816028078ba" integrity sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==