Add talent decoding and spiffing up icon frames
@@ -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
|
||||
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 784 B |
|
After Width: | Height: | Size: 789 B |
|
After Width: | Height: | Size: 5.9 KiB |
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ const App: React.FC = () => {
|
||||
return (
|
||||
<Router>
|
||||
<div className="App">
|
||||
<Route path="/:selectedClass?/:points?" component={IndexRoute} />
|
||||
<Route path="/:selectedClass?/:pointString?" component={IndexRoute} />
|
||||
</div>
|
||||
</Router>
|
||||
);
|
||||
|
||||
@@ -13,9 +13,9 @@ import { History } from 'history'
|
||||
interface Props {
|
||||
selectedClass: string
|
||||
history: History
|
||||
initialTalents?: Map<number, number>
|
||||
}
|
||||
|
||||
// const EMPTY_TALENTS = Map<number, number>()
|
||||
const EMPTY_TALENTS = Map<number, number>()
|
||||
// .set(30, 5)
|
||||
// .set(26, 5)
|
||||
@@ -33,6 +33,13 @@ export class Calculator extends React.PureComponent<Props> {
|
||||
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<Props> {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
updateURL(knownTalents: Map<number, number>) {
|
||||
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<Props> {
|
||||
<div className="calculator__points">
|
||||
Points: {availablePoints}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<a href="/shaman/-5505000055523051-55">Shaman test</a>
|
||||
<a href="/shaman/-5595000055523051-55">Shaman test broken</a>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -29,7 +29,10 @@ export class ClassPicker extends React.PureComponent<Props> {
|
||||
{Object.values(classByName).map((c) =>
|
||||
<li key={c.id} className={classNameForItem(c, selected)}>
|
||||
<Link to={`/${c.name.toLowerCase()}`} title={c.name}>
|
||||
<Icon name={c.icon} />
|
||||
<Icon
|
||||
name={c.icon}
|
||||
golden={selected === c.name.toLowerCase()}
|
||||
/>
|
||||
</Link>
|
||||
</li>
|
||||
)}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<Props> = ({ 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<Props> = ({ 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 (
|
||||
<div className={className} style={{ backgroundImage: `url(${url})`}}>
|
||||
<div className={className}>
|
||||
<div className="icon__bg" style={bgStyle} />
|
||||
<div className="icon__frame" />
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -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<Props> {
|
||||
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 (
|
||||
<div className="index">
|
||||
<ClassPicker selected={selectedClass} />
|
||||
|
||||
{selectedClass &&
|
||||
<Calculator
|
||||
initialTalents={pointString && decodeKnownTalents(pointString, selectedClass)}
|
||||
selectedClass={selectedClass}
|
||||
history={history}
|
||||
/>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -37,9 +37,14 @@ export const TalentSlot: FC<Props> = (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> = (props) => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn}
|
||||
className={containerClassNames}
|
||||
title={talent.ranks[0].toString()}
|
||||
data-row={talent.row}
|
||||
data-col={talent.col}
|
||||
onClick={!disabled ? () => props.onClick(talent.id) : () => {}}
|
||||
onContextMenu={handleContextMenu}
|
||||
>
|
||||
<Icon name={talent.icon} />
|
||||
<div className="talent__status" />
|
||||
<Icon name={talent.icon} size="medium" />
|
||||
|
||||
{showPoints &&
|
||||
<div className="talent__points">{points}/{talent.ranks.length}</div>
|
||||
{showPoints && !disabled &&
|
||||
<div className={pointsClassNames}>
|
||||
{points}
|
||||
/{talent.ranks.length}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -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<number, number>, 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<number, number>, 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<number, number>, 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<number, number>, 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<number, number>, className: string
|
||||
* Decodes a string of points into a Map of talents.
|
||||
*/
|
||||
export function decodeKnownTalents(pointString: string, className: string): Map<number, number> {
|
||||
return Map()
|
||||
console.log(pointString, className)
|
||||
|
||||
const { specs } = classByName[className]
|
||||
let known = Map<number, number>()
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||