diff --git a/package.json b/package.json index 3d752e722f..1e224d0e04 100644 --- a/package.json +++ b/package.json @@ -1,25 +1,25 @@ { - "name": "sod", - "version": "0.1.0", - "private": true, - "scripts": { - "build": "bazel build //...", - "test": "bazel test //...", - "format": "npm run lint:fix && npm run prettier:fix", - "lint": "npm run lint:js && npm run lint:css", - "lint:fix": "npm run lint:js:fix && npm run lint:css:fix", - "lint:js": "npx eslint \"./ui/**/*.{js,jsx,ts,tsx}\"", - "lint:js:fix": "npm run lint:js -- --fix", - "lint:css": "stylelint \"./ui/**/*.scss\"", - "lint:css:fix": "npm run lint:css -- --fix", - "lint:json": "npx eslint \"./{ui,jsonschema}/**/*.json\"", - "lint:json:fix": "npm run lint:json -- --fix", - "prettier": "npx prettier \"./ui/**/*.{js,jsx,ts,tsx,scss,css,json}\" --check", - "prettier:fix": "npm run prettier -- --write", - "type-check": "tsc --noEmit" - }, - "dependencies": { - "@popperjs/core": "^2.11.6", + "name": "sod", + "version": "0.1.0", + "private": true, + "scripts": { + "build": "bazel build //...", + "test": "bazel test //...", + "format": "npm run lint:fix && npm run prettier:fix", + "lint": "npm run lint:js && npm run lint:css", + "lint:fix": "npm run lint:js:fix && npm run lint:css:fix", + "lint:js": "npx eslint \"./ui/**/*.{js,jsx,ts,tsx}\"", + "lint:js:fix": "npm run lint:js -- --fix", + "lint:css": "stylelint \"./ui/**/*.scss\"", + "lint:css:fix": "npm run lint:css -- --fix", + "lint:json": "npx eslint \"./{ui,jsonschema}/**/*.json\"", + "lint:json:fix": "npm run lint:json -- --fix", + "prettier": "npx prettier \"./ui/**/*.{js,jsx,ts,tsx,scss,css,json}\" --check", + "prettier:fix": "npm run prettier -- --write", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "@popperjs/core": "^2.11.6", "@types/golang-wasm-exec": "^1.15.2", "@types/pako": "^2.0.0", "bootstrap": "^5.3.0", @@ -28,13 +28,13 @@ "tippy.js": "^6.3.7", "tsx-vanilla": "^1.0.0", "uuid": "^9.0.1" - }, - "devDependencies": { - "@protobuf-ts/plugin": "2.9.1", - "@protobuf-ts/plugin-framework": "2.9.1", - "@protobuf-ts/protoc": "2.9.1", - "@protobuf-ts/runtime": "2.9.1", - "@types/bootstrap": "^5.2.0", + }, + "devDependencies": { + "@protobuf-ts/plugin": "2.9.1", + "@protobuf-ts/plugin-framework": "2.9.1", + "@protobuf-ts/protoc": "2.9.1", + "@protobuf-ts/runtime": "2.9.1", + "@types/bootstrap": "^5.2.0", "@types/eslint": "^8.56.10", "@types/glob": "^8.0.3", "@types/node": "^18.16.1", @@ -61,5 +61,5 @@ "typescript-formatter": "^7.2.2", "vite": "^5.0.0", "vite-plugin-checker": "^0.6.4" - } + } } diff --git a/ui/core/components/importers.tsx b/ui/core/components/importers.tsx index 0e4212a526..8cbcacc0d1 100644 --- a/ui/core/components/importers.tsx +++ b/ui/core/components/importers.tsx @@ -153,7 +153,7 @@ export class IndividualLinkImporter { return map; })(); - static tryParseUrlLocation(location: Location): UrlParseData | null { + static tryParseUrlLocation(location: Location | URL): UrlParseData | null { let hash = location.hash; if (hash.length <= 1) { return null; diff --git a/ui/core/components/individual_sim_ui/gear_tab.ts b/ui/core/components/individual_sim_ui/gear_tab.ts index 27d7b33e1d..b0910fcc52 100644 --- a/ui/core/components/individual_sim_ui/gear_tab.ts +++ b/ui/core/components/individual_sim_ui/gear_tab.ts @@ -7,6 +7,7 @@ import { EventID, TypedEvent } from '../../typed_event'; import GearPicker from '../gear_picker/gear_picker'; import { SavedDataManager } from '../saved_data_manager'; import { SimTab } from '../sim_tab'; +import { PresetConfigurationPicker } from './preset_configuration_picker'; export class GearTab extends SimTab { protected simUI: IndividualSimUI; @@ -32,7 +33,7 @@ export class GearTab extends SimTab { protected buildTabContent() { this.buildGearPickers(); - + this.buildPresetConfigurationPicker(); this.buildSavedGearsetPicker(); } @@ -40,6 +41,10 @@ export class GearTab extends SimTab { new GearPicker(this.leftPanel, this.simUI, this.simUI.player); } + private buildPresetConfigurationPicker() { + new PresetConfigurationPicker(this.rightPanel, this.simUI, 'gear'); + } + private buildSavedGearsetPicker() { const savedGearManager = new SavedDataManager, SavedGearSet>(this.rightPanel, this.simUI.player, { header: { title: 'Gear Sets' }, diff --git a/ui/core/components/individual_sim_ui/preset_builds_picker.tsx b/ui/core/components/individual_sim_ui/preset_builds_picker.tsx deleted file mode 100644 index 6eb3b1d003..0000000000 --- a/ui/core/components/individual_sim_ui/preset_builds_picker.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import tippy from 'tippy.js'; -import { ref } from 'tsx-vanilla'; - -import { IndividualSimUI } from '../../individual_sim_ui'; -import { PresetBuild } from '../../preset_utils'; -import { APLRotation } from '../../proto/apl'; -import { EquipmentSpec, Spec } from '../../proto/common'; -import { TypedEvent } from '../../typed_event'; -import { Component } from '../component'; - -export class PresetBuildsPicker extends Component { - readonly simUI: IndividualSimUI; - readonly builds: Array; - - constructor(parentElem: HTMLElement, simUI: IndividualSimUI) { - super(parentElem, 'preset-builds-picker-root'); - - this.simUI = simUI; - this.builds = this.simUI.individualConfig.presets.builds ?? []; - - if (!this.builds.length) { - this.rootElem.classList.add('hide'); - return; - } - - const buildsContainerRef = ref(); - const infoElemRef = ref(); - this.rootElem.appendChild( - <> - -
- , - ); - - tippy(infoElemRef.value!, { - content: 'Preset builds apply an optimal combination of gear, talents, and rotation.', - }); - - this.simUI.sim.waitForInit().then(() => { - this.builds.forEach(build => { - const dataElemRef = ref(); - buildsContainerRef.value!.appendChild( - , - ); - - const checkActive = () => { - if (this.isBuildActive(build)) { - dataElemRef.value!.classList.add('active'); - } else { - dataElemRef.value!.classList.remove('active'); - } - }; - - checkActive(); - TypedEvent.onAny([this.simUI.player.gearChangeEmitter, this.simUI.player.talentsChangeEmitter, this.simUI.player.rotationChangeEmitter]).on( - checkActive, - ); - }); - }); - } - - private applyBuild(build: PresetBuild) { - const eventID = TypedEvent.nextEventID(); - TypedEvent.freezeAllAndDo(() => { - this.simUI.player.setGear(eventID, this.simUI.sim.db.lookupEquipmentSpec(build.gear.gear)); - this.simUI.player.setTalentsString(eventID, build.talents.data.talentsString); - this.simUI.player.setAplRotation(eventID, build.rotation.rotation.rotation!); - }); - } - - private isBuildActive(build: PresetBuild): boolean { - return ( - EquipmentSpec.equals(build.gear.gear, this.simUI.player.getGear().asSpec()) && - build.talents.data.talentsString == this.simUI.player.getTalentsString() && - APLRotation.equals(build.rotation.rotation.rotation, this.simUI.player.getResolvedAplRotation()) - ); - } -} diff --git a/ui/core/components/individual_sim_ui/preset_configuration_picker.tsx b/ui/core/components/individual_sim_ui/preset_configuration_picker.tsx new file mode 100644 index 0000000000..ff85721a8a --- /dev/null +++ b/ui/core/components/individual_sim_ui/preset_configuration_picker.tsx @@ -0,0 +1,122 @@ +import tippy from 'tippy.js'; +import { ref } from 'tsx-vanilla'; + +import { IndividualSimUI } from '../../individual_sim_ui'; +import { PresetBuild } from '../../preset_utils'; +import { APLRotation, APLRotation_Type } from '../../proto/apl'; +import { Encounter, EquipmentSpec, HealingModel, Spec } from '../../proto/common'; +import { TypedEvent } from '../../typed_event'; +import { Component } from '../component'; +import { ContentBlock } from '../content_block'; + +type PresetConfigurationCategory = 'gear' | 'talents' | 'rotation' | 'encounter'; + +export class PresetConfigurationPicker extends Component { + readonly simUI: IndividualSimUI; + readonly builds: Array; + + constructor(parentElem: HTMLElement, simUI: IndividualSimUI, type?: PresetConfigurationCategory) { + super(parentElem, 'preset-configuration-picker-root'); + this.rootElem.classList.add('saved-data-manager-root'); + + this.simUI = simUI; + this.builds = (this.simUI.individualConfig.presets.builds ?? []).filter(build => + Object.keys(build).some(category => category === type && !!build[category]), + ); + + if (!this.builds.length) { + this.rootElem.classList.add('hide'); + return; + } + + const contentBlock = new ContentBlock(this.rootElem, 'saved-data', { + header: { + title: 'Preset Configurations', + tooltip: 'Preset configurations can apply an optimal combination of gear, talents, rotation and encounter settings.', + }, + }); + + const buildsContainerRef = ref(); + + const container = ( +
+
+
+ ); + + this.simUI.sim.waitForInit().then(() => { + this.builds.forEach(build => { + const dataElemRef = ref(); + buildsContainerRef.value!.appendChild( + , + ); + + tippy(dataElemRef.value!, { + content: ( + <> +

This preset affects the following settings:

+
    + {Object.keys(build) + .filter(c => c !== 'name') + .map(category => (build[category as PresetConfigurationCategory] ?
  • {category}
  • : undefined))} +
+ + ), + }); + + const checkActive = () => dataElemRef.value!.classList[this.isBuildActive(build) ? 'add' : 'remove']('active'); + + checkActive(); + TypedEvent.onAny([ + this.simUI.player.changeEmitter, + this.simUI.sim.settingsChangeEmitter, + this.simUI.sim.raid.changeEmitter, + this.simUI.sim.encounter.changeEmitter, + ]).on(checkActive); + }); + contentBlock.bodyElement.replaceChildren(container); + }); + } + + private applyBuild({ gear, rotation, talents, epWeights, encounter }: PresetBuild) { + const eventID = TypedEvent.nextEventID(); + TypedEvent.freezeAllAndDo(() => { + if (gear) this.simUI.player.setGear(eventID, this.simUI.sim.db.lookupEquipmentSpec(gear.gear)); + if (talents) this.simUI.player.setTalentsString(eventID, talents.data.talentsString); + if (rotation?.rotation.rotation) { + this.simUI.player.setAplRotation(eventID, rotation.rotation.rotation); + } + if (epWeights) this.simUI.player.setEpWeights(eventID, epWeights.epWeights); + if (encounter) { + if (encounter.encounter) this.simUI.sim.encounter.fromProto(eventID, encounter.encounter); + if (encounter.healingModel) this.simUI.player.setHealingModel(eventID, encounter.healingModel); + if (encounter.tanks) this.simUI.sim.raid.setTanks(eventID, encounter.tanks); + if (encounter.buffs) this.simUI.player.setBuffs(eventID, encounter.buffs); + if (encounter.debuffs) this.simUI.sim.raid.setDebuffs(eventID, encounter.debuffs); + if (encounter.raidBuffs) this.simUI.sim.raid.setBuffs(eventID, encounter.raidBuffs); + if (encounter.consumes) this.simUI.player.setConsumes(eventID, encounter.consumes); + } + }); + } + + private isBuildActive({ gear, rotation, talents, epWeights, encounter }: PresetBuild): boolean { + const hasGear = gear ? EquipmentSpec.equals(gear.gear, this.simUI.player.getGear().asSpec()) : true; + const hasTalents = talents ? talents.data.talentsString == this.simUI.player.getTalentsString() : true; + let hasRotation = true; + if (rotation) { + const activeRotation = this.simUI.player.getResolvedAplRotation(); + // Ensure that the auto rotation can be matched with a preset + if (activeRotation.type === APLRotation_Type.TypeAuto) activeRotation.type = APLRotation_Type.TypeAPL; + hasRotation = APLRotation.equals(rotation.rotation.rotation, activeRotation); + } + const hasEpWeights = epWeights ? this.simUI.player.getEpWeights().equals(epWeights.epWeights) : true; + const hasEncounter = encounter?.encounter ? Encounter.equals(encounter.encounter, this.simUI.sim.encounter.toProto()) : true; + const hasHealingModel = encounter?.healingModel ? HealingModel.equals(encounter.healingModel, this.simUI.player.getHealingModel()) : true; + + return hasGear && hasTalents && hasRotation && hasEpWeights && hasEncounter && hasHealingModel; + } +} diff --git a/ui/core/components/individual_sim_ui/rotation_tab.ts b/ui/core/components/individual_sim_ui/rotation_tab.ts index 4751f89ac4..672ac7397a 100644 --- a/ui/core/components/individual_sim_ui/rotation_tab.ts +++ b/ui/core/components/individual_sim_ui/rotation_tab.ts @@ -15,6 +15,7 @@ import { SavedDataManager } from '../saved_data_manager'; import { SimTab } from '../sim_tab'; import { APLRotationPicker } from './apl_rotation_picker'; import { CooldownsPicker } from './cooldowns_picker'; +import { PresetConfigurationPicker } from './preset_configuration_picker'; export class RotationTab extends SimTab { protected simUI: IndividualSimUI; @@ -48,6 +49,7 @@ export class RotationTab extends SimTab { this.buildAplContent(); this.buildSimpleContent(); + this.buildPresetConfigurationPicker(); this.buildSavedDataPickers(); } @@ -173,6 +175,10 @@ export class RotationTab extends SimTab { } } + private buildPresetConfigurationPicker() { + new PresetConfigurationPicker(this.rightPanel, this.simUI, 'rotation'); + } + private buildSavedDataPickers() { const savedRotationsManager = new SavedDataManager, SavedRotation>(this.rightPanel, this.simUI.player, { label: 'Rotation', diff --git a/ui/core/components/individual_sim_ui/settings_tab.ts b/ui/core/components/individual_sim_ui/settings_tab.ts index 2fb88a47e6..328fdacaaa 100644 --- a/ui/core/components/individual_sim_ui/settings_tab.ts +++ b/ui/core/components/individual_sim_ui/settings_tab.ts @@ -25,7 +25,7 @@ import { SimTab } from '../sim_tab'; import { IsbConfig } from './../other_inputs'; import { ConsumesPicker } from './consumes_picker'; import { ItemSwapPicker } from './item_swap_picker'; -import { PresetBuildsPicker } from './preset_builds_picker'; +import { PresetConfigurationPicker } from './preset_configuration_picker'; export class SettingsTab extends SimTab { protected simUI: IndividualSimUI; @@ -80,6 +80,7 @@ export class SettingsTab extends SimTab { this.buildBuffsSettings(); this.buildWorldBuffsSettings(); this.buildDebuffsSettings(); + this.buildPresetConfigurationPicker(); this.buildSavedDataPickers(); } }); @@ -108,8 +109,6 @@ export class SettingsTab extends SimTab { true, ); - new PresetBuildsPicker(contentBlock.bodyElement, this.simUI); - new EnumPicker(contentBlock.bodyElement, this.simUI.player, { id: 'player-level', label: 'Level', @@ -313,6 +312,10 @@ export class SettingsTab extends SimTab { this.simUI.player.getRaid()?.debuffsChangeEmitter.emit(TypedEvent.nextEventID()); } + private buildPresetConfigurationPicker() { + new PresetConfigurationPicker(this.rightPanel, this.simUI, 'encounter'); + } + private buildSavedDataPickers() { const savedEncounterManager = new SavedDataManager(this.rightPanel, this.simUI.sim.encounter, { label: 'Encounter', diff --git a/ui/core/components/individual_sim_ui/talents_tab.ts b/ui/core/components/individual_sim_ui/talents_tab.ts index fa34c06340..fb9394cffb 100644 --- a/ui/core/components/individual_sim_ui/talents_tab.ts +++ b/ui/core/components/individual_sim_ui/talents_tab.ts @@ -7,6 +7,7 @@ import { TalentsPicker } from '../../talents/talents_picker'; import { EventID, TypedEvent } from '../../typed_event'; import { SavedDataManager } from '../saved_data_manager'; import { SimTab } from '../sim_tab'; +import { PresetConfigurationPicker } from './preset_configuration_picker'; export class TalentsTab extends SimTab { protected simUI: IndividualSimUI; @@ -31,6 +32,8 @@ export class TalentsTab extends SimTab { protected buildTabContent() { this.buildTalentsPicker(this.leftPanel); + + this.buildPresetConfigurationPicker(); this.buildSavedTalentsPicker(); } @@ -47,6 +50,10 @@ export class TalentsTab extends SimTab { }); } + private buildPresetConfigurationPicker() { + new PresetConfigurationPicker(this.rightPanel, this.simUI, 'talents'); + } + private buildSavedTalentsPicker() { const savedTalentsManager = new SavedDataManager, SavedTalents>(this.rightPanel, this.simUI.player, { label: 'Talents', diff --git a/ui/core/components/saved_data_manager.tsx b/ui/core/components/saved_data_manager.tsx index b4a1daaf86..7fb466ef00 100644 --- a/ui/core/components/saved_data_manager.tsx +++ b/ui/core/components/saved_data_manager.tsx @@ -26,14 +26,15 @@ export type SavedDataConfig = { // If set, will automatically hide the saved data when this evaluates to false. enableWhen?: (obj: ModObject) => boolean; + // Will execute when the saved data is loaded. + onLoad?: (obj: ModObject) => void; }; type SavedData = { name: string; data: T; elem: HTMLElement; - enableWhen?: (obj: ModObject) => boolean; -}; +} & Pick, 'enableWhen' | 'onLoad'>; export class SavedDataManager extends Component { private readonly modObject: ModObject; @@ -117,6 +118,7 @@ export class SavedDataManager extends Component { dataElem.addEventListener('click', () => { this.config.setData(TypedEvent.nextEventID(), this.modObject, config.data); + config.onLoad?.(this.modObject); if (this.saveInput) this.saveInput.value = config.name; }); @@ -146,6 +148,7 @@ export class SavedDataManager extends Component { const checkActive = () => { if (this.config.equals(config.data, this.config.getData(this.modObject))) { dataElem.classList.add('active'); + if (this.saveInput) this.saveInput.value = config.name; } else { dataElem.classList.remove('active'); } @@ -165,6 +168,7 @@ export class SavedDataManager extends Component { data: config.data, elem: dataElem, enableWhen: config.enableWhen, + onLoad: config.onLoad, }; } diff --git a/ui/core/components/toast.tsx b/ui/core/components/toast.tsx index 28b9baadec..23462615f6 100644 --- a/ui/core/components/toast.tsx +++ b/ui/core/components/toast.tsx @@ -1,7 +1,7 @@ import { Toast as BootstrapToast } from 'bootstrap'; import clsx from 'clsx'; -type ToastOptions = { +export type ToastOptions = { title?: string; variant: 'info' | 'success' | 'error' | 'warning'; body: string | Element; diff --git a/ui/core/preset_utils.ts b/ui/core/preset_utils.ts deleted file mode 100644 index 1e367dc13c..0000000000 --- a/ui/core/preset_utils.ts +++ /dev/null @@ -1,136 +0,0 @@ -import * as Tooltips from './constants/tooltips.js'; -import { Player } from './player'; -import { APLRotation, APLRotation_Type as APLRotationType } from './proto/apl'; -import { EquipmentSpec, Faction, Spec } from './proto/common'; -import { SavedRotation, SavedTalents } from './proto/ui'; -import { SpecRotation, specTypeFunctions } from './proto_utils/utils'; - -export interface PresetGear { - name: string; - gear: EquipmentSpec; - tooltip?: string; - enableWhen?: (obj: Player) => boolean; -} - -export interface PresetTalents { - name: string; - data: SavedTalents; - enableWhen?: (obj: Player) => boolean; -} - -export interface PresetRotation { - name: string; - rotation: SavedRotation; - tooltip?: string; - enableWhen?: (obj: Player) => boolean; -} - -export interface PresetBuild { - name: string; - gear: PresetGear; - talents: PresetTalents; - rotation: PresetRotation; -} - -export interface PresetGearOptions { - talentTree?: number; - talentTrees?: Array; - faction?: Faction; - customCondition?: (player: Player) => boolean; - - tooltip?: string; -} - -export interface PresetRotationOptions { - talentTree?: number; - customCondition?: (player: Player) => boolean; -} - -export function makePresetGear(name: string, gearJson: any, options?: PresetGearOptions): PresetGear { - const gear = EquipmentSpec.fromJson(gearJson); - return makePresetGearHelper(name, gear, options || {}); -} - -function makePresetGearHelper(name: string, gear: EquipmentSpec, options: PresetGearOptions): PresetGear { - const conditions: Array<(player: Player) => boolean> = []; - if (options.talentTree != undefined) { - conditions.push((player: Player) => player.getTalentTree() == options.talentTree); - } - if (options.talentTrees != undefined) { - conditions.push((player: Player) => (options.talentTrees || []).includes(player.getTalentTree())); - } - if (options.faction != undefined) { - conditions.push((player: Player) => player.getFaction() == options.faction); - } - if (options.customCondition != undefined) { - conditions.push(options.customCondition); - } - - return { - name: name, - tooltip: options.tooltip || Tooltips.BASIC_BIS_DISCLAIMER, - gear: gear, - enableWhen: conditions.length > 0 ? (player: Player) => conditions.every(cond => cond(player)) : undefined, - }; -} - -export interface PresetTalentsOptions { - customCondition?: (player: Player) => boolean; -} - -export function makePresetTalents(name: string, data: SavedTalents, options?: PresetTalentsOptions): PresetTalents { - const conditions: Array<(player: Player) => boolean> = []; - if (options && options.customCondition) { - conditions.push(options.customCondition); - } - - return { - name, - data, - enableWhen: conditions.length > 0 ? (player: Player) => conditions.every(cond => cond(player)) : undefined, - }; -} - -export function makePresetAPLRotation(name: string, rotationJson: any, options?: PresetRotationOptions): PresetRotation { - const rotation = SavedRotation.create({ - rotation: APLRotation.fromJson(rotationJson), - }); - return makePresetRotationHelper(name, rotation, options); -} - -export function makePresetSimpleRotation( - name: string, - spec: SpecType, - simpleRotation: SpecRotation, - options?: PresetRotationOptions, -): PresetRotation { - const rotation = SavedRotation.create({ - rotation: { - type: APLRotationType.TypeSimple, - simple: { - specRotationJson: JSON.stringify(specTypeFunctions[spec].rotationToJson(simpleRotation)), - }, - }, - }); - return makePresetRotationHelper(name, rotation, options); -} - -function makePresetRotationHelper(name: string, rotation: SavedRotation, options?: PresetRotationOptions): PresetRotation { - const conditions: Array<(player: Player) => boolean> = []; - if (options?.talentTree != undefined) { - conditions.push((player: Player) => player.getTalentTree() == options.talentTree); - } - if (options?.customCondition != undefined) { - conditions.push(options.customCondition); - } - - return { - name: name, - rotation: rotation, - enableWhen: conditions.length > 0 ? (player: Player) => conditions.every(cond => cond(player)) : undefined, - }; -} - -export function makePresetBuild(name: string, gear: PresetGear, talents: PresetTalents, rotation: PresetRotation): PresetBuild { - return { name, gear, talents, rotation }; -} diff --git a/ui/core/preset_utils.tsx b/ui/core/preset_utils.tsx new file mode 100644 index 0000000000..11d1ba9de4 --- /dev/null +++ b/ui/core/preset_utils.tsx @@ -0,0 +1,251 @@ +import { IndividualLinkImporter } from './components/importers'; +import Toast, { ToastOptions } from './components/toast'; +import * as Tooltips from './constants/tooltips.js'; +import { Player } from './player.js'; +import { APLRotation, APLRotation_Type as APLRotationType } from './proto/apl.js'; +import { + Consumes, + Debuffs, + Encounter, + Encounter as EncounterProto, + EquipmentSpec, + Faction, + HealingModel, + IndividualBuffs, + RaidBuffs, + Spec, + UnitReference, +} from './proto/common.js'; +import { SavedRotation, SavedTalents } from './proto/ui.js'; +import { Stats } from './proto_utils/stats.js'; +import { SpecRotation, specTypeFunctions } from './proto_utils/utils.js'; + +interface PresetBase { + name: string; + tooltip?: string; + enableWhen?: (obj: Player) => boolean; + onLoad?: (player: Player) => void; +} + +interface PresetOptionsBase extends Pick { + customCondition?: (player: Player) => boolean; +} + +export interface PresetGear extends PresetBase { + name: string; + gear: EquipmentSpec; + tooltip?: string; + enableWhen?: (obj: Player) => boolean; +} +export interface PresetGearOptions extends PresetOptionsBase, Pick { + talentTree?: number; + talentTrees?: Array; + faction?: Faction; + customCondition?: (player: Player) => boolean; +} + +export interface PresetTalents { + name: string; + data: SavedTalents; + enableWhen?: (obj: Player) => boolean; +} +export interface PresetTalentsOptions { + customCondition?: (player: Player) => boolean; +} + +export interface PresetRotation extends PresetBase { + name: string; + rotation: SavedRotation; + tooltip?: string; + enableWhen?: (obj: Player) => boolean; +} +export interface PresetRotationOptions extends Pick { + talentTree?: number; + customCondition?: (player: Player) => boolean; +} + +export interface PresetEpWeights extends PresetBase { + epWeights: Stats; +} +export interface PresetEpWeightsOptions extends PresetOptionsBase {} + +export interface PresetEncounter extends PresetBase { + encounter?: EncounterProto; + healingModel?: HealingModel; + tanks?: UnitReference[]; + raidBuffs?: RaidBuffs; + debuffs?: Debuffs; + buffs?: IndividualBuffs; + consumes?: Consumes; +} +export interface PresetEncounterOptions extends PresetOptionsBase {} + +export interface PresetBuild { + name: string; + gear?: PresetGear; + talents?: PresetTalents; + rotation?: PresetRotation; + epWeights?: PresetEpWeights; + encounter?: PresetEncounter; +} + +export interface PresetBuildOptions extends Omit {} + +export function makePresetGear(name: string, gearJson: any, options?: PresetGearOptions): PresetGear { + const gear = EquipmentSpec.fromJson(gearJson); + return makePresetGearHelper(name, gear, options || {}); +} + +function makePresetGearHelper(name: string, gear: EquipmentSpec, options: PresetGearOptions): PresetGear { + const conditions: Array<(player: Player) => boolean> = []; + if (options.talentTree != undefined) { + conditions.push((player: Player) => player.getTalentTree() == options.talentTree); + } + if (options.talentTrees != undefined) { + conditions.push((player: Player) => (options.talentTrees || []).includes(player.getTalentTree())); + } + if (options.faction != undefined) { + conditions.push((player: Player) => player.getFaction() == options.faction); + } + if (options.customCondition != undefined) { + conditions.push(options.customCondition); + } + + return { + name: name, + tooltip: options.tooltip || Tooltips.BASIC_BIS_DISCLAIMER, + gear: gear, + enableWhen: conditions.length > 0 ? (player: Player) => conditions.every(cond => cond(player)) : undefined, + onLoad: options?.onLoad, + }; +} + +export function makePresetTalents(name: string, data: SavedTalents, options?: PresetTalentsOptions): PresetTalents { + const conditions: Array<(player: Player) => boolean> = []; + if (options && options.customCondition) { + conditions.push(options.customCondition); + } + + return { + name, + data, + enableWhen: conditions.length > 0 ? (player: Player) => conditions.every(cond => cond(player)) : undefined, + }; +} + +export const makePresetEpWeights = (name: string, epWeights: Stats, options?: PresetEpWeightsOptions): PresetEpWeights => { + return makePresetEpWeightHelper(name, epWeights, options || {}); +}; + +const makePresetEpWeightHelper = (name: string, epWeights: Stats, options?: PresetEpWeightsOptions): PresetEpWeights => { + const conditions: Array<(player: Player) => boolean> = []; + if (options?.customCondition !== undefined) { + conditions.push(options.customCondition); + } + + return { + name, + epWeights, + enableWhen: !!conditions.length ? (player: Player) => conditions.every(cond => cond(player)) : undefined, + onLoad: options?.onLoad, + }; +}; + +export function makePresetAPLRotation(name: string, rotationJson: any, options?: PresetRotationOptions): PresetRotation { + const rotation = SavedRotation.create({ + rotation: APLRotation.fromJson(rotationJson), + }); + return makePresetRotationHelper(name, rotation, options); +} + +export function makePresetSimpleRotation( + name: string, + spec: SpecType, + simpleRotation: SpecRotation, + options?: PresetRotationOptions, +): PresetRotation { + const rotation = SavedRotation.create({ + rotation: { + type: APLRotationType.TypeSimple, + simple: { + specRotationJson: JSON.stringify(specTypeFunctions[spec].rotationToJson(simpleRotation)), + }, + }, + }); + return makePresetRotationHelper(name, rotation, options); +} + +function makePresetRotationHelper(name: string, rotation: SavedRotation, options?: PresetRotationOptions): PresetRotation { + const conditions: Array<(player: Player) => boolean> = []; + if (options?.talentTree != undefined) { + conditions.push((player: Player) => player.getTalentTree() == options.talentTree); + } + if (options?.customCondition != undefined) { + conditions.push(options.customCondition); + } + + return { + name: name, + rotation: rotation, + enableWhen: conditions.length > 0 ? (player: Player) => conditions.every(cond => cond(player)) : undefined, + onLoad: options?.onLoad, + }; +} + +export const makePresetEncounter = (name: string, encounter?: PresetEncounter['encounter'] | string, options?: PresetEncounterOptions): PresetEncounter => { + let healingModel: PresetEncounter['healingModel'] = undefined; + let tanks: PresetEncounter['tanks'] = undefined; + let raidBuffs: PresetEncounter['raidBuffs'] = undefined; + let debuffs: PresetEncounter['debuffs'] = undefined; + let buffs: PresetEncounter['buffs'] = undefined; + let consumes: PresetEncounter['consumes'] = undefined; + if (typeof encounter === 'string') { + const parsedUrl = IndividualLinkImporter.tryParseUrlLocation(new URL(encounter)); + const settings = parsedUrl?.settings; + encounter = settings?.encounter; + healingModel = settings?.player?.healingModel; + tanks = settings?.tanks; + raidBuffs = settings?.raidBuffs; + debuffs = settings?.debuffs; + buffs = settings?.player?.buffs; + consumes = settings?.player?.consumes; + } + + return { + name, + encounter, + tanks, + healingModel, + raidBuffs, + debuffs, + buffs, + consumes, + ...options, + }; +}; + +export const makePresetBuild = (name: string, { gear, talents, rotation, epWeights, encounter }: PresetBuildOptions): PresetBuild => { + return { name, gear, talents, rotation, epWeights, encounter }; +}; + +export type SpecCheckWarning = { + condition: (player: Player) => boolean; + message: string; +}; + +export const makeSpecChangeWarningToast = (checks: SpecCheckWarning[], player: Player, options?: Partial) => { + const messages: string[] = checks.map(({ condition, message }) => condition(player) && message).filter((m): m is string => !!m); + if (messages.length) + new Toast({ + variant: 'warning', + body: ( + <> + {messages.map(message => ( +

{message}

+ ))} + + ), + delay: 5000 * messages.length, + ...options, + }); +}; diff --git a/ui/hunter/presets.ts b/ui/hunter/presets.ts index 3ce518858d..8c06a6c96e 100644 --- a/ui/hunter/presets.ts +++ b/ui/hunter/presets.ts @@ -172,9 +172,17 @@ export const DefaultTalentsRangedSV = TalentPresets[Phase.Phase4][2]; export const DefaultTalents = DefaultTalentsWeave; -export const PresetBuildWeave = PresetUtils.makePresetBuild('Weave', DefaultGearWeave, DefaultTalentsWeave, DefaultAPLWeave); -export const PresetBuildRangedMM = PresetUtils.makePresetBuild('Ranged MM', DefaultGearRangedMM, DefaultTalentsRangedMM, DefaultAPLRanged); -export const PresetBuildRangedSV = PresetUtils.makePresetBuild('Ranged SV', DefaultGearRangedSV, DefaultTalentsRangedSV, DefaultAPLRanged); +export const PresetBuildWeave = PresetUtils.makePresetBuild('Weave', { gear: DefaultGearWeave, talents: DefaultTalentsWeave, rotation: DefaultAPLWeave }); +export const PresetBuildRangedMM = PresetUtils.makePresetBuild('Ranged MM', { + gear: DefaultGearRangedMM, + talents: DefaultTalentsRangedMM, + rotation: DefaultAPLRanged, +}); +export const PresetBuildRangedSV = PresetUtils.makePresetBuild('Ranged SV', { + gear: DefaultGearRangedSV, + talents: DefaultTalentsRangedSV, + rotation: DefaultAPLRanged, +}); /////////////////////////////////////////////////////////////////////////// // Options diff --git a/ui/mage/presets.ts b/ui/mage/presets.ts index 890c6f65c4..192fa785fe 100644 --- a/ui/mage/presets.ts +++ b/ui/mage/presets.ts @@ -257,9 +257,13 @@ export const DefaultTalentsFrost = TalentPresets[Phase.Phase5][2]; export const DefaultTalents = DefaultTalentsFire; -export const PresetBuildArcane = PresetUtils.makePresetBuild('Arcane', DefaultGearArcane, DefaultTalentsArcane, DefaultAPLs[60][0]); -export const PresetBuildFire = PresetUtils.makePresetBuild('Fire', DefaultGearFire, DefaultTalentsFire, DefaultAPLs[60][1]); -export const PresetBuildFrost = PresetUtils.makePresetBuild('Frost', DefaultGearFrost, DefaultTalentsFrost, DefaultAPLs[60][2]); +export const PresetBuildArcane = PresetUtils.makePresetBuild('Arcane', { + gear: DefaultGearArcane, + talents: DefaultTalentsArcane, + rotation: DefaultAPLs[60][0], +}); +export const PresetBuildFire = PresetUtils.makePresetBuild('Fire', { gear: DefaultGearFire, talents: DefaultTalentsFire, rotation: DefaultAPLs[60][1] }); +export const PresetBuildFrost = PresetUtils.makePresetBuild('Frost', { gear: DefaultGearFrost, talents: DefaultTalentsFrost, rotation: DefaultAPLs[60][2] }); /////////////////////////////////////////////////////////////////////////// // Options diff --git a/ui/rogue/presets.ts b/ui/rogue/presets.ts index 665c1bc066..4e7a2f6675 100644 --- a/ui/rogue/presets.ts +++ b/ui/rogue/presets.ts @@ -289,21 +289,27 @@ export const DefaultTalentsSaber = TalentPresets[Phase.Phase5][2]; export const DefaultTalents = DefaultTalentsAssassin; -export const PresetBuildBackstab = PresetUtils.makePresetBuild('Backstab', DefaultGearBackstab, P5TalentBackstabAssassination, DefaultAPLBackstab); -export const PresetBuildMutilate = PresetUtils.makePresetBuild('Mutilate', DefaultGearMutilate, DefaultTalentsMutilate, DefaultAPLMutilate); -export const PresetBuildSaber = PresetUtils.makePresetBuild('Saber Slash', DefaultGearSaber, DefaultTalentsSaber, DefaultAPLSaber); -export const PresetBuildMutilateIEA = PresetUtils.makePresetBuild( - 'Mutilate IEA', - DefaultGearMutilate, - P5TalentMutilateSaberslashCTTCIEA, - ROTATION_PRESET_MUTILATE_IEA_P5, -); -export const PresetBuildSaberIEA = PresetUtils.makePresetBuild( - 'Saber Slash IEA', - DefaultGearSaber, - P5TalentMutilateSaberslashCTTCIEA, - ROTATION_PRESET_SABER_IEA_P5, -); +export const PresetBuildBackstab = PresetUtils.makePresetBuild('Backstab', { + gear: DefaultGearBackstab, + talents: P5TalentBackstabAssassination, + rotation: DefaultAPLBackstab, +}); +export const PresetBuildMutilate = PresetUtils.makePresetBuild('Mutilate', { + gear: DefaultGearMutilate, + talents: DefaultTalentsMutilate, + rotation: DefaultAPLMutilate, +}); +export const PresetBuildSaber = PresetUtils.makePresetBuild('Saber Slash', { gear: DefaultGearSaber, talents: DefaultTalentsSaber, rotation: DefaultAPLSaber }); +export const PresetBuildMutilateIEA = PresetUtils.makePresetBuild('Mutilate IEA', { + gear: DefaultGearMutilate, + talents: P5TalentMutilateSaberslashCTTCIEA, + rotation: ROTATION_PRESET_MUTILATE_IEA_P5, +}); +export const PresetBuildSaberIEA = PresetUtils.makePresetBuild('Saber Slash IEA', { + gear: DefaultGearSaber, + talents: P5TalentMutilateSaberslashCTTCIEA, + rotation: ROTATION_PRESET_SABER_IEA_P5, +}); /////////////////////////////////////////////////////////////////////////// // Options diff --git a/ui/tank_rogue/presets.ts b/ui/tank_rogue/presets.ts index fb63eda663..9f07f7fad7 100644 --- a/ui/tank_rogue/presets.ts +++ b/ui/tank_rogue/presets.ts @@ -185,6 +185,23 @@ export const DefaultTalentsSubtlety = TalentPresets[Phase.Phase5][0]; export const DefaultTalents = DefaultTalentsCombat; +/////////////////////////////////////////////////////////////////////////// +// Encounters +/////////////////////////////////////////////////////////////////////////// +export const PresetBuildEncounterDefault = PresetUtils.makePresetBuild('Default', { + encounter: PresetUtils.makePresetEncounter( + 'Default', + 'http://localhost:5173/sod/tank_rogue/?i=ce#eJyTklFgM5LgYBRg1GC0YHRgrGBvYJSYwMi8gJH5BiOr0gFmTgYwiHMQhDD0HCRnzQSBk/aWEJEL9oppYHDN3mgCM8eNJl4hDp/UstQcBTMDCXutB0wMgwlodDpQxRy1tRSZIyD1nzruAAJDMFmw3MEyM/9D68mQq/aOUBmHCEYAjL4g7A==', + ), +}); + +export const PresetBuildEncounterVael = PresetUtils.makePresetBuild('Vael', { + encounter: PresetUtils.makePresetEncounter( + 'Vael', + 'http://localhost:5173/sod/tank_rogue/?i=ce#eJyTkldgM5LmYBRg1GC0YHRgrGBvYGSZwMi8gJH5EiPDDUZWpe8snAxgEOcgCGHoOUjOmgkCJ+0tISIX7BXTwOCavdFxFo47qUJqTjmJydnlmXnpCj6JmUUKYYmpOYnFJUWJxVUKJRmpCs75RUWlBSUS9grMWg+YGAYT0Oh0oIo5amspMiejyd+RKu4AAkMw2bHZwVI2KsX6vv9Ve5jRDhGMRVMZORiFFEIyc1MVnEqL8kBx5phSlJqXmJOZl6oQlJqcmlmWmqKV75FfrpCTD5TNzCvJB8diWmZ6Rgk2TZnFCsnA6FbIzwOrK8hJrEwt0lNwyywCCoJlgCqMDIoVNIJTc1KTSxQMFIBG5qWWpRYpFEEs1AQAp5dhnw==', + ), +}); + /////////////////////////////////////////////////////////////////////////// // Options /////////////////////////////////////////////////////////////////////////// diff --git a/ui/tank_rogue/sim.ts b/ui/tank_rogue/sim.ts index 2649be1c21..94d4603c35 100644 --- a/ui/tank_rogue/sim.ts +++ b/ui/tank_rogue/sim.ts @@ -155,6 +155,7 @@ const SPEC_CONFIG = registerSpecConfig(Spec.SpecTankRogue, { ...Presets.GearPresets[Phase.Phase2], ...Presets.GearPresets[Phase.Phase1], ], + builds: [Presets.PresetBuildEncounterDefault, Presets.PresetBuildEncounterVael], }, autoRotation: player => { diff --git a/ui/tank_warlock/presets.ts b/ui/tank_warlock/presets.ts index aeb82e2a78..01656d081e 100644 --- a/ui/tank_warlock/presets.ts +++ b/ui/tank_warlock/presets.ts @@ -195,9 +195,13 @@ export const DefaultTalentsDestro = TalentPresets[Phase.Phase4][2]; export const DefaultTalents = DefaultTalentsDestro; -export const PresetBuildAff = PresetUtils.makePresetBuild('Aff', DefaultGearAff, DefaultTalentsAff, DefaultAPLs[60][0]); -export const PresetBuildDemo = PresetUtils.makePresetBuild('Demo', DefaultGearDemo, DefaultTalentsDemo, DefaultAPLs[60][1]); -export const PresetBuildDestro = PresetUtils.makePresetBuild('Destro', DefaultGearDestro, DefaultTalentsDestro, DefaultAPLs[60][2]); +export const PresetBuildAff = PresetUtils.makePresetBuild('Aff', { gear: DefaultGearAff, talents: DefaultTalentsAff, rotation: DefaultAPLs[60][0] }); +export const PresetBuildDemo = PresetUtils.makePresetBuild('Demo', { gear: DefaultGearDemo, talents: DefaultTalentsDemo, rotation: DefaultAPLs[60][1] }); +export const PresetBuildDestro = PresetUtils.makePresetBuild('Destro', { + gear: DefaultGearDestro, + talents: DefaultTalentsDestro, + rotation: DefaultAPLs[60][2], +}); /////////////////////////////////////////////////////////////////////////// // Options diff --git a/ui/tank_warrior/presets.ts b/ui/tank_warrior/presets.ts index 103acb8e7e..7bbe9fb771 100644 --- a/ui/tank_warrior/presets.ts +++ b/ui/tank_warrior/presets.ts @@ -94,8 +94,8 @@ export const TalentPresets = { export const DefaultTalents = TalentPresets[Phase.Phase4][0]; -export const PresetBuildTanky = PresetUtils.makePresetBuild('Tanky', DefaultGearTanky, TalentsPhase4Prot, DefaultAPLs[60]); -export const PresetBuildDamage = PresetUtils.makePresetBuild('Damage', DefaultGearDamage, TalentsPhase4Fury, DefaultAPLs[60]); +export const PresetBuildTanky = PresetUtils.makePresetBuild('Tanky', { gear: DefaultGearTanky, talents: TalentsPhase4Prot, rotation: DefaultAPLs[60] }); +export const PresetBuildDamage = PresetUtils.makePresetBuild('Damage', { gear: DefaultGearDamage, talents: TalentsPhase4Fury, rotation: DefaultAPLs[60] }); /////////////////////////////////////////////////////////////////////////// // Options Presets diff --git a/ui/warlock/presets.ts b/ui/warlock/presets.ts index 79c5cda6c1..cf8c5d5b6e 100644 --- a/ui/warlock/presets.ts +++ b/ui/warlock/presets.ts @@ -220,8 +220,12 @@ export const DefaultTalentsDestro = TalentPresets[Phase.Phase4][1]; export const DefaultTalents = DefaultTalentsDestro; -export const PresetBuildAff = PresetUtils.makePresetBuild('Aff', DefaultGearAff, DefaultTalentsAff, DefaultAPLs[60][0]); -export const PresetBuildDestro = PresetUtils.makePresetBuild('Destro', DefaultGearDestro, DefaultTalentsDestro, DefaultAPLs[60][2]); +export const PresetBuildAff = PresetUtils.makePresetBuild('Aff', { gear: DefaultGearAff, talents: DefaultTalentsAff, rotation: DefaultAPLs[60][0] }); +export const PresetBuildDestro = PresetUtils.makePresetBuild('Destro', { + gear: DefaultGearDestro, + talents: DefaultTalentsDestro, + rotation: DefaultAPLs[60][2], +}); /////////////////////////////////////////////////////////////////////////// // Options diff --git a/ui/warrior/presets.ts b/ui/warrior/presets.ts index f438a6f51d..9bf2e03e7d 100644 --- a/ui/warrior/presets.ts +++ b/ui/warrior/presets.ts @@ -205,9 +205,9 @@ export const DefaultTalentsDW = TalentPresets[Phase.Phase5][1]; export const DefaultTalents = DefaultTalents2H; -export const PresetBuild2H = PresetUtils.makePresetBuild('Two-Handed', DefaultGear2H, DefaultTalents2H, DefaultAPLs[60][0]); -export const PresetBuildDW = PresetUtils.makePresetBuild('Dual-Wield', DefaultGearDW, DefaultTalentsDW, DefaultAPLs[60][1]); -// export const PresetBuildGlad = PresetUtils.makePresetBuild('Glad', DefaultGearGlad, DefaultTalentsGlad, DefaultAPLs[60][3]); +export const PresetBuild2H = PresetUtils.makePresetBuild('Two-Handed', { gear: DefaultGear2H, talents: DefaultTalents2H, rotation: DefaultAPLs[60][0] }); +export const PresetBuildDW = PresetUtils.makePresetBuild('Dual-Wield', { gear: DefaultGearDW, talents: DefaultTalentsDW, rotation: DefaultAPLs[60][1] }); +// export const PresetBuildGlad = PresetUtils.makePresetBuild('Glad', { gear: DefaultGearGlad, talents: DefaultTalentsGlad, rotation: DefaultAPLs[60][3] }); /////////////////////////////////////////////////////////////////////////// // Options Presets