Adds quality selection, all dungeons
This commit is contained in:
7
package-lock.json
generated
7
package-lock.json
generated
@@ -9,6 +9,7 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^2.2.7",
|
"@headlessui/react": "^2.2.7",
|
||||||
|
"@types/lodash": "^4.17.20",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1"
|
"react-dom": "^19.1.1"
|
||||||
@@ -1614,6 +1615,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/lodash": {
|
||||||
|
"version": "4.17.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz",
|
||||||
|
"integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "19.1.10",
|
"version": "19.1.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.10.tgz",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^2.2.7",
|
"@headlessui/react": "^2.2.7",
|
||||||
|
"@types/lodash": "^4.17.20",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1"
|
"react-dom": "^19.1.1"
|
||||||
|
|||||||
12
src/App.tsx
12
src/App.tsx
@@ -1,9 +1,10 @@
|
|||||||
import { useEffect, useReducer } from "react";
|
import { useEffect, useReducer } from "react";
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
import { Equipment } from "./components/Equipment";
|
import { Equipment } from "./components/Equipment";
|
||||||
|
import { SourceList } from "./components/SourceList";
|
||||||
|
import { StateContext } from "./lib/context/StateContext";
|
||||||
import { reducer, withLoadingAction } from "./lib/state";
|
import { reducer, withLoadingAction } from "./lib/state";
|
||||||
import type { ItemId } from "./lib/types";
|
import type { ItemId } from "./lib/types";
|
||||||
import { SourceList } from "./components/SourceList";
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [state, dispatch] = useReducer(withLoadingAction(reducer), "loading");
|
const [state, dispatch] = useReducer(withLoadingAction(reducer), "loading");
|
||||||
@@ -38,7 +39,12 @@ function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<StateContext.Provider
|
||||||
|
value={{
|
||||||
|
state,
|
||||||
|
dispatch,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<h1>WoW Gear Finder</h1>
|
<h1>WoW Gear Finder</h1>
|
||||||
<Equipment
|
<Equipment
|
||||||
state={state}
|
state={state}
|
||||||
@@ -46,7 +52,7 @@ function App() {
|
|||||||
onUnequip={(item) => dispatch({ action: "unequipItem", item })}
|
onUnequip={(item) => dispatch({ action: "unequipItem", item })}
|
||||||
/>
|
/>
|
||||||
<SourceList state={state} />
|
<SourceList state={state} />
|
||||||
</>
|
</StateContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.equipedItems {
|
.equipedItems {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
grid-template-columns: 200px 1fr;
|
grid-template-columns: 10em 1fr 1fr 5em;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11,7 +11,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.slot {
|
.slot {
|
||||||
|
grid-column: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.items {
|
.item {
|
||||||
|
grid-column: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quality {
|
||||||
|
grid-column: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
grid-column: 4;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Slots, type Item, type Slot } from "../lib/types";
|
|||||||
import { ItemLink } from "./ItemLink";
|
import { ItemLink } from "./ItemLink";
|
||||||
import { ItemTypeahead } from "./ItemTypeahead";
|
import { ItemTypeahead } from "./ItemTypeahead";
|
||||||
import styles from "./Equipment.module.css";
|
import styles from "./Equipment.module.css";
|
||||||
|
import { QualitySelector } from "./QualitySelector";
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
state: State;
|
state: State;
|
||||||
@@ -28,19 +29,37 @@ type SlotProps = {
|
|||||||
onUnequip: (item: Item) => void;
|
onUnequip: (item: Item) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Slot = ({ state, slot, onUnequip }: SlotProps) => (
|
const Slot = ({ state, slot, onUnequip }: SlotProps) => {
|
||||||
<>
|
const items = state.equipedItems
|
||||||
<div className={`${styles.slot} ${styles.gridItem}`}>{slot}</div>
|
.map((item) => ({ ...item, item: ItemsById[item.id] }))
|
||||||
<div className={styles.gridItem}>
|
.filter((item) => item && item.item.slot === slot);
|
||||||
{state.equipedItems
|
|
||||||
.map((item) => ({ ...item, item: ItemsById[item.id] }))
|
return (
|
||||||
.filter((item) => item && item.item.slot === slot)
|
<>
|
||||||
.map((item) => (
|
<div className={`${styles.slot} ${styles.gridItem}`}>{slot}</div>
|
||||||
<div>
|
{
|
||||||
<ItemLink item={item.item} />({item.quality})
|
// Show placeholder if there are no items
|
||||||
|
items.length === 0 && (
|
||||||
|
<>
|
||||||
|
<div className={`${styles.gridItem} ${styles.item}`}></div>
|
||||||
|
<div className={`${styles.gridItem} ${styles.quality}`}></div>
|
||||||
|
<div className={`${styles.gridItem} ${styles.actions}`}></div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{items.map((item) => (
|
||||||
|
<>
|
||||||
|
<div className={`${styles.gridItem} ${styles.item}`}>
|
||||||
|
<ItemLink item={item.item} />
|
||||||
|
</div>
|
||||||
|
<div className={`${styles.gridItem} ${styles.quality}`}>
|
||||||
|
<QualitySelector item={item} />
|
||||||
|
</div>
|
||||||
|
<div className={`${styles.gridItem} ${styles.actions}`}>
|
||||||
<button onClick={() => onUnequip(item.item)}>X</button>
|
<button onClick={() => onUnequip(item.item)}>X</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
</>
|
||||||
</div>
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|||||||
27
src/components/QualitySelector.tsx
Normal file
27
src/components/QualitySelector.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { useAppState } from "../lib/context/StateContext";
|
||||||
|
import { Quality, type EquipedItem } from "../lib/types";
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
item: EquipedItem;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const QualitySelector = ({ item }: Props) => {
|
||||||
|
const { dispatch } = useAppState();
|
||||||
|
|
||||||
|
const handleOnChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
dispatch({
|
||||||
|
action: "changeQuality",
|
||||||
|
itemId: item.id,
|
||||||
|
quality: event.target.value as Quality,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<select onChange={handleOnChange}>
|
||||||
|
{Object.values(Quality).map((quality) => (
|
||||||
|
<option value={quality} selected={item.quality === quality}>
|
||||||
|
{quality.charAt(0).toUpperCase() + quality.slice(1)}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,7 +1,12 @@
|
|||||||
import * as _ from "lodash";
|
import * as _ from "lodash";
|
||||||
import { getUpgrades, qualityAtMost } from "../lib/items";
|
import { getUpgrades, hasUpgradeType } from "../lib/items";
|
||||||
import type { State } from "../lib/state";
|
import type { State } from "../lib/state";
|
||||||
import type { Item } from "../lib/types";
|
import {
|
||||||
|
UpgradeType,
|
||||||
|
type Item,
|
||||||
|
type Source,
|
||||||
|
type Upgrade,
|
||||||
|
} from "../lib/types";
|
||||||
import styles from "./SourceList.module.css";
|
import styles from "./SourceList.module.css";
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
@@ -11,7 +16,7 @@ export type Props = {
|
|||||||
export const SourceList = ({ state }: Props) => {
|
export const SourceList = ({ state }: Props) => {
|
||||||
const upgrades = getUpgrades(state.equipedItems);
|
const upgrades = getUpgrades(state.equipedItems);
|
||||||
|
|
||||||
const upgradesBySource = _.groupBy(
|
const upgradesBySource: Record<Source, Upgrade[]> = _.groupBy(
|
||||||
upgrades,
|
upgrades,
|
||||||
(upgrade: { item: Item }) => upgrade.item.source,
|
(upgrade: { item: Item }) => upgrade.item.source,
|
||||||
);
|
);
|
||||||
@@ -22,19 +27,23 @@ export const SourceList = ({ state }: Props) => {
|
|||||||
<div>Champion</div>
|
<div>Champion</div>
|
||||||
<div>Hero</div>
|
<div>Hero</div>
|
||||||
<div>Myth</div>
|
<div>Myth</div>
|
||||||
{Object.entries(upgradesBySource).map(([source, items]) => {
|
{Object.entries(upgradesBySource).map(
|
||||||
const champion = items.filter(qualityAtMost("champion")).length;
|
([source, items]: [string, Upgrade[]]) => {
|
||||||
const hero = items.filter(qualityAtMost("hero")).length;
|
const champion = items.filter(
|
||||||
const myth = items.filter(qualityAtMost("myth")).length;
|
hasUpgradeType(UpgradeType.Champion),
|
||||||
return (
|
).length;
|
||||||
<>
|
const hero = items.filter(hasUpgradeType(UpgradeType.Hero)).length;
|
||||||
<div>{source}</div>
|
const myth = items.filter(hasUpgradeType(UpgradeType.Myth)).length;
|
||||||
<div>{champion}</div>
|
return (
|
||||||
<div>{hero}</div>
|
<>
|
||||||
<div>{myth}</div>
|
<div>{source}</div>
|
||||||
</>
|
<div>{champion}</div>
|
||||||
);
|
<div>{hero}</div>
|
||||||
})}
|
<div>{myth}</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
17
src/lib/context/StateContext.ts
Normal file
17
src/lib/context/StateContext.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { createContext, useContext } from "react";
|
||||||
|
import type { State, Action } from "../state";
|
||||||
|
|
||||||
|
export type StateContextType = {
|
||||||
|
state: State;
|
||||||
|
dispatch: React.ActionDispatch<[Action]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const StateContext = createContext<StateContextType | null>(null);
|
||||||
|
|
||||||
|
export function useAppState(): StateContextType {
|
||||||
|
const value = useContext(StateContext);
|
||||||
|
if (!value) {
|
||||||
|
throw new Error("useState must be used within a StateContext.Provider");
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
46
src/lib/drops/aldani.ts
Normal file
46
src/lib/drops/aldani.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import type { ItemId } from "../types";
|
||||||
|
|
||||||
|
export const Aldani = [
|
||||||
|
{
|
||||||
|
id: 242494 as ItemId,
|
||||||
|
slot: "trinket",
|
||||||
|
name: "Lily of the Eternal Weave",
|
||||||
|
source: "aldani" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 242495 as ItemId,
|
||||||
|
slot: "trinket",
|
||||||
|
name: "Incorporeal Warpclaw",
|
||||||
|
source: "aldani" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 242481 as ItemId,
|
||||||
|
slot: "2h-weapon",
|
||||||
|
name: "Spellstrike Warplance",
|
||||||
|
source: "aldani" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 242473 as ItemId,
|
||||||
|
slot: "legs",
|
||||||
|
name: "Spittle-Stained Trousers",
|
||||||
|
source: "aldani" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 242470 as ItemId,
|
||||||
|
slot: "1h-weapon",
|
||||||
|
name: "Mandibular Bonewhacker",
|
||||||
|
source: "aldani" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 242486 as ItemId,
|
||||||
|
slot: "shoulders",
|
||||||
|
name: "Mantle of Wounded Fate",
|
||||||
|
source: "aldani" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 242482 as ItemId,
|
||||||
|
slot: "chest",
|
||||||
|
name: "Reinforced Stalkerhide Vest",
|
||||||
|
source: "aldani" as const,
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
52
src/lib/drops/ara-kara.ts
Normal file
52
src/lib/drops/ara-kara.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import type { ItemId } from "../types";
|
||||||
|
|
||||||
|
export const AraKara = [
|
||||||
|
{
|
||||||
|
id: 221159 as ItemId,
|
||||||
|
slot: "2h-weapon",
|
||||||
|
name: "Harvester's Interdiction",
|
||||||
|
source: "ara-kara",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 219317 as ItemId,
|
||||||
|
slot: "trinket",
|
||||||
|
name: "Harvester's Edict",
|
||||||
|
source: "ara-kara",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 219316 as ItemId,
|
||||||
|
slot: "trinket",
|
||||||
|
name: "Ceaseless Swarmgland",
|
||||||
|
source: "ara-kara",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 221157 as ItemId,
|
||||||
|
slot: "wrist",
|
||||||
|
name: "Unbreakable Beetlebane Bindings",
|
||||||
|
source: "ara-kara",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 221153 as ItemId,
|
||||||
|
slot: "legs",
|
||||||
|
name: "Gauzewoven Legguards",
|
||||||
|
source: "ara-kara",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 221154 as ItemId,
|
||||||
|
slot: "back",
|
||||||
|
name: "Swarmcaller's Shroud",
|
||||||
|
source: "ara-kara",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 221163 as ItemId,
|
||||||
|
slot: "head",
|
||||||
|
name: "Whispering Mask",
|
||||||
|
source: "ara-kara",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 219315 as ItemId,
|
||||||
|
slot: "trinket",
|
||||||
|
name: "Refracting Aggression Module",
|
||||||
|
source: "ara-kara",
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
76
src/lib/drops/dawnbreaker.ts
Normal file
76
src/lib/drops/dawnbreaker.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import type { Item } from "../types";
|
||||||
|
|
||||||
|
export const Dawnbreaker = [
|
||||||
|
{
|
||||||
|
id: 219312,
|
||||||
|
slot: "trinket",
|
||||||
|
name: "Empowering Crystal of Anub'ikkaj",
|
||||||
|
source: "dawnbreaker" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 225574,
|
||||||
|
slot: "back",
|
||||||
|
name: "Wings of Shattered Sorrow",
|
||||||
|
source: "dawnbreaker" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 219311,
|
||||||
|
slot: "trinket",
|
||||||
|
name: "Void Pactstone",
|
||||||
|
source: "dawnbreaker" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 221142,
|
||||||
|
slot: "wrist",
|
||||||
|
name: "Scheming Assailer's Bands",
|
||||||
|
source: "dawnbreaker" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 221137,
|
||||||
|
slot: "2h-weapon",
|
||||||
|
name: "Black Shepherd's Guisarme",
|
||||||
|
source: "dawnbreaker" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 221144,
|
||||||
|
slot: "1h-weapon",
|
||||||
|
name: "Zephyrous Sail Carver",
|
||||||
|
source: "dawnbreaker" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 221134,
|
||||||
|
slot: "waist",
|
||||||
|
name: "Shadow Congregant's Belt",
|
||||||
|
source: "dawnbreaker" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 221145,
|
||||||
|
slot: "1h-weapon",
|
||||||
|
name: "Shipwrecker's Bludgeon",
|
||||||
|
source: "dawnbreaker" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 221148,
|
||||||
|
slot: "shoulders",
|
||||||
|
name: "Epaulets of the Clipped Wings",
|
||||||
|
source: "dawnbreaker" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 225583,
|
||||||
|
slot: "waist",
|
||||||
|
name: "Behemoth's Eroded Cinch",
|
||||||
|
source: "dawnbreaker" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 212453,
|
||||||
|
slot: "trinket",
|
||||||
|
name: "Skyterror's Corrosive Organ",
|
||||||
|
source: "dawnbreaker" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 212398,
|
||||||
|
slot: "1h-weapon",
|
||||||
|
name: "Bludgeons of Blistering Wind",
|
||||||
|
source: "dawnbreaker" as const,
|
||||||
|
},
|
||||||
|
] as Item[];
|
||||||
46
src/lib/drops/floodgate.ts
Normal file
46
src/lib/drops/floodgate.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import type { ItemId } from "../types";
|
||||||
|
|
||||||
|
export const Floodgate = [
|
||||||
|
{
|
||||||
|
id: 232541 as ItemId,
|
||||||
|
slot: "trinket",
|
||||||
|
name: "Improvised Seaforium Pacemaker",
|
||||||
|
source: "floodgate" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 234499 as ItemId,
|
||||||
|
slot: "wrist",
|
||||||
|
name: "Disturbed Kelp Wraps",
|
||||||
|
source: "floodgate" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 246274 as ItemId,
|
||||||
|
slot: "feet",
|
||||||
|
name: "Geezle's Zapstep Boots",
|
||||||
|
source: "floodgate" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 234494 as ItemId,
|
||||||
|
slot: "2h-weapon",
|
||||||
|
name: "Gallytech Turbo-Tiller",
|
||||||
|
source: "floodgate" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 234498 as ItemId,
|
||||||
|
slot: "head",
|
||||||
|
name: "Waterworks Filtration Mask",
|
||||||
|
source: "floodgate" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 234507 as ItemId,
|
||||||
|
slot: "back",
|
||||||
|
name: "Electrician's Siphoning Filter",
|
||||||
|
source: "floodgate" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 234500 as ItemId,
|
||||||
|
slot: "shoulders",
|
||||||
|
name: "Mechanized Junkpads",
|
||||||
|
source: "floodgate" as const,
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
52
src/lib/drops/halls-of-atonement.ts
Normal file
52
src/lib/drops/halls-of-atonement.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import type { ItemId } from "../types";
|
||||||
|
|
||||||
|
export const HallsOfAtonement = [
|
||||||
|
{
|
||||||
|
id: 246344 as ItemId,
|
||||||
|
slot: "trinket",
|
||||||
|
name: "Cursed Stone Idol",
|
||||||
|
source: "halls-of-atonement" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 178832 as ItemId,
|
||||||
|
slot: "hands",
|
||||||
|
name: "Gloves of Haunting Fixation",
|
||||||
|
source: "halls-of-atonement" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 178819 as ItemId,
|
||||||
|
slot: "legs",
|
||||||
|
name: "Skyterror's Stonehide Leggings",
|
||||||
|
source: "halls-of-atonement" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 246273 as ItemId,
|
||||||
|
slot: "chest",
|
||||||
|
name: "Vest of Refracted Shadows",
|
||||||
|
source: "halls-of-atonement" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 178834 as ItemId,
|
||||||
|
slot: "1h-weapon",
|
||||||
|
name: "Stoneguardian's Morningstar",
|
||||||
|
source: "halls-of-atonement" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 178823 as ItemId,
|
||||||
|
slot: "waist",
|
||||||
|
name: "Waistcord of Dark Devotion",
|
||||||
|
source: "halls-of-atonement" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 178825 as ItemId,
|
||||||
|
slot: "trinket",
|
||||||
|
name: "Pulsating Stoneheart",
|
||||||
|
source: "halls-of-atonement" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 178817 as ItemId,
|
||||||
|
slot: "head",
|
||||||
|
name: "Hood of Refracted Shadows",
|
||||||
|
source: "halls-of-atonement" as const,
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
8
src/lib/drops/index.ts
Normal file
8
src/lib/drops/index.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export { Aldani } from "./aldani";
|
||||||
|
export { AraKara } from "./ara-kara";
|
||||||
|
export { Dawnbreaker } from "./dawnbreaker";
|
||||||
|
export { Floodgate } from "./floodgate";
|
||||||
|
export { HallsOfAtonement } from "./halls-of-atonement";
|
||||||
|
export { PrioryOfTheSacredFlame } from "./priory";
|
||||||
|
export { Streets, Gambit } from "./tazavesh";
|
||||||
|
export { ManaforgeOmega } from "./manaforge-omega";
|
||||||
40
src/lib/drops/manaforge-omega.ts
Normal file
40
src/lib/drops/manaforge-omega.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import type { ItemId } from "../types";
|
||||||
|
|
||||||
|
export const ManaforgeOmega = [
|
||||||
|
{
|
||||||
|
id: 237673 as ItemId,
|
||||||
|
name: "Half-Mask of Fallen Storms",
|
||||||
|
slot: "head",
|
||||||
|
source: "manaforge omega",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 237674 as ItemId,
|
||||||
|
name: "Grasp of Fallen Storms",
|
||||||
|
slot: "hands",
|
||||||
|
source: "manaforge omega",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 237676 as ItemId,
|
||||||
|
name: "Gi of Fallen Storms",
|
||||||
|
slot: "chest",
|
||||||
|
source: "manaforge omega",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 237672 as ItemId,
|
||||||
|
name: "Legwraps of Fallen Storms",
|
||||||
|
slot: "legs",
|
||||||
|
source: "manaforge omega",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 237671 as ItemId,
|
||||||
|
name: "Glyphs of Fallen Storms",
|
||||||
|
slot: "shoulders",
|
||||||
|
source: "manaforge omega",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 235799 as ItemId,
|
||||||
|
name: "Reshii Wraps",
|
||||||
|
slot: "back",
|
||||||
|
source: "manaforge omega",
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
34
src/lib/drops/priory.ts
Normal file
34
src/lib/drops/priory.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import type { ItemId } from "../types";
|
||||||
|
|
||||||
|
export const PrioryOfTheSacredFlame = [
|
||||||
|
{
|
||||||
|
id: 219309 as ItemId,
|
||||||
|
slot: "trinket",
|
||||||
|
name: "Tome of Light's Devotion",
|
||||||
|
source: "priory" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 219308 as ItemId,
|
||||||
|
slot: "trinket",
|
||||||
|
name: "Signet of the Priory",
|
||||||
|
source: "priory" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 252009 as ItemId,
|
||||||
|
slot: "neck",
|
||||||
|
name: "Bloodstained Memento",
|
||||||
|
source: "priory" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 221200 as ItemId,
|
||||||
|
slot: "finger",
|
||||||
|
name: "Radiant Necromancer's Band",
|
||||||
|
source: "priory" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 221125 as ItemId,
|
||||||
|
slot: "head",
|
||||||
|
name: "Helm of the Righteous Crusade",
|
||||||
|
source: "priory" as const,
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
115
src/lib/drops/tazavesh.ts
Normal file
115
src/lib/drops/tazavesh.ts
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import type { ItemId } from "../types";
|
||||||
|
|
||||||
|
export const Gambit = [
|
||||||
|
{
|
||||||
|
id: 190958 as ItemId,
|
||||||
|
slot: "trinket",
|
||||||
|
name: "So'leah's Secret Technique",
|
||||||
|
source: "gambit" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 185818 as ItemId,
|
||||||
|
slot: "trinket",
|
||||||
|
name: "So'leah's Secret Technique",
|
||||||
|
source: "gambit" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 185823 as ItemId,
|
||||||
|
slot: "1h-weapon",
|
||||||
|
name: "Fatebreaker, Destroyer of Futures",
|
||||||
|
source: "gambit" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 246280 as ItemId,
|
||||||
|
slot: "feet",
|
||||||
|
name: "Boots of Titanic Deconversion",
|
||||||
|
source: "gambit" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 185781 as ItemId,
|
||||||
|
slot: "back",
|
||||||
|
name: "Drape of Titanic Dreams",
|
||||||
|
source: "gambit" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 185790 as ItemId,
|
||||||
|
slot: "feet",
|
||||||
|
name: "Treads of Titanic Deconversion",
|
||||||
|
source: "gambit" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 185797 as ItemId,
|
||||||
|
slot: "head",
|
||||||
|
name: "Rakishly Tipped Tricorne",
|
||||||
|
source: "gambit" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 185801 as ItemId,
|
||||||
|
slot: "legs",
|
||||||
|
name: "Anomalous Starlit Breeches",
|
||||||
|
source: "gambit" as const,
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const Streets = [
|
||||||
|
{
|
||||||
|
id: 185780 as ItemId,
|
||||||
|
slot: "1h-weapon",
|
||||||
|
name: "Interrogator's Flensing Blade",
|
||||||
|
source: "streets" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 185778 as ItemId,
|
||||||
|
slot: "1h-weapon",
|
||||||
|
name: "First Fist of the So Cartel",
|
||||||
|
source: "streets" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 185824 as ItemId,
|
||||||
|
slot: "1h-weapon",
|
||||||
|
name: "Blade of Grievous Harm",
|
||||||
|
source: "streets" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 185809 as ItemId,
|
||||||
|
slot: "waist",
|
||||||
|
name: "Venza's Powderbelt",
|
||||||
|
source: "streets" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 185779 as ItemId,
|
||||||
|
slot: "2h-weapon",
|
||||||
|
name: "Spire of Expurgation",
|
||||||
|
source: "streets" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 185802 as ItemId,
|
||||||
|
slot: "shoulders",
|
||||||
|
name: "Breakbeat Shoulderguards",
|
||||||
|
source: "streets" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 185786 as ItemId,
|
||||||
|
slot: "chest",
|
||||||
|
name: "So'azmi's Fractal Vest",
|
||||||
|
source: "streets" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 185791 as ItemId,
|
||||||
|
slot: "hands",
|
||||||
|
name: "Knuckle-Dusting Handwraps",
|
||||||
|
source: "streets" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 185817 as ItemId,
|
||||||
|
slot: "wrist",
|
||||||
|
name: "Bracers of Autonomous Classification",
|
||||||
|
source: "streets" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 185843 as ItemId,
|
||||||
|
slot: "back",
|
||||||
|
name: "Duplicating Drape",
|
||||||
|
source: "streets" as const,
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
128
src/lib/items.ts
128
src/lib/items.ts
@@ -1,25 +1,35 @@
|
|||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import {
|
import {
|
||||||
|
UpgradeType,
|
||||||
type EquipedItem,
|
type EquipedItem,
|
||||||
type Item,
|
type Item,
|
||||||
type ItemId,
|
type ItemId,
|
||||||
type Quality,
|
type Quality,
|
||||||
type Slot,
|
type Slot,
|
||||||
|
type Upgrade,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
import {
|
||||||
export const PrioryOfTheSacredFlame = [
|
Aldani,
|
||||||
{ id: 219309 as ItemId, slot: "trinket", name: "Tome of Light's Devotion" },
|
AraKara,
|
||||||
{ id: 219308 as ItemId, slot: "trinket", name: "Signet of the Priory" },
|
Dawnbreaker,
|
||||||
{ id: 252009 as ItemId, slot: "neck", name: "Bloodstained Memento" },
|
Floodgate,
|
||||||
{ id: 221200 as ItemId, slot: "finger", name: "Radiant Necromancer's Band" },
|
HallsOfAtonement,
|
||||||
{ id: 221125 as ItemId, slot: "head", name: "Helm of the Righteous Crusade" },
|
PrioryOfTheSacredFlame,
|
||||||
] as const;
|
Streets,
|
||||||
|
Gambit,
|
||||||
|
ManaforgeOmega,
|
||||||
|
} from "./drops";
|
||||||
|
|
||||||
export const AllItems: Item[] = [
|
export const AllItems: Item[] = [
|
||||||
...PrioryOfTheSacredFlame.map((item) => ({
|
...PrioryOfTheSacredFlame,
|
||||||
...item,
|
...Aldani,
|
||||||
source: "priory" as const,
|
...AraKara,
|
||||||
})),
|
...Dawnbreaker,
|
||||||
|
...Floodgate,
|
||||||
|
...HallsOfAtonement,
|
||||||
|
...Streets,
|
||||||
|
...Gambit,
|
||||||
|
...ManaforgeOmega,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const ItemsById: Record<string, Item> = AllItems.reduce(
|
export const ItemsById: Record<string, Item> = AllItems.reduce(
|
||||||
@@ -33,36 +43,27 @@ export const ItemsById: Record<string, Item> = AllItems.reduce(
|
|||||||
export const ItemsBySlot: Record<Slot, Item[]> = _.groupBy(
|
export const ItemsBySlot: Record<Slot, Item[]> = _.groupBy(
|
||||||
AllItems,
|
AllItems,
|
||||||
(item: Item) => item.slot,
|
(item: Item) => item.slot,
|
||||||
);
|
) as Record<Slot, Item[]>;
|
||||||
|
|
||||||
export const itemsForSlot = (slot: Slot): Item[] => ItemsBySlot[slot] ?? [];
|
export const itemsForSlot = (slot: Slot): Item[] => ItemsBySlot[slot] ?? [];
|
||||||
|
|
||||||
function qualityToNumber(quality: Quality): number {
|
function qualityToNumber(quality: Quality): number {
|
||||||
switch (quality) {
|
switch (quality) {
|
||||||
case "champion":
|
case "explorer":
|
||||||
return 1;
|
return 1;
|
||||||
case "hero":
|
case "veteran":
|
||||||
return 2;
|
return 2;
|
||||||
case "myth":
|
case "champion":
|
||||||
return 3;
|
return 3;
|
||||||
|
case "hero":
|
||||||
|
return 4;
|
||||||
|
case "myth":
|
||||||
|
return 5;
|
||||||
default:
|
default:
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function numberToQuality(quality: number): Quality | null {
|
|
||||||
switch (quality) {
|
|
||||||
case 1:
|
|
||||||
return "champion";
|
|
||||||
case 2:
|
|
||||||
return "hero";
|
|
||||||
case 3:
|
|
||||||
return "myth";
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const qualityAtLeast =
|
export const qualityAtLeast =
|
||||||
(desiredQuality: Quality) =>
|
(desiredQuality: Quality) =>
|
||||||
({ quality: actualQuality }: { quality: Quality }): boolean =>
|
({ quality: actualQuality }: { quality: Quality }): boolean =>
|
||||||
@@ -73,9 +74,20 @@ export const qualityAtMost =
|
|||||||
({ quality: actualQuality }: { quality: Quality }): boolean =>
|
({ quality: actualQuality }: { quality: Quality }): boolean =>
|
||||||
qualityToNumber(actualQuality) <= qualityToNumber(desiredQuality);
|
qualityToNumber(actualQuality) <= qualityToNumber(desiredQuality);
|
||||||
|
|
||||||
export const getUpgrades = (
|
export const upgradesForItem = ({ quality }: { quality: Quality }) => {
|
||||||
equipedItemIds: EquipedItem[],
|
switch (quality) {
|
||||||
): { item: Item; quality: Quality }[] => {
|
case "myth":
|
||||||
|
return [];
|
||||||
|
case "hero":
|
||||||
|
return [UpgradeType.Myth];
|
||||||
|
case "champion":
|
||||||
|
return [UpgradeType.Hero, UpgradeType.Myth];
|
||||||
|
default:
|
||||||
|
return [UpgradeType.Champion, UpgradeType.Hero, UpgradeType.Myth];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUpgrades = (equipedItemIds: EquipedItem[]): Upgrade[] => {
|
||||||
return Object.entries(ItemsBySlot).flatMap(([slot, items]) => {
|
return Object.entries(ItemsBySlot).flatMap(([slot, items]) => {
|
||||||
const equipedItems = equipedItemIds.flatMap(({ id, quality }) => ({
|
const equipedItems = equipedItemIds.flatMap(({ id, quality }) => ({
|
||||||
item: ItemsById[id],
|
item: ItemsById[id],
|
||||||
@@ -84,21 +96,45 @@ export const getUpgrades = (
|
|||||||
const equipedInSlot = equipedItems.filter(
|
const equipedInSlot = equipedItems.filter(
|
||||||
(item) => item.item.slot === slot,
|
(item) => item.item.slot === slot,
|
||||||
);
|
);
|
||||||
return items.flatMap((item) => {
|
const itemsForSlot = itemsPerSlot(slot as Slot);
|
||||||
if (equipedInSlot.length === 0) {
|
const numChampion = equipedInSlot.filter(qualityAtLeast("champion")).length;
|
||||||
return [{ item, quality: "champion" as Quality }];
|
const numHero = equipedInSlot.filter(qualityAtLeast("hero")).length;
|
||||||
|
const numMyth = equipedInSlot.filter(qualityAtLeast("myth")).length;
|
||||||
|
const upgradeTypes = [
|
||||||
|
numChampion >= itemsForSlot ? [] : [UpgradeType.Champion],
|
||||||
|
numHero >= itemsForSlot ? [] : [UpgradeType.Hero],
|
||||||
|
numMyth >= itemsForSlot ? [] : [UpgradeType.Myth],
|
||||||
|
].flat();
|
||||||
|
console.log(slot, numChampion, numHero, numMyth, upgradeTypes);
|
||||||
|
return items.map((item) => {
|
||||||
|
const equiped = equipedInSlot.find((i) => i.item.id === item.id);
|
||||||
|
if (equiped) {
|
||||||
|
const upgradesForThisItem = upgradesForItem(equiped);
|
||||||
|
return {
|
||||||
|
item,
|
||||||
|
upgradeTypes: upgradeTypes.filter((u) =>
|
||||||
|
upgradesForThisItem.includes(u),
|
||||||
|
),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
// Change this to some sort of quality-plus-one logic
|
|
||||||
if (equipedInSlot.every(qualityAtLeast("myth"))) {
|
return { item, upgradeTypes };
|
||||||
return [];
|
|
||||||
}
|
|
||||||
if (equipedInSlot.every(qualityAtLeast("hero"))) {
|
|
||||||
return [{ item, quality: "myth" as Quality }];
|
|
||||||
}
|
|
||||||
if (equipedInSlot.every(qualityAtLeast("champion"))) {
|
|
||||||
return [{ item, quality: "hero" as Quality }];
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const hasUpgradeType =
|
||||||
|
(type: UpgradeType) =>
|
||||||
|
(upgrade: Upgrade): boolean =>
|
||||||
|
upgrade.upgradeTypes.includes(type);
|
||||||
|
|
||||||
|
export const itemsPerSlot = (slot: Slot): number => {
|
||||||
|
switch (slot) {
|
||||||
|
case "trinket":
|
||||||
|
case "finger":
|
||||||
|
case "1h-weapon":
|
||||||
|
return 2;
|
||||||
|
default:
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import type { EquipedItem, Item, ItemId } from "./types";
|
import type { EquipedItem, Item, ItemId, Quality } from "./types";
|
||||||
|
|
||||||
export type State = {
|
export type State = {
|
||||||
equipedItems: EquipedItem[];
|
equipedItems: EquipedItem[];
|
||||||
bisList: ItemId[];
|
bisList: ItemId[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const emptyState: State = {
|
||||||
|
equipedItems: [],
|
||||||
|
bisList: [],
|
||||||
|
};
|
||||||
|
|
||||||
export type Action =
|
export type Action =
|
||||||
| {
|
| {
|
||||||
action: "equipItem";
|
action: "equipItem";
|
||||||
@@ -13,6 +18,11 @@ export type Action =
|
|||||||
| {
|
| {
|
||||||
action: "unequipItem";
|
action: "unequipItem";
|
||||||
item: Item;
|
item: Item;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
action: "changeQuality";
|
||||||
|
itemId: ItemId;
|
||||||
|
quality: Quality;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const reducer = (state: State, action: Action): State => {
|
export const reducer = (state: State, action: Action): State => {
|
||||||
@@ -35,6 +45,19 @@ export const reducer = (state: State, action: Action): State => {
|
|||||||
(item) => item.id !== action.item.id,
|
(item) => item.id !== action.item.id,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
case "changeQuality":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
equipedItems: state.equipedItems.map((item) => {
|
||||||
|
if (item.id === action.itemId) {
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
quality: action.quality,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -18,11 +18,28 @@ export const Slots = [
|
|||||||
|
|
||||||
export type Slot = (typeof Slots)[number];
|
export type Slot = (typeof Slots)[number];
|
||||||
|
|
||||||
export type Quality = "champion" | "hero" | "myth";
|
export const Quality = {
|
||||||
|
Explorer: "explorer",
|
||||||
|
Veteran: "veteran",
|
||||||
|
Champion: "champion",
|
||||||
|
Hero: "hero",
|
||||||
|
Myth: "myth",
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Quality = (typeof Quality)[keyof typeof Quality];
|
||||||
|
|
||||||
export type ItemId = number & { __type: "ItemId" };
|
export type ItemId = number & { __type: "ItemId" };
|
||||||
|
|
||||||
export type Source = "priory";
|
export type Source =
|
||||||
|
| "aldani"
|
||||||
|
| "ara-kara"
|
||||||
|
| "dawnbreaker"
|
||||||
|
| "floodgate"
|
||||||
|
| "gambit"
|
||||||
|
| "halls-of-atonement"
|
||||||
|
| "priory"
|
||||||
|
| "streets"
|
||||||
|
| "manaforge omega";
|
||||||
|
|
||||||
export type Item = {
|
export type Item = {
|
||||||
id: ItemId;
|
id: ItemId;
|
||||||
@@ -35,3 +52,15 @@ export type EquipedItem = {
|
|||||||
id: ItemId;
|
id: ItemId;
|
||||||
quality: Quality;
|
quality: Quality;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const UpgradeType = {
|
||||||
|
ILvl: "ilvl",
|
||||||
|
Champion: "champion",
|
||||||
|
Hero: "hero",
|
||||||
|
Myth: "myth",
|
||||||
|
BiS: "bis",
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpgradeType = (typeof UpgradeType)[keyof typeof UpgradeType];
|
||||||
|
|
||||||
|
export type Upgrade = { item: Item; upgradeTypes: UpgradeType[] };
|
||||||
|
|||||||
Reference in New Issue
Block a user