Honor RoM off-hand and ranged equipment slots
This commit is contained in:
1 parent
0206fda9e2
commit
7449abdad2
15 files changed
+150
-29
No files matched your search
@@ -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
@@ -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
@@ -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}.` };
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user