From 87c908ca68bf62670b557eb33a23f3a6357cad8f Mon Sep 17 00:00:00 2001 From: Drew Haven Date: Wed, 20 Aug 2025 19:52:51 -0700 Subject: [PATCH] Adds quality selection, all dungeons --- package-lock.json | 7 ++ package.json | 1 + src/App.tsx | 12 ++- src/components/Equipment.module.css | 14 ++- src/components/Equipment.tsx | 47 +++++++--- src/components/QualitySelector.tsx | 27 ++++++ src/components/SourceList.tsx | 41 +++++---- src/lib/context/StateContext.ts | 17 ++++ src/lib/drops/aldani.ts | 46 ++++++++++ src/lib/drops/ara-kara.ts | 52 +++++++++++ src/lib/drops/dawnbreaker.ts | 76 +++++++++++++++++ src/lib/drops/floodgate.ts | 46 ++++++++++ src/lib/drops/halls-of-atonement.ts | 52 +++++++++++ src/lib/drops/index.ts | 8 ++ src/lib/drops/manaforge-omega.ts | 40 +++++++++ src/lib/drops/priory.ts | 34 ++++++++ src/lib/drops/tazavesh.ts | 115 +++++++++++++++++++++++++ src/lib/items.ts | 128 ++++++++++++++++++---------- src/lib/state.ts | 25 +++++- src/lib/types.ts | 33 ++++++- 20 files changed, 737 insertions(+), 84 deletions(-) create mode 100644 src/components/QualitySelector.tsx create mode 100644 src/lib/context/StateContext.ts create mode 100644 src/lib/drops/aldani.ts create mode 100644 src/lib/drops/ara-kara.ts create mode 100644 src/lib/drops/dawnbreaker.ts create mode 100644 src/lib/drops/floodgate.ts create mode 100644 src/lib/drops/halls-of-atonement.ts create mode 100644 src/lib/drops/index.ts create mode 100644 src/lib/drops/manaforge-omega.ts create mode 100644 src/lib/drops/priory.ts create mode 100644 src/lib/drops/tazavesh.ts 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[] };