Add talent decoding and spiffing up icon frames

This commit is contained in:
Melvin Valster
2019-07-22 21:16:55 +02:00
parent 2f99089377
commit 9d91bc32fb
18 changed files with 269 additions and 82 deletions
+9 -6
View File
@@ -1,11 +1,14 @@
# TODO # TODO
- [ ] Add redux - [ ] General: Add redux
- [ ] Talent tooltips - [ ] General: Talent tooltips
- [ ] Generate URL for chosen talents - [ ] General: Responsive on mobile
- [ ] Responsive on mobile - [ ] System: Generate URL for chosen talents
- [ ] Prettier talent frames - [ ] Talent tree: Prettier talent frames
- [ ] Prettier icon frames & coloring - [ ] 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] Pretty ClassPicker
- [x] Add react-router - [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 - [x] Prevent reducing talent points on a row when it is a dependency for points already spent in the next row
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 784 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 789 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

+13 -2
View File
@@ -1,5 +1,6 @@
body { body {
background-color: #111; background-color: #111;
font-family: Verdana;
} }
.calculator { .calculator {
@@ -19,6 +20,7 @@ body {
min-width: 300px; min-width: 300px;
color: white; color: white;
margin-right: 1em; margin-right: 1em;
background-color: #111;
&:last-child { &:last-child {
margin-right: 0; margin-right: 0;
@@ -26,6 +28,11 @@ body {
&__header { &__header {
text-align: center; text-align: center;
h3 {
margin-top: .75em;
margin-bottom: .75em;
}
} }
&__body { &__body {
@@ -40,10 +47,14 @@ body {
.class-picker { .class-picker {
display: flex; display: flex;
justify-content: center; justify-content: center;
list-style: none;
margin-top: 2em;
margin-bottom: 2em;
&__class { &__class {
margin-right: 2em; margin-right: 1em;
opacity: .8; opacity: .8;
transition: all .1s ease-out;
&:hover { &:hover {
opacity: 1; opacity: 1;
@@ -57,7 +68,7 @@ body {
opacity: .4; opacity: .4;
&:hover { &:hover {
opacity: .6; opacity: .5;
} }
} }
} }
+1 -1
View File
@@ -7,7 +7,7 @@ const App: React.FC = () => {
return ( return (
<Router> <Router>
<div className="App"> <div className="App">
<Route path="/:selectedClass?/:points?" component={IndexRoute} /> <Route path="/:selectedClass?/:pointString?" component={IndexRoute} />
</div> </div>
</Router> </Router>
); );
+22 -5
View File
@@ -13,9 +13,9 @@ import { History } from 'history'
interface Props { interface Props {
selectedClass: string selectedClass: string
history: History history: History
initialTalents?: Map<number, number>
} }
// const EMPTY_TALENTS = Map<number, number>()
const EMPTY_TALENTS = Map<number, number>() const EMPTY_TALENTS = Map<number, number>()
// .set(30, 5) // .set(30, 5)
// .set(26, 5) // .set(26, 5)
@@ -33,6 +33,13 @@ export class Calculator extends React.PureComponent<Props> {
knownTalents: EMPTY_TALENTS knownTalents: EMPTY_TALENTS
} }
componentDidMount() {
if (this.props.initialTalents) {
this.setState({ knownTalents: this.props.initialTalents })
this.updateURL(this.props.initialTalents)
}
}
componentDidUpdate(prevProps: Props) { componentDidUpdate(prevProps: Props) {
if (prevProps.selectedClass !== this.props.selectedClass) { if (prevProps.selectedClass !== this.props.selectedClass) {
this.setState({ this.setState({
@@ -41,16 +48,21 @@ export class Calculator extends React.PureComponent<Props> {
} }
} }
handleTalentPress = (specId: number, talentId: number, modifier: 1 | -1) => { updateURL(knownTalents: Map<number, number>) {
const { selectedClass } = this.props 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 talent = talentsBySpec[specId][talentId] const talent = talentsBySpec[specId][talentId]
console.log('Clicked talent: ' + talentId) console.log('Clicked talent: ' + talentId)
const newKnownTalents = modifyTalentPoint(this.state.knownTalents, talent, modifier) const newKnownTalents = modifyTalentPoint(this.state.knownTalents, talent, modifier)
if (newKnownTalents !== this.state.knownTalents) {
this.updateURL(newKnownTalents)
}
this.setState({ knownTalents: newKnownTalents }) this.setState({ knownTalents: newKnownTalents })
const pointString = encodeKnownTalents(newKnownTalents, selectedClass)
this.props.history.replace(`/${selectedClass}` + (pointString ? `/${pointString}` : ''))
} }
render() { render() {
@@ -77,6 +89,11 @@ export class Calculator extends React.PureComponent<Props> {
<div className="calculator__points"> <div className="calculator__points">
Points: {availablePoints} Points: {availablePoints}
</div> </div>
<div>
<a href="/shaman/-5505000055523051-55">Shaman test</a>
<a href="/shaman/-5595000055523051-55">Shaman test broken</a>
</div>
</div> </div>
) )
} }
+4 -1
View File
@@ -29,7 +29,10 @@ export class ClassPicker extends React.PureComponent<Props> {
{Object.values(classByName).map((c) => {Object.values(classByName).map((c) =>
<li key={c.id} className={classNameForItem(c, selected)}> <li key={c.id} className={classNameForItem(c, selected)}>
<Link to={`/${c.name.toLowerCase()}`} title={c.name}> <Link to={`/${c.name.toLowerCase()}`} title={c.name}>
<Icon name={c.icon} /> <Icon
name={c.icon}
golden={selected === c.name.toLowerCase()}
/>
</Link> </Link>
</li> </li>
)} )}
+40 -1
View File
@@ -1,10 +1,49 @@
.icon { .icon {
position: relative;
background-position: center; background-position: center;
background-repeat: no-repeat; 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 { &--medium {
width: 40px; width: 40px;
height: 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;
} }
} }
+17 -4
View File
@@ -1,17 +1,30 @@
import React, { FC } from 'react' import React, { FC } from 'react'
import classNames from 'classnames'
import './Icon.scss' import './Icon.scss'
interface Props { interface Props {
name: string name: string
size?: 'small' | 'medium' | 'large' size?: 'small' | 'medium' | 'large'
golden?: boolean
} }
export const Icon: FC<Props> = ({ name, size = 'medium', children }) => { export const Icon: FC<Props> = ({ name, size = 'medium', golden = false, children }) => {
const url = `https://wow.zamimg.com/images/wow/icons/${size}/${name}.jpg` const className = classNames(
const className = `icon icon--${size}` '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 ( return (
<div className={className} style={{ backgroundImage: `url(${url})`}}> <div className={className}>
<div className="icon__bg" style={bgStyle} />
<div className="icon__frame" />
{children} {children}
</div> </div>
) )
+14
View File
@@ -3,6 +3,8 @@ import { Calculator } from './Calculator'
import { ClassPicker } from './ClassPicker' import { ClassPicker } from './ClassPicker'
import { match } from 'react-router-dom' import { match } from 'react-router-dom'
import { RouteComponentProps } from 'react-router' import { RouteComponentProps } from 'react-router'
import { decodeKnownTalents } from '../lib/tree'
import { classByName } from '../data/classes'
interface Props extends RouteComponentProps { interface Props extends RouteComponentProps {
match: match<{ match: match<{
@@ -14,16 +16,28 @@ interface Props extends RouteComponentProps {
export class IndexRoute extends React.PureComponent<Props> { export class IndexRoute extends React.PureComponent<Props> {
static whyDidYouRender = true static whyDidYouRender = true
componentDidMount() {
const { selectedClass } = this.props.match.params
if (selectedClass && !classByName[selectedClass]) {
this.props.history.replace('/')
}
}
render() { render() {
const { match, history } = this.props const { match, history } = this.props
const { selectedClass, pointString } = match.params const { selectedClass, pointString } = match.params
if (selectedClass && !classByName[selectedClass]) {
return null
}
return ( return (
<div className="index"> <div className="index">
<ClassPicker selected={selectedClass} /> <ClassPicker selected={selectedClass} />
{selectedClass && {selectedClass &&
<Calculator <Calculator
initialTalents={pointString && decodeKnownTalents(pointString, selectedClass)}
selectedClass={selectedClass} selectedClass={selectedClass}
history={history} history={history}
/> />
+79 -43
View File
@@ -1,66 +1,102 @@
$row-offset: 30px;
$row-distance: 70px; $row-distance: 70px;
@mixin rowStyle($rowNr) { $col-offset: 44px;
top: 30px + (($rowNr) * 70px); $col-distance: 56px;
}
$color-yellow: #ffd100;
$color-green: #1eff00;
$color-dark-green: #40bf40;
$color-subtle: #9d9d9d;
.talent { .talent {
position: absolute; position: absolute;
width: 40px; width: 40px;
height: 40px; height: 40px;
border: 1px solid black; border-radius: 5px;
border-radius: 2px; transition: filter .1s linear;
filter: none;
&: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; cursor: pointer;
&--available {
.talent__status::after {
// background-color: rgba($color-green, .8);
box-shadow: inset 0px 0px 6px 3px rgba($color-green, .8);
}
.point-label {
color: $color-green;
}
} }
&--maxed { &--maxed {
.talent__status::after {
box-shadow: inset 0px 0px 6px 3px rgba($color-yellow, .8);
} }
.point-label {
color: $color-yellow;
}
}
&--disabled {
filter: grayscale(100%);
.talent__status {
opacity: .7;
}
}
// Rows
@for $i from 0 through 6 { @for $i from 0 through 6 {
&[data-row="#{$i}"] { &[data-row="#{$i}"] {
@include rowStyle($i) top: $row-offset + (($i) * $row-distance);
} }
} }
// Columns
@for $i from 0 through 3 {
&[data-col="#{$i}"] {
left: $col-offset + ($col-distance * $i);
}
}
$col-distance-offset: 44px; &__status {
$col-distance: 56px;
&[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; position: absolute;
padding: 1px 2px; width: 48px;
bottom: -2px; height: 46px;
right: -2px; 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; color: white;
font-size: 12px;
background: black;
border-radius: 2px;
} }
} }
+15 -6
View File
@@ -37,9 +37,14 @@ export const TalentSlot: FC<Props> = (props) => {
const showPoints = points > 0 || availablePoints > 0 const showPoints = points > 0 || availablePoints > 0
const disabled = props.disabled || !showPoints || !isAvailable(talent, specId, knownTalents) const disabled = props.disabled || !showPoints || !isAvailable(talent, specId, knownTalents)
const cn = classNames('talent', { const containerClassNames = classNames('talent', {
'talent--disabled': !!disabled, '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) => { const handleContextMenu = (e) => {
@@ -50,17 +55,21 @@ export const TalentSlot: FC<Props> = (props) => {
return ( return (
<div <div
className={cn} className={containerClassNames}
title={talent.ranks[0].toString()} title={talent.ranks[0].toString()}
data-row={talent.row} data-row={talent.row}
data-col={talent.col} data-col={talent.col}
onClick={!disabled ? () => props.onClick(talent.id) : () => {}} onClick={!disabled ? () => props.onClick(talent.id) : () => {}}
onContextMenu={handleContextMenu} onContextMenu={handleContextMenu}
> >
<Icon name={talent.icon} /> <div className="talent__status" />
<Icon name={talent.icon} size="medium" />
{showPoints && {showPoints && !disabled &&
<div className="talent__points">{points}/{talent.ranks.length}</div> <div className={pointsClassNames}>
{points}
/{talent.ranks.length}
</div>
} }
</div> </div>
) )
+50 -8
View File
@@ -9,6 +9,13 @@ import { classByName } from '../data/classes'
export const MAX_POINTS = 51 export const MAX_POINTS = 51
export const MAX_ROWS = 7 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. * 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 // No points to reduce for this talent
if (currentPoints === 0) { if (currentPoints === 0) {
console.warn('no points to reduce')
return known return known
} }
@@ -85,8 +93,11 @@ export const removeTalentPoint = (known: Map<number, number>, talent: TalentData
known.forEach((points, talentId) => { known.forEach((points, talentId) => {
const t = talentsBySpec[specId][talentId] const t = talentsBySpec[specId][talentId]
if (t) { if (t && points > 0) {
isDependency = isDependency || t.requires.some((req) => req.id === talent.id) 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 highestRow = t.row > highestRow ? t.row : highestRow
for (let row = t.row; row < MAX_ROWS; row++) { for (let row = t.row; row < MAX_ROWS; row++) {
cumulativePointsPerRow[row] = (cumulativePointsPerRow[row] || 0) + points 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 pointsUntilHighestRow = cumulativePointsPerRow[highestRow - 1]
const targetPointsHighestRow = highestRow * 5 const targetPointsHighestRow = highestRow * 5
if (talent.row < highestRow && pointsUntilHighestRow - 1 < targetPointsHighestRow) { 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 return known
} }
// Prevent if another talent depends on this // Prevent if another talent depends on this
if (isDependency) { if (isDependency) {
console.warn('is dependency')
return known return known
} }
@@ -143,12 +161,7 @@ export function encodeKnownTalents(known: Map<number, number>, className: string
const { specs } = classByName[className] const { specs } = classByName[className]
for (let i = 0; i < specs.length; i++) { for (let i = 0; i < specs.length; i++) {
const specId = specs[i] const specId = specs[i]
const talents = talentsBySpecArray[specId].sort((a, b) => { const talents = talentsBySpecArray[specId].sort(SORT_TALENTS)
if (a.row === b.row) {
return a.col - b.col
}
return a.row - b.row
})
string += i > 0 ? '-' : '' string += i > 0 ? '-' : ''
string += removeTrailingCharacters( string += removeTrailingCharacters(
talents.map((talent) => known.get(talent.id, 0)).join(''), 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. * Decodes a string of points into a Map of talents.
*/ */
export function decodeKnownTalents(pointString: string, className: string): Map<number, number> { 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
} }
/** /**