Honor RoM off-hand and ranged equipment slots

This commit is contained in:
phenom committed 2026-09-23 19:59:21 -04:00
1 parent 0206fda9e2
commit 7449abdad2
15 files changed
+150 -29

No files matched your search

+1
View File
@@ -316,6 +316,7 @@ export function OnlineSessionBridge() {
ownerId: PLAYER_EQUIPMENT_OWNER_ID,
ownerKind: "player",
classId: combat.classId,
secondaryClassId: combat.secondaryClassId,
specializationId: combat.specializationId,
role: combat.gearRole,
level: combat.level,
+15 -2
View File
@@ -4045,6 +4045,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
ownerId: PLAYER_EQUIPMENT_OWNER_ID,
ownerKind: "player",
classId,
secondaryClassId,
specializationId,
role: gearRole,
level: progression.level,
@@ -4110,6 +4111,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
ownerId: PLAYER_EQUIPMENT_OWNER_ID,
ownerKind: "player",
classId: state.classId,
secondaryClassId: state.secondaryClassId,
specializationId: state.specializationId,
role: state.gearRole,
level: state.level,
@@ -4382,9 +4384,12 @@ export const useCombatStore = create<CombatState>((set, get) => ({
equipItem: (instanceId, ownerId = PLAYER_EQUIPMENT_OWNER_ID) => {
const state = get();
const ownerMember = ownerId === PLAYER_EQUIPMENT_OWNER_ID
? null
: usePartyStore.getState().members.find((member) => member.id === ownerId);
const ownerClassId = ownerId === PLAYER_EQUIPMENT_OWNER_ID
? state.classId
: usePartyStore.getState().members.find((member) => member.id === ownerId)?.classId;
: ownerMember?.classId;
if (!ownerClassId) {
set({ feedback: nextFeedback("error", "That party member is not available.") });
return false;
@@ -4395,6 +4400,10 @@ export const useCombatStore = create<CombatState>((set, get) => ({
ownerId,
ownerClassId,
instanceId,
{
secondaryClassId: ownerId === PLAYER_EQUIPMENT_OWNER_ID ? state.secondaryClassId : ownerMember?.secondaryClassId,
level: ownerId === PLAYER_EQUIPMENT_OWNER_ID ? state.level : ownerMember?.level,
},
);
if (!change.ok) {
set({ feedback: nextFeedback("error", change.reason ?? "That item cannot be equipped.") });
@@ -4403,11 +4412,12 @@ export const useCombatStore = create<CombatState>((set, get) => ({
const item = state.inventory.find((candidate) => candidate.instanceId === instanceId);
const ownerName = ownerId === PLAYER_EQUIPMENT_OWNER_ID
? "You"
: usePartyStore.getState().members.find((member) => member.id === ownerId)?.name ?? "Companion";
: ownerMember?.name ?? "Companion";
const gearStats = state.specializationId !== null || state.classId.startsWith("rom-") ? resolvedEquipmentStats(change.assignments, state.inventory, {
ownerId: PLAYER_EQUIPMENT_OWNER_ID,
ownerKind: "player",
classId: state.classId,
secondaryClassId: state.secondaryClassId,
specializationId: state.specializationId,
role: state.gearRole,
level: state.level,
@@ -4458,6 +4468,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
ownerId: PLAYER_EQUIPMENT_OWNER_ID,
ownerKind: "player",
classId: state.classId,
secondaryClassId: state.secondaryClassId,
specializationId: state.specializationId,
role: state.gearRole,
level: state.level,
@@ -5040,6 +5051,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
ownerId: PLAYER_EQUIPMENT_OWNER_ID,
ownerKind: "player",
classId: state.classId,
secondaryClassId: state.secondaryClassId,
specializationId: state.specializationId,
role: state.gearRole,
level: state.level,
@@ -5356,6 +5368,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
ownerId: PLAYER_EQUIPMENT_OWNER_ID,
ownerKind: "player",
classId: state.classId,
secondaryClassId: state.secondaryClassId,
specializationId: state.specializationId,
role: state.gearRole,
level: state.level,
+26 -3
View File
@@ -44,6 +44,11 @@ export interface EquipmentChange {
readonly reason?: string;
}
export interface EquipmentClassContext {
readonly secondaryClassId?: ClassId | null;
readonly level?: number;
}
const equipmentSlotSet = new Set<string>(EQUIPMENT_SLOTS);
const ARMOR_RANK: Readonly<Record<string, number>> = Object.freeze({
@@ -62,6 +67,7 @@ const SHIELD_CLASSES = new Set<ClassId>([
const WEAPON_CLASSES: Readonly<Record<string, ReadonlySet<BaseClassId>>> = Object.freeze({
bow: new Set<BaseClassId>(["warrior", "hunter", "rogue"]),
crossbow: new Set<BaseClassId>(["warrior", "hunter", "rogue"]),
dagger: new Set<BaseClassId>(["warrior", "hunter", "rogue", "priest", "shaman", "mage", "warlock", "druid"]),
mace: new Set<BaseClassId>(["warrior", "paladin", "priest", "death-knight", "shaman", "druid"]),
staff: new Set<BaseClassId>(["warrior", "hunter", "priest", "shaman", "mage", "warlock", "druid"]),
@@ -75,7 +81,23 @@ export function equipmentSlotForItem(item: InventoryItem): EquipmentSlot | null
return equipmentSlotSet.has(slot) ? slot as EquipmentSlot : null;
}
export function classCanEquipItem(classId: ClassId, item: InventoryItem): boolean {
export function classCanEquipItem(classId: ClassId, item: InventoryItem, context: EquipmentClassContext = {}): boolean {
const slot = equipmentSlotForItem(item);
if (!slot) return false;
const level = context.level ?? Number.POSITIVE_INFINITY;
if (classId.startsWith("rom-")) {
if (slot === "off-hand" && item.weaponType
&& level < 16) return false;
if (slot === "off-hand" && item.weaponType
&& classId !== "rom-rogue" && context.secondaryClassId !== "rom-rogue") return false;
if (slot === "ranged") {
if (item.weaponType === "bow" || item.weaponType === "crossbow") return classId === "rom-scout"
|| (context.secondaryClassId === "rom-scout" && level >= 10);
if (item.weaponType === "projectile" || item.weaponType === "throwing") return (classId === "rom-rogue"
|| context.secondaryClassId === "rom-rogue") && level >= 12;
return false;
}
}
if (item.armorType === "shield" && !SHIELD_CLASSES.has(classId)) return false;
if (item.armorType && item.armorType !== "shield") {
const requiredRank = ARMOR_RANK[item.armorType] ?? Number.POSITIVE_INFINITY;
@@ -85,7 +107,7 @@ export function classCanEquipItem(classId: ClassId, item: InventoryItem): boolea
const allowedClasses = WEAPON_CLASSES[item.weaponType];
if (!allowedClasses?.has(baseClassFor(classId))) return false;
}
return equipmentSlotForItem(item) !== null;
return true;
}
function mutableAssignments(assignments: EquipmentAssignments): Record<string, EquipmentLoadout> {
@@ -114,12 +136,13 @@ export function equipInventoryItem(
ownerId: string,
ownerClassId: ClassId,
instanceId: string,
classContext: EquipmentClassContext = {},
): EquipmentChange {
const item = inventory.find((candidate) => candidate.instanceId === instanceId);
if (!item) return { ok: false, assignments, reason: "That item is no longer in your inventory." };
const slot = equipmentSlotForItem(item);
if (!slot) return { ok: false, assignments, reason: `${item.name} does not use an equippable slot.` };
if (!classCanEquipItem(ownerClassId, item)) {
if (!classCanEquipItem(ownerClassId, item, classContext)) {
return { ok: false, assignments, reason: `That class cannot equip ${item.name}.` };
}
+23 -9
View File
@@ -41,6 +41,7 @@ export interface HeirloomContext {
readonly ownerId: string;
readonly ownerKind: HeirloomOwnerKind;
readonly classId: ClassId;
readonly secondaryClassId?: ClassId | null;
readonly specializationId: string | null;
readonly role: PartyRole;
readonly level: number;
@@ -96,18 +97,30 @@ function effectiveLevel(context: HeirloomContext): number {
return Math.max(1, Math.min(cap, Math.trunc(Number.isFinite(context.level) ? context.level : 1)));
}
function weaponSlots(layout: WeaponLayout): readonly EquipmentSlot[] {
function weaponSlots(context: HeirloomContext, layout: WeaponLayout): readonly EquipmentSlot[] {
let slots: readonly EquipmentSlot[];
switch (layout) {
case "dual-wield":
case "weapon-shield":
return ["main-hand", "off-hand"];
slots = ["main-hand", "off-hand"];
break;
case "ranged":
return ["main-hand", "ranged"];
slots = ["main-hand", "ranged"];
break;
case "caster":
return ["main-hand", "off-hand", "ranged"];
slots = ["main-hand", "off-hand", "ranged"];
break;
default:
return ["main-hand"];
slots = ["main-hand"];
}
if (!isRomHybridGearClass(context.classId)) return slots;
const level = effectiveLevel(context);
const resolved = new Set(slots);
if (context.classId === "rom-rogue" && level < 16) resolved.delete("off-hand");
if (context.classId === "rom-rogue" && level >= 12) resolved.add("ranged");
if (context.classId === "rom-knight" && context.secondaryClassId === "rom-scout" && level >= 10) resolved.add("ranged");
if (context.classId === "rom-scout" && context.secondaryClassId === "rom-rogue" && level >= 16) resolved.add("off-hand");
return [...resolved];
}
function armorTypeForSlot(classId: ClassId, slot: EquipmentSlot, layout: WeaponLayout): string | null {
@@ -117,8 +130,9 @@ function armorTypeForSlot(classId: ClassId, slot: EquipmentSlot, layout: WeaponL
}
function weaponTypeFor(classId: ClassId, slot: EquipmentSlot, profile: SpecializationDefinition): string | null {
if (slot === "off-hand" && profile.weaponLayout === "dual-wield") return "dagger";
if (slot === "ranged") return profile.gearArchetype === "spell-dps" || profile.gearArchetype === "healer" ? "wand" : "bow";
if (slot === "off-hand" && (profile.weaponLayout === "dual-wield" || classId === "rom-scout")) return "dagger";
if (slot === "ranged") return classId === "rom-rogue" ? "projectile"
: profile.gearArchetype === "spell-dps" || profile.gearArchetype === "healer" ? "wand" : "bow";
if (slot !== "main-hand") return null;
if (classId === "rom-scout" && profile.weaponLayout === "ranged") return "dagger";
if (classId === "rom-mage") return profile.weaponLayout === "two-hand" ? "staff" : "dagger";
@@ -283,7 +297,7 @@ export function resolvedEquipmentLoadout(
): Readonly<Partial<Record<EquipmentSlot, InventoryItem>>> {
const profile = gearProfileFor(context.classId, context.specializationId, context.role);
const result: Partial<Record<EquipmentSlot, InventoryItem>> = {};
for (const slot of [...BASE_SLOTS, ...weaponSlots(profile.weaponLayout)]) {
for (const slot of [...BASE_SLOTS, ...weaponSlots(context, profile.weaponLayout)]) {
if (isRomHybridGearClass(context.classId) && slot === "wrist") continue;
result[slot] = heirloomItem(context, profile, slot);
}
@@ -291,7 +305,7 @@ export function resolvedEquipmentLoadout(
const earned = assignments[context.ownerId] ?? {};
for (const slot of EQUIPMENT_SLOTS) {
const item = earned[slot] ? itemById.get(earned[slot]!) : null;
if (!item || !classCanEquipItem(context.classId, item)) continue;
if (!item || !classCanEquipItem(context.classId, item, context)) continue;
if (slot === "main-hand" && item.inventorySlot === "two-hand") delete result["off-hand"];
if (slot === "off-hand" && result["main-hand"]?.inventorySlot === "two-hand") {
result["main-hand"] = heirloomItem(context, profile, "main-hand", "one-hand");
+2
View File
@@ -789,6 +789,7 @@ function partyAbilityTargetIsValid(
if (ability.dbcSpellId === 490311 || romAbilityNeedsShield(ability)) {
const loadout = resolvedEquipmentLoadout(combat.equipment, combat.inventory, {
ownerId: member.id, ownerKind: "party", classId: member.classId,
secondaryClassId: member.secondaryClassId,
specializationId: member.specializationId, role: member.role, level: member.level,
});
if (ability.dbcSpellId === 490311 && loadout["main-hand"]?.weaponType !== "dagger") return false;
@@ -1222,6 +1223,7 @@ function executePartyAbility(
&& aura.definition.utilityTags?.includes("rom-charged")),
mainHandDagger: member.classId === "rom-rogue" && resolvedEquipmentLoadout(combat.equipment, combat.inventory, {
ownerId: member.id, ownerKind: "party", classId: member.classId,
secondaryClassId: member.secondaryClassId,
specializationId: member.specializationId, role: member.role, level: member.level,
})["main-hand"]?.weaponType === "dagger",
casterMaxHealth: member.maxHealth,
+2
View File
@@ -350,6 +350,7 @@ function createMember(
ownerId: id,
ownerKind: "party",
classId: template.classId,
secondaryClassId: level >= ROM_SECONDARY_UNLOCK_LEVEL ? template.secondaryClassId ?? null : null,
specializationId,
role,
level,
@@ -696,6 +697,7 @@ export const usePartyStore = create<PartyState>((set, get) => ({
ownerId: member.id,
ownerKind: "party",
classId: member.classId,
secondaryClassId: member.secondaryClassId,
specializationId: member.specializationId,
role: member.role,
level: member.level,
+42 -3
View File
@@ -3,7 +3,7 @@ import { abilitiesForCharacter, learnedAbilityForBuild } from "./abilityCatalog"
import { isValidRomClassPair } from "../app/romDualClass";
import { loadCanonicalBenchmarkBuilds } from "./benchmark/benchmarkBuilds";
import { readinessStatusFor } from "./companionSpecReadiness";
import { classCanEquipItem } from "./equipment";
import { classCanEquipItem, equipInventoryItem } from "./equipment";
import { equipmentStatsForItems, resolvedEquipmentLoadout } from "./heirloomEquipment";
import { partyRolesForClass } from "./partyRoles";
import { createConfiguredPartyMember, createDungeonParty, createSpectatorDungeonParty, usePartyStore } from "./partyStore";
@@ -152,34 +152,72 @@ describe("RoM five class validation scope", () => {
ownerId: `rom-five:${classId}`,
ownerKind: "party",
classId,
secondaryClassId: ROM_FIVE_IDENTITIES.find((identity) => identity.classId === classId)?.secondaryClassId,
specializationId: null,
role,
level: 60,
});
if (classId === "rom-knight") expect(loadout["off-hand"]?.armorType).toBe("shield");
if (classId === "rom-knight") {
expect(loadout["off-hand"]?.armorType).toBe("shield");
expect(loadout.ranged?.weaponType).toBe("bow");
}
if (classId === "rom-priest") {
expect(loadout["main-hand"]?.weaponType).toBe("mace");
expect(loadout["off-hand"]?.armorType).toBe("shield");
expect(loadout.ranged).toBeUndefined();
}
if (classId === "rom-scout") {
expect(loadout["main-hand"]?.weaponType).toBe("dagger");
expect(loadout["main-hand"]?.inventorySlot).toBe("one-hand");
expect(loadout.ranged?.weaponType).toBe("bow");
expect(loadout["off-hand"]?.weaponType).toBe("dagger");
}
if (classId === "rom-rogue") {
expect(loadout["main-hand"]?.weaponType).toBe("dagger");
expect(loadout["off-hand"]?.weaponType).toBe("dagger");
expect(loadout.ranged?.weaponType).toBe("projectile");
}
if (classId === "rom-mage") {
expect(loadout["main-hand"]?.weaponType).toBe("staff");
expect(loadout["main-hand"]?.inventorySlot).toBe("two-hand");
expect(loadout["off-hand"]).toBeUndefined();
expect(loadout.ranged).toBeUndefined();
}
expect(loadout.wrist).toBeUndefined();
for (const item of Object.values(loadout)) expect(classCanEquipItem(classId, item!)).toBe(true);
for (const item of Object.values(loadout)) expect(classCanEquipItem(classId, item!, {
secondaryClassId: ROM_FIVE_IDENTITIES.find((identity) => identity.classId === classId)?.secondaryClassId,
level: 60,
})).toBe(true);
}
});
it("opens the independent ranged and off-hand positions at the source training levels", () => {
const loadout = (classId: "rom-knight" | "rom-scout" | "rom-rogue", secondaryClassId: "rom-scout" | "rom-rogue" | "rom-warrior", level: number) =>
resolvedEquipmentLoadout({}, [], {
ownerId: `rom-slots:${classId}`,
ownerKind: "player",
classId,
secondaryClassId: level >= 10 ? secondaryClassId : null,
specializationId: null,
role: classId === "rom-knight" ? "tank" : "damage",
level,
});
expect(loadout("rom-knight", "rom-scout", 9).ranged).toBeUndefined();
expect(loadout("rom-knight", "rom-scout", 10).ranged?.weaponType).toBe("bow");
expect(loadout("rom-scout", "rom-rogue", 15)["off-hand"]).toBeUndefined();
expect(loadout("rom-scout", "rom-rogue", 16)["off-hand"]?.weaponType).toBe("dagger");
expect(loadout("rom-rogue", "rom-warrior", 11).ranged).toBeUndefined();
expect(loadout("rom-rogue", "rom-warrior", 12).ranged?.weaponType).toBe("projectile");
expect(loadout("rom-rogue", "rom-warrior", 15)["off-hand"]).toBeUndefined();
expect(loadout("rom-rogue", "rom-warrior", 16)["off-hand"]?.weaponType).toBe("dagger");
const knightBow = loadout("rom-knight", "rom-scout", 60).ranged!;
expect(classCanEquipItem("rom-knight", knightBow, { level: 60 })).toBe(false);
expect(equipInventoryItem({}, [knightBow], "knight", "rom-knight", knightBow.instanceId,
{ secondaryClassId: "rom-scout", level: 60 }).ok).toBe(true);
expect(equipInventoryItem({}, [knightBow], "knight", "rom-knight", knightBow.instanceId,
{ secondaryClassId: "rom-priest", level: 60 }).ok).toBe(false);
});
it("grows the five hybrid combat loadouts through the heirloom levels", () => {
for (const { classId, role } of CASES) {
const identity = ROM_FIVE_IDENTITIES.find((entry) => entry.classId === classId && entry.role === role)!;
@@ -190,6 +228,7 @@ describe("RoM five class validation scope", () => {
ownerId: `rom-hybrid:${classId}`,
ownerKind: "player",
classId,
secondaryClassId: level >= 10 ? identity.secondaryClassId : null,
specializationId: null,
role,
level,
+4 -1
View File
@@ -144,6 +144,7 @@ function AvatarAssetProbe({ onSettled }: { readonly onSettled: () => void }) {
const equipment = useCombatStore((state) => state.equipment);
const level = useCombatStore((state) => state.level);
const specializationId = useCombatStore((state) => state.specializationId);
const secondaryClassId = useCombatStore((state) => state.secondaryClassId);
const gearRole = useCombatStore((state) => state.gearRole);
const [payload, setPayload] = useState<AvatarWarmupPayload | null>(null);
@@ -162,6 +163,7 @@ function AvatarAssetProbe({ onSettled }: { readonly onSettled: () => void }) {
ownerId: PLAYER_EQUIPMENT_OWNER_ID,
ownerKind: "player",
classId: activeCharacter.classId,
secondaryClassId,
specializationId,
role: gearRole,
level,
@@ -179,6 +181,7 @@ function AvatarAssetProbe({ onSettled }: { readonly onSettled: () => void }) {
ownerId: member.id,
ownerKind: "party",
classId: member.classId,
secondaryClassId: member.secondaryClassId,
specializationId: member.specializationId,
role: member.role,
level: member.level,
@@ -210,7 +213,7 @@ function AvatarAssetProbe({ onSettled }: { readonly onSettled: () => void }) {
return () => {
active = false;
};
}, [activeCharacter, equipment, gearRole, inventory, level, members, onSettled, specializationId]);
}, [activeCharacter, equipment, gearRole, inventory, level, members, onSettled, secondaryClassId, specializationId]);
if (!payload) return null;
if (!payload.modelUrls.length) {
+1
View File
@@ -301,6 +301,7 @@ function PartyActors({
ownerId: member.id,
ownerKind: "party",
classId: member.classId,
secondaryClassId: member.secondaryClassId,
specializationId: member.specializationId,
role: member.role,
level: member.level,
+3 -1
View File
@@ -476,6 +476,7 @@ export function PlayerRig() {
const equipment = useCombatStore((state) => state.equipment);
const level = useCombatStore((state) => state.level);
const specializationId = useCombatStore((state) => state.specializationId);
const secondaryClassId = useCombatStore((state) => state.secondaryClassId);
const gearRole = useCombatStore((state) => state.gearRole);
const equipmentItems = useMemo(
() => activeCharacter ? resolvedEquipmentItems(
@@ -485,12 +486,13 @@ export function PlayerRig() {
ownerId: PLAYER_EQUIPMENT_OWNER_ID,
ownerKind: "player",
classId: activeCharacter.classId,
secondaryClassId,
specializationId,
role: gearRole,
level,
},
) : [],
[activeCharacter, equipment, gearRole, inventory, level, specializationId],
[activeCharacter, equipment, gearRole, inventory, level, secondaryClassId, specializationId],
);
const bodyRef = useRef<RapierRigidBody>(null);
const avatarRef = useRef<Object3D>(null);
+3 -1
View File
@@ -30,6 +30,7 @@ function itemType(inventorySlot: string): string {
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);
@@ -52,6 +53,7 @@ export function BossLootWindow() {
ownerId: PLAYER_EQUIPMENT_OWNER_ID,
ownerKind: "player" as const,
classId,
secondaryClassId,
specializationId,
role,
level,
@@ -88,7 +90,7 @@ export function BossLootWindow() {
<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)
const comparison = classCanEquipItem(classId, item, { secondaryClassId, level })
? compareEquipmentUpgrade(item, loadout, profile, heirloomContext)
: null;
return <article
+10 -4
View File
@@ -53,6 +53,7 @@ interface EquipmentOwner {
readonly name: string;
readonly detail: string;
readonly classId: ClassId;
readonly secondaryClassId?: ClassId | null;
readonly specializationId: string | null;
readonly role: PartyRole;
readonly level: number;
@@ -189,6 +190,7 @@ function EquipmentDialog({ readOnly = false }: { readOnly?: boolean }) {
const inventory = useCombatStore((state) => state.inventory);
const equipment = useCombatStore((state) => state.equipment);
const playerClassId = useCombatStore((state) => state.classId);
const playerSecondaryClassId = useCombatStore((state) => state.secondaryClassId);
const playerLevel = useCombatStore((state) => state.level);
const playerMaxHealth = useCombatStore((state) => state.maxHealth);
const playerGearStats = useCombatStore((state) => state.gearStats);
@@ -204,6 +206,7 @@ function EquipmentDialog({ readOnly = false }: { readOnly?: boolean }) {
name: character?.name ?? "You",
detail: "Player",
classId: playerClassId,
secondaryClassId: playerSecondaryClassId,
specializationId: playerSpecializationId,
role: playerGearRole,
level: playerLevel,
@@ -214,12 +217,13 @@ function EquipmentDialog({ readOnly = false }: { readOnly?: boolean }) {
name: member.name,
detail: `${member.specialization} · ${member.role === "damage" ? "DPS" : member.role}`,
classId: member.classId,
secondaryClassId: member.secondaryClassId,
specializationId: member.specializationId,
role: member.role,
level: member.level,
ownerKind: "party" as const,
})),
], [character?.name, members, playerClassId, playerGearRole, playerLevel, playerSpecializationId]);
], [character?.name, members, playerClassId, playerSecondaryClassId, playerGearRole, playerLevel, playerSpecializationId]);
const [ownerId, setOwnerId] = useState(PLAYER_EQUIPMENT_OWNER_ID);
const [inspectedId, setInspectedId] = useState(() => inventory.at(-1)?.instanceId ?? "");
const [statPage, setStatPage] = useState<StatPage>("attributes");
@@ -240,13 +244,14 @@ function EquipmentDialog({ readOnly = false }: { readOnly?: boolean }) {
ownerId: owner.id,
ownerKind: owner.ownerKind,
classId: owner.classId,
secondaryClassId: owner.secondaryClassId,
specializationId: owner.specializationId,
role: owner.role,
level: owner.level,
};
const loadout = useMemo(
() => resolvedEquipmentLoadout(equipment, inventory, heirloomContext),
[equipment, inventory, owner.classId, owner.id, owner.level, owner.ownerKind, owner.role, owner.specializationId],
[equipment, inventory, owner.classId, owner.secondaryClassId, owner.id, owner.level, owner.ownerKind, owner.role, owner.specializationId],
);
const earnedLoadout = equipment[owner.id] ?? {};
const sortedInventory = useMemo(() => [...inventory].sort((left, right) => (
@@ -285,7 +290,7 @@ function EquipmentDialog({ readOnly = false }: { readOnly?: boolean }) {
const inspectedOwnerId = inspected ? equippedOwnerId(equipment, inspected.instanceId) : null;
const inspectedOwner = inspectedOwnerId ? ownerById.get(inspectedOwnerId) : null;
const compatible = inspected ? classCanEquipItem(owner.classId, inspected) : false;
const compatible = inspected ? classCanEquipItem(owner.classId, inspected, owner) : false;
const gearLevel = resolvedAverageItemLevel(equipment, inventory, heirloomContext);
const gearProfile = gearProfileFor(owner.classId, owner.specializationId, owner.role);
const inspectedComparison = inspected && compatible
@@ -345,6 +350,7 @@ function EquipmentDialog({ readOnly = false }: { readOnly?: boolean }) {
ownerId: candidate.id,
ownerKind: candidate.ownerKind,
classId: candidate.classId,
secondaryClassId: candidate.secondaryClassId,
specializationId: candidate.specializationId,
role: candidate.role,
level: candidate.level,
@@ -415,7 +421,7 @@ function EquipmentDialog({ readOnly = false }: { readOnly?: boolean }) {
{sortedInventory.map((item) => {
const assignedId = equippedOwnerId(equipment, item.instanceId);
const assignedOwner = assignedId ? ownerById.get(assignedId) : null;
const canEquip = classCanEquipItem(owner.classId, item);
const canEquip = classCanEquipItem(owner.classId, item, owner);
const comparison = canEquip ? compareEquipmentUpgrade(item, loadout, gearProfile, heirloomContext) : null;
return (
<ControllerButton