diff --git a/TODO.md b/TODO.md index 05442f3..30c4050 100644 --- a/TODO.md +++ b/TODO.md @@ -1,11 +1,14 @@ # TODO -- [ ] Add redux -- [ ] Talent tooltips -- [ ] Generate URL for chosen talents -- [ ] Responsive on mobile -- [ ] Prettier talent frames -- [ ] Prettier icon frames & coloring +- [ ] General: Add redux +- [ ] General: Talent tooltips +- [ ] General: Responsive on mobile +- [ ] System: Generate URL for chosen talents +- [ ] Talent tree: Prettier talent frames +- [ ] Talent tree: Downward arrow for dependencies +- [ ] Talent tree: Colour markings on icons +- [ ] Talent tree: Reset button per tree (?) +- [x] Prettier icon frames - [x] Pretty ClassPicker - [x] Add react-router - [x] Prevent reducing talent points on a row when it is a dependency for points already spent in the next row diff --git a/public/images/down.png b/public/images/down.png new file mode 100644 index 0000000..19a59ae Binary files /dev/null and b/public/images/down.png differ diff --git a/public/images/down2.png b/public/images/down2.png new file mode 100644 index 0000000..8419744 Binary files /dev/null and b/public/images/down2.png differ diff --git a/public/images/icons/large/default.png b/public/images/icons/large/default.png new file mode 100644 index 0000000..5a2221a Binary files /dev/null and b/public/images/icons/large/default.png differ diff --git a/public/images/icons/large/gold.png b/public/images/icons/large/gold.png new file mode 100644 index 0000000..eba088a Binary files /dev/null and b/public/images/icons/large/gold.png differ diff --git a/public/images/icons/medium/default.png b/public/images/icons/medium/default.png new file mode 100644 index 0000000..9d91ac9 Binary files /dev/null and b/public/images/icons/medium/default.png differ diff --git a/public/images/icons/medium/gold.png b/public/images/icons/medium/gold.png new file mode 100644 index 0000000..bafc206 Binary files /dev/null and b/public/images/icons/medium/gold.png differ diff --git a/public/images/tooltip-background.png b/public/images/tooltip-background.png new file mode 100644 index 0000000..806edf0 Binary files /dev/null and b/public/images/tooltip-background.png differ diff --git a/src/App.scss b/src/App.scss index 55eba5e..870190a 100644 --- a/src/App.scss +++ b/src/App.scss @@ -1,5 +1,6 @@ body { background-color: #111; + font-family: Verdana; } .calculator { @@ -19,6 +20,7 @@ body { min-width: 300px; color: white; margin-right: 1em; + background-color: #111; &:last-child { margin-right: 0; @@ -26,6 +28,11 @@ body { &__header { text-align: center; + + h3 { + margin-top: .75em; + margin-bottom: .75em; + } } &__body { @@ -40,10 +47,14 @@ body { .class-picker { display: flex; justify-content: center; + list-style: none; + margin-top: 2em; + margin-bottom: 2em; &__class { - margin-right: 2em; + margin-right: 1em; opacity: .8; + transition: all .1s ease-out; &:hover { opacity: 1; @@ -57,7 +68,7 @@ body { opacity: .4; &:hover { - opacity: .6; + opacity: .5; } } } diff --git a/src/App.tsx b/src/App.tsx index f78a301..9cf2b7d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,7 +7,7 @@ const App: React.FC = () => { return (
- +
); diff --git a/src/components/Calculator.tsx b/src/components/Calculator.tsx index 21295f8..6255931 100644 --- a/src/components/Calculator.tsx +++ b/src/components/Calculator.tsx @@ -13,9 +13,9 @@ import { History } from 'history' interface Props { selectedClass: string history: History + initialTalents?: Map } -// const EMPTY_TALENTS = Map() const EMPTY_TALENTS = Map() // .set(30, 5) // .set(26, 5) @@ -33,6 +33,13 @@ export class Calculator extends React.PureComponent { knownTalents: EMPTY_TALENTS } + componentDidMount() { + if (this.props.initialTalents) { + this.setState({ knownTalents: this.props.initialTalents }) + this.updateURL(this.props.initialTalents) + } + } + componentDidUpdate(prevProps: Props) { if (prevProps.selectedClass !== this.props.selectedClass) { this.setState({ @@ -40,17 +47,22 @@ export class Calculator extends React.PureComponent { }) } } + + updateURL(knownTalents: Map) { + const { selectedClass } = this.props + const pointString = encodeKnownTalents(knownTalents, selectedClass) + this.props.history.replace(`/${selectedClass}` + (pointString ? `/${pointString}` : '')) + } handleTalentPress = (specId: number, talentId: number, modifier: 1 | -1) => { - const { selectedClass } = this.props const talent = talentsBySpec[specId][talentId] console.log('Clicked talent: ' + talentId) const newKnownTalents = modifyTalentPoint(this.state.knownTalents, talent, modifier) + if (newKnownTalents !== this.state.knownTalents) { + this.updateURL(newKnownTalents) + } this.setState({ knownTalents: newKnownTalents }) - - const pointString = encodeKnownTalents(newKnownTalents, selectedClass) - this.props.history.replace(`/${selectedClass}` + (pointString ? `/${pointString}` : '')) } render() { @@ -77,6 +89,11 @@ export class Calculator extends React.PureComponent {
Points: {availablePoints}
+ + ) } diff --git a/src/components/ClassPicker.tsx b/src/components/ClassPicker.tsx index 0bc3b57..f22ebf8 100644 --- a/src/components/ClassPicker.tsx +++ b/src/components/ClassPicker.tsx @@ -29,7 +29,10 @@ export class ClassPicker extends React.PureComponent { {Object.values(classByName).map((c) =>
  • - +
  • )} diff --git a/src/components/Icon.scss b/src/components/Icon.scss index 7758797..e7d6210 100644 --- a/src/components/Icon.scss +++ b/src/components/Icon.scss @@ -1,10 +1,49 @@ .icon { + position: relative; background-position: center; background-repeat: no-repeat; - background-size: cover; + background-size: contain; + background-color: #222; + &:hover { + .icon__bg { + box-shadow: inset 0px 0px 6px 3px rgba(99, 150, 214, .8); + } + } + &--medium { width: 40px; height: 40px; + border-radius: 5px; + + .icon__bg { + width: 36px; + height: 36px; + top: 2px; + left: 2px; + } + } + + &--golden { + .icon__frame { + background-image: url('/images/icons/large/gold.png'); + } + } + + &__bg { + position: absolute; + background-size: cover; + border-radius: 5px; + } + + &__frame { + position: absolute; + width: 44px; + height: 44px; + top: -2px; + left: -2px; + background-image: url('/images/icons/large/default.png'); + background-repeat: no-repeat; + background-size: contain; } } \ No newline at end of file diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx index efa7a60..d5b6986 100644 --- a/src/components/Icon.tsx +++ b/src/components/Icon.tsx @@ -1,17 +1,30 @@ import React, { FC } from 'react' +import classNames from 'classnames' import './Icon.scss' interface Props { name: string size?: 'small' | 'medium' | 'large' + golden?: boolean } -export const Icon: FC = ({ name, size = 'medium', children }) => { - const url = `https://wow.zamimg.com/images/wow/icons/${size}/${name}.jpg` - const className = `icon icon--${size}` +export const Icon: FC = ({ name, size = 'medium', golden = false, children }) => { + const className = classNames( + 'icon', + `icon--${size}`, { + 'icon--golden': golden + } + ) + + const bgSize = size === 'medium' ? 'large' : 'medium' + const bgStyle = { + backgroundImage: `url(https://wow.zamimg.com/images/wow/icons/${bgSize}/${name}.jpg)` + } return ( -
    +
    +
    +
    {children}
    ) diff --git a/src/components/IndexRoute.tsx b/src/components/IndexRoute.tsx index 765aaa7..da63762 100644 --- a/src/components/IndexRoute.tsx +++ b/src/components/IndexRoute.tsx @@ -3,6 +3,8 @@ import { Calculator } from './Calculator' import { ClassPicker } from './ClassPicker' import { match } from 'react-router-dom' import { RouteComponentProps } from 'react-router' +import { decodeKnownTalents } from '../lib/tree' +import { classByName } from '../data/classes' interface Props extends RouteComponentProps { match: match<{ @@ -14,16 +16,28 @@ interface Props extends RouteComponentProps { export class IndexRoute extends React.PureComponent { static whyDidYouRender = true + componentDidMount() { + const { selectedClass } = this.props.match.params + if (selectedClass && !classByName[selectedClass]) { + this.props.history.replace('/') + } + } + render() { const { match, history } = this.props const { selectedClass, pointString } = match.params - + + if (selectedClass && !classByName[selectedClass]) { + return null + } + return (
    {selectedClass && diff --git a/src/components/TalentSlot.scss b/src/components/TalentSlot.scss index 7cd8cb7..457508f 100644 --- a/src/components/TalentSlot.scss +++ b/src/components/TalentSlot.scss @@ -1,66 +1,102 @@ +$row-offset: 30px; $row-distance: 70px; -@mixin rowStyle($rowNr) { - top: 30px + (($rowNr) * 70px); -} +$col-offset: 44px; +$col-distance: 56px; + +$color-yellow: #ffd100; +$color-green: #1eff00; +$color-dark-green: #40bf40; +$color-subtle: #9d9d9d; .talent { position: absolute; width: 40px; height: 40px; - border: 1px solid black; - border-radius: 2px; + border-radius: 5px; + transition: filter .1s linear; + filter: none; + cursor: pointer; - &:after { - content: ""; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 10; - } + &--available { + .talent__status::after { + // background-color: rgba($color-green, .8); + box-shadow: inset 0px 0px 6px 3px rgba($color-green, .8); + } - small { - font-size: 8px; - line-height: 1em; - } - - &--disabled { - filter: grayscale(100%) - } - - &:not(&--disabled) { - cursor: pointer; - } - - &--maxed { - - } - - @for $i from 0 through 6 { - &[data-row="#{$i}"] { - @include rowStyle($i) + .point-label { + color: $color-green; } } + &--maxed { + .talent__status::after { + box-shadow: inset 0px 0px 6px 3px rgba($color-yellow, .8); + } - $col-distance-offset: 44px; - $col-distance: 56px; + .point-label { + color: $color-yellow; + } + } - &[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); } + &--disabled { + filter: grayscale(100%); - &__points { + .talent__status { + opacity: .7; + } + } + + // Rows + @for $i from 0 through 6 { + &[data-row="#{$i}"] { + top: $row-offset + (($i) * $row-distance); + } + } + + // Columns + @for $i from 0 through 3 { + &[data-col="#{$i}"] { + left: $col-offset + ($col-distance * $i); + } + } + + &__status { position: absolute; - padding: 1px 2px; - bottom: -2px; - right: -2px; + width: 48px; + height: 46px; + bottom: -1px; + left: -4px; + background-image: url('/images/icons/large/default.png'); + background-size: cover; + + &:after { + content: ''; + position: absolute; + width: 44px; + height: 44px; + top: 2px; + left: 2px; + border-radius: 5px; + } + } +} + +.point-label { + position: absolute; + bottom: -5px; + right: -5px; + min-width: 7px; + text-align: center; + padding: 1px 3px; + color: #999; + font-size: 11px; + font-family: Arial, Helvetica, sans-serif; + background: #111; + border-radius: 4px; + user-select: none; + + &--enabled { 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 6707bbe..802f7aa 100644 --- a/src/components/TalentSlot.tsx +++ b/src/components/TalentSlot.tsx @@ -37,9 +37,14 @@ export const TalentSlot: FC = (props) => { const showPoints = points > 0 || availablePoints > 0 const disabled = props.disabled || !showPoints || !isAvailable(talent, specId, knownTalents) - const cn = classNames('talent', { + const containerClassNames = classNames('talent', { 'talent--disabled': !!disabled, - 'talent--maxed': points >= talent.ranks.length + 'talent--available': !disabled && points < talent.ranks.length, + 'talent--maxed': points >= talent.ranks.length || (points > 0 && availablePoints === 0) + }) + + const pointsClassNames = classNames('point-label', { + 'point-label--enabled': !disabled }) const handleContextMenu = (e) => { @@ -50,17 +55,21 @@ export const TalentSlot: FC = (props) => { return (
    props.onClick(talent.id) : () => {}} onContextMenu={handleContextMenu} > - +
    + - {showPoints && -
    {points}/{talent.ranks.length}
    + {showPoints && !disabled && +
    + {points} + /{talent.ranks.length} +
    }
    ) diff --git a/src/lib/tree.ts b/src/lib/tree.ts index 51ab682..efc7483 100644 --- a/src/lib/tree.ts +++ b/src/lib/tree.ts @@ -9,6 +9,13 @@ import { classByName } from '../data/classes' export const MAX_POINTS = 51 export const MAX_ROWS = 7 +export const SORT_TALENTS = (a: TalentData, b: TalentData) => { + if (a.row === b.row) { + return a.col - b.col + } + return a.row - b.row +} + /** * Returns the overall points spent in the tree. */ @@ -76,6 +83,7 @@ export const removeTalentPoint = (known: Map, talent: TalentData // No points to reduce for this talent if (currentPoints === 0) { + console.warn('no points to reduce') return known } @@ -85,8 +93,11 @@ export const removeTalentPoint = (known: Map, talent: TalentData known.forEach((points, talentId) => { const t = talentsBySpec[specId][talentId] - if (t) { + if (t && points > 0) { isDependency = isDependency || t.requires.some((req) => req.id === talent.id) + if (t.row > highestRow) { + console.info('new highest row:', t) + } highestRow = t.row > highestRow ? t.row : highestRow for (let row = t.row; row < MAX_ROWS; row++) { cumulativePointsPerRow[row] = (cumulativePointsPerRow[row] || 0) + points @@ -98,11 +109,18 @@ export const removeTalentPoint = (known: Map, talent: TalentData const pointsUntilHighestRow = cumulativePointsPerRow[highestRow - 1] const targetPointsHighestRow = highestRow * 5 if (talent.row < highestRow && pointsUntilHighestRow - 1 < targetPointsHighestRow) { + console.warn('would not break the requirements for talents spent in later rows', { + talent, + highestRow, + pointsUntilHighestRow, + targetPointsHighestRow + }) return known } // Prevent if another talent depends on this if (isDependency) { + console.warn('is dependency') return known } @@ -143,12 +161,7 @@ export function encodeKnownTalents(known: Map, className: string const { specs } = classByName[className] for (let i = 0; i < specs.length; i++) { const specId = specs[i] - const talents = talentsBySpecArray[specId].sort((a, b) => { - if (a.row === b.row) { - return a.col - b.col - } - return a.row - b.row - }) + const talents = talentsBySpecArray[specId].sort(SORT_TALENTS) string += i > 0 ? '-' : '' string += removeTrailingCharacters( talents.map((talent) => known.get(talent.id, 0)).join(''), @@ -162,7 +175,36 @@ export function encodeKnownTalents(known: Map, className: string * Decodes a string of points into a Map of talents. */ export function decodeKnownTalents(pointString: string, className: string): Map { - return Map() + console.log(pointString, className) + + const { specs } = classByName[className] + let known = Map() + + // TODO: Make sure we validate the point string + const parts = pointString.split('-') + for (let i = 0; i < parts.length; i++) { + const specId = specs[i] + const specPointStr = parts[i] + console.log(specPointStr, { specId }) + const talents = talentsBySpecArray[specId].sort(SORT_TALENTS) + + for (let y = 0; y < specPointStr.length; y++) { + const talent = talents[y] + const points = parseInt(specPointStr[y], 10) + + // Validation: break out loop if there's more points in the string than this talent can have + if (points > talent.ranks.length) { + break + } + + if (points > 0) { + console.log(`Spent ${points} in ${talent.id}`) + known = known.set(talent.id, points) + } + } + } + + return known } /**