Add tests for canLearnTalent and canUnlearnTalent
This commit is contained in:
+1
-1
@@ -55,7 +55,7 @@ body {
|
||||
|
||||
&__class {
|
||||
margin-right: 1em;
|
||||
opacity: .8;
|
||||
opacity: 1;
|
||||
transition: all .1s ease-out;
|
||||
|
||||
&:hover {
|
||||
|
||||
+2
-1
@@ -5,7 +5,8 @@ import { BrowserRouter as Router, Route } from 'react-router-dom'
|
||||
|
||||
const App: React.FC = () => {
|
||||
return (
|
||||
<Router basename={process.env.NODE_ENV !== 'development' ? '%PUBLIC_URL%' : ''}>
|
||||
<Router basename={process.env.NODE_ENV === 'production' ? '/wow-talent-calculator' : ''}>
|
||||
{/* <Router basename={process.env.NODE_ENV !== 'development' ? '%PUBLIC_URL%' : ''}> */}
|
||||
<div className="App">
|
||||
<Route path="/:selectedClass?/:pointString?" component={IndexRoute} />
|
||||
</div>
|
||||
|
||||
+47
-11
@@ -1,5 +1,21 @@
|
||||
@import "../sass/config";
|
||||
|
||||
@function baseRowTopOffset($row) {
|
||||
@return $row-offset + ($row * $row-distance);
|
||||
}
|
||||
|
||||
@function calcLeftOffset($col) {
|
||||
@return $col-offset + ($col-gutter * ($col - 1)) + $icon-size;
|
||||
}
|
||||
|
||||
@function calcRightOffset($col) {
|
||||
@return $col-offset + ($col-distance * $col) + $icon-size;
|
||||
}
|
||||
|
||||
@function calcArrowHeight($length) {
|
||||
@return 2px + ($row-offset * $length) + ($icon-size * ($length - 1))
|
||||
}
|
||||
|
||||
.arrow {
|
||||
$arrow-width: 15px;
|
||||
position: absolute;
|
||||
@@ -11,7 +27,7 @@
|
||||
// Rows
|
||||
@for $i from 0 through 6 {
|
||||
&[data-row="#{$i}"] {
|
||||
top: 12px + $row-offset + (($i) * $row-distance);
|
||||
top: 12px + baseRowTopOffset($i);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,50 +40,50 @@
|
||||
}
|
||||
|
||||
&--right {
|
||||
background-image: url('/images/arrows/right.png');
|
||||
background-image: url('../images/arrows/right.png');
|
||||
background-position: center right;
|
||||
|
||||
&.arrow--active {
|
||||
background-image: url('/images/arrows/right-active.png');
|
||||
background-image: url('../images/arrows/right-active.png');
|
||||
}
|
||||
|
||||
// Cols
|
||||
@for $i from 0 through 3 {
|
||||
&[data-col="#{$i}"] {
|
||||
left: 3px + $col-offset + ($col-distance * $i) + $icon-size;
|
||||
left: 3px + calcRightOffset($i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--left {
|
||||
background-image: url('/images/arrows/left.png');
|
||||
background-image: url('../images/arrows/left.png');
|
||||
background-position: center left;
|
||||
|
||||
&.arrow--active {
|
||||
background-image: url('/images/arrows/left-active.png');
|
||||
background-image: url('../images/arrows/left-active.png');
|
||||
}
|
||||
|
||||
// Cols
|
||||
@for $i from 0 through 3 {
|
||||
&[data-col="#{$i}"] {
|
||||
left: -3px + $col-offset + ($col-gutter * ($i - 1)) + $icon-size;
|
||||
left: -3px + calcLeftOffset($i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--down {
|
||||
width: $arrow-width;
|
||||
background-image: url('/images/arrows/down.png');
|
||||
background-image: url('../images/arrows/down.png');
|
||||
background-position: center bottom;
|
||||
|
||||
&.arrow--active {
|
||||
background-image: url('/images/arrows/down-active.png');
|
||||
background-image: url('../images/arrows/down-active.png');
|
||||
}
|
||||
|
||||
// Rows
|
||||
@for $i from 0 through 6 {
|
||||
&[data-row="#{$i}"] {
|
||||
top: $row-offset + (($i) * $row-distance) + 40px;
|
||||
top: 40px + baseRowTopOffset($i);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,8 +97,28 @@
|
||||
// Lengths
|
||||
@for $i from 0 through 3 {
|
||||
&[data-length="#{$i}"] {
|
||||
height: 2px + ($row-offset * $i) + ($icon-size * ($i - 1));
|
||||
height: calcArrowHeight($i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--right-down {
|
||||
// Horizontal
|
||||
::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
height: $arrow-width;
|
||||
background-image: url('../images/arrows/rightdown.png');
|
||||
background-position: center right;
|
||||
}
|
||||
|
||||
// Vertical
|
||||
::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: $arrow-width;
|
||||
background-image: url('../images/arrows/down.png');
|
||||
background-position: center bottom;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,14 @@ import { TalentTree } from './TalentTree'
|
||||
import {
|
||||
modifyTalentPoint,
|
||||
calcAvailablePoints,
|
||||
encodeKnownTalents
|
||||
encodeKnownTalents,
|
||||
SORT_TALENTS_BY_SPEC
|
||||
} from '../lib/tree'
|
||||
import { talentsBySpec } from '../data/talents'
|
||||
import { talentsBySpec, talentsById } from '../data/talents'
|
||||
import { classByName } from '../data/classes'
|
||||
import { History } from 'history'
|
||||
import { spells } from '../data/spells'
|
||||
import { debugPrintKnown } from '../lib/debug'
|
||||
|
||||
interface Props {
|
||||
selectedClass: string
|
||||
@@ -56,13 +59,16 @@ export class Calculator extends React.PureComponent<Props> {
|
||||
|
||||
handleTalentPress = (specId: number, talentId: number, modifier: 1 | -1) => {
|
||||
const talent = talentsBySpec[specId][talentId]
|
||||
console.log('Clicked talent: ' + 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 })
|
||||
|
||||
// Debug
|
||||
debugPrintKnown(newKnownTalents)
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -93,7 +99,8 @@ export class Calculator extends React.PureComponent<Props> {
|
||||
<ul>
|
||||
<li><a href="/shaman/-5505000055523051-55">Shaman test</a></li>
|
||||
<li><a href="/shaman/-5595000055523051-55">Shaman test broken</a></li>
|
||||
<li><a href="/rogue/-005055-50205302332212051">Rogue can unlearn first row</a></li>
|
||||
<li><a href="/rogue/-005055-50205302332212051">Rogue (should break, does not meet requirement)</a></li>
|
||||
<li><a href="/rogue/-005055-50205302333212041">Rogue can unlearn first row AND dependency</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import './Tooltip.scss'
|
||||
import React, { FC } from 'react'
|
||||
|
||||
interface Props {
|
||||
}
|
||||
|
||||
export const Tooltip: FC<Props> = (props) => {
|
||||
return <div className="tooltip">
|
||||
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Map } from 'immutable'
|
||||
import { SORT_TALENTS_BY_SPEC } from './tree'
|
||||
import { talentsById } from '../data/talents'
|
||||
|
||||
export const debugPrintKnown = (known: Map<number, number>) => {
|
||||
const obj = {}
|
||||
known.toArray()
|
||||
.map(([talentId]) => talentsById[talentId])
|
||||
.sort(SORT_TALENTS_BY_SPEC)
|
||||
.forEach(talent => { obj[talent.id] = known.get(talent.id) })
|
||||
|
||||
console.log(JSON.stringify(obj, null, 2))
|
||||
}
|
||||
+89
-35
@@ -1,40 +1,94 @@
|
||||
import im from 'immutable'
|
||||
import {
|
||||
// setTalentPointsInTree,
|
||||
getPointsInSpec
|
||||
import im, {
|
||||
Map
|
||||
} from 'immutable'
|
||||
import {
|
||||
canUnlearnTalent, canLearnTalent
|
||||
} from './tree'
|
||||
import {
|
||||
talentsById
|
||||
} from '../data/talents'
|
||||
|
||||
const createKnownTalents = (obj: object): Map <number, number> => {
|
||||
let m = Map<number, number>()
|
||||
Object.keys(obj).forEach((key) => {
|
||||
m = m.set(parseInt(key, 10), obj[key])
|
||||
})
|
||||
return m
|
||||
}
|
||||
|
||||
// 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 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)
|
||||
// })
|
||||
// })
|
||||
const ROGUE_TALENTS = createKnownTalents({
|
||||
241: 5, // Master of Deception
|
||||
181: 5,
|
||||
186: 5,
|
||||
187: 5,
|
||||
244: 5,
|
||||
245: 3,
|
||||
246: 3,
|
||||
262: 3,
|
||||
263: 2,
|
||||
265: 2,
|
||||
284: 1,
|
||||
381: 1,
|
||||
1123: 3,
|
||||
1700: 2,
|
||||
1701: 2,
|
||||
1702: 5
|
||||
})
|
||||
|
||||
// describe('getTreePointCount', () => {
|
||||
// it('returns proper count', () => {
|
||||
// const result = getPointsInSpec(im.List([0, 0, 4, 5, 3, 0, 0]))
|
||||
// expect(result).toBe(12)
|
||||
// })
|
||||
describe('canUnlearnTalent', () => {
|
||||
it('returns false for the incorrect Rogue talent case', () => {
|
||||
const result = canUnlearnTalent(ROGUE_TALENTS, talentsById[241])
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
|
||||
// it('returns 0 for empty list', () => {
|
||||
// const result = getPointsInSpec(im.List())
|
||||
// expect(result).toBe(0)
|
||||
// })
|
||||
// })
|
||||
it('returns false if no points are spent for the talent', () => {
|
||||
const result = canUnlearnTalent(Map(), talentsById[241])
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false if the talent is a dependency for another learnt talent', () => {
|
||||
// http://localhost:3000/rogue/-00505001
|
||||
const known = createKnownTalents({
|
||||
186: 5, // Row 1
|
||||
187: 5, // Row 2
|
||||
301: 1, // Row 3
|
||||
})
|
||||
const result = canUnlearnTalent(known, talentsById[187])
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
|
||||
it('returns true with points only spent in the first row', () => {
|
||||
const known = createKnownTalents({
|
||||
241: 5
|
||||
})
|
||||
const result = canUnlearnTalent(known, talentsById[241])
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('canLearnTalent', () => {
|
||||
it('returns false if 51 points are already spent', () => {
|
||||
// http://localhost:3000/rogue/-005055010055505-55005
|
||||
const known = createKnownTalents({
|
||||
"181": 5,
|
||||
"182": 5,
|
||||
"184": 5,
|
||||
"186": 5,
|
||||
"187": 5,
|
||||
"221": 5,
|
||||
"222": 1,
|
||||
"241": 5,
|
||||
"242": 4,
|
||||
"244": 5,
|
||||
"261": 5,
|
||||
"301": 1
|
||||
})
|
||||
const result = canLearnTalent(known, talentsById[222])
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
|
||||
it('returns true for talent in the first row', () => {
|
||||
const result = canLearnTalent(Map(), talentsById[186])
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
})
|
||||
+60
-57
@@ -16,6 +16,15 @@ export const SORT_TALENTS = (a: TalentData, b: TalentData) => {
|
||||
return a.row - b.row
|
||||
}
|
||||
|
||||
export const SORT_TALENTS_BY_SPEC = (a: TalentData, b: TalentData) => {
|
||||
const aSpec = talentToSpec[a.id]
|
||||
const bSpec = talentToSpec[b.id]
|
||||
if (aSpec === bSpec) {
|
||||
return SORT_TALENTS(a, b)
|
||||
}
|
||||
return aSpec - bSpec
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the overall points spent in the tree.
|
||||
*/
|
||||
@@ -69,6 +78,56 @@ export const canLearnTalent = (known: Map<number, number>, talent: TalentData):
|
||||
return true
|
||||
}
|
||||
|
||||
export const getCumulativePointsPerRow = (known: Map<number, number>, specId: number): number[] => {
|
||||
return known.reduce((reduction, points, talentId) => {
|
||||
const t = talentsBySpec[specId][talentId]
|
||||
if (t && points > 0) {
|
||||
for (let row = t.row; row < MAX_ROWS; row++) {
|
||||
reduction[row] = (reduction[row] || 0) + points
|
||||
}
|
||||
}
|
||||
return reduction
|
||||
}, [])
|
||||
}
|
||||
|
||||
export const canUnlearnTalent = (known: Map<number, number>, talent: TalentData): boolean => {
|
||||
const currentPoints = known.get(talent.id, 0)
|
||||
const specId = talentToSpec[talent.id]
|
||||
|
||||
// No points to reduce for this talent
|
||||
if (currentPoints === 0) {
|
||||
console.warn('no points to reduce')
|
||||
return false
|
||||
}
|
||||
|
||||
// Prevent if another talent depends on this
|
||||
const isDependency = known.some((points, talentId) => {
|
||||
const t = talentsBySpec[specId][talentId]
|
||||
return t && points > 0 && t.requires.some((req) => req.id === talent.id)
|
||||
})
|
||||
if (isDependency) {
|
||||
console.warn('is dependency')
|
||||
return false
|
||||
}
|
||||
|
||||
// Walk through every talent and ensure no requirements are breached
|
||||
let cumulativePointsPerRow = getCumulativePointsPerRow(known, specId)
|
||||
for (let r = talent.row; r < cumulativePointsPerRow.length; r++) {
|
||||
// Calculate what the points would look like when this one is removed
|
||||
cumulativePointsPerRow[r] = cumulativePointsPerRow[r] - 1
|
||||
}
|
||||
const wouldBreach = known.some((points, talentId) => {
|
||||
const t = talentsBySpec[specId][talentId]
|
||||
return t && points > 0 && t.row > 0 && cumulativePointsPerRow[t.row - 1] < t.row * 5
|
||||
})
|
||||
if (wouldBreach) {
|
||||
console.warn('point requirements would be breached')
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a single talent point to the Map, if possible.
|
||||
*/
|
||||
@@ -87,48 +146,8 @@ export const addTalentPoint = (known: Map<number, number>, talent: TalentData):
|
||||
*/
|
||||
export const removeTalentPoint = (known: Map<number, number>, talent: TalentData): Map<number, number> => {
|
||||
const currentPoints = known.get(talent.id, 0)
|
||||
const specId = talentToSpec[talent.id]
|
||||
|
||||
// No points to reduce for this talent
|
||||
if (currentPoints === 0) {
|
||||
console.warn('no points to reduce')
|
||||
return known
|
||||
}
|
||||
|
||||
let isDependency = false
|
||||
let highestRow = 0
|
||||
let cumulativePointsPerRow = {}
|
||||
|
||||
known.forEach((points, talentId) => {
|
||||
const t = talentsBySpec[specId][talentId]
|
||||
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
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Check if removing this talent would not break the requirements for talents spent in later rows
|
||||
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')
|
||||
if (!canUnlearnTalent(known, talent)) {
|
||||
return known
|
||||
}
|
||||
|
||||
@@ -148,19 +167,6 @@ export const modifyTalentPoint = (known: Map<number, number>, talent: TalentData
|
||||
}
|
||||
}
|
||||
|
||||
// TODO
|
||||
export function parsePointString(str: string): List<List<number>> {
|
||||
const list: Array<number[]> = []
|
||||
const trees = str.split('-')
|
||||
|
||||
trees.forEach((stringForTree, index) => {
|
||||
const points = stringForTree.split('').map(a => parseInt(a, 10))
|
||||
list[index] = points
|
||||
})
|
||||
|
||||
return fromJS(list)
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a Map of known talents into a URL-friendly string.
|
||||
*/
|
||||
@@ -183,8 +189,6 @@ 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> {
|
||||
console.log(pointString, className)
|
||||
|
||||
const { specs } = classByName[className]
|
||||
let known = Map<number, number>()
|
||||
|
||||
@@ -206,7 +210,6 @@ export function decodeKnownTalents(pointString: string, className: string): Map<
|
||||
}
|
||||
|
||||
if (points > 0) {
|
||||
console.log(`Spent ${points} in ${talent.id}`)
|
||||
known = known.set(talent.id, points)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user