From 7556fade3d5b66c346ca886bbe1e4a6f9170cd38 Mon Sep 17 00:00:00 2001 From: Drew Haven Date: Fri, 22 Aug 2025 13:11:47 -0700 Subject: [PATCH] Adds crafted items, weapon config, generally working --- src/App.tsx | 8 +- src/components/Equipment.tsx | 32 +++-- src/components/ItemLink.module.css | 7 ++ src/components/ItemLink.tsx | 7 ++ src/components/ItemTypeahead.tsx | 2 +- src/components/QualitySelector.tsx | 4 +- src/components/SourceList.module.css | 4 + src/components/SourceList.tsx | 92 ++++++++++++--- src/components/WeaponConfigPicker.tsx | 28 +++++ src/lib/drops/aldani.ts | 1 + src/lib/drops/crafted.ts | 16 +++ src/lib/drops/dawnbreaker.ts | 14 +++ src/lib/drops/floodgate.ts | 6 + src/lib/drops/index.ts | 31 +++-- src/lib/drops/manaforge-omega.ts | 163 ++++++++++++++++++++++++++ src/lib/items.ts | 61 +++++----- src/lib/state.ts | 12 ++ src/lib/types.ts | 14 ++- 18 files changed, 431 insertions(+), 71 deletions(-) create mode 100644 src/components/ItemLink.module.css create mode 100644 src/components/WeaponConfigPicker.tsx create mode 100644 src/lib/drops/crafted.ts diff --git a/src/App.tsx b/src/App.tsx index 898da94..7fca1df 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,7 +4,8 @@ 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 { WeaponConfig, type ItemId } from "./lib/types"; +import { WeaponConfigPicker } from "./components/WeaponConfigPicker"; function App() { const [state, dispatch] = useReducer(withLoadingAction(reducer), "loading"); @@ -25,6 +26,7 @@ function App() { state: { equipedItems: [{ id: 219309 as ItemId, quality: "champion" }], bisList: [], + weaponConfig: WeaponConfig.TwoHander, }, }); } @@ -46,11 +48,15 @@ function App() { }} >

WoW Gear Finder

+

Config

+ +

Equipment

dispatch({ action: "equipItem", item })} onUnequip={(item) => dispatch({ action: "unequipItem", item })} /> +

Upgrades

); diff --git a/src/components/Equipment.tsx b/src/components/Equipment.tsx index cc24afc..3a7e89a 100644 --- a/src/components/Equipment.tsx +++ b/src/components/Equipment.tsx @@ -1,4 +1,4 @@ -import { ItemsById } from "../lib/items"; +import { ItemsById, itemsPerSlot } from "../lib/items"; import type { State } from "../lib/state"; import { Slots, type Item, type Slot } from "../lib/types"; import { ItemLink } from "./ItemLink"; @@ -12,16 +12,28 @@ export type Props = { onUnequip: (item: Item) => void; }; -export const Equipment = ({ state, onEquip, onUnequip }: Props) => ( -
- -
- {Slots.map((slot) => ( - - ))} +export const Equipment = ({ state, onEquip, onUnequip }: Props) => { + return ( +
+ item && onEquip(item)} /> +
+ {Slots.map((slot) => { + if (itemsPerSlot(slot, state.weaponConfig) > 0) { + return ( + + ); + } + return null; + })} +
-
-); + ); +}; type SlotProps = { state: State; diff --git a/src/components/ItemLink.module.css b/src/components/ItemLink.module.css new file mode 100644 index 0000000..09167be --- /dev/null +++ b/src/components/ItemLink.module.css @@ -0,0 +1,7 @@ +.itemLink { + display: inline-flex; + flex-direction: row; + align-items: center; + gap: 8px; + font-size: 20px; +} diff --git a/src/components/ItemLink.tsx b/src/components/ItemLink.tsx index c897503..5bfa550 100644 --- a/src/components/ItemLink.tsx +++ b/src/components/ItemLink.tsx @@ -1,4 +1,5 @@ import type { Item } from "../lib/types"; +import * as styles from "./ItemLink.module.css"; export type Props = { item: Item; @@ -9,7 +10,13 @@ export const ItemLink = ({ item }: Props) => ( href={`https://www.wowhead.com/item=${item.id}`} target="_blank" rel="noopener noreferrer" + className={styles.itemLink} > + {item.icon && ( + + )} {item.name} ); diff --git a/src/components/ItemTypeahead.tsx b/src/components/ItemTypeahead.tsx index 60499ec..a8e3612 100644 --- a/src/components/ItemTypeahead.tsx +++ b/src/components/ItemTypeahead.tsx @@ -5,8 +5,8 @@ import { ComboboxOptions, } from "@headlessui/react"; import { useState } from "react"; -import { AllItems } from "../lib/items"; import type { Item } from "../lib/types"; +import { AllItems } from "../lib/drops"; export type Props = { value: Item | null; diff --git a/src/components/QualitySelector.tsx b/src/components/QualitySelector.tsx index 7796584..67911dc 100644 --- a/src/components/QualitySelector.tsx +++ b/src/components/QualitySelector.tsx @@ -16,9 +16,9 @@ export const QualitySelector = ({ item }: Props) => { }); }; return ( - {Object.values(Quality).map((quality) => ( - ))} diff --git a/src/components/SourceList.module.css b/src/components/SourceList.module.css index 5d0d646..c4e1e36 100644 --- a/src/components/SourceList.module.css +++ b/src/components/SourceList.module.css @@ -3,3 +3,7 @@ gap: 8px; grid-template-columns: 1fr 6em 6em 6em; } + +.itemList { + grid-column: 1 / span 4; +} diff --git a/src/components/SourceList.tsx b/src/components/SourceList.tsx index 0eda1db..74e7be9 100644 --- a/src/components/SourceList.tsx +++ b/src/components/SourceList.tsx @@ -1,4 +1,5 @@ -import * as _ from "lodash"; +import _ from "lodash"; +import { useState } from "react"; import { getUpgrades, hasUpgradeType } from "../lib/items"; import type { State } from "../lib/state"; import { @@ -7,6 +8,7 @@ import { type Source, type Upgrade, } from "../lib/types"; +import { ItemLink } from "./ItemLink"; import styles from "./SourceList.module.css"; export type Props = { @@ -14,12 +16,12 @@ export type Props = { }; export const SourceList = ({ state }: Props) => { - const upgrades = getUpgrades(state.equipedItems); + const upgrades = getUpgrades(state.equipedItems, state.weaponConfig); - const upgradesBySource: Record = _.groupBy( + const upgradesBySource = _.groupBy( upgrades, (upgrade: { item: Item }) => upgrade.item.source, - ); + ) as Record; return (
@@ -28,22 +30,74 @@ export const SourceList = ({ state }: Props) => {
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}
- - ); - }, + ([source, items]: [string, Upgrade[]]) => ( + + ), )}
); }; + +type SourceInfoProps = { + source: Source; + items: Upgrade[]; +}; + +const SourceInfo = ({ source, items }: SourceInfoProps) => { + const [upgradeTypeShown, setUpgradeTypeShown] = useState( + null, + ); + + const championUpgrades = items.filter(hasUpgradeType(UpgradeType.Champion)); + const heroUpgrades = items.filter(hasUpgradeType(UpgradeType.Hero)); + const mythUpgrades = items.filter(hasUpgradeType(UpgradeType.Myth)); + + const toggleIsOpen = (upgradeType: UpgradeType) => { + if (upgradeTypeShown === upgradeType) { + setUpgradeTypeShown(null); + } + setUpgradeTypeShown(upgradeType); + }; + + return ( + <> +
{source}
+
toggleIsOpen(UpgradeType.Champion)}> + {championUpgrades.length} +
+
toggleIsOpen(UpgradeType.Hero)}> + {heroUpgrades.length} +
+
toggleIsOpen(UpgradeType.Myth)}> + {mythUpgrades.length} +
+ {upgradeTypeShown === UpgradeType.Champion && ( +
+ {championUpgrades.map((upgrade) => ( +
+ ({upgrade.item.slot}) +
+ ))} +
+ )} + {upgradeTypeShown === UpgradeType.Hero && ( +
+ {heroUpgrades.map((upgrade) => ( +
+ +
+ ))} +
+ )} + {upgradeTypeShown === UpgradeType.Myth && ( +
+ {mythUpgrades.map((upgrade) => ( +
+ +
+ ))} +
+ )} + + ); +}; diff --git a/src/components/WeaponConfigPicker.tsx b/src/components/WeaponConfigPicker.tsx new file mode 100644 index 0000000..d17fed0 --- /dev/null +++ b/src/components/WeaponConfigPicker.tsx @@ -0,0 +1,28 @@ +import { useAppState } from "../lib/context/StateContext"; +import { WeaponConfig } from "../lib/types"; + +export const WeaponConfigPicker = () => { + const { state, dispatch } = useAppState(); + + const handleOnChange = (event: React.ChangeEvent) => { + const value = event.target.value?.trim(); + if (value && value.length > 0) { + dispatch({ + action: "changeWeaponConfig", + weaponConfig: value as WeaponConfig, + }); + } + }; + + return ( +
+ +
+ ); +}; diff --git a/src/lib/drops/aldani.ts b/src/lib/drops/aldani.ts index 8380bd2..871f53b 100644 --- a/src/lib/drops/aldani.ts +++ b/src/lib/drops/aldani.ts @@ -12,6 +12,7 @@ export const Aldani = [ slot: "trinket", name: "Incorporeal Warpclaw", source: "aldani" as const, + icon: "inv_misc_nightsaberclaw_mana", }, { id: 242481 as ItemId, diff --git a/src/lib/drops/crafted.ts b/src/lib/drops/crafted.ts new file mode 100644 index 0000000..f922dfa --- /dev/null +++ b/src/lib/drops/crafted.ts @@ -0,0 +1,16 @@ +import type { Item } from "../types"; + +export const Crafted: Item[] = [ + { + id: 219334, + name: "Rune-Branded Armguards", + slot: "wrist", + source: "crafted", + }, + { + id: 219502, + name: "Adrenal Surge Clasp", + slot: "waist", + source: "crafted", + }, +] as Item[]; diff --git a/src/lib/drops/dawnbreaker.ts b/src/lib/drops/dawnbreaker.ts index e7946af..9166aca 100644 --- a/src/lib/drops/dawnbreaker.ts +++ b/src/lib/drops/dawnbreaker.ts @@ -73,4 +73,18 @@ export const Dawnbreaker = [ name: "Bludgeons of Blistering Wind", source: "dawnbreaker" as const, }, + { + id: 221136, + slot: "finger", + name: "Devout Zealot's Ring", + source: "dawnbreaker" as const, + icon: "inv_11_0_nerubian_ring_01_color4", + }, + { + id: 221141, + slot: "finger", + name: "High Nerubian Signet", + source: "dawnbreaker" as const, + icon: "inv_11_0_nerubian_ring_02_color5", + }, ] as Item[]; diff --git a/src/lib/drops/floodgate.ts b/src/lib/drops/floodgate.ts index ffe4fc8..2d673b0 100644 --- a/src/lib/drops/floodgate.ts +++ b/src/lib/drops/floodgate.ts @@ -7,6 +7,12 @@ export const Floodgate = [ name: "Improvised Seaforium Pacemaker", source: "floodgate" as const, }, + { + id: 232543 as ItemId, + slot: "trinket", + name: "Ringing Ritual Mud", + source: "floodgate" as const, + }, { id: 234499 as ItemId, slot: "wrist", diff --git a/src/lib/drops/index.ts b/src/lib/drops/index.ts index d4f0b7a..c533710 100644 --- a/src/lib/drops/index.ts +++ b/src/lib/drops/index.ts @@ -1,8 +1,23 @@ -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"; +import type { Item } from "../types"; +import { Aldani } from "./aldani"; +import { AraKara } from "./ara-kara"; +import { Crafted } from "./crafted"; +import { Dawnbreaker } from "./dawnbreaker"; +import { Floodgate } from "./floodgate"; +import { HallsOfAtonement } from "./halls-of-atonement"; +import { ManaforgeOmega } from "./manaforge-omega"; +import { PrioryOfTheSacredFlame } from "./priory"; +import { Streets, Gambit } from "./tazavesh"; + +export const AllItems: Item[] = [ + ...PrioryOfTheSacredFlame, + ...Aldani, + ...AraKara, + ...Dawnbreaker, + ...Floodgate, + ...HallsOfAtonement, + ...Streets, + ...Gambit, + ...ManaforgeOmega, + ...Crafted, +]; diff --git a/src/lib/drops/manaforge-omega.ts b/src/lib/drops/manaforge-omega.ts index 1abce8a..5d5ef83 100644 --- a/src/lib/drops/manaforge-omega.ts +++ b/src/lib/drops/manaforge-omega.ts @@ -37,4 +37,167 @@ export const ManaforgeOmega = [ slot: "back", source: "manaforge omega", }, + { + id: 242395, + name: "Astral Antenna", + slot: "trinket", + source: "manaforge omega", + }, + { + id: 243306, + name: "Interloper's Reinforced Sandals", + slot: "feet", + source: "manaforge omega", + }, + { + id: 242397, + name: "Sigil of the Cosmic Hunt", + slot: "trinket", + source: "manaforge omega", + }, + { + id: 242394, + name: "Eradicating Arcanocore", + slot: "trinket", + source: "manaforge omega", + }, + { + id: 237562, + name: "Time-Compressed Wristguards", + slot: "wrist", + source: "manaforge omega", + }, + { + id: 237739, + name: "Obliteration Beamglaive", + slot: "2h-weapon", + source: "manaforge omega", + }, + { + id: 242391, + name: "Soulbinder's Embrace", + slot: "trinket", + source: "manaforge omega", + }, + { + id: 237734, + name: "Oath-Breaker's Recompense", + slot: "1h-weapon", + source: "manaforge omega", + }, + { + id: 237726, + name: "Marvel of Technomancy", + slot: "2h-weapon", + source: "manaforge omega", + }, + { + id: 237533, + name: "Atomic Phasebelt", + slot: "waist", + source: "manaforge omega", + }, + { + id: 237738, + name: "Unbound Training Claws", + slot: "1h-weapon", + source: "manaforge omega", + }, + { + id: 238027, + name: "Harvested Creephide Cord", + slot: "waist", + source: "manaforge omega", + }, + { + id: 238031, + name: "Veiled Manta Vest", + slot: "chest", + source: "manaforge omega", + }, + { + id: 237813, + name: "Factory-Issue Plexhammer", + slot: "1h-weapon", + source: "manaforge omega", + }, + { + id: 237565, + name: "Kinetic Dunerunners", + slot: "feet", + source: "manaforge omega", + }, + { + id: 237525, + name: "Irradiated Impurity Filter", + slot: "head", + source: "manaforge omega", + }, + { + id: 237541, + name: "Darksorrow's Corrupted Carapace", + slot: "chest", + source: "manaforge omega", + }, + { + id: 237731, + name: "Ergospheric Cudgel", + slot: "1h-weapon", + source: "manaforge omega", + }, + { + id: 237552, + name: "Deathbound Shoulderpads", + slot: "shoulder", + source: "manaforge omega", + }, + { + id: 237546, + name: "Bindings of Lost Essence", + slot: "wrist", + source: "manaforge omega", + }, + { + id: 237540, + name: "Winged Gamma Handlers", + slot: "hands", + source: "manaforge omega", + }, + { + id: 237557, + name: "Reaper's Dreadbelt", + slot: "waist", + source: "manaforge omega", + }, + { + id: 237553, + name: "Laboratory Test Slippers", + slot: "feet", + source: "manaforge omega", + }, + { + id: 237531, + name: "Elite Shadowguard Legwraps", + slot: "legs", + source: "manaforge omega", + }, + { + id: 230026, + name: "Scrapfield 9001", + slot: "trinket", + source: "manaforge omega", + }, + { + id: 228854, + name: "Bilgerat's Discarded Slacks", + slot: "legs", + source: "manaforge omega", + }, + { + id: 242401, + name: "Brand of Ceaseless Ire", + slot: "trinket", + source: "manaforge omega", + icon: "inv_112_raidtrinkets_manaforgetanktrinket3", + }, ] as const; diff --git a/src/lib/items.ts b/src/lib/items.ts index 3e2333f..3cc4d90 100644 --- a/src/lib/items.ts +++ b/src/lib/items.ts @@ -1,6 +1,7 @@ import _ from "lodash"; import { UpgradeType, + WeaponConfig, type EquipedItem, type Item, type ItemId, @@ -8,29 +9,7 @@ import { type Slot, type Upgrade, } from "./types"; -import { - Aldani, - AraKara, - Dawnbreaker, - Floodgate, - HallsOfAtonement, - PrioryOfTheSacredFlame, - Streets, - Gambit, - ManaforgeOmega, -} from "./drops"; - -export const AllItems: Item[] = [ - ...PrioryOfTheSacredFlame, - ...Aldani, - ...AraKara, - ...Dawnbreaker, - ...Floodgate, - ...HallsOfAtonement, - ...Streets, - ...Gambit, - ...ManaforgeOmega, -]; +import { AllItems } from "./drops"; export const ItemsById: Record = AllItems.reduce( (db, item) => { @@ -87,7 +66,10 @@ export const upgradesForItem = ({ quality }: { quality: Quality }) => { } }; -export const getUpgrades = (equipedItemIds: EquipedItem[]): Upgrade[] => { +export const getUpgrades = ( + equipedItemIds: EquipedItem[], + weaponConfig: WeaponConfig, +): Upgrade[] => { return Object.entries(ItemsBySlot).flatMap(([slot, items]) => { const equipedItems = equipedItemIds.flatMap(({ id, quality }) => ({ item: ItemsById[id], @@ -96,7 +78,16 @@ export const getUpgrades = (equipedItemIds: EquipedItem[]): Upgrade[] => { const equipedInSlot = equipedItems.filter( (item) => item.item.slot === slot, ); - const itemsForSlot = itemsPerSlot(slot as Slot); + + const itemsForSlot = itemsPerSlot(slot as Slot, weaponConfig); + + if (itemsForSlot === 0) { + console.log( + `Skipping ${slot} because it has no items with ${weaponConfig}`, + ); + return []; + } + const numChampion = equipedInSlot.filter(qualityAtLeast("champion")).length; const numHero = equipedInSlot.filter(qualityAtLeast("hero")).length; const numMyth = equipedInSlot.filter(qualityAtLeast("myth")).length; @@ -105,7 +96,6 @@ export const getUpgrades = (equipedItemIds: EquipedItem[]): Upgrade[] => { 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) { @@ -128,12 +118,27 @@ export const hasUpgradeType = (upgrade: Upgrade): boolean => upgrade.upgradeTypes.includes(type); -export const itemsPerSlot = (slot: Slot): number => { +export const itemsPerSlot = ( + slot: Slot, + weaponConfig: WeaponConfig, +): number => { switch (slot) { case "trinket": case "finger": - case "1h-weapon": return 2; + case "1h-weapon": + switch (weaponConfig) { + case WeaponConfig.DualWield: + return 2; + case WeaponConfig.TwoHander: + return 0; + default: + return 1; + } + case "off-hand": + return weaponConfig === WeaponConfig.WeaponAndOffHand ? 1 : 0; + case "2h-weapon": + return weaponConfig === WeaponConfig.TwoHander ? 1 : 0; default: return 1; } diff --git a/src/lib/state.ts b/src/lib/state.ts index e8724ec..b687c6f 100644 --- a/src/lib/state.ts +++ b/src/lib/state.ts @@ -1,13 +1,16 @@ import type { EquipedItem, Item, ItemId, Quality } from "./types"; +import { WeaponConfig } from "./types"; export type State = { equipedItems: EquipedItem[]; bisList: ItemId[]; + weaponConfig: WeaponConfig; }; export const emptyState: State = { equipedItems: [], bisList: [], + weaponConfig: WeaponConfig.TwoHander, // Default weapon config }; export type Action = @@ -23,6 +26,10 @@ export type Action = action: "changeQuality"; itemId: ItemId; quality: Quality; + } + | { + action: "changeWeaponConfig"; + weaponConfig: WeaponConfig; }; export const reducer = (state: State, action: Action): State => { @@ -58,6 +65,11 @@ export const reducer = (state: State, action: Action): State => { return item; }), }; + case "changeWeaponConfig": + return { + ...state, + weaponConfig: action.weaponConfig, + }; } }; diff --git a/src/lib/types.ts b/src/lib/types.ts index c6b2ed6..727e093 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -13,7 +13,7 @@ export const Slots = [ "trinket", "1h-weapon", "2h-weapon", - "shield", + "off-hand", ] as const; export type Slot = (typeof Slots)[number]; @@ -39,13 +39,15 @@ export type Source = | "halls-of-atonement" | "priory" | "streets" - | "manaforge omega"; + | "manaforge omega" + | "crafted"; export type Item = { id: ItemId; slot: Slot; name: string; source: Source; + icon?: string; // Wowhead icon name (not ID) }; export type EquipedItem = { @@ -64,3 +66,11 @@ export const UpgradeType = { export type UpgradeType = (typeof UpgradeType)[keyof typeof UpgradeType]; export type Upgrade = { item: Item; upgradeTypes: UpgradeType[] }; + +export const WeaponConfig = { + WeaponAndOffHand: "weapon and off-hand", + DualWield: "dual wield", + TwoHander: "two-hander", +}; + +export type WeaponConfig = (typeof WeaponConfig)[keyof typeof WeaponConfig];