diff --git a/assets/locales/en/translation.json b/assets/locales/en/translation.json index fda7c3a753..bf25bb2f5f 100644 --- a/assets/locales/en/translation.json +++ b/assets/locales/en/translation.json @@ -815,6 +815,15 @@ "freeze_trinket": { "label": "Freeze trinket slot", "tooltip": "Freeze one equipped trinket to reduce combination counts" + }, + "freeze_weapon": { + "label": "Freeze weapon slot", + "tooltip": "Freeze the equipped main-hand or off-hand weapon to reduce combination counts" + }, + "freeze_weapon_types": { + "mainhand_label": "Freeze MH weapon type", + "offhand_label": "Freeze OH weapon type", + "tooltip": "Leave all unchecked to allow any weapon type for this slot." } }, "progress": { diff --git a/assets/locales/fr/translation.json b/assets/locales/fr/translation.json index b3b54d3ca7..90e79cc9fe 100644 --- a/assets/locales/fr/translation.json +++ b/assets/locales/fr/translation.json @@ -815,6 +815,15 @@ "freeze_trinket": { "label": "Geler un emplacement de bijou", "tooltip": "Geler un bijou équipé pour réduire le nombre de combinaisons" + }, + "freeze_weapon": { + "label": "Geler un emplacement d'arme", + "tooltip": "Geler l'arme équipée en main principale ou secondaire pour réduire le nombre de combinaisons" + }, + "freeze_weapon_types": { + "mainhand_label": "Geler le type d'arme MH", + "offhand_label": "Geler le type d'arme OH", + "tooltip": "Laissez tout décoché pour autoriser n'importe quel type d'arme pour cet emplacement." } }, "progress": { diff --git a/proto/api.proto b/proto/api.proto index 2818f39d22..0a232ee20b 100644 --- a/proto/api.proto +++ b/proto/api.proto @@ -595,4 +595,9 @@ message BulkSettings { int32 default_prismatic_gem = 7; bool inherit_upgrades = 8; + int32 freeze_ring_slot = 9; + int32 freeze_trinket_slot = 10; + int32 freeze_weapon_slot = 11; + repeated WeaponType freeze_mainhand_weapon_slots = 12; + repeated WeaponType freeze_offhand_weapon_slots = 13; } diff --git a/schemas/translation.schema.json b/schemas/translation.schema.json index a146321271..eb63a64d3a 100644 --- a/schemas/translation.schema.json +++ b/schemas/translation.schema.json @@ -2886,6 +2886,35 @@ }, "additionalProperties": false, "required": ["label", "tooltip"] + }, + "freeze_weapon": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "tooltip": { + "type": "string" + } + }, + "additionalProperties": false, + "required": ["label", "tooltip"] + }, + "freeze_weapon_types": { + "type": "object", + "properties": { + "mainhand_label": { + "type": "string" + }, + "offhand_label": { + "type": "string" + }, + "tooltip": { + "type": "string" + } + }, + "additionalProperties": false, + "required": ["mainhand_label", "offhand_label", "tooltip"] } }, "additionalProperties": false, @@ -2896,7 +2925,9 @@ "fallback_gems", "inherit_upgrades", "freeze_ring", - "freeze_trinket" + "freeze_trinket", + "freeze_weapon", + "freeze_weapon_types" ] }, "progress": { diff --git a/ui/core/components/individual_sim_ui/bulk/bulk_item_picker.tsx b/ui/core/components/individual_sim_ui/bulk/bulk_item_picker.tsx index 8105c51fb3..db99f3e15b 100644 --- a/ui/core/components/individual_sim_ui/bulk/bulk_item_picker.tsx +++ b/ui/core/components/individual_sim_ui/bulk/bulk_item_picker.tsx @@ -12,10 +12,11 @@ import { ItemRenderer } from '../../gear_picker/gear_picker'; import { GearData } from '../../gear_picker/item_list'; import { SelectorModalTabs } from '../../gear_picker/selector_modal'; import { BulkTab } from '../bulk_tab'; -import { BulkSimItemSlot } from './utils'; +import { BulkSimItemSlot, bulkSimItemSlotToItemSlotPairs } from './utils'; export default class BulkItemPicker extends Component { private readonly itemElem: ItemRenderer; + private removeBtn: HTMLButtonElement | null = null; readonly simUI: IndividualSimUI; readonly bulkUI: BulkTab; readonly bulkSlot: BulkSimItemSlot; @@ -40,7 +41,7 @@ export default class BulkItemPicker extends Component { this.abortController = new AbortController(); this.signal = this.abortController.signal; - if (!this.isEditable()) { + if (!this.indexIsEditable()) { this.rootElem.classList.add('bulk-item-picker-equipped'); parent.insertAdjacentElement('afterbegin', this.rootElem); } @@ -51,18 +52,18 @@ export default class BulkItemPicker extends Component { this.addOnDisposeCallback(() => this.rootElem.remove()); - const updateBorder = () => { - if (this.bulkUI.frozenItems.get(this.bulkSlot)?.equals(this.item)) { - this.rootElem.classList.remove('bulk-item-picker-equipped'); - this.rootElem.classList.add('bulk-item-picker-frozen'); - } else { - this.rootElem.classList.remove('bulk-item-picker-frozen'); - this.rootElem.classList.add('bulk-item-picker-equipped'); - } + const updatePickerState = () => { + const isFrozen = this.isFrozen(); + const isEditable = this.isEditable(); + + this.rootElem.classList.toggle('bulk-item-picker-frozen', isFrozen); + this.rootElem.classList.toggle('bulk-item-picker-equipped', !isFrozen && !isEditable); + this.removeBtn?.classList.toggle('hide', !isEditable); }; - updateBorder(); - TypedEvent.onAny([this.bulkUI.settingsChangedEmitter, this.bulkUI.itemsChangedEmitter]).on(() => updateBorder()); + updatePickerState(); + const events = TypedEvent.onAny([this.bulkUI.settingsChangedEmitter, this.bulkUI.itemsChangedEmitter]).on(() => updatePickerState()); + this.addOnDisposeCallback(() => events.dispose()); } setItem(newItem: EquippedItem) { @@ -72,10 +73,59 @@ export default class BulkItemPicker extends Component { this.setupHandlers(); } - private isEditable(): boolean { + private indexIsEditable(): boolean { return this.index >= 0; } + private isCurrentlyEquipped(): boolean { + if (this.bulkSlot === BulkSimItemSlot.ItemSlotHandWeapon) { + return false; + } + + return this.simUI.player.getEquippedItems().some(equippedItem => equippedItem?.id === this.item.id); + } + + private isEditable(): boolean { + return this.indexIsEditable() && !this.isCurrentlyEquipped(); + } + + private getEquippedSlot(): ItemSlot | null { + if (this.indexIsEditable()) { + return null; + } + + const slots = bulkSimItemSlotToItemSlotPairs.get(this.bulkSlot); + if (!slots) { + return null; + } + + return this.index === -1 ? slots[0] : slots[1]; + } + + private getFrozenBulkItemSlot(): ItemSlot | null { + const frozenItem = this.bulkUI.frozenItems.get(this.bulkSlot); + const slots = bulkSimItemSlotToItemSlotPairs.get(this.bulkSlot); + if (!frozenItem || !slots) { + return null; + } + + const gear = this.simUI.player.getGear(); + return slots.find(slot => gear.getEquippedItem(slot) === frozenItem) ?? slots.find(slot => gear.getEquippedItem(slot)?.equals(frozenItem)) ?? null; + } + + private isFrozen(): boolean { + const equippedSlot = this.getEquippedSlot(); + if (!equippedSlot) { + return false; + } + + if (equippedSlot === this.bulkUI.frozenWeaponSlot) { + return this.simUI.player.getGear().getEquippedItem(equippedSlot)?.equals(this.item) ?? false; + } + + return equippedSlot === this.getFrozenBulkItemSlot(); + } + private setupHandlers() { const slot = getEligibleItemSlots(this.item.item)[0]; const hasEligibleEnchants = !!this.simUI.sim.db.getEnchants(slot).length; @@ -143,7 +193,7 @@ export default class BulkItemPicker extends Component { this.itemElem.rootElem.appendChild(
- {this.isEditable() && ( + {this.indexIsEditable() && ( @@ -153,6 +203,8 @@ export default class BulkItemPicker extends Component { if (removeBtnRef.value) { const removeBtn = removeBtnRef.value; + this.removeBtn = removeBtn; + this.removeBtn.classList.toggle('hide', !this.isEditable()); tippy(removeBtn, { content: i18n.t('bulk_tab.picker.remove_tooltip') }); const removeItem = () => this.bulkUI.removeItemByIndex(this.index); removeBtn.addEventListener('click', removeItem); diff --git a/ui/core/components/individual_sim_ui/bulk/utils.ts b/ui/core/components/individual_sim_ui/bulk/utils.ts index 2dc8685f1f..83955a44f3 100644 --- a/ui/core/components/individual_sim_ui/bulk/utils.ts +++ b/ui/core/components/individual_sim_ui/bulk/utils.ts @@ -1,5 +1,4 @@ import { ItemSlot } from '../../../proto/common'; -import { getEnumValues } from '../../../utils'; // Combines Fingers 1 and 2 and Trinket 1 and 2 into single groups export enum BulkSimItemSlot { @@ -20,18 +19,6 @@ export enum BulkSimItemSlot { ItemSlotHandWeapon, // Weapon grouping slot for specs that can dual-wield } -// Return all eligible bulk item slots. -// If the player can dual-wield, exclude main-hand/off-hand in favor of the grouped weapons slot -// Otherwise include main-hand/off-hand instead of the grouped weapons slot -export const getBulkItemSlots = (canDualWield: boolean) => { - const allSlots = getEnumValues(BulkSimItemSlot); - if (canDualWield) { - return allSlots.filter(bulkSlot => ![BulkSimItemSlot.ItemSlotMainHand, BulkSimItemSlot.ItemSlotOffHand].includes(bulkSlot)); - } else { - return allSlots.filter(bulkSlot => bulkSlot !== BulkSimItemSlot.ItemSlotHandWeapon); - } -}; - export const itemSlotToBulkSimItemSlot: Map = new Map([ [ItemSlot.ItemSlotHead, BulkSimItemSlot.ItemSlotHead], [ItemSlot.ItemSlotNeck, BulkSimItemSlot.ItemSlotNeck], @@ -52,7 +39,7 @@ export const itemSlotToBulkSimItemSlot: Map = new Map ]); export const bulkSimItemSlotToSingleItemSlot: Map = new Map([ - [BulkSimItemSlot.ItemSlotHead, ItemSlot.ItemSlotHead,], + [BulkSimItemSlot.ItemSlotHead, ItemSlot.ItemSlotHead], [BulkSimItemSlot.ItemSlotNeck, ItemSlot.ItemSlotNeck], [BulkSimItemSlot.ItemSlotShoulder, ItemSlot.ItemSlotShoulder], [BulkSimItemSlot.ItemSlotBack, ItemSlot.ItemSlotBack], @@ -67,9 +54,9 @@ export const bulkSimItemSlotToSingleItemSlot: Map = n ]); export const bulkSimItemSlotToItemSlotPairs: Map = new Map([ - [BulkSimItemSlot.ItemSlotFinger, [ItemSlot.ItemSlotFinger1, ItemSlot.ItemSlotFinger2]], - [BulkSimItemSlot.ItemSlotTrinket, [ItemSlot.ItemSlotTrinket1, ItemSlot.ItemSlotTrinket2]], - [BulkSimItemSlot.ItemSlotHandWeapon, [ItemSlot.ItemSlotMainHand, ItemSlot.ItemSlotOffHand]], + [BulkSimItemSlot.ItemSlotFinger, [ItemSlot.ItemSlotFinger2, ItemSlot.ItemSlotFinger1]], + [BulkSimItemSlot.ItemSlotTrinket, [ItemSlot.ItemSlotTrinket2, ItemSlot.ItemSlotTrinket1]], + [BulkSimItemSlot.ItemSlotHandWeapon, [ItemSlot.ItemSlotOffHand, ItemSlot.ItemSlotMainHand]], ]); export const getBulkItemSlotFromSlot = (slot: ItemSlot, canDualWield: boolean): BulkSimItemSlot => { @@ -80,22 +67,22 @@ export const getBulkItemSlotFromSlot = (slot: ItemSlot, canDualWield: boolean): }; export const binomialCoefficient = (n: number, k: number): number => { - if (Number.isNaN(n) || Number.isNaN(k)) return NaN; - if (k < 0 || k > n) return 0; - if (k === 0 || k === n) return 1; - if (k === 1 || k === n - 1) return n; - if (n - k < k) k = n - k; - let res = n; - for (let j = 2; j <= k; j++) res *= (n - j + 1) / j; - return Math.round(res); + if (Number.isNaN(n) || Number.isNaN(k)) return NaN; + if (k < 0 || k > n) return 0; + if (k === 0 || k === n) return 1; + if (k === 1 || k === n - 1) return n; + if (n - k < k) k = n - k; + let res = n; + for (let j = 2; j <= k; j++) res *= (n - j + 1) / j; + return Math.round(res); }; export function getAllPairs(arr: T[]): [T, T][] { - const pairs: [T, T][] = []; - for (let i = 0; i < arr.length; i++) { - for (let j = i + 1; j < arr.length; j++) { - pairs.push([arr[i], arr[j]]); - } - } - return pairs; + const pairs: [T, T][] = []; + for (let i = 0; i < arr.length; i++) { + for (let j = i + 1; j < arr.length; j++) { + pairs.push([arr[i], arr[j]]); + } + } + return pairs; } diff --git a/ui/core/components/individual_sim_ui/bulk_tab.tsx b/ui/core/components/individual_sim_ui/bulk_tab.tsx index ea57b82dff..73d53ae1dd 100644 --- a/ui/core/components/individual_sim_ui/bulk_tab.tsx +++ b/ui/core/components/individual_sim_ui/bulk_tab.tsx @@ -7,7 +7,7 @@ import { REPO_RELEASES_URL } from '../../constants/other'; import { IndividualSimUI } from '../../individual_sim_ui'; import i18n from '../../../i18n/config'; import { BulkSettings, DistributionMetrics, ProgressMetrics, RaidSimResult } from '../../proto/api'; -import { Class, GemColor, HandType, ItemRandomSuffix, ItemSlot, ItemSpec, RangedWeaponType, ReforgeStat, Spec } from '../../proto/common'; +import { Class, GemColor, HandType, ItemRandomSuffix, ItemSlot, ItemSpec, RangedWeaponType, ReforgeStat, Spec, WeaponType } from '../../proto/common'; import { ItemEffectRandPropPoints, SimDatabase, SimEnchant, SimGem, SimItem } from '../../proto/db'; import { UIEnchant, UIGem, UIItem } from '../../proto/ui'; import { ActionId } from '../../proto_utils/action_id'; @@ -39,13 +39,16 @@ import { BulkGearJsonImporter } from './importers'; import { BooleanPicker } from '../pickers/boolean_picker'; import { trackEvent } from '../../../tracking/utils'; import { EnumPicker } from '../pickers/enum_picker'; -import { translateBulkSlotName } from '../../../i18n/localization'; +import { translateBulkSlotName, translateWeaponType } from '../../../i18n/localization'; import { ProgressTrackerModal } from '../progress_tracker_modal'; -const WEB_DEFAULT_ITERATIONS = 1000; +const WEB_DEFAULT_ITERATIONS = 5_000; const WEB_ITERATIONS_LIMIT = 100_000; const LOCAL_ITERATIONS_LIMIT = 5_000_000; +const WEB_COMBINATIONS_LIMIT = 50_000; +const LOCAL_COMBINATIONS_LIMIT = 100_000; + export interface TopGearResult { gear: Gear; dpsMetrics: DistributionMetrics; @@ -87,6 +90,11 @@ export class BulkTab extends SimTab { [BulkSimItemSlot.ItemSlotFinger, null], [BulkSimItemSlot.ItemSlotTrinket, null], ]); + frozenWeaponSlot: ItemSlot.ItemSlotMainHand | ItemSlot.ItemSlotOffHand | undefined = undefined; + weaponTypeFilters: Map = new Map([ + [ItemSlot.ItemSlotMainHand, []], + [ItemSlot.ItemSlotOffHand, []], + ]); fallbackGems: SimGem[]; gemIconElements: HTMLImageElement[]; @@ -161,7 +169,7 @@ export class BulkTab extends SimTab {
-
+