From 3653c09d0816e8056944669cf278e24255081661 Mon Sep 17 00:00:00 2001 From: Adrian Klingen Date: Thu, 26 Sep 2024 21:17:48 +0200 Subject: [PATCH 1/6] Extend reforge success toast --- .../components/suggest_reforges_action.tsx | 88 ++++++++++++++++--- .../shared/_bootstrap_style_overrides.scss | 10 +-- 2 files changed, 79 insertions(+), 19 deletions(-) diff --git a/ui/core/components/suggest_reforges_action.tsx b/ui/core/components/suggest_reforges_action.tsx index b6ba24a6c2..40ceb4516d 100644 --- a/ui/core/components/suggest_reforges_action.tsx +++ b/ui/core/components/suggest_reforges_action.tsx @@ -5,17 +5,19 @@ import { Constraint, greaterEq, lessEq, Model, Options, Solution, solve } from ' import * as Mechanics from '../constants/mechanics.js'; import { IndividualSimUI } from '../individual_sim_ui'; -import { Player } from '../player'; -import { Class, ItemSlot, PseudoStat, Spec, Stat } from '../proto/common'; -import { StatCapType } from '../proto/ui'; +import { Player, ReforgeData } from '../player'; +import { Class, ItemSlot, PseudoStat, ReforgeStat, Spec, Stat } from '../proto/common'; +import { IndividualSimSettings, StatCapType } from '../proto/ui'; +import { EquippedItem } from '../proto_utils/equipped_item'; import { Gear } from '../proto_utils/gear'; -import { slotNames, statCapTypeNames } from '../proto_utils/names'; +import { shortSecondaryStatNames, slotNames, statCapTypeNames } from '../proto_utils/names'; import { pseudoStatIsCapped, StatCap, Stats, UnitStat, UnitStatPresets } from '../proto_utils/stats'; import { SpecTalents } from '../proto_utils/utils'; import { Sim } from '../sim'; import { ActionGroupItem } from '../sim_ui'; import { EventID, TypedEvent } from '../typed_event'; import { isDevMode, sleep } from '../utils'; +import { CopyButton } from './copy_button'; import { BooleanPicker } from './pickers/boolean_picker'; import { EnumPicker } from './pickers/enum_picker'; import { NumberPicker, NumberPickerConfig } from './pickers/number_picker'; @@ -87,6 +89,8 @@ export class ReforgeOptimizer { readonly freezeItemSlotsChangeEmitter = new TypedEvent(); protected freezeItemSlots = false; protected frozenItemSlots = new Map(); + protected previousGear: (EquippedItem | null)[] | null = null; + protected currentGear: (EquippedItem | null)[] | null = null; constructor(simUI: IndividualSimUI, options?: ReforgeOptimizerOptions) { this.simUI = simUI; @@ -117,15 +121,10 @@ export class ReforgeOptimizer { try { performance.mark('reforge-optimization-start'); await this.optimizeReforges(); - new Toast({ - variant: 'success', - body: 'Reforge optimization complete!', - }); - } catch { - new Toast({ - variant: 'error', - body: 'Reforge optimization failed. Please try again, or report the issue if it persists.', - }); + this.onReforgeDone(); + } catch (error) { + console.log(error); + this.onReforgeError(); } finally { performance.mark('reforge-optimization-end'); if (isDevMode()) @@ -650,7 +649,9 @@ export class ReforgeOptimizer { console.log('The following slots will not be cleared:'); console.log(Array.from(this.frozenItemSlots.keys()).filter(key => this.frozenItemSlots.get(key))); } - const baseGear = this.player.getGear().withoutReforges(this.player.canDualWield2H(), this.frozenItemSlots); + const previousGear = this.player.getGear(); + this.previousGear = previousGear.getEquippedItems(); + const baseGear = previousGear.withoutReforges(this.player.canDualWield2H(), this.frozenItemSlots); const baseStats = await this.updateGear(baseGear); // Compute effective stat caps for just the Reforge contribution @@ -673,6 +674,7 @@ export class ReforgeOptimizer { // Solve in multiple passes to enforce caps await this.solveModel(baseGear, validatedWeights, reforgeCaps, reforgeSoftCaps, variables, constraints); + this.currentGear = this.player.getGear().getEquippedItems(); } async updateGear(gear: Gear): Promise { @@ -1001,4 +1003,62 @@ export class ReforgeOptimizer { } return statPoints; } + + onReforgeDone() { + const itemSlots = this.player.getGear().getItemSlots(); + const changedSlots: (ReforgeData | undefined)[] = Array(itemSlots.length); + for (const slot of itemSlots) { + const prev = this.previousGear?.[slot]; + const current = this.currentGear?.[slot]; + const currentReforge = current?.reforge ? this.player.getReforgeData(current, current?.reforge) : undefined; + if (!ReforgeStat.equals(prev?.reforge || undefined, current?.reforge || undefined)) changedSlots[slot] = currentReforge; + } + const hasReforgeChanges = changedSlots.some(Boolean); + + const copyButtonContainerRef = ref(); + const changedReforgeMessage = ( + <> +

The following items were reforged:

+
    + {changedSlots.map((reforge, slot) => { + if (reforge) { + const slotName = slotNames.get(slot); + const { fromStat, toStat } = reforge; + const fromText = shortSecondaryStatNames.get(fromStat); + const toText = shortSecondaryStatNames.get(toStat); + return ( +
  • + {slotName}: {fromText} → {toText} +
  • + ); + } + })} +
+
+ + ); + + if (hasReforgeChanges) { + const settingsExport = IndividualSimSettings.toJson(this.simUI.toProto()); + if (settingsExport) + new CopyButton(copyButtonContainerRef.value!, { + extraCssClasses: ['btn-outline-primary'], + getContent: () => JSON.stringify(settingsExport), + text: 'Copy to WoWSims', + }); + } + + new Toast({ + variant: 'success', + body: hasReforgeChanges ? changedReforgeMessage : <>No reforge changes were made!, + delay: hasReforgeChanges ? 5000 : 3000, + }); + } + + onReforgeError() { + new Toast({ + variant: 'error', + body: 'Reforge optimization failed. Please try again, or report the issue if it persists.', + }); + } } diff --git a/ui/scss/shared/_bootstrap_style_overrides.scss b/ui/scss/shared/_bootstrap_style_overrides.scss index 400fcbcb6d..0d21fe4dbd 100644 --- a/ui/scss/shared/_bootstrap_style_overrides.scss +++ b/ui/scss/shared/_bootstrap_style_overrides.scss @@ -233,14 +233,14 @@ $gray-800: #323232; .toast { --icon-color: var(--bs-warning); - i { - line-height: 1; - color: var(--icon-color); - } - .toast-header { border-bottom: 0; padding-bottom: 0; + + i { + line-height: 1; + color: var(--icon-color); + } } .btn-close { From 019b45acbdfe0b10bf77fa583a0ba45edae9bfb7 Mon Sep 17 00:00:00 2001 From: Adrian Klingen Date: Thu, 26 Sep 2024 21:19:40 +0200 Subject: [PATCH 2/6] Change button typo --- ui/core/components/suggest_reforges_action.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/core/components/suggest_reforges_action.tsx b/ui/core/components/suggest_reforges_action.tsx index 40ceb4516d..a83dda80e6 100644 --- a/ui/core/components/suggest_reforges_action.tsx +++ b/ui/core/components/suggest_reforges_action.tsx @@ -1044,7 +1044,7 @@ export class ReforgeOptimizer { new CopyButton(copyButtonContainerRef.value!, { extraCssClasses: ['btn-outline-primary'], getContent: () => JSON.stringify(settingsExport), - text: 'Copy to WoWSims', + text: 'Copy to Reforge Lite', }); } From 834ddf8e87b464bd88fa58ff160be71b1f905164 Mon Sep 17 00:00:00 2001 From: Adrian Klingen Date: Fri, 27 Sep 2024 06:22:46 +0200 Subject: [PATCH 3/6] PR feedback --- .../components/gear_picker/gear_picker.tsx | 2 +- .../components/gear_picker/selector_modal.tsx | 2 +- .../individual_sim_ui/reforge_summary.tsx | 2 +- .../components/suggest_reforges_action.tsx | 22 +++++++++---------- ui/core/player.ts | 4 ++-- ui/core/proto_utils/gear.ts | 11 ++++++++++ 6 files changed, 26 insertions(+), 17 deletions(-) diff --git a/ui/core/components/gear_picker/gear_picker.tsx b/ui/core/components/gear_picker/gear_picker.tsx index 6f15200ec8..dff5b2d8a0 100644 --- a/ui/core/components/gear_picker/gear_picker.tsx +++ b/ui/core/components/gear_picker/gear_picker.tsx @@ -156,7 +156,7 @@ export class ItemRenderer extends Component { } if (newItem.reforge) { - const reforgeData = this.player.getReforgeData(newItem, newItem.reforge); + const reforgeData = Player.getReforgeData(newItem, newItem.reforge); const fromText = shortSecondaryStatNames.get(newItem.reforge?.fromStat); const toText = shortSecondaryStatNames.get(newItem.reforge?.toStat); this.reforgeElem.innerText = `Reforged ${Math.abs(reforgeData.fromAmount)} ${fromText} → ${reforgeData.toAmount} ${toText}`; diff --git a/ui/core/components/gear_picker/selector_modal.tsx b/ui/core/components/gear_picker/selector_modal.tsx index a7631f7415..5eabc9a005 100644 --- a/ui/core/components/gear_picker/selector_modal.tsx +++ b/ui/core/components/gear_picker/selector_modal.tsx @@ -448,7 +448,7 @@ export default class SelectorModal extends BaseModal { }), computeEP: (reforge: ReforgeData) => this.player.computeReforgingEP(reforge), equippedToItemFn: (equippedItem: EquippedItem | null) => - equippedItem?.reforge ? this.player.getReforgeData(equippedItem, equippedItem.reforge) : null, + equippedItem?.reforge ? Player.getReforgeData(equippedItem, equippedItem.reforge) : null, onRemove: (eventID: number) => { const equippedItem = gearData.getEquippedItem(); if (equippedItem) { diff --git a/ui/core/components/individual_sim_ui/reforge_summary.tsx b/ui/core/components/individual_sim_ui/reforge_summary.tsx index 556bf52e18..200a704bbe 100644 --- a/ui/core/components/individual_sim_ui/reforge_summary.tsx +++ b/ui/core/components/individual_sim_ui/reforge_summary.tsx @@ -36,7 +36,7 @@ export class ReforgeSummary extends Component { gear.getItemSlots().forEach(itemSlot => { const item = gear.getEquippedItem(itemSlot); if (item?.reforge && item.reforge?.id !== 0) { - const reforge = this.player.getReforgeData(item, item.reforge); + const reforge = Player.getReforgeData(item, item.reforge); if (reforge) { const { fromStat, toStat, fromAmount, toAmount } = reforge; diff --git a/ui/core/components/suggest_reforges_action.tsx b/ui/core/components/suggest_reforges_action.tsx index a83dda80e6..b85ca344c7 100644 --- a/ui/core/components/suggest_reforges_action.tsx +++ b/ui/core/components/suggest_reforges_action.tsx @@ -8,7 +8,6 @@ import { IndividualSimUI } from '../individual_sim_ui'; import { Player, ReforgeData } from '../player'; import { Class, ItemSlot, PseudoStat, ReforgeStat, Spec, Stat } from '../proto/common'; import { IndividualSimSettings, StatCapType } from '../proto/ui'; -import { EquippedItem } from '../proto_utils/equipped_item'; import { Gear } from '../proto_utils/gear'; import { shortSecondaryStatNames, slotNames, statCapTypeNames } from '../proto_utils/names'; import { pseudoStatIsCapped, StatCap, Stats, UnitStat, UnitStatPresets } from '../proto_utils/stats'; @@ -89,8 +88,8 @@ export class ReforgeOptimizer { readonly freezeItemSlotsChangeEmitter = new TypedEvent(); protected freezeItemSlots = false; protected frozenItemSlots = new Map(); - protected previousGear: (EquippedItem | null)[] | null = null; - protected currentGear: (EquippedItem | null)[] | null = null; + protected previousGear = new Map(); + protected currentGear = new Map(); constructor(simUI: IndividualSimUI, options?: ReforgeOptimizerOptions) { this.simUI = simUI; @@ -650,7 +649,7 @@ export class ReforgeOptimizer { console.log(Array.from(this.frozenItemSlots.keys()).filter(key => this.frozenItemSlots.get(key))); } const previousGear = this.player.getGear(); - this.previousGear = previousGear.getEquippedItems(); + this.previousGear = previousGear.getAllReforges(); const baseGear = previousGear.withoutReforges(this.player.canDualWield2H(), this.frozenItemSlots); const baseStats = await this.updateGear(baseGear); @@ -674,7 +673,7 @@ export class ReforgeOptimizer { // Solve in multiple passes to enforce caps await this.solveModel(baseGear, validatedWeights, reforgeCaps, reforgeSoftCaps, variables, constraints); - this.currentGear = this.player.getGear().getEquippedItems(); + this.currentGear = this.player.getGear().getAllReforges(); } async updateGear(gear: Gear): Promise { @@ -1006,21 +1005,20 @@ export class ReforgeOptimizer { onReforgeDone() { const itemSlots = this.player.getGear().getItemSlots(); - const changedSlots: (ReforgeData | undefined)[] = Array(itemSlots.length); + const changedSlots = new Map(); for (const slot of itemSlots) { - const prev = this.previousGear?.[slot]; - const current = this.currentGear?.[slot]; - const currentReforge = current?.reforge ? this.player.getReforgeData(current, current?.reforge) : undefined; - if (!ReforgeStat.equals(prev?.reforge || undefined, current?.reforge || undefined)) changedSlots[slot] = currentReforge; + const prev = this.previousGear.get(slot); + const current = this.currentGear.get(slot); + if (!ReforgeStat.equals(prev?.reforge, current?.reforge)) changedSlots.set(slot, current); } - const hasReforgeChanges = changedSlots.some(Boolean); + const hasReforgeChanges = changedSlots.size; const copyButtonContainerRef = ref(); const changedReforgeMessage = ( <>

The following items were reforged:

    - {changedSlots.map((reforge, slot) => { + {[...changedSlots].map(([slot, reforge]) => { if (reforge) { const slotName = slotNames.get(slot); const { fromStat, toStat } = reforge; diff --git a/ui/core/player.ts b/ui/core/player.ts index 872f06b954..f1d49a4b46 100644 --- a/ui/core/player.ts +++ b/ui/core/player.ts @@ -456,7 +456,7 @@ export class Player { getAvailableReforgings(equippedItem: EquippedItem): Array { const withRandomSuffixStats = equippedItem.getWithRandomSuffixStats(); return this.sim.db.getAvailableReforges(withRandomSuffixStats.item).map(reforge => { - return this.getReforgeData(equippedItem, reforge); + return Player.getReforgeData(equippedItem, reforge); }); } @@ -465,7 +465,7 @@ export class Player { return this.sim.db.getReforgeById(id); } - getReforgeData(equippedItem: EquippedItem, reforge: ReforgeStat): ReforgeData { + static getReforgeData(equippedItem: EquippedItem, reforge: ReforgeStat): ReforgeData { const withRandomSuffixStats = equippedItem.getWithRandomSuffixStats(); const item = withRandomSuffixStats.item; const fromAmount = Math.ceil(-item.stats[reforge.fromStat] * reforge.multiplier); diff --git a/ui/core/proto_utils/gear.ts b/ui/core/proto_utils/gear.ts index 3f6ca3bae6..ab9356eca8 100644 --- a/ui/core/proto_utils/gear.ts +++ b/ui/core/proto_utils/gear.ts @@ -1,3 +1,4 @@ +import { Player, ReforgeData } from '../player'; import { EquipmentSpec, GemColor, ItemSlot, ItemSpec, ItemSwap, Profession, SimDatabase, SimEnchant, SimGem, SimItem } from '../proto/common.js'; import { UIEnchant as Enchant, UIGem as Gem, UIItem as Item } from '../proto/ui.js'; import { isBluntWeaponType, isSharpWeaponType } from '../proto_utils/utils.js'; @@ -372,6 +373,16 @@ export class Gear extends BaseGear { return setItemCount; } + + getAllReforges() { + const reforgedItems = new Map(); + this.getEquippedItems().forEach((item, slot) => { + if (!item?.reforge) return; + const reforgeData = Player.getReforgeData(item, item.reforge); + reforgedItems.set(slot, reforgeData); + }); + return reforgedItems; + } } /** From 1ade9d4bfb13e67ca73a84a78fdeb4100eadb906 Mon Sep 17 00:00:00 2001 From: Adrian Klingen Date: Fri, 27 Sep 2024 06:24:04 +0200 Subject: [PATCH 4/6] Move error --- ui/core/components/suggest_reforges_action.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ui/core/components/suggest_reforges_action.tsx b/ui/core/components/suggest_reforges_action.tsx index b85ca344c7..4a327d655b 100644 --- a/ui/core/components/suggest_reforges_action.tsx +++ b/ui/core/components/suggest_reforges_action.tsx @@ -122,8 +122,7 @@ export class ReforgeOptimizer { await this.optimizeReforges(); this.onReforgeDone(); } catch (error) { - console.log(error); - this.onReforgeError(); + this.onReforgeError(error); } finally { performance.mark('reforge-optimization-end'); if (isDevMode()) @@ -1053,7 +1052,9 @@ export class ReforgeOptimizer { }); } - onReforgeError() { + onReforgeError(error: any) { + if (isDevMode()) console.log(error); + new Toast({ variant: 'error', body: 'Reforge optimization failed. Please try again, or report the issue if it persists.', From b68caea79f42cde09ce2dc58481a0b64cc4e2ab2 Mon Sep 17 00:00:00 2001 From: Adrian Klingen Date: Fri, 27 Sep 2024 09:44:59 +0200 Subject: [PATCH 5/6] More PR feedback: move reforgeData to equipped_item --- .../components/gear_picker/gear_picker.tsx | 8 ++--- ui/core/components/gear_picker/item_list.tsx | 4 +-- .../components/gear_picker/selector_modal.tsx | 7 ++-- .../individual_sim_ui/reforge_summary.tsx | 31 ++++++++---------- .../components/suggest_reforges_action.tsx | 15 +++++---- ui/core/player.ts | 32 ++----------------- ui/core/proto_utils/equipped_item.ts | 32 ++++++++++++++++++- ui/core/proto_utils/gear.ts | 7 ++-- 8 files changed, 66 insertions(+), 70 deletions(-) diff --git a/ui/core/components/gear_picker/gear_picker.tsx b/ui/core/components/gear_picker/gear_picker.tsx index dff5b2d8a0..2c6f971b63 100644 --- a/ui/core/components/gear_picker/gear_picker.tsx +++ b/ui/core/components/gear_picker/gear_picker.tsx @@ -155,10 +155,10 @@ export class ItemRenderer extends Component { this.nameContainerElem.appendChild(this.notice.rootElem); } - if (newItem.reforge) { - const reforgeData = Player.getReforgeData(newItem, newItem.reforge); - const fromText = shortSecondaryStatNames.get(newItem.reforge?.fromStat); - const toText = shortSecondaryStatNames.get(newItem.reforge?.toStat); + const reforgeData = newItem.getReforgeData(); + if (reforgeData) { + const fromText = shortSecondaryStatNames.get(reforgeData.reforge?.fromStat); + const toText = shortSecondaryStatNames.get(reforgeData.reforge?.toStat); this.reforgeElem.innerText = `Reforged ${Math.abs(reforgeData.fromAmount)} ${fromText} → ${reforgeData.toAmount} ${toText}`; this.reforgeElem.classList.remove('hide'); } else { diff --git a/ui/core/components/gear_picker/item_list.tsx b/ui/core/components/gear_picker/item_list.tsx index 025b9d6c37..f3d3cb9931 100644 --- a/ui/core/components/gear_picker/item_list.tsx +++ b/ui/core/components/gear_picker/item_list.tsx @@ -5,12 +5,12 @@ import { SortDirection } from '../../constants/other'; import { EP_TOOLTIP } from '../../constants/tooltips'; import { setItemQualityCssClass } from '../../css_utils'; import { IndividualSimUI } from '../../individual_sim_ui'; -import { Player, ReforgeData } from '../../player'; +import { Player } from '../../player'; import { Class, GemColor, ItemQuality, ItemRandomSuffix, ItemSlot, ItemSpec } from '../../proto/common'; import { DatabaseFilters, RepFaction, UIEnchant as Enchant, UIGem as Gem, UIItem as Item, UIItem_FactionRestriction } from '../../proto/ui'; import { ActionId } from '../../proto_utils/action_id'; import { getUniqueEnchantString } from '../../proto_utils/enchants'; -import { EquippedItem } from '../../proto_utils/equipped_item'; +import { EquippedItem, ReforgeData } from '../../proto_utils/equipped_item'; import { difficultyNames, professionNames, REP_FACTION_NAMES, REP_FACTION_QUARTERMASTERS, REP_LEVEL_NAMES } from '../../proto_utils/names'; import { getPVPSeasonFromItem, isPVPItem } from '../../proto_utils/utils'; import { Sim } from '../../sim'; diff --git a/ui/core/components/gear_picker/selector_modal.tsx b/ui/core/components/gear_picker/selector_modal.tsx index 5eabc9a005..5f84f38f90 100644 --- a/ui/core/components/gear_picker/selector_modal.tsx +++ b/ui/core/components/gear_picker/selector_modal.tsx @@ -2,11 +2,11 @@ import clsx from 'clsx'; import tippy from 'tippy.js'; import { ref } from 'tsx-vanilla'; -import { Player, ReforgeData } from '../../player'; +import { Player } from '../../player'; import { GemColor, ItemQuality, ItemRandomSuffix, ItemSlot } from '../../proto/common'; import { UIEnchant as Enchant, UIGem as Gem, UIItem as Item } from '../../proto/ui'; import { ActionId } from '../../proto_utils/action_id'; -import { EquippedItem } from '../../proto_utils/equipped_item'; +import { EquippedItem, ReforgeData } from '../../proto_utils/equipped_item'; import { gemMatchesSocket, getEmptyGemSocketIconUrl } from '../../proto_utils/gems'; import { shortSecondaryStatNames, slotNames } from '../../proto_utils/names'; import { Stats } from '../../proto_utils/stats'; @@ -447,8 +447,7 @@ export default class SelectorModal extends BaseModal { }; }), computeEP: (reforge: ReforgeData) => this.player.computeReforgingEP(reforge), - equippedToItemFn: (equippedItem: EquippedItem | null) => - equippedItem?.reforge ? Player.getReforgeData(equippedItem, equippedItem.reforge) : null, + equippedToItemFn: (equippedItem: EquippedItem | null) => equippedItem?.getReforgeData() || null, onRemove: (eventID: number) => { const equippedItem = gearData.getEquippedItem(); if (equippedItem) { diff --git a/ui/core/components/individual_sim_ui/reforge_summary.tsx b/ui/core/components/individual_sim_ui/reforge_summary.tsx index 200a704bbe..3e25585722 100644 --- a/ui/core/components/individual_sim_ui/reforge_summary.tsx +++ b/ui/core/components/individual_sim_ui/reforge_summary.tsx @@ -31,26 +31,21 @@ export class ReforgeSummary extends Component { private updateTable() { const body = <>; - let gear = this.player.getGear(); + const reforges = this.player.getGear().getAllReforges(); const totals: ReforgeSummaryTotal = {}; - gear.getItemSlots().forEach(itemSlot => { - const item = gear.getEquippedItem(itemSlot); - if (item?.reforge && item.reforge?.id !== 0) { - const reforge = Player.getReforgeData(item, item.reforge); - if (reforge) { - const { fromStat, toStat, fromAmount, toAmount } = reforge; - if (typeof totals[fromStat] !== 'number') { - totals[fromStat] = 0; - } - if (typeof totals[toStat] !== 'number') { - totals[toStat] = 0; - } - if (fromAmount) totals[fromStat]! += fromAmount; - if (toAmount) totals[toStat]! += toAmount; - } + for (const [_, reforgeData] of reforges) { + const { fromStat, toStat, fromAmount, toAmount } = reforgeData; + + if (typeof totals[fromStat] !== 'number') { + totals[fromStat] = 0; } - }); + if (typeof totals[toStat] !== 'number') { + totals[toStat] = 0; + } + if (fromAmount) totals[fromStat]! += fromAmount; + if (toAmount) totals[toStat]! += toAmount; + } const hasReforgedItems = !!Object.keys(totals).length; this.rootElem.classList[!hasReforgedItems ? 'add' : 'remove']('hide'); @@ -77,7 +72,7 @@ export class ReforgeSummary extends Component {