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
+13 -2
View File
@@ -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;
}
}
}
+1 -1
View File
@@ -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>
);
+22 -5
View File
@@ -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>
)
}
+4 -1
View File
@@ -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>
)}
+40 -1
View File
@@ -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;
}
}
+17 -4
View File
@@ -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>
)
+15 -1
View File
@@ -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}
/>
+83 -47
View File
@@ -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;
}
}
+15 -6
View File
@@ -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>
)
+50 -8
View File
@@ -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
}
/**