122 lines
5.1 KiB
TypeScript
122 lines
5.1 KiB
TypeScript
import { useEffect, type CSSProperties } from "react";
|
||
import { BOSS_CARD_RARITY_INFO } from "../game/bossCards";
|
||
import { useCombatStore } from "../game/combatStore";
|
||
import { useGameStore } from "../game/store";
|
||
import { classCanEquipItem, PLAYER_EQUIPMENT_OWNER_ID } from "../game/equipment";
|
||
import {
|
||
compareEquipmentUpgrade,
|
||
resolvedEquipmentLoadout,
|
||
} from "../game/heirloomEquipment";
|
||
import { gearProfileFor } from "../game/specializationCatalog";
|
||
|
||
export const BOSS_LOOT_AUTO_DISMISS_MS = 10_000;
|
||
|
||
export function scheduleBossLootAutoDismiss(
|
||
rewardId: number,
|
||
dismiss: (id: number) => void,
|
||
): () => void {
|
||
const timeoutId = setTimeout(() => dismiss(rewardId), BOSS_LOOT_AUTO_DISMISS_MS);
|
||
return () => clearTimeout(timeoutId);
|
||
}
|
||
|
||
function itemType(inventorySlot: string): string {
|
||
return inventorySlot
|
||
.split(/[-_]/)
|
||
.filter(Boolean)
|
||
.map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`)
|
||
.join(" ");
|
||
}
|
||
|
||
export function BossLootWindow() {
|
||
const reward = useCombatStore((state) => state.bossLootRewards[0] ?? null);
|
||
const classId = useCombatStore((state) => state.classId);
|
||
const secondaryClassId = useCombatStore((state) => state.secondaryClassId);
|
||
const specializationId = useCombatStore((state) => state.specializationId);
|
||
const role = useCombatStore((state) => state.gearRole);
|
||
const level = useCombatStore((state) => state.level);
|
||
const equipment = useCombatStore((state) => state.equipment);
|
||
const inventory = useCombatStore((state) => state.inventory);
|
||
const visibleRewardId = reward?.id;
|
||
|
||
useEffect(() => {
|
||
if (visibleRewardId === undefined) return;
|
||
return scheduleBossLootAutoDismiss(
|
||
visibleRewardId,
|
||
(rewardId) => useCombatStore.getState().dismissBossLootReward(rewardId),
|
||
);
|
||
}, [visibleRewardId]);
|
||
|
||
if (!reward) return null;
|
||
|
||
const profile = gearProfileFor(classId, specializationId, role);
|
||
const heirloomContext = {
|
||
ownerId: PLAYER_EQUIPMENT_OWNER_ID,
|
||
ownerKind: "player" as const,
|
||
classId,
|
||
secondaryClassId,
|
||
specializationId,
|
||
role,
|
||
level,
|
||
};
|
||
const loadout = resolvedEquipmentLoadout(equipment, inventory, heirloomContext);
|
||
|
||
const dismiss = () => useCombatStore.getState().dismissBossLootReward(reward.id);
|
||
const viewInventory = () => {
|
||
dismiss();
|
||
useGameStore.getState().openOverlay("inventory");
|
||
};
|
||
|
||
return (
|
||
<section
|
||
className="boss-loot-window"
|
||
role="dialog"
|
||
aria-modal="false"
|
||
aria-labelledby={`boss-loot-title-${reward.id}`}
|
||
onPointerDown={(event) => event.stopPropagation()}
|
||
>
|
||
<header>
|
||
<span aria-hidden="true">◆</span>
|
||
<p>
|
||
<small>Boss defeated</small>
|
||
<strong id={`boss-loot-title-${reward.id}`}>{reward.bossName}</strong>
|
||
</p>
|
||
<button type="button" onClick={dismiss} aria-label="Close loot window">×</button>
|
||
</header>
|
||
|
||
<div className="boss-loot-window__body">
|
||
<p className="boss-loot-window__eyebrow">Loot acquired automatically</p>
|
||
<div className="boss-loot-window__items">
|
||
{reward.cards?.rarities.map((rarity) => <article key={rarity} className="boss-loot-window__item" style={{ "--item-quality": BOSS_CARD_RARITY_INFO[rarity].color } as CSSProperties}>
|
||
<b aria-hidden="true">◆</b><p><strong>{BOSS_CARD_RARITY_INFO[rarity].label} Boss Card</strong><small>{reward.bossName} · Collection log</small></p><em>+1</em>
|
||
</article>)}
|
||
{reward.items.map((item) => {
|
||
const comparison = classCanEquipItem(classId, item, { secondaryClassId, level })
|
||
? compareEquipmentUpgrade(item, loadout, profile, heirloomContext)
|
||
: null;
|
||
return <article
|
||
key={item.instanceId}
|
||
className={`boss-loot-window__item equipment-quality--${item.quality}`}
|
||
>
|
||
<b aria-hidden="true">{item.name.slice(0, 2).toUpperCase()}</b>
|
||
<p>
|
||
<strong>{item.name}</strong>
|
||
<small>{item.quality} · Item level {item.itemLevel} · {itemType(item.inventorySlot)}</small>
|
||
{comparison && <small>{comparison.isUpgrade ? `Upgrade · +${comparison.scoreDelta.toFixed(1)} score` : `${comparison.scoreDelta.toFixed(1)} score`}</small>}
|
||
</p>
|
||
{item.quantity > 1 ? <em>×{item.quantity}</em> : null}
|
||
</article>;
|
||
})}
|
||
{!reward.items.length && !reward.cards?.rarities.length ? (
|
||
<p className="boss-loot-window__empty">No items dropped this time.</p>
|
||
) : null}
|
||
</div>
|
||
</div>
|
||
|
||
<footer>
|
||
<span>{reward.cards?.rarities.length ? `${reward.cards.rarities.length} card${reward.cards.rarities.length === 1 ? "" : "s"} added to collection${reward.items.length ? ` · ${reward.items.length} items acquired` : ""}` : `${reward.items.length} items added to shared inventory`}</span>
|
||
{reward.cards ? <button type="button" onClick={() => { dismiss(); useGameStore.getState().openOverlay("boss-cards"); }}>View collection</button> : <button type="button" onClick={viewInventory}>View inventory</button>}
|
||
</footer>
|
||
</section>
|
||
);
|
||
}
|