Adds source list.
This commit is contained in:
37
src/App.css
37
src/App.css
@@ -2,41 +2,4 @@
|
|||||||
max-width: 1280px;
|
max-width: 1280px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 2rem;
|
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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import "./App.css";
|
|||||||
import { Equipment } from "./components/Equipment";
|
import { Equipment } from "./components/Equipment";
|
||||||
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");
|
||||||
@@ -44,6 +45,7 @@ function App() {
|
|||||||
onEquip={(item) => dispatch({ action: "equipItem", item })}
|
onEquip={(item) => dispatch({ action: "equipItem", item })}
|
||||||
onUnequip={(item) => dispatch({ action: "unequipItem", item })}
|
onUnequip={(item) => dispatch({ action: "unequipItem", item })}
|
||||||
/>
|
/>
|
||||||
|
<SourceList state={state} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
17
src/components/Equipment.module.css
Normal file
17
src/components/Equipment.module.css
Normal 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 {
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import type { State } from "../lib/state";
|
|||||||
import { Slots, type Item, type Slot } from "../lib/types";
|
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";
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
state: State;
|
state: State;
|
||||||
@@ -13,10 +14,12 @@ export type Props = {
|
|||||||
export const Equipment = ({ state, onEquip, onUnequip }: Props) => (
|
export const Equipment = ({ state, onEquip, onUnequip }: Props) => (
|
||||||
<div>
|
<div>
|
||||||
<ItemTypeahead value={null} onSelect={onEquip} />
|
<ItemTypeahead value={null} onSelect={onEquip} />
|
||||||
|
<div className={styles.equipedItems}>
|
||||||
{Slots.map((slot) => (
|
{Slots.map((slot) => (
|
||||||
<Slot state={state} slot={slot} onUnequip={onUnequip} key={slot} />
|
<Slot state={state} slot={slot} onUnequip={onUnequip} key={slot} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
type SlotProps = {
|
type SlotProps = {
|
||||||
@@ -26,16 +29,18 @@ type SlotProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Slot = ({ state, slot, onUnequip }: SlotProps) => (
|
const Slot = ({ state, slot, onUnequip }: SlotProps) => (
|
||||||
<div>
|
<>
|
||||||
{slot}:{" "}
|
<div className={`${styles.slot} ${styles.gridItem}`}>{slot}</div>
|
||||||
|
<div className={styles.gridItem}>
|
||||||
{state.equipedItems
|
{state.equipedItems
|
||||||
.map((item) => ({ ...item, item: ItemsById[item.id] }))
|
.map((item) => ({ ...item, item: ItemsById[item.id] }))
|
||||||
.filter((item) => item && item.item.slot === slot)
|
.filter((item) => item && item.item.slot === slot)
|
||||||
.map((item) => (
|
.map((item) => (
|
||||||
<span>
|
<div>
|
||||||
<ItemLink item={item.item} />({item.quality})
|
<ItemLink item={item.item} />({item.quality})
|
||||||
<button onClick={() => onUnequip(item.item)}>X</button>
|
<button onClick={() => onUnequip(item.item)}>X</button>
|
||||||
</span>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
5
src/components/SourceList.module.css
Normal file
5
src/components/SourceList.module.css
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
.sourceList {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
grid-template-columns: 1fr 6em 6em 6em;
|
||||||
|
}
|
||||||
40
src/components/SourceList.tsx
Normal file
40
src/components/SourceList.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -18,6 +18,7 @@ a {
|
|||||||
color: #646cff;
|
color: #646cff;
|
||||||
text-decoration: inherit;
|
text-decoration: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
color: #535bf2;
|
color: #535bf2;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import _ from "lodash";
|
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 = [
|
export const PrioryOfTheSacredFlame = [
|
||||||
{ id: 219309 as ItemId, slot: "trinket", name: "Tome of Light's Devotion" },
|
{ 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(
|
export const ItemsBySlot: Record<Slot, Item[]> = _.groupBy(
|
||||||
AllItems,
|
AllItems,
|
||||||
(item) => item.slot,
|
(item: Item) => item.slot,
|
||||||
);
|
);
|
||||||
|
|
||||||
export const itemsForSlot = (slot: Slot): Item[] => ItemsBySlot[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 [];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user