diff --git a/src/App.scss b/src/App.scss index 8642602..d7bc0f5 100644 --- a/src/App.scss +++ b/src/App.scss @@ -55,7 +55,7 @@ body { &__class { margin-right: 1em; - opacity: .8; + opacity: 1; transition: all .1s ease-out; &:hover { diff --git a/src/App.tsx b/src/App.tsx index 4c2222e..509664a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,7 +5,8 @@ import { BrowserRouter as Router, Route } from 'react-router-dom' const App: React.FC = () => { return ( - + + {/* */}
diff --git a/src/components/Arrow.scss b/src/components/Arrow.scss index 306ef98..557ad49 100644 --- a/src/components/Arrow.scss +++ b/src/components/Arrow.scss @@ -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; + } + } } \ No newline at end of file diff --git a/src/components/Calculator.tsx b/src/components/Calculator.tsx index 70eb600..68b8e42 100644 --- a/src/components/Calculator.tsx +++ b/src/components/Calculator.tsx @@ -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 { 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 { ) diff --git a/src/components/Tooltip.scss b/src/components/Tooltip.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/components/Tooltip.tsx b/src/components/Tooltip.tsx new file mode 100644 index 0000000..8a7d81a --- /dev/null +++ b/src/components/Tooltip.tsx @@ -0,0 +1,11 @@ +import './Tooltip.scss' +import React, { FC } from 'react' + +interface Props { +} + +export const Tooltip: FC = (props) => { + return
+ +
+} \ No newline at end of file diff --git a/src/lib/debug.ts b/src/lib/debug.ts new file mode 100644 index 0000000..91dc4bc --- /dev/null +++ b/src/lib/debug.ts @@ -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) => { + 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)) +} \ No newline at end of file diff --git a/src/lib/tree.test.ts b/src/lib/tree.test.ts index a556689..144c6b8 100644 --- a/src/lib/tree.test.ts +++ b/src/lib/tree.test.ts @@ -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 => { + let m = Map() + 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) -// }) -// }) \ No newline at end of file + 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) + }) +}) \ No newline at end of file diff --git a/src/lib/tree.ts b/src/lib/tree.ts index ddaf6bf..91de613 100644 --- a/src/lib/tree.ts +++ b/src/lib/tree.ts @@ -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, talent: TalentData): return true } +export const getCumulativePointsPerRow = (known: Map, 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, 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, talent: TalentData): */ export const removeTalentPoint = (known: Map, talent: TalentData): Map => { 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, talent: TalentData } } -// TODO -export function parsePointString(str: string): List> { - const list: Array = [] - 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, className: string * Decodes a string of points into a Map of talents. */ export function decodeKnownTalents(pointString: string, className: string): Map { - console.log(pointString, className) - const { specs } = classByName[className] let known = Map() @@ -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) } }