From 33d648398ec8c340615568cdc57c44a0f69264a3 Mon Sep 17 00:00:00 2001 From: XP Date: Thu, 19 Sep 2024 16:49:54 -0700 Subject: [PATCH] Logic to find identical-or-better items, UI WIP --- packages/common-ui/styles/common.less | 18 +++++++ packages/core/src/gear.ts | 47 +++++++++++++++++++ packages/core/src/sheet.ts | 25 +++++++++- .../frontend/src/scripts/components/items.ts | 19 ++++++-- 4 files changed, 103 insertions(+), 6 deletions(-) diff --git a/packages/common-ui/styles/common.less b/packages/common-ui/styles/common.less index bd38118c..d5b528fe 100644 --- a/packages/common-ui/styles/common.less +++ b/packages/common-ui/styles/common.less @@ -2437,6 +2437,24 @@ gear-set-viewer { //box-shadow: 0 0 3px 3px var(--stat-color) inset; } } + + .item-name-holder-view { + display: flex; + flex-direction: row; + + span.item-name { + flex-basis: 1px; + flex-grow: 99; + overflow: hidden; + text-overflow: ellipsis; + } + + span.item-alts { + flex-basis: fit-content; + flex-shrink: 0; + flex-grow: 0; + } + } } .food-view-table { diff --git a/packages/core/src/gear.ts b/packages/core/src/gear.ts index f2b0fc08..0aa96c8c 100644 --- a/packages/core/src/gear.ts +++ b/packages/core/src/gear.ts @@ -944,3 +944,50 @@ export type ItemSingleStatDetail = { }; +/** + * Returns true if 'candidateItem' has identical or better stats than 'baseItem'. + * + * In order to be true, every stat must be identical or greater. + * + * @param candidateItem + * @param baseItem + */ +export function isSameOrBetterItem(candidateItem: GearItem, baseItem: GearItem): boolean { + // TODO: consider materia slots + // Ultimate weapons are equivalent to savage raid but with an extra materia slot. So an ultimate weapon should + // be considered an acceptable replacement for a savage weapon, but not the other way around. + + + // Phase 1: Raw stats + const candidateStats = candidateItem.stats; + const baseStats = baseItem.stats; + for (const [statKey, baseValue] of Object.entries(baseStats)) { + const candidateValue = candidateStats[statKey] as number; + if (candidateValue < baseValue) { + return false; + } + } + // Phase 2: Materia + // The logic here is to just check that every materia slot in the base item: + // 1. Exists in the candidate item, + // 2. is at least the same grade, and + // 3. is high grade if the source slot is also high grade + for (const baseSlot of baseItem.materiaSlots) { + const index = baseItem.materiaSlots.indexOf(baseSlot); + if (index in candidateItem.materiaSlots) { + const candidateSlot = candidateItem.materiaSlots[index]; + if (candidateSlot.maxGrade < baseSlot.maxGrade) { + return false; + } + else if (baseSlot.allowsHighGrade && !candidateSlot.allowsHighGrade) { + return false; + } + } + else { + // Not enough slots + return false; + } + } + + return true; +} diff --git a/packages/core/src/sheet.ts b/packages/core/src/sheet.ts index 98f19dab..19b0059e 100644 --- a/packages/core/src/sheet.ts +++ b/packages/core/src/sheet.ts @@ -1,4 +1,4 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ // TODO: get back to fixing this at some point +/* eslint-disable @typescript-eslint/no-explicit-any */ // TODO: get back to fixing this at some point import { CURRENT_MAX_LEVEL, defaultItemDisplaySettings, @@ -36,7 +36,7 @@ import { SimExport, Substat } from "@xivgear/xivmath/geartypes"; -import {CharacterGearSet} from "./gear"; +import {CharacterGearSet, isSameOrBetterItem} from "./gear"; import {DataManager, makeDataManager} from "./datamanager"; import {Inactivitytimer} from "./util/inactivitytimer"; import {writeProxy} from "./util/proxies"; @@ -837,4 +837,25 @@ export class GearPlanSheet { } this._sets.forEach(set => set.forceRecalc()); } + + /** + * Get items that could replace the given item - either identical or better. + * + * @param thisItem + */ + getAltItemsFor(thisItem: GearItem): GearItem[] { + // Ignore this for relics - consider them to be incompatible until we can + // figure out a good way to do this. + if (thisItem.isCustomRelic) { + return []; + } + return this.dataManager.allItems.filter(otherItem => { + // Cannot be the same item + return otherItem.id !== thisItem.id + // Must be same slot + && otherItem.occGearSlotName === thisItem.occGearSlotName + // Must be better or same stats + && isSameOrBetterItem(otherItem, thisItem); + }); + } } \ No newline at end of file diff --git a/packages/frontend/src/scripts/components/items.ts b/packages/frontend/src/scripts/components/items.ts index f3be237d..604cd290 100644 --- a/packages/frontend/src/scripts/components/items.ts +++ b/packages/frontend/src/scripts/components/items.ts @@ -744,7 +744,8 @@ export class GearItemsViewTable extends CustomTable const item = { slot: slot, item: equippedItem, - slotId: slotId + slotId: slotId, + alts: sheet.getAltItemsFor(equippedItem) }; slotItem = equippedItem; data.push(item); @@ -786,10 +787,20 @@ export class GearItemsViewTable extends CustomTable shortName: "itemname", displayName: headingText, getter: item => { - return item.item.name; + return item; }, - renderer: (name: string) => { - return document.createTextNode(shortenItemName(name)); + renderer: (item) => { + const name = item.item.name; + const itemNameSpan = quickElement('span', ['item-name'], [shortenItemName(name)]); + const out = quickElement('div', ['item-name-holder-view'], [itemNameSpan]); + if (item.alts.length > 0) { + const altSpan = quickElement('span', ['item-alts'], [`(+${item.alts.length} others)`]); + altSpan.addEventListener('click', () => { + console.log("Alts", item.alts); + }); + out.appendChild(altSpan); + } + return out; }, // initialWidth: 300, },