Adds quality selection, all dungeons

This commit is contained in:
2025-08-20 19:52:51 -07:00
parent 8086e9a91f
commit 87c908ca68
20 changed files with 737 additions and 84 deletions

7
package-lock.json generated
View File

@@ -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",

View File

@@ -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"

View File

@@ -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>
); );
} }

View File

@@ -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;
} }

View File

@@ -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
.map((item) => ({ ...item, item: ItemsById[item.id] }))
.filter((item) => item && item.item.slot === slot);
return (
<> <>
<div className={`${styles.slot} ${styles.gridItem}`}>{slot}</div> <div className={`${styles.slot} ${styles.gridItem}`}>{slot}</div>
<div className={styles.gridItem}> {
{state.equipedItems // Show placeholder if there are no items
.map((item) => ({ ...item, item: ItemsById[item.id] })) items.length === 0 && (
.filter((item) => item && item.item.slot === slot) <>
.map((item) => ( <div className={`${styles.gridItem} ${styles.item}`}></div>
<div> <div className={`${styles.gridItem} ${styles.quality}`}></div>
<ItemLink item={item.item} />({item.quality}) <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>
</> </>
); );
};

View 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>
);
};

View File

@@ -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,10 +27,13 @@ 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),
).length;
const hero = items.filter(hasUpgradeType(UpgradeType.Hero)).length;
const myth = items.filter(hasUpgradeType(UpgradeType.Myth)).length;
return ( return (
<> <>
<div>{source}</div> <div>{source}</div>
@@ -34,7 +42,8 @@ export const SourceList = ({ state }: Props) => {
<div>{myth}</div> <div>{myth}</div>
</> </>
); );
})} },
)}
</div> </div>
); );
}; };

View 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
View 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
View 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;

View 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[];

View 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;

View 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
View 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";

View 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
View 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
View 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;

View File

@@ -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;
}
};

View File

@@ -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;
}),
};
} }
}; };

View File

@@ -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[] };