diff --git a/package-lock.json b/package-lock.json
index d6c0821..be9214e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
"version": "0.0.0",
"dependencies": {
"@headlessui/react": "^2.2.7",
+ "@types/lodash": "^4.17.20",
"lodash": "^4.17.21",
"react": "^19.1.1",
"react-dom": "^19.1.1"
@@ -1614,6 +1615,12 @@
"dev": true,
"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": {
"version": "19.1.10",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.10.tgz",
diff --git a/package.json b/package.json
index 06714da..8aa6905 100644
--- a/package.json
+++ b/package.json
@@ -11,6 +11,7 @@
},
"dependencies": {
"@headlessui/react": "^2.2.7",
+ "@types/lodash": "^4.17.20",
"lodash": "^4.17.21",
"react": "^19.1.1",
"react-dom": "^19.1.1"
diff --git a/src/App.tsx b/src/App.tsx
index b72279c..898da94 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,9 +1,10 @@
import { useEffect, useReducer } from "react";
import "./App.css";
import { Equipment } from "./components/Equipment";
+import { SourceList } from "./components/SourceList";
+import { StateContext } from "./lib/context/StateContext";
import { reducer, withLoadingAction } from "./lib/state";
import type { ItemId } from "./lib/types";
-import { SourceList } from "./components/SourceList";
function App() {
const [state, dispatch] = useReducer(withLoadingAction(reducer), "loading");
@@ -38,7 +39,12 @@ function App() {
}
return (
- <>
+
WoW Gear Finder
dispatch({ action: "unequipItem", item })}
/>
- >
+
);
}
diff --git a/src/components/Equipment.module.css b/src/components/Equipment.module.css
index b89f4aa..ff58c34 100644
--- a/src/components/Equipment.module.css
+++ b/src/components/Equipment.module.css
@@ -1,7 +1,7 @@
.equipedItems {
display: grid;
gap: 8px;
- grid-template-columns: 200px 1fr;
+ grid-template-columns: 10em 1fr 1fr 5em;
align-items: stretch;
}
@@ -11,7 +11,17 @@
}
.slot {
+ grid-column: 1;
}
-.items {
+.item {
+ grid-column: 2;
+}
+
+.quality {
+ grid-column: 3;
+}
+
+.actions {
+ grid-column: 4;
}
diff --git a/src/components/Equipment.tsx b/src/components/Equipment.tsx
index c9032ca..cc24afc 100644
--- a/src/components/Equipment.tsx
+++ b/src/components/Equipment.tsx
@@ -4,6 +4,7 @@ import { Slots, type Item, type Slot } from "../lib/types";
import { ItemLink } from "./ItemLink";
import { ItemTypeahead } from "./ItemTypeahead";
import styles from "./Equipment.module.css";
+import { QualitySelector } from "./QualitySelector";
export type Props = {
state: State;
@@ -28,19 +29,37 @@ type SlotProps = {
onUnequip: (item: Item) => void;
};
-const Slot = ({ state, slot, onUnequip }: SlotProps) => (
- <>
-
{slot}
-
- {state.equipedItems
- .map((item) => ({ ...item, item: ItemsById[item.id] }))
- .filter((item) => item && item.item.slot === slot)
- .map((item) => (
-
-
({item.quality})
+const Slot = ({ state, slot, onUnequip }: SlotProps) => {
+ const items = state.equipedItems
+ .map((item) => ({ ...item, item: ItemsById[item.id] }))
+ .filter((item) => item && item.item.slot === slot);
+
+ return (
+ <>
+
{slot}
+ {
+ // Show placeholder if there are no items
+ items.length === 0 && (
+ <>
+
+
+
+ >
+ )
+ }
+ {items.map((item) => (
+ <>
+
+
+
+
+
+
+
- ))}
-
- >
-);
+ >
+ ))}
+ >
+ );
+};
diff --git a/src/components/QualitySelector.tsx b/src/components/QualitySelector.tsx
new file mode 100644
index 0000000..7796584
--- /dev/null
+++ b/src/components/QualitySelector.tsx
@@ -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
) => {
+ dispatch({
+ action: "changeQuality",
+ itemId: item.id,
+ quality: event.target.value as Quality,
+ });
+ };
+ return (
+
+ );
+};
diff --git a/src/components/SourceList.tsx b/src/components/SourceList.tsx
index 0886a18..0eda1db 100644
--- a/src/components/SourceList.tsx
+++ b/src/components/SourceList.tsx
@@ -1,7 +1,12 @@
import * as _ from "lodash";
-import { getUpgrades, qualityAtMost } from "../lib/items";
+import { getUpgrades, hasUpgradeType } from "../lib/items";
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";
export type Props = {
@@ -11,7 +16,7 @@ export type Props = {
export const SourceList = ({ state }: Props) => {
const upgrades = getUpgrades(state.equipedItems);
- const upgradesBySource = _.groupBy(
+ const upgradesBySource: Record = _.groupBy(
upgrades,
(upgrade: { item: Item }) => upgrade.item.source,
);
@@ -22,19 +27,23 @@ export const SourceList = ({ state }: Props) => {
Champion
Hero
Myth
- {Object.entries(upgradesBySource).map(([source, items]) => {
- const champion = items.filter(qualityAtMost("champion")).length;
- const hero = items.filter(qualityAtMost("hero")).length;
- const myth = items.filter(qualityAtMost("myth")).length;
- return (
- <>
- {source}
- {champion}
- {hero}
- {myth}
- >
- );
- })}
+ {Object.entries(upgradesBySource).map(
+ ([source, items]: [string, Upgrade[]]) => {
+ const champion = items.filter(
+ hasUpgradeType(UpgradeType.Champion),
+ ).length;
+ const hero = items.filter(hasUpgradeType(UpgradeType.Hero)).length;
+ const myth = items.filter(hasUpgradeType(UpgradeType.Myth)).length;
+ return (
+ <>
+ {source}
+ {champion}
+ {hero}
+ {myth}
+ >
+ );
+ },
+ )}
);
};
diff --git a/src/lib/context/StateContext.ts b/src/lib/context/StateContext.ts
new file mode 100644
index 0000000..3fa1b85
--- /dev/null
+++ b/src/lib/context/StateContext.ts
@@ -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(null);
+
+export function useAppState(): StateContextType {
+ const value = useContext(StateContext);
+ if (!value) {
+ throw new Error("useState must be used within a StateContext.Provider");
+ }
+ return value;
+}
diff --git a/src/lib/drops/aldani.ts b/src/lib/drops/aldani.ts
new file mode 100644
index 0000000..8380bd2
--- /dev/null
+++ b/src/lib/drops/aldani.ts
@@ -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;
diff --git a/src/lib/drops/ara-kara.ts b/src/lib/drops/ara-kara.ts
new file mode 100644
index 0000000..68c7a16
--- /dev/null
+++ b/src/lib/drops/ara-kara.ts
@@ -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;
diff --git a/src/lib/drops/dawnbreaker.ts b/src/lib/drops/dawnbreaker.ts
new file mode 100644
index 0000000..e7946af
--- /dev/null
+++ b/src/lib/drops/dawnbreaker.ts
@@ -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[];
diff --git a/src/lib/drops/floodgate.ts b/src/lib/drops/floodgate.ts
new file mode 100644
index 0000000..ffe4fc8
--- /dev/null
+++ b/src/lib/drops/floodgate.ts
@@ -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;
diff --git a/src/lib/drops/halls-of-atonement.ts b/src/lib/drops/halls-of-atonement.ts
new file mode 100644
index 0000000..8a957c8
--- /dev/null
+++ b/src/lib/drops/halls-of-atonement.ts
@@ -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;
diff --git a/src/lib/drops/index.ts b/src/lib/drops/index.ts
new file mode 100644
index 0000000..d4f0b7a
--- /dev/null
+++ b/src/lib/drops/index.ts
@@ -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";
diff --git a/src/lib/drops/manaforge-omega.ts b/src/lib/drops/manaforge-omega.ts
new file mode 100644
index 0000000..1abce8a
--- /dev/null
+++ b/src/lib/drops/manaforge-omega.ts
@@ -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;
diff --git a/src/lib/drops/priory.ts b/src/lib/drops/priory.ts
new file mode 100644
index 0000000..0853762
--- /dev/null
+++ b/src/lib/drops/priory.ts
@@ -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;
diff --git a/src/lib/drops/tazavesh.ts b/src/lib/drops/tazavesh.ts
new file mode 100644
index 0000000..bb0244a
--- /dev/null
+++ b/src/lib/drops/tazavesh.ts
@@ -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;
diff --git a/src/lib/items.ts b/src/lib/items.ts
index 03f8bba..3e2333f 100644
--- a/src/lib/items.ts
+++ b/src/lib/items.ts
@@ -1,25 +1,35 @@
import _ from "lodash";
import {
+ UpgradeType,
type EquipedItem,
type Item,
type ItemId,
type Quality,
type Slot,
+ type Upgrade,
} from "./types";
-
-export const PrioryOfTheSacredFlame = [
- { id: 219309 as ItemId, slot: "trinket", name: "Tome of Light's Devotion" },
- { id: 219308 as ItemId, slot: "trinket", name: "Signet of the Priory" },
- { id: 252009 as ItemId, slot: "neck", name: "Bloodstained Memento" },
- { id: 221200 as ItemId, slot: "finger", name: "Radiant Necromancer's Band" },
- { id: 221125 as ItemId, slot: "head", name: "Helm of the Righteous Crusade" },
-] as const;
+import {
+ Aldani,
+ AraKara,
+ Dawnbreaker,
+ Floodgate,
+ HallsOfAtonement,
+ PrioryOfTheSacredFlame,
+ Streets,
+ Gambit,
+ ManaforgeOmega,
+} from "./drops";
export const AllItems: Item[] = [
- ...PrioryOfTheSacredFlame.map((item) => ({
- ...item,
- source: "priory" as const,
- })),
+ ...PrioryOfTheSacredFlame,
+ ...Aldani,
+ ...AraKara,
+ ...Dawnbreaker,
+ ...Floodgate,
+ ...HallsOfAtonement,
+ ...Streets,
+ ...Gambit,
+ ...ManaforgeOmega,
];
export const ItemsById: Record = AllItems.reduce(
@@ -33,36 +43,27 @@ export const ItemsById: Record = AllItems.reduce(
export const ItemsBySlot: Record = _.groupBy(
AllItems,
(item: Item) => item.slot,
-);
+) as Record;
export const itemsForSlot = (slot: Slot): Item[] => ItemsBySlot[slot] ?? [];
function qualityToNumber(quality: Quality): number {
switch (quality) {
- case "champion":
+ case "explorer":
return 1;
- case "hero":
+ case "veteran":
return 2;
- case "myth":
+ case "champion":
return 3;
+ case "hero":
+ return 4;
+ case "myth":
+ return 5;
default:
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 =
(desiredQuality: Quality) =>
({ quality: actualQuality }: { quality: Quality }): boolean =>
@@ -73,9 +74,20 @@ export const qualityAtMost =
({ quality: actualQuality }: { quality: Quality }): boolean =>
qualityToNumber(actualQuality) <= qualityToNumber(desiredQuality);
-export const getUpgrades = (
- equipedItemIds: EquipedItem[],
-): { item: Item; quality: Quality }[] => {
+export const upgradesForItem = ({ quality }: { quality: Quality }) => {
+ switch (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]) => {
const equipedItems = equipedItemIds.flatMap(({ id, quality }) => ({
item: ItemsById[id],
@@ -84,21 +96,45 @@ export const getUpgrades = (
const equipedInSlot = equipedItems.filter(
(item) => item.item.slot === slot,
);
- return items.flatMap((item) => {
- if (equipedInSlot.length === 0) {
- return [{ item, quality: "champion" as Quality }];
+ const itemsForSlot = itemsPerSlot(slot as Slot);
+ const numChampion = equipedInSlot.filter(qualityAtLeast("champion")).length;
+ 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 [];
- }
- if (equipedInSlot.every(qualityAtLeast("hero"))) {
- return [{ item, quality: "myth" as Quality }];
- }
- if (equipedInSlot.every(qualityAtLeast("champion"))) {
- return [{ item, quality: "hero" as Quality }];
- }
- return [];
+
+ return { item, upgradeTypes };
});
});
};
+
+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;
+ }
+};
diff --git a/src/lib/state.ts b/src/lib/state.ts
index 37b6d7e..e8724ec 100644
--- a/src/lib/state.ts
+++ b/src/lib/state.ts
@@ -1,10 +1,15 @@
-import type { EquipedItem, Item, ItemId } from "./types";
+import type { EquipedItem, Item, ItemId, Quality } from "./types";
export type State = {
equipedItems: EquipedItem[];
bisList: ItemId[];
};
+export const emptyState: State = {
+ equipedItems: [],
+ bisList: [],
+};
+
export type Action =
| {
action: "equipItem";
@@ -13,6 +18,11 @@ export type Action =
| {
action: "unequipItem";
item: Item;
+ }
+ | {
+ action: "changeQuality";
+ itemId: ItemId;
+ quality: Quality;
};
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,
),
};
+ case "changeQuality":
+ return {
+ ...state,
+ equipedItems: state.equipedItems.map((item) => {
+ if (item.id === action.itemId) {
+ return {
+ id: item.id,
+ quality: action.quality,
+ };
+ }
+ return item;
+ }),
+ };
}
};
diff --git a/src/lib/types.ts b/src/lib/types.ts
index ee64bae..c6b2ed6 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -18,11 +18,28 @@ export const Slots = [
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 Source = "priory";
+export type Source =
+ | "aldani"
+ | "ara-kara"
+ | "dawnbreaker"
+ | "floodgate"
+ | "gambit"
+ | "halls-of-atonement"
+ | "priory"
+ | "streets"
+ | "manaforge omega";
export type Item = {
id: ItemId;
@@ -35,3 +52,15 @@ export type EquipedItem = {
id: ItemId;
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[] };