Adds source list.

This commit is contained in:
2025-08-18 12:20:33 -07:00
parent 493596e505
commit 8086e9a91f
8 changed files with 159 additions and 54 deletions

View File

@@ -2,41 +2,4 @@
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

View File

@@ -3,6 +3,7 @@ import "./App.css";
import { Equipment } from "./components/Equipment";
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");
@@ -44,6 +45,7 @@ function App() {
onEquip={(item) => dispatch({ action: "equipItem", item })}
onUnequip={(item) => dispatch({ action: "unequipItem", item })}
/>
<SourceList state={state} />
</>
);
}

View File

@@ -0,0 +1,17 @@
.equipedItems {
display: grid;
gap: 8px;
grid-template-columns: 200px 1fr;
align-items: stretch;
}
.gridItem {
padding: 4px 8px;
border: 1px solid #666;
}
.slot {
}
.items {
}

View File

@@ -3,6 +3,7 @@ import type { State } from "../lib/state";
import { Slots, type Item, type Slot } from "../lib/types";
import { ItemLink } from "./ItemLink";
import { ItemTypeahead } from "./ItemTypeahead";
import styles from "./Equipment.module.css";
export type Props = {
state: State;
@@ -13,10 +14,12 @@ export type Props = {
export const Equipment = ({ state, onEquip, onUnequip }: Props) => (
<div>
<ItemTypeahead value={null} onSelect={onEquip} />
<div className={styles.equipedItems}>
{Slots.map((slot) => (
<Slot state={state} slot={slot} onUnequip={onUnequip} key={slot} />
))}
</div>
</div>
);
type SlotProps = {
@@ -26,16 +29,18 @@ type SlotProps = {
};
const Slot = ({ state, slot, onUnequip }: SlotProps) => (
<div>
{slot}:{" "}
<>
<div className={`${styles.slot} ${styles.gridItem}`}>{slot}</div>
<div className={styles.gridItem}>
{state.equipedItems
.map((item) => ({ ...item, item: ItemsById[item.id] }))
.filter((item) => item && item.item.slot === slot)
.map((item) => (
<span>
<div>
<ItemLink item={item.item} />({item.quality})
<button onClick={() => onUnequip(item.item)}>X</button>
</span>
</div>
))}
</div>
</>
);

View File

@@ -0,0 +1,5 @@
.sourceList {
display: grid;
gap: 8px;
grid-template-columns: 1fr 6em 6em 6em;
}

View File

@@ -0,0 +1,40 @@
import * as _ from "lodash";
import { getUpgrades, qualityAtMost } from "../lib/items";
import type { State } from "../lib/state";
import type { Item } from "../lib/types";
import styles from "./SourceList.module.css";
export type Props = {
state: State;
};
export const SourceList = ({ state }: Props) => {
const upgrades = getUpgrades(state.equipedItems);
const upgradesBySource = _.groupBy(
upgrades,
(upgrade: { item: Item }) => upgrade.item.source,
);
return (
<div className={styles.sourceList}>
<div>Source</div>
<div>Champion</div>
<div>Hero</div>
<div>Myth</div>
{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 (
<>
<div>{source}</div>
<div>{champion}</div>
<div>{hero}</div>
<div>{myth}</div>
</>
);
})}
</div>
);
};

View File

@@ -18,6 +18,7 @@ a {
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}

View File

@@ -1,5 +1,11 @@
import _ from "lodash";
import { type Item, type ItemId, type Slot } from "./types";
import {
type EquipedItem,
type Item,
type ItemId,
type Quality,
type Slot,
} from "./types";
export const PrioryOfTheSacredFlame = [
{ id: 219309 as ItemId, slot: "trinket", name: "Tome of Light's Devotion" },
@@ -26,7 +32,73 @@ export const ItemsById: Record<string, Item> = AllItems.reduce(
export const ItemsBySlot: Record<Slot, Item[]> = _.groupBy(
AllItems,
(item) => item.slot,
(item: Item) => item.slot,
);
export const itemsForSlot = (slot: Slot): Item[] => ItemsBySlot[slot] ?? [];
function qualityToNumber(quality: Quality): number {
switch (quality) {
case "champion":
return 1;
case "hero":
return 2;
case "myth":
return 3;
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 =>
qualityToNumber(actualQuality) >= qualityToNumber(desiredQuality);
export const qualityAtMost =
(desiredQuality: Quality) =>
({ quality: actualQuality }: { quality: Quality }): boolean =>
qualityToNumber(actualQuality) <= qualityToNumber(desiredQuality);
export const getUpgrades = (
equipedItemIds: EquipedItem[],
): { item: Item; quality: Quality }[] => {
return Object.entries(ItemsBySlot).flatMap(([slot, items]) => {
const equipedItems = equipedItemIds.flatMap(({ id, quality }) => ({
item: ItemsById[id],
quality,
}));
const equipedInSlot = equipedItems.filter(
(item) => item.item.slot === slot,
);
return items.flatMap((item) => {
if (equipedInSlot.length === 0) {
return [{ item, quality: "champion" as Quality }];
}
// 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 [];
});
});
};