diff --git a/ui/balance_druid/sim.ts b/ui/balance_druid/sim.ts
index 075217e95f..069cced2c8 100644
--- a/ui/balance_druid/sim.ts
+++ b/ui/balance_druid/sim.ts
@@ -1,13 +1,11 @@
 import { Spec } from '../core/proto/common.js';
 import { Stat } from '../core/proto/common.js';
 import {
-	APLAction,
-	APLListItem,
 	APLRotation,
 } from '../core/proto/apl.js';
 import { Stats } from '../core/proto_utils/stats.js';
 import { Player } from '../core/player.js';
-import { IndividualSimUI } from '../core/individual_sim_ui.js';
+import { IndividualSimUI, registerSpecConfig } from '../core/individual_sim_ui.js';
 
 import * as IconInputs from '../core/components/icon_inputs.js';
 import * as OtherInputs from '../core/components/other_inputs.js';
@@ -15,130 +13,131 @@ import * as OtherInputs from '../core/components/other_inputs.js';
 import * as DruidInputs from './inputs.js';
 import * as Presets from './presets.js';
 
-// noinspection TypeScriptValidateTypes
-export class BalanceDruidSimUI extends IndividualSimUI<Spec.SpecBalanceDruid> {
-	constructor(parentElem: HTMLElement, player: Player<Spec.SpecBalanceDruid>) {
-		super(parentElem, player, {
-			cssClass: 'balance-druid-sim-ui',
-			cssScheme: 'druid',
-			// List any known bugs / issues here, and they'll be shown on the site.
-			knownIssues: [
-			],
+const SPEC_CONFIG = registerSpecConfig(Spec.SpecBalanceDruid, {
+	cssClass: 'balance-druid-sim-ui',
+	cssScheme: 'druid',
+	// List any known bugs / issues here, and they'll be shown on the site.
+	knownIssues: [
+	],
+
+	// All stats for which EP should be calculated.
+	epStats: [
+		Stat.StatIntellect,
+		Stat.StatSpirit,
+		Stat.StatSpellPower,
+		Stat.StatSpellHit,
+		Stat.StatSpellCrit,
+		Stat.StatSpellHaste,
+		Stat.StatMP5,
+	],
+	// Reference stat against which to calculate EP. I think all classes use either spell power or attack power.
+	epReferenceStat: Stat.StatSpellPower,
+	// Which stats to display in the Character Stats section, at the bottom of the left-hand sidebar.
+	displayStats: [
+		Stat.StatHealth,
+		Stat.StatStamina,
+		Stat.StatIntellect,
+		Stat.StatSpirit,
+		Stat.StatSpellPower,
+		Stat.StatSpellHit,
+		Stat.StatSpellCrit,
+		Stat.StatSpellHaste,
+		Stat.StatMP5,
+	],
 
-			// All stats for which EP should be calculated.
-			epStats: [
-				Stat.StatIntellect,
-				Stat.StatSpirit,
-				Stat.StatSpellPower,
-				Stat.StatSpellHit,
-				Stat.StatSpellCrit,
-				Stat.StatSpellHaste,
-				Stat.StatMP5,
-			],
-			// Reference stat against which to calculate EP. I think all classes use either spell power or attack power.
-			epReferenceStat: Stat.StatSpellPower,
-			// Which stats to display in the Character Stats section, at the bottom of the left-hand sidebar.
-			displayStats: [
-				Stat.StatHealth,
-				Stat.StatStamina,
-				Stat.StatIntellect,
-				Stat.StatSpirit,
-				Stat.StatSpellPower,
-				Stat.StatSpellHit,
-				Stat.StatSpellCrit,
-				Stat.StatSpellHaste,
-				Stat.StatMP5,
-			],
+	defaults: {
+		// Default equipped gear.
+		gear: Presets.P3_PRESET_HORDE.gear,
+		// Default EP weights for sorting gear in the gear picker.
+		epWeights: Stats.fromMap({
+			[Stat.StatIntellect]: 0.43,
+			[Stat.StatSpirit]: 0.34,
+			[Stat.StatSpellPower]: 1,
+			[Stat.StatSpellCrit]: 0.82,
+			[Stat.StatSpellHaste]: 0.80,
+			[Stat.StatMP5]: 0.00,
+		}),
+		// Default consumes settings.
+		consumes: Presets.DefaultConsumes,
+		// Default rotation settings.
+		rotation: Presets.DefaultRotation,
+		// Default talents.
+		talents: Presets.Phase3Talents.data,
+		// Default spec-specific settings.
+		specOptions: Presets.DefaultOptions,
+		// Default raid/party buffs settings.
+		raidBuffs: Presets.DefaultRaidBuffs,
+		partyBuffs: Presets.DefaultPartyBuffs,
+		individualBuffs: Presets.DefaultIndividualBuffs,
+		debuffs: Presets.DefaultDebuffs,
+		other: Presets.OtherDefaults,
+	},
 
-			defaults: {
-				// Default equipped gear.
-				gear: Presets.P3_PRESET_HORDE.gear,
-				// Default EP weights for sorting gear in the gear picker.
-				epWeights: Stats.fromMap({
-					[Stat.StatIntellect]: 0.43,
-					[Stat.StatSpirit]: 0.34,
-					[Stat.StatSpellPower]: 1,
-					[Stat.StatSpellCrit]: 0.82,
-					[Stat.StatSpellHaste]: 0.80,
-					[Stat.StatMP5]: 0.00,
-				}),
-				// Default consumes settings.
-				consumes: Presets.DefaultConsumes,
-				// Default rotation settings.
-				rotation: Presets.DefaultRotation,
-				// Default talents.
-				talents: Presets.Phase3Talents.data,
-				// Default spec-specific settings.
-				specOptions: Presets.DefaultOptions,
-				// Default raid/party buffs settings.
-				raidBuffs: Presets.DefaultRaidBuffs,
-				partyBuffs: Presets.DefaultPartyBuffs,
-				individualBuffs: Presets.DefaultIndividualBuffs,
-				debuffs: Presets.DefaultDebuffs,
-				other: Presets.OtherDefaults,
-			},
+	// IconInputs to include in the 'Player' section on the settings tab.
+	playerIconInputs: [
+		DruidInputs.SelfInnervate,
+	],
+	// Inputs to include in the 'Rotation' section on the settings tab.
+	rotationInputs: DruidInputs.BalanceDruidRotationConfig,
+	// Buff and Debuff inputs to include/exclude, overriding the EP-based defaults.
+	includeBuffDebuffInputs: [
+		IconInputs.MeleeHasteBuff,
+		IconInputs.MeleeCritBuff,
+		IconInputs.AttackPowerPercentBuff,
+		IconInputs.AttackPowerBuff,
+		IconInputs.MajorArmorDebuff,
+		IconInputs.MinorArmorDebuff,
+		IconInputs.PhysicalDamageDebuff,
+	],
+	excludeBuffDebuffInputs: [
+	],
+	// Inputs to include in the 'Other' section on the settings tab.
+	otherInputs: {
+		inputs: [
+			DruidInputs.OkfUptime,
+			OtherInputs.TankAssignment,
+			OtherInputs.ReactionTime,
+			OtherInputs.DistanceFromTarget,
+			OtherInputs.nibelungAverageCasts,
+		],
+	},
+	encounterPicker: {
+		// Whether to include 'Execute Duration (%)' in the 'Encounter' section of the settings tab.
+		showExecuteProportion: false,
+	},
 
-			// IconInputs to include in the 'Player' section on the settings tab.
-			playerIconInputs: [
-				DruidInputs.SelfInnervate,
-			],
-			// Inputs to include in the 'Rotation' section on the settings tab.
-			rotationInputs: DruidInputs.BalanceDruidRotationConfig,
-			// Buff and Debuff inputs to include/exclude, overriding the EP-based defaults.
-			includeBuffDebuffInputs: [
-				IconInputs.MeleeHasteBuff,
-				IconInputs.MeleeCritBuff,
-				IconInputs.AttackPowerPercentBuff,
-				IconInputs.AttackPowerBuff,
-				IconInputs.MajorArmorDebuff,
-				IconInputs.MinorArmorDebuff,
-				IconInputs.PhysicalDamageDebuff,
-			],
-			excludeBuffDebuffInputs: [
-			],
-			// Inputs to include in the 'Other' section on the settings tab.
-			otherInputs: {
-				inputs: [
-					DruidInputs.OkfUptime,
-					OtherInputs.TankAssignment,
-					OtherInputs.ReactionTime,
-					OtherInputs.DistanceFromTarget,
-					OtherInputs.nibelungAverageCasts,
-				],
-			},
-			encounterPicker: {
-				// Whether to include 'Execute Duration (%)' in the 'Encounter' section of the settings tab.
-				showExecuteProportion: false,
-			},
+	presets: {
+		// Preset talents that the user can quickly select.
+		talents: [
+			Presets.Phase1Talents,
+			Presets.Phase2Talents,
+			Presets.Phase3Talents,
+			Presets.Phase4Talents,
+		],
+		rotations: [
+			Presets.ROTATION_PRESET_P3_APL,
+			Presets.ROTATION_PRESET_P4_FOCUS_APL,
+			Presets.ROTATION_PRESET_P4_STARFIRE_APL,
+		],
+		// Preset gear configurations that the user can quickly select.
+		gear: [
+			Presets.PRERAID_PRESET,
+			Presets.P1_PRESET,
+			Presets.P2_PRESET,
+			Presets.P3_PRESET_HORDE,
+			Presets.P3_PRESET_ALLI,
+			Presets.P4_PRESET_HORDE,
+			Presets.P4_PRESET_ALLI,
+		],
+	},
 
-			presets: {
-				// Preset talents that the user can quickly select.
-				talents: [
-					Presets.Phase1Talents,
-					Presets.Phase2Talents,
-					Presets.Phase3Talents,
-					Presets.Phase4Talents,
-				],
-				rotations: [
-					Presets.ROTATION_PRESET_P3_APL,
-					Presets.ROTATION_PRESET_P4_FOCUS_APL,
-					Presets.ROTATION_PRESET_P4_STARFIRE_APL,
-				],
-				// Preset gear configurations that the user can quickly select.
-				gear: [
-					Presets.PRERAID_PRESET,
-					Presets.P1_PRESET,
-					Presets.P2_PRESET,
-					Presets.P3_PRESET_HORDE,
-					Presets.P3_PRESET_ALLI,
-					Presets.P4_PRESET_HORDE,
-					Presets.P4_PRESET_ALLI,
-				],
-			},
+	autoRotation: (_player: Player<Spec.SpecBalanceDruid>): APLRotation => {
+		return Presets.ROTATION_PRESET_P3_APL.rotation.rotation!;
+	},
+});
 
-			autoRotation: (player: Player<Spec.SpecBalanceDruid>): APLRotation => {
-				return Presets.ROTATION_PRESET_P3_APL.rotation.rotation!;
-			},
-		});
+export class BalanceDruidSimUI extends IndividualSimUI<Spec.SpecBalanceDruid> {
+	constructor(parentElem: HTMLElement, player: Player<Spec.SpecBalanceDruid>) {
+		super(parentElem, player, SPEC_CONFIG);
 	}
 }
diff --git a/ui/core/individual_sim_ui.ts b/ui/core/individual_sim_ui.ts
index 648d4c06fd..059b8b4e1f 100644
--- a/ui/core/individual_sim_ui.ts
+++ b/ui/core/individual_sim_ui.ts
@@ -1,5 +1,5 @@
 import { aplLaunchStatuses, LaunchStatus, simLaunchStatuses } from './launched_sims';
-import { Player, AutoRotationGenerator, SimpleRotationGenerator } from './player';
+import { Player, PlayerConfig, registerSpecConfig as registerPlayerConfig } from './player';
 import { SimUI, SimWarning } from './sim_ui';
 import { EventID, TypedEvent } from './typed_event';
 
@@ -88,7 +88,7 @@ export interface OtherDefaults {
 	nibelungAverageCasts?: number,
 }
 
-export interface IndividualSimUIConfig<SpecType extends Spec> {
+export interface IndividualSimUIConfig<SpecType extends Spec> extends PlayerConfig<SpecType> {
 	// Additional css class to add to the root element.
 	cssClass: string,
 	// Used to generate schemed components. E.g. 'shaman', 'druid', 'raid'
@@ -137,11 +137,13 @@ export interface IndividualSimUIConfig<SpecType extends Spec> {
 	presets: {
 		gear: Array<PresetGear>,
 		talents: Array<SavedDataConfig<Player<any>, SavedTalents>>,
-		rotations?: Array<PresetRotation>,
+		rotations: Array<PresetRotation>,
 	},
+}
 
-	autoRotation: AutoRotationGenerator<SpecType>,
-	simpleRotation?: SimpleRotationGenerator<SpecType>,
+export function registerSpecConfig<SpecType extends Spec>(spec: SpecType, config: IndividualSimUIConfig<SpecType>): IndividualSimUIConfig<SpecType> {
+	registerPlayerConfig(spec, config);
+	return config;
 }
 
 export interface Settings {
@@ -184,11 +186,6 @@ export abstract class IndividualSimUI<SpecType extends Spec> extends SimUI {
 		this.prevEpIterations = 0;
 		this.prevEpSimResult = null;
 
-		player.setAutoRotationGenerator(config.autoRotation);
-		if (aplLaunchStatuses[player.spec] == LaunchStatus.Launched && config.simpleRotation) {
-			player.setSimpleRotationGenerator(config.simpleRotation);
-		}
-
 		this.addWarning({
 			updateOn: this.player.gearChangeEmitter,
 			getContent: () => {
diff --git a/ui/core/player.ts b/ui/core/player.ts
index 9e5a870a81..92adc23b0e 100644
--- a/ui/core/player.ts
+++ b/ui/core/player.ts
@@ -209,6 +209,17 @@ export interface MeleeCritCapInfo {
 export type AutoRotationGenerator<SpecType extends Spec> = (player: Player<SpecType>) => APLRotation;
 export type SimpleRotationGenerator<SpecType extends Spec> = (player: Player<SpecType>, simpleRotation: SpecRotation<SpecType>, cooldowns: Cooldowns) => APLRotation;
 
+export interface PlayerConfig<SpecType extends Spec> {
+	autoRotation: AutoRotationGenerator<SpecType>,
+	simpleRotation?: SimpleRotationGenerator<SpecType>,
+}
+
+const SPEC_CONFIGS: Partial<Record<Spec, PlayerConfig<any>>> = {};
+
+export function registerSpecConfig(spec: Spec, config: PlayerConfig<any>) {
+	SPEC_CONFIGS[spec] = config;
+}
+
 // Manages all the gear / consumes / other settings for a single Player.
 export class Player<SpecType extends Spec> {
 	readonly sim: Sim;
@@ -241,8 +252,8 @@ export class Player<SpecType extends Spec> {
 	private healingModel: HealingModel = HealingModel.create();
 	private healingEnabled: boolean = false;
 
-	private autoRotationGenerator: AutoRotationGenerator<SpecType> | null = null;
-	private simpleRotationGenerator: SimpleRotationGenerator<SpecType> | null = null;
+	private readonly autoRotationGenerator: AutoRotationGenerator<SpecType> | null = null;
+	private readonly simpleRotationGenerator: SimpleRotationGenerator<SpecType> | null = null;
 
 	private itemEPCache = new Array<Map<number, number>>();
 	private gemEPCache = new Map<number, number>();
@@ -294,6 +305,17 @@ export class Player<SpecType extends Spec> {
 		this.rotation = this.specTypeFunctions.rotationCreate();
 		this.specOptions = this.specTypeFunctions.optionsCreate();
 
+		const specConfig = SPEC_CONFIGS[this.spec] as PlayerConfig<SpecType>;
+		if (!specConfig) {
+			throw new Error('Could not find spec config for spec: ' + this.spec);
+		}
+		this.autoRotationGenerator = specConfig.autoRotation;
+		if (aplLaunchStatuses[this.spec] == LaunchStatus.Launched && specConfig.simpleRotation) {
+			this.simpleRotationGenerator = specConfig.simpleRotation;
+		} else {
+			this.simpleRotationGenerator = null;
+		}
+
 		for(let i = 0; i < ItemSlot.ItemSlotRanged+1; ++i) {
 			this.itemEPCache[i] = new Map();
 		}
@@ -789,14 +811,6 @@ export class Player<SpecType extends Spec> {
 		}
 	}
 
-	setAutoRotationGenerator(generator: AutoRotationGenerator<SpecType>) {
-		this.autoRotationGenerator = generator;
-	}
-
-	setSimpleRotationGenerator(generator: SimpleRotationGenerator<SpecType>) {
-		this.simpleRotationGenerator = generator;
-	}
-
 	hasSimpleRotationGenerator(): boolean {
 		return this.simpleRotationGenerator != null;
 	}
diff --git a/ui/deathknight/sim.ts b/ui/deathknight/sim.ts
index 2a4e28efad..0a46dbd2ac 100644
--- a/ui/deathknight/sim.ts
+++ b/ui/deathknight/sim.ts
@@ -9,9 +9,7 @@ import { Stat, PseudoStat } from '../core/proto/common.js';
 import { TristateEffect } from '../core/proto/common.js'
 import { Player } from '../core/player.js';
 import { Stats } from '../core/proto_utils/stats.js';
-import { IndividualSimUI } from '../core/individual_sim_ui.js';
-
-import { Deathknight, Deathknight_Rotation as DeathKnightRotation, DeathknightTalents as DeathKnightTalents, Deathknight_Options as DeathKnightOptions } from '../core/proto/deathknight.js';
+import { IndividualSimUI, registerSpecConfig } from '../core/individual_sim_ui.js';
 
 import * as IconInputs from '../core/components/icon_inputs.js';
 import * as OtherInputs from '../core/components/other_inputs.js';
@@ -19,235 +17,237 @@ import * as OtherInputs from '../core/components/other_inputs.js';
 import * as DeathKnightInputs from './inputs.js';
 import * as Presets from './presets.js';
 
-export class DeathknightSimUI extends IndividualSimUI<Spec.SpecDeathknight> {
-	constructor(parentElem: HTMLElement, player: Player<Spec.SpecDeathknight>) {
-		super(parentElem, player, {
-			cssClass: 'deathknight-sim-ui',
-			cssScheme: 'death-knight',
-			// List any known bugs / issues here and they'll be shown on the site.
-			knownIssues: [
-			],
+const SPEC_CONFIG = registerSpecConfig(Spec.SpecDeathknight, {
+	cssClass: 'deathknight-sim-ui',
+	cssScheme: 'death-knight',
+	// List any known bugs / issues here and they'll be shown on the site.
+	knownIssues: [
+	],
 
-			// All stats for which EP should be calculated.
-			epStats: [
-				Stat.StatStrength,
-				Stat.StatArmor,
-				Stat.StatAgility,
-				Stat.StatAttackPower,
-				Stat.StatExpertise,
-				Stat.StatMeleeHit,
-				Stat.StatMeleeCrit,
-				Stat.StatMeleeHaste,
-				Stat.StatArmorPenetration,
-				Stat.StatSpellHit,
-				Stat.StatSpellCrit,
-				Stat.StatSpellHaste,
-			],
-			epPseudoStats: [
-				PseudoStat.PseudoStatMainHandDps,
-				PseudoStat.PseudoStatOffHandDps,
-			],
-			// Reference stat against which to calculate EP. I think all classes use either spell power or attack power.
-			epReferenceStat: Stat.StatAttackPower,
-			// Which stats to display in the Character Stats section, at the bottom of the left-hand sidebar.
-			displayStats: [
-				Stat.StatHealth,
-				Stat.StatArmor,
-				Stat.StatStrength,
-				Stat.StatAgility,
-				Stat.StatSpellHit,
-				Stat.StatSpellCrit,
-				Stat.StatAttackPower,
-				Stat.StatMeleeHit,
-				Stat.StatMeleeCrit,
-				Stat.StatMeleeHaste,
-				Stat.StatArmorPenetration,
-				Stat.StatExpertise,
-			],
-			defaults: {
-				// Default equipped gear.
-				gear: Presets.P2_UNHOLY_DW_PRESET.gear,
-				// Default EP weights for sorting gear in the gear picker.
-				epWeights: Stats.fromMap({
-					[Stat.StatStrength]: 3.22,
-					[Stat.StatAgility]: 0.62,
-					[Stat.StatArmor]: 0.01,
-					[Stat.StatAttackPower]: 1,
-					[Stat.StatExpertise]: 1.13,
-					[Stat.StatMeleeHaste]: 1.85,
-					[Stat.StatMeleeHit]: 1.92,
-					[Stat.StatMeleeCrit]: 0.76,
-					[Stat.StatArmorPenetration]: 0.77,
-					[Stat.StatSpellHit]: 0.80,
-					[Stat.StatSpellCrit]: 0.34,
-				}, {
-					[PseudoStat.PseudoStatMainHandDps]: 3.10,
-					[PseudoStat.PseudoStatOffHandDps]: 1.79,
-				}),
-				// Default consumes settings.
-				consumes: Presets.DefaultConsumes,
-				// Default rotation settings.
-				rotation: Presets.DefaultUnholyRotation,
-				// Default talents.
-				talents: Presets.UnholyDualWieldTalents.data,
-				// Default spec-specific settings.
-				specOptions: Presets.DefaultUnholyOptions,
-				// Default raid/party buffs settings.
-				raidBuffs: RaidBuffs.create({
-					giftOfTheWild: TristateEffect.TristateEffectImproved,
-					swiftRetribution: true,
-					strengthOfEarthTotem: TristateEffect.TristateEffectImproved,
-					icyTalons: true,
-					abominationsMight: true,
-					leaderOfThePack: TristateEffect.TristateEffectRegular,
-					sanctifiedRetribution: true,
-					bloodlust: true,
-					devotionAura: TristateEffect.TristateEffectImproved,
-					stoneskinTotem: TristateEffect.TristateEffectImproved,
-					moonkinAura: TristateEffect.TristateEffectRegular,
-					wrathOfAirTotem: true,
-					powerWordFortitude: TristateEffect.TristateEffectImproved,
-				}),
-				partyBuffs: PartyBuffs.create({
-					heroicPresence: false,
-				}),
-				individualBuffs: IndividualBuffs.create({
-					blessingOfKings: true,
-					blessingOfMight: TristateEffect.TristateEffectImproved,
-				}),
-				debuffs: Debuffs.create({
-					bloodFrenzy: true,
-					faerieFire: TristateEffect.TristateEffectImproved,
-					sunderArmor: true,
-					ebonPlaguebringer: true,
-					mangle: true,
-					heartOfTheCrusader: true,
-					shadowMastery: true,
-				}),
-			},
+	// All stats for which EP should be calculated.
+	epStats: [
+		Stat.StatStrength,
+		Stat.StatArmor,
+		Stat.StatAgility,
+		Stat.StatAttackPower,
+		Stat.StatExpertise,
+		Stat.StatMeleeHit,
+		Stat.StatMeleeCrit,
+		Stat.StatMeleeHaste,
+		Stat.StatArmorPenetration,
+		Stat.StatSpellHit,
+		Stat.StatSpellCrit,
+		Stat.StatSpellHaste,
+	],
+	epPseudoStats: [
+		PseudoStat.PseudoStatMainHandDps,
+		PseudoStat.PseudoStatOffHandDps,
+	],
+	// Reference stat against which to calculate EP. I think all classes use either spell power or attack power.
+	epReferenceStat: Stat.StatAttackPower,
+	// Which stats to display in the Character Stats section, at the bottom of the left-hand sidebar.
+	displayStats: [
+		Stat.StatHealth,
+		Stat.StatArmor,
+		Stat.StatStrength,
+		Stat.StatAgility,
+		Stat.StatSpellHit,
+		Stat.StatSpellCrit,
+		Stat.StatAttackPower,
+		Stat.StatMeleeHit,
+		Stat.StatMeleeCrit,
+		Stat.StatMeleeHaste,
+		Stat.StatArmorPenetration,
+		Stat.StatExpertise,
+	],
+	defaults: {
+		// Default equipped gear.
+		gear: Presets.P2_UNHOLY_DW_PRESET.gear,
+		// Default EP weights for sorting gear in the gear picker.
+		epWeights: Stats.fromMap({
+			[Stat.StatStrength]: 3.22,
+			[Stat.StatAgility]: 0.62,
+			[Stat.StatArmor]: 0.01,
+			[Stat.StatAttackPower]: 1,
+			[Stat.StatExpertise]: 1.13,
+			[Stat.StatMeleeHaste]: 1.85,
+			[Stat.StatMeleeHit]: 1.92,
+			[Stat.StatMeleeCrit]: 0.76,
+			[Stat.StatArmorPenetration]: 0.77,
+			[Stat.StatSpellHit]: 0.80,
+			[Stat.StatSpellCrit]: 0.34,
+		}, {
+			[PseudoStat.PseudoStatMainHandDps]: 3.10,
+			[PseudoStat.PseudoStatOffHandDps]: 1.79,
+		}),
+		// Default consumes settings.
+		consumes: Presets.DefaultConsumes,
+		// Default rotation settings.
+		rotation: Presets.DefaultUnholyRotation,
+		// Default talents.
+		talents: Presets.UnholyDualWieldTalents.data,
+		// Default spec-specific settings.
+		specOptions: Presets.DefaultUnholyOptions,
+		// Default raid/party buffs settings.
+		raidBuffs: RaidBuffs.create({
+			giftOfTheWild: TristateEffect.TristateEffectImproved,
+			swiftRetribution: true,
+			strengthOfEarthTotem: TristateEffect.TristateEffectImproved,
+			icyTalons: true,
+			abominationsMight: true,
+			leaderOfThePack: TristateEffect.TristateEffectRegular,
+			sanctifiedRetribution: true,
+			bloodlust: true,
+			devotionAura: TristateEffect.TristateEffectImproved,
+			stoneskinTotem: TristateEffect.TristateEffectImproved,
+			moonkinAura: TristateEffect.TristateEffectRegular,
+			wrathOfAirTotem: true,
+			powerWordFortitude: TristateEffect.TristateEffectImproved,
+		}),
+		partyBuffs: PartyBuffs.create({
+			heroicPresence: false,
+		}),
+		individualBuffs: IndividualBuffs.create({
+			blessingOfKings: true,
+			blessingOfMight: TristateEffect.TristateEffectImproved,
+		}),
+		debuffs: Debuffs.create({
+			bloodFrenzy: true,
+			faerieFire: TristateEffect.TristateEffectImproved,
+			sunderArmor: true,
+			ebonPlaguebringer: true,
+			mangle: true,
+			heartOfTheCrusader: true,
+			shadowMastery: true,
+		}),
+	},
 
-			autoRotation: (player: Player<Spec.SpecDeathknight>): APLRotation => {
-				const talentTree = player.getTalentTree();
-				const numTargets = player.sim.encounter.targets.length;
-				switch (talentTree) {
-					case 0: 
-						if (player.getSpecOptions().drwPestiApply || numTargets > 1) {
-							if (numTargets > 5) {
-								return Presets.BLOOD_PESTI_AOE_ROTATION_PRESET_DEFAULT.rotation.rotation!;
-							} else {
-								return Presets.BLOOD_DPS_ROTATION_PRESET_DEFAULT.rotation.rotation!;
-							}
-						} else {
-							return Presets.BLOOD_DPS_ROTATION_PRESET_DEFAULT.rotation.rotation!;
-						}
-					case 1: 
-						const talentPoints = player.getTalentTreePoints()
-						// TODO: Add Frost AOE rotation
-						if (talentPoints[0] > talentPoints[2]) {
-							return Presets.FROST_BL_PESTI_ROTATION_PRESET_DEFAULT.rotation.rotation!;
-						} else {
-							return Presets.FROST_UH_PESTI_ROTATION_PRESET_DEFAULT.rotation.rotation!;
-						}
-					default: 
-						if (numTargets > 1) {
-							return Presets.UNHOLY_DND_AOE_ROTATION_PRESET_DEFAULT.rotation.rotation!;
-						} else {
-							if (player.getEquippedItem(ItemSlot.ItemSlotMainHand)!.item.handType == HandType.HandTypeTwoHand) {
-								return Presets.UNHOLY_2H_ROTATION_PRESET_DEFAULT.rotation.rotation!;
-							} else {
-								return Presets.UNHOLY_DW_ROTATION_PRESET_DEFAULT.rotation.rotation!;
-							}
-						}
+	autoRotation: (player: Player<Spec.SpecDeathknight>): APLRotation => {
+		const talentTree = player.getTalentTree();
+		const numTargets = player.sim.encounter.targets.length;
+		switch (talentTree) {
+			case 0: 
+				if (player.getSpecOptions().drwPestiApply || numTargets > 1) {
+					if (numTargets > 5) {
+						return Presets.BLOOD_PESTI_AOE_ROTATION_PRESET_DEFAULT.rotation.rotation!;
+					} else {
+						return Presets.BLOOD_DPS_ROTATION_PRESET_DEFAULT.rotation.rotation!;
+					}
+				} else {
+					return Presets.BLOOD_DPS_ROTATION_PRESET_DEFAULT.rotation.rotation!;
+				}
+			case 1: 
+				const talentPoints = player.getTalentTreePoints()
+				// TODO: Add Frost AOE rotation
+				if (talentPoints[0] > talentPoints[2]) {
+					return Presets.FROST_BL_PESTI_ROTATION_PRESET_DEFAULT.rotation.rotation!;
+				} else {
+					return Presets.FROST_UH_PESTI_ROTATION_PRESET_DEFAULT.rotation.rotation!;
 				}
-			},
+			default: 
+				if (numTargets > 1) {
+					return Presets.UNHOLY_DND_AOE_ROTATION_PRESET_DEFAULT.rotation.rotation!;
+				} else {
+					if (player.getEquippedItem(ItemSlot.ItemSlotMainHand)!.item.handType == HandType.HandTypeTwoHand) {
+						return Presets.UNHOLY_2H_ROTATION_PRESET_DEFAULT.rotation.rotation!;
+					} else {
+						return Presets.UNHOLY_DW_ROTATION_PRESET_DEFAULT.rotation.rotation!;
+					}
+				}
+		}
+	},
+
+	// IconInputs to include in the 'Player' section on the settings tab.
+	playerIconInputs: [
+	],
+	// Inputs to include in the 'Rotation' section on the settings tab.
+	rotationInputs: DeathKnightInputs.DeathKnightRotationConfig,
+	petConsumeInputs: [
+		IconInputs.SpicedMammothTreats,
+	],
+	// Buff and Debuff inputs to include/exclude, overriding the EP-based defaults.
+	includeBuffDebuffInputs: [
+		IconInputs.SpellDamageDebuff,
+		IconInputs.StaminaBuff,
+	],
+	excludeBuffDebuffInputs: [
+		IconInputs.AttackPowerDebuff,
+		IconInputs.DamageReductionPercentBuff,
+		IconInputs.MeleeAttackSpeedDebuff,
+	],
+	// Inputs to include in the 'Other' section on the settings tab.
+	otherInputs: {
+		inputs: [
+			DeathKnightInputs.DiseaseDowntime,
+			DeathKnightInputs.DrwPestiApply,
+			DeathKnightInputs.SelfUnholyFrenzy,
+			DeathKnightInputs.StartingRunicPower,
+			DeathKnightInputs.PetUptime,
 
-			// IconInputs to include in the 'Player' section on the settings tab.
-			playerIconInputs: [
-			],
-			// Inputs to include in the 'Rotation' section on the settings tab.
-			rotationInputs: DeathKnightInputs.DeathKnightRotationConfig,
-			petConsumeInputs: [
-				IconInputs.SpicedMammothTreats,
-			],
-			// Buff and Debuff inputs to include/exclude, overriding the EP-based defaults.
-			includeBuffDebuffInputs: [
-				IconInputs.SpellDamageDebuff,
-				IconInputs.StaminaBuff,
-			],
-			excludeBuffDebuffInputs: [
-				IconInputs.AttackPowerDebuff,
-				IconInputs.DamageReductionPercentBuff,
-				IconInputs.MeleeAttackSpeedDebuff,
-			],
-			// Inputs to include in the 'Other' section on the settings tab.
-			otherInputs: {
-				inputs: [
-					DeathKnightInputs.DiseaseDowntime,
-					DeathKnightInputs.DrwPestiApply,
-					DeathKnightInputs.SelfUnholyFrenzy,
-					DeathKnightInputs.StartingRunicPower,
-					DeathKnightInputs.PetUptime,
+			DeathKnightInputs.PrecastGhoulFrenzy,
+			DeathKnightInputs.PrecastHornOfWinter,
 
-					DeathKnightInputs.PrecastGhoulFrenzy,
-					DeathKnightInputs.PrecastHornOfWinter,
+			OtherInputs.TankAssignment,
+			OtherInputs.InFrontOfTarget,
+		],
+	},
+	encounterPicker: {
+		// Whether to include 'Execute Duration (%)' in the 'Encounter' section of the settings tab.
+		showExecuteProportion: false,
+	},
 
-					OtherInputs.TankAssignment,
-					OtherInputs.InFrontOfTarget,
-				],
-			},
-			encounterPicker: {
-				// Whether to include 'Execute Duration (%)' in the 'Encounter' section of the settings tab.
-				showExecuteProportion: false,
-			},
+	presets: {
+		// Preset talents that the user can quickly select.
+		talents: [
+			Presets.BloodTalents,
+			Presets.FrostTalents,
+			Presets.FrostUnholyTalents,
+			Presets.UnholyDualWieldTalents,
+			Presets.UnholyDualWieldSSTalents,
+			Presets.Unholy2HTalents,
+			Presets.UnholyAoeTalents,
+		],
+		// Preset rotations that the user can quickly select.
+		rotations: [
+			Presets.BLOOD_ROTATION_PRESET_LEGACY_DEFAULT,
+			Presets.FROST_ROTATION_PRESET_LEGACY_DEFAULT,
+			Presets.UNHOLY_DW_ROTATION_PRESET_LEGACY_DEFAULT,
+			Presets.BLOOD_DPS_ROTATION_PRESET_DEFAULT,
+			Presets.BLOOD_PESTI_AOE_ROTATION_PRESET_DEFAULT,
+			Presets.FROST_BL_PESTI_ROTATION_PRESET_DEFAULT,
+			Presets.FROST_UH_PESTI_ROTATION_PRESET_DEFAULT,
+			Presets.UNHOLY_DW_ROTATION_PRESET_DEFAULT,
+			Presets.UNHOLY_2H_ROTATION_PRESET_DEFAULT,
+			Presets.UNHOLY_DND_AOE_ROTATION_PRESET_DEFAULT,
+		],
+		// Preset gear configurations that the user can quickly select.
+		gear: [
+			Presets.P1_BLOOD_PRESET,
+			Presets.P2_BLOOD_PRESET,
+			Presets.P3_BLOOD_PRESET,
+			Presets.P4_BLOOD_PRESET,
+			Presets.P1_FROST_PRESET,
+			Presets.P2_FROST_PRESET,
+			Presets.P3_FROST_PRESET,
+			Presets.P4_FROST_PRESET,
+			Presets.P1_UNHOLY_DW_PRESET,
+			Presets.P2_UNHOLY_DW_PRESET,
+			Presets.P3_UNHOLY_DW_PRESET,
+			Presets.P4_UNHOLY_DW_PRESET,
+			Presets.P4_UNHOLY_2H_PRESET,
+			// Not needed anymore just filling ui Space
+			// Disabled on purpose
+			//Presets.P1_FROSTSUBUNH_PRESET,
+			//Presets.P1_FROST_PRE_BIS_PRESET,
+			//Presets.PRERAID_UNHOLY_DW_PRESET,
+			//Presets.PRERAID_UNHOLY_2H_PRESET,
+			//Presets.P1_UNHOLY_2H_PRESET,
+		],
+	},
+});
 
-			presets: {
-				// Preset talents that the user can quickly select.
-				talents: [
-					Presets.BloodTalents,
-					Presets.FrostTalents,
-					Presets.FrostUnholyTalents,
-					Presets.UnholyDualWieldTalents,
-					Presets.UnholyDualWieldSSTalents,
-					Presets.Unholy2HTalents,
-					Presets.UnholyAoeTalents,
-				],
-				// Preset rotations that the user can quickly select.
-				rotations: [
-					Presets.BLOOD_ROTATION_PRESET_LEGACY_DEFAULT,
-					Presets.FROST_ROTATION_PRESET_LEGACY_DEFAULT,
-					Presets.UNHOLY_DW_ROTATION_PRESET_LEGACY_DEFAULT,
-					Presets.BLOOD_DPS_ROTATION_PRESET_DEFAULT,
-					Presets.BLOOD_PESTI_AOE_ROTATION_PRESET_DEFAULT,
-					Presets.FROST_BL_PESTI_ROTATION_PRESET_DEFAULT,
-					Presets.FROST_UH_PESTI_ROTATION_PRESET_DEFAULT,
-					Presets.UNHOLY_DW_ROTATION_PRESET_DEFAULT,
-					Presets.UNHOLY_2H_ROTATION_PRESET_DEFAULT,
-					Presets.UNHOLY_DND_AOE_ROTATION_PRESET_DEFAULT,
-				],
-				// Preset gear configurations that the user can quickly select.
-				gear: [
-					Presets.P1_BLOOD_PRESET,
-					Presets.P2_BLOOD_PRESET,
-					Presets.P3_BLOOD_PRESET,
-					Presets.P4_BLOOD_PRESET,
-					Presets.P1_FROST_PRESET,
-					Presets.P2_FROST_PRESET,
-					Presets.P3_FROST_PRESET,
-					Presets.P4_FROST_PRESET,
-					Presets.P1_UNHOLY_DW_PRESET,
-					Presets.P2_UNHOLY_DW_PRESET,
-					Presets.P3_UNHOLY_DW_PRESET,
-					Presets.P4_UNHOLY_DW_PRESET,
-					Presets.P4_UNHOLY_2H_PRESET,
-					// Not needed anymore just filling ui Space
-					// Disabled on purpose
-					//Presets.P1_FROSTSUBUNH_PRESET,
-					//Presets.P1_FROST_PRE_BIS_PRESET,
-					//Presets.PRERAID_UNHOLY_DW_PRESET,
-					//Presets.PRERAID_UNHOLY_2H_PRESET,
-					//Presets.P1_UNHOLY_2H_PRESET,
-				],
-			},
-		});
+export class DeathknightSimUI extends IndividualSimUI<Spec.SpecDeathknight> {
+	constructor(parentElem: HTMLElement, player: Player<Spec.SpecDeathknight>) {
+		super(parentElem, player, SPEC_CONFIG);
 	}
 }
diff --git a/ui/elemental_shaman/sim.ts b/ui/elemental_shaman/sim.ts
index 8218352fd4..31c765daec 100644
--- a/ui/elemental_shaman/sim.ts
+++ b/ui/elemental_shaman/sim.ts
@@ -6,179 +6,178 @@ import { Spec } from '../core/proto/common.js';
 import { Stat } from '../core/proto/common.js';
 import { TristateEffect } from '../core/proto/common.js'
 import {
-	APLAction,
-	APLListItem,
 	APLRotation,
 } from '../core/proto/apl.js';
 import { Player } from '../core/player.js';
 import { Stats } from '../core/proto_utils/stats.js';
-import { IndividualSimUI } from '../core/individual_sim_ui.js';
-import { EventID, TypedEvent } from '../core/typed_event.js';
+import { IndividualSimUI, registerSpecConfig } from '../core/individual_sim_ui.js';
+import { TypedEvent } from '../core/typed_event.js';
 import { TotemsSection } from '../core/components/totem_inputs.js';
 import * as OtherInputs from '../core/components/other_inputs.js';
 import * as Mechanics from '../core/constants/mechanics.js';
 import * as ShamanInputs from './inputs.js';
 import * as Presets from './presets.js';
-import { ElementalShaman_Options_ThunderstormRange, ElementalShaman_Rotation_BloodlustUse} from '../core/proto/shaman.js';
 
-export class ElementalShamanSimUI extends IndividualSimUI<Spec.SpecElementalShaman> {
-	constructor(parentElem: HTMLElement, player: Player<Spec.SpecElementalShaman>) {
-		super(parentElem, player, {
-			cssClass: 'elemental-shaman-sim-ui',
-			cssScheme: 'shaman',
-			// List any known bugs / issues here and they'll be shown on the site.
-			knownIssues: [
-			],
-			warnings: [
-				// Warning to use all 4 totems if T6 2pc bonus is active.
-				(simUI: IndividualSimUI<Spec.SpecElementalShaman>) => {
-					return {
-						updateOn: TypedEvent.onAny([simUI.player.rotationChangeEmitter, simUI.player.currentStatsEmitter]),
-						getContent: () => {
-							const hasT62P = simUI.player.getCurrentStats().sets.includes('Skyshatter Regalia (2pc)');
-							const totems = simUI.player.getRotation().totems!;
-							const hasAll4Totems = totems && totems.earth && totems.air && totems.fire && totems.water;
-							if (hasT62P && !hasAll4Totems) {
-								return 'T6 2pc bonus is equipped, but inactive because not all 4 totem types are being used.';
-							} else {
-								return '';
-							}
-						},
-					};
+const SPEC_CONFIG = registerSpecConfig(Spec.SpecElementalShaman, {
+	cssClass: 'elemental-shaman-sim-ui',
+	cssScheme: 'shaman',
+	// List any known bugs / issues here and they'll be shown on the site.
+	knownIssues: [
+	],
+	warnings: [
+		// Warning to use all 4 totems if T6 2pc bonus is active.
+		(simUI: IndividualSimUI<Spec.SpecElementalShaman>) => {
+			return {
+				updateOn: TypedEvent.onAny([simUI.player.rotationChangeEmitter, simUI.player.currentStatsEmitter]),
+				getContent: () => {
+					const hasT62P = simUI.player.getCurrentStats().sets.includes('Skyshatter Regalia (2pc)');
+					const totems = simUI.player.getRotation().totems!;
+					const hasAll4Totems = totems && totems.earth && totems.air && totems.fire && totems.water;
+					if (hasT62P && !hasAll4Totems) {
+						return 'T6 2pc bonus is equipped, but inactive because not all 4 totem types are being used.';
+					} else {
+						return '';
+					}
 				},
-			],
+			};
+		},
+	],
+
+	// All stats for which EP should be calculated.
+	epStats: [
+		Stat.StatIntellect,
+		Stat.StatSpellPower,
+		Stat.StatSpellHit,
+		Stat.StatSpellCrit,
+		Stat.StatSpellHaste,
+		Stat.StatMP5,
+	],
+	// Reference stat against which to calculate EP. I think all classes use either spell power or attack power.
+	epReferenceStat: Stat.StatSpellPower,
+	// Which stats to display in the Character Stats section, at the bottom of the left-hand sidebar.
+	displayStats: [
+		Stat.StatHealth,
+		Stat.StatMana,
+		Stat.StatStamina,
+		Stat.StatIntellect,
+		Stat.StatSpellPower,
+		Stat.StatSpellHit,
+		Stat.StatSpellCrit,
+		Stat.StatSpellHaste,
+		Stat.StatMP5,
+	],
+	modifyDisplayStats: (player: Player<Spec.SpecElementalShaman>) => {
+		let stats = new Stats();
+		stats = stats.addStat(Stat.StatSpellHit, player.getTalents().elementalPrecision * Mechanics.SPELL_HIT_RATING_PER_HIT_CHANCE);
+		stats = stats.addStat(Stat.StatSpellCrit,
+			player.getTalents().tidalMastery * 1 * Mechanics.SPELL_CRIT_RATING_PER_CRIT_CHANCE);
+		return {
+			talents: stats,
+		};
+	},
 
-			// All stats for which EP should be calculated.
-			epStats: [
-				Stat.StatIntellect,
-				Stat.StatSpellPower,
-				Stat.StatSpellHit,
-				Stat.StatSpellCrit,
-				Stat.StatSpellHaste,
-				Stat.StatMP5,
-			],
-			// Reference stat against which to calculate EP. I think all classes use either spell power or attack power.
-			epReferenceStat: Stat.StatSpellPower,
-			// Which stats to display in the Character Stats section, at the bottom of the left-hand sidebar.
-			displayStats: [
-				Stat.StatHealth,
-				Stat.StatMana,
-				Stat.StatStamina,
-				Stat.StatIntellect,
-				Stat.StatSpellPower,
-				Stat.StatSpellHit,
-				Stat.StatSpellCrit,
-				Stat.StatSpellHaste,
-				Stat.StatMP5,
-			],
-			modifyDisplayStats: (player: Player<Spec.SpecElementalShaman>) => {
-				let stats = new Stats();
-				stats = stats.addStat(Stat.StatSpellHit, player.getTalents().elementalPrecision * Mechanics.SPELL_HIT_RATING_PER_HIT_CHANCE);
-				stats = stats.addStat(Stat.StatSpellCrit,
-					player.getTalents().tidalMastery * 1 * Mechanics.SPELL_CRIT_RATING_PER_CRIT_CHANCE);
-				return {
-					talents: stats,
-				};
-			},
+	defaults: {
+		// Default equipped gear.
+		gear: Presets.P3_PRESET_HORDE.gear,
+		// Default EP weights for sorting gear in the gear picker.
+		epWeights: Stats.fromMap({
+			[Stat.StatIntellect]: 0.22,
+			[Stat.StatSpellPower]: 1,
+			[Stat.StatSpellCrit]: 0.67,
+			[Stat.StatSpellHaste]: 1.29,
+			[Stat.StatMP5]: 0.08,
+		}),
+		// Default consumes settings.
+		consumes: Presets.DefaultConsumes,
+		// Default rotation settings.
+		rotation: Presets.DefaultRotation,
+		// Default talents.
+		talents: Presets.StandardTalents.data,
+		// Default spec-specific settings.
+		specOptions: Presets.DefaultOptions,
+		other: Presets.OtherDefaults,
+		// Default raid/party buffs settings.
+		raidBuffs: RaidBuffs.create({
+			arcaneBrilliance: true,
+			divineSpirit: true,
+			giftOfTheWild: TristateEffect.TristateEffectImproved,
+			moonkinAura: TristateEffect.TristateEffectImproved,
+			sanctifiedRetribution: true,
+			demonicPact: 500,
+			wrathOfAirTotem: true,
+		}),
+		partyBuffs: PartyBuffs.create({
+		}),
+		individualBuffs: IndividualBuffs.create({
+			blessingOfKings: true,
+			blessingOfWisdom: 2,
+			vampiricTouch: true,
+		}),
+		debuffs: Debuffs.create({
+			faerieFire: TristateEffect.TristateEffectImproved,
+			judgementOfWisdom: true,
+			misery: true,
+			curseOfElements: true,
+			shadowMastery: true,
+			heartOfTheCrusader: true,
+		}),
+	},
+	// IconInputs to include in the 'Player' section on the settings tab.
+	playerIconInputs: [
+		ShamanInputs.ShamanShieldInput,
+	],
+	// Inputs to include in the 'Rotation' section on the settings tab.
+	rotationInputs: ShamanInputs.ElementalShamanRotationConfig,
+	// Buff and Debuff inputs to include/exclude, overriding the EP-based defaults.
+	includeBuffDebuffInputs: [
+	],
+	excludeBuffDebuffInputs: [
+	],
+	// Inputs to include in the 'Other' section on the settings tab.
+	otherInputs: {
+		inputs: [
+			ShamanInputs.InThunderstormRange,
+			OtherInputs.TankAssignment,
+			OtherInputs.nibelungAverageCasts,
+		],
+	},
+	customSections: [
+		TotemsSection,
+	],
+	encounterPicker: {
+		// Whether to include 'Execute Duration (%)' in the 'Encounter' section of the settings tab.
+		showExecuteProportion: false,
+	},
 
-			defaults: {
-				// Default equipped gear.
-				gear: Presets.P3_PRESET_HORDE.gear,
-				// Default EP weights for sorting gear in the gear picker.
-				epWeights: Stats.fromMap({
-					[Stat.StatIntellect]: 0.22,
-					[Stat.StatSpellPower]: 1,
-					[Stat.StatSpellCrit]: 0.67,
-					[Stat.StatSpellHaste]: 1.29,
-					[Stat.StatMP5]: 0.08,
-				}),
-				// Default consumes settings.
-				consumes: Presets.DefaultConsumes,
-				// Default rotation settings.
-				rotation: Presets.DefaultRotation,
-				// Default talents.
-				talents: Presets.StandardTalents.data,
-				// Default spec-specific settings.
-				specOptions: Presets.DefaultOptions,
-				other: Presets.OtherDefaults,
-				// Default raid/party buffs settings.
-				raidBuffs: RaidBuffs.create({
-					arcaneBrilliance: true,
-					divineSpirit: true,
-					giftOfTheWild: TristateEffect.TristateEffectImproved,
-					moonkinAura: TristateEffect.TristateEffectImproved,
-					sanctifiedRetribution: true,
-					demonicPact: 500,
-					wrathOfAirTotem: true,
-				}),
-				partyBuffs: PartyBuffs.create({
-				}),
-				individualBuffs: IndividualBuffs.create({
-					blessingOfKings: true,
-					blessingOfWisdom: 2,
-					vampiricTouch: true,
-				}),
-				debuffs: Debuffs.create({
-					faerieFire: TristateEffect.TristateEffectImproved,
-					judgementOfWisdom: true,
-					misery: true,
-					curseOfElements: true,
-					shadowMastery: true,
-					heartOfTheCrusader: true,
-				}),
-			},
-			// IconInputs to include in the 'Player' section on the settings tab.
-			playerIconInputs: [
-				ShamanInputs.ShamanShieldInput,
-			],
-			// Inputs to include in the 'Rotation' section on the settings tab.
-			rotationInputs: ShamanInputs.ElementalShamanRotationConfig,
-			// Buff and Debuff inputs to include/exclude, overriding the EP-based defaults.
-			includeBuffDebuffInputs: [
-			],
-			excludeBuffDebuffInputs: [
-			],
-			// Inputs to include in the 'Other' section on the settings tab.
-			otherInputs: {
-				inputs: [
-					ShamanInputs.InThunderstormRange,
-					OtherInputs.TankAssignment,
-					OtherInputs.nibelungAverageCasts,
-				],
-			},
-			customSections: [
-				TotemsSection,
-			],
-			encounterPicker: {
-				// Whether to include 'Execute Duration (%)' in the 'Encounter' section of the settings tab.
-				showExecuteProportion: false,
-			},
+	presets: {
+		// Preset talents that the user can quickly select.
+		talents: [
+			Presets.StandardTalents,
+		],
+		// Preset rotations that the user can quickly select.
+		rotations: [
+			Presets.ROTATION_PRESET_LEGACY,
+			Presets.ROTATION_PRESET_DEFAULT,
+			Presets.ROTATION_PRESET_ADVANCED,
+		],
+		// Preset gear configurations that the user can quickly select.
+		gear: [
+			Presets.PRERAID_PRESET,
+			Presets.P1_PRESET,
+			Presets.P2_PRESET,
+			Presets.P3_PRESET_ALLI,
+			Presets.P3_PRESET_HORDE,
+			Presets.P4_PRESET,
+		],
+	},
 
-			presets: {
-				// Preset talents that the user can quickly select.
-				talents: [
-					Presets.StandardTalents,
-				],
-				// Preset rotations that the user can quickly select.
-				rotations: [
-					Presets.ROTATION_PRESET_LEGACY,
-					Presets.ROTATION_PRESET_DEFAULT,
-					Presets.ROTATION_PRESET_ADVANCED,
-				],
-				// Preset gear configurations that the user can quickly select.
-				gear: [
-					Presets.PRERAID_PRESET,
-					Presets.P1_PRESET,
-					Presets.P2_PRESET,
-					Presets.P3_PRESET_ALLI,
-					Presets.P3_PRESET_HORDE,
-					Presets.P4_PRESET,
-				],
-			},
+	autoRotation: (_player: Player<Spec.SpecElementalShaman>): APLRotation => {
+		return Presets.ROTATION_PRESET_DEFAULT.rotation.rotation!;
+	},
+});
 
-			autoRotation: (player: Player<Spec.SpecElementalShaman>): APLRotation => {
-				return Presets.ROTATION_PRESET_DEFAULT.rotation.rotation!;
-			},
-		});
+export class ElementalShamanSimUI extends IndividualSimUI<Spec.SpecElementalShaman> {
+	constructor(parentElem: HTMLElement, player: Player<Spec.SpecElementalShaman>) {
+		super(parentElem, player, SPEC_CONFIG);
 	}
 }
diff --git a/ui/enhancement_shaman/sim.ts b/ui/enhancement_shaman/sim.ts
index a7ab7f5cb5..9145a39340 100644
--- a/ui/enhancement_shaman/sim.ts
+++ b/ui/enhancement_shaman/sim.ts
@@ -4,14 +4,12 @@ import { Spec } from '../core/proto/common.js';
 import { Stat, PseudoStat } from '../core/proto/common.js';
 import { TristateEffect } from '../core/proto/common.js'
 import {
-	APLAction,
-	APLListItem,
 	APLRotation,
 } from '../core/proto/apl.js';
 import { ShamanImbue } from '../core/proto/shaman.js';
 import { Player } from '../core/player.js';
 import { Stats } from '../core/proto_utils/stats.js';
-import { IndividualSimUI } from '../core/individual_sim_ui.js';
+import { IndividualSimUI, registerSpecConfig } from '../core/individual_sim_ui.js';
 import { TotemsSection } from '../core/components/totem_inputs.js';
 import * as IconInputs from '../core/components/icon_inputs.js';
 import * as OtherInputs from '../core/components/other_inputs.js';
@@ -20,175 +18,177 @@ import * as ShamanInputs from './inputs.js';
 import * as Presets from './presets.js';
 import { FireElementalSection } from '../core/components/fire_elemental_inputs.js';
 
-export class EnhancementShamanSimUI extends IndividualSimUI<Spec.SpecEnhancementShaman> {
-	constructor(parentElem: HTMLElement, player: Player<Spec.SpecEnhancementShaman>) {
-		super(parentElem, player, {
-			cssClass: 'enhancement-shaman-sim-ui',
-			cssScheme: 'shaman',
-			// List any known bugs / issues here and they'll be shown on the site.
-			knownIssues: [
-			],
+const SPEC_CONFIG = registerSpecConfig(Spec.SpecEnhancementShaman, {
+	cssClass: 'enhancement-shaman-sim-ui',
+	cssScheme: 'shaman',
+	// List any known bugs / issues here and they'll be shown on the site.
+	knownIssues: [
+	],
+
+	// All stats for which EP should be calculated.
+	epStats: [
+		Stat.StatIntellect,
+		Stat.StatAgility,
+		Stat.StatStrength,
+		Stat.StatAttackPower,
+		Stat.StatMeleeHit,
+		Stat.StatMeleeCrit,
+		Stat.StatMeleeHaste,
+		Stat.StatArmorPenetration,
+		Stat.StatExpertise,
+		Stat.StatSpellPower,
+		Stat.StatSpellCrit,
+		Stat.StatSpellHit,
+		Stat.StatSpellHaste,
+	],
+	epPseudoStats: [
+		PseudoStat.PseudoStatMainHandDps,
+		PseudoStat.PseudoStatOffHandDps,
+	],
+	// Reference stat against which to calculate EP. I think all classes use either spell power or attack power.
+	epReferenceStat: Stat.StatAttackPower,
+	// Which stats to display in the Character Stats section, at the bottom of the left-hand sidebar.
+	displayStats: [
+		Stat.StatHealth,
+		Stat.StatStamina,
+		Stat.StatStrength,
+		Stat.StatAgility,
+		Stat.StatIntellect,
+		Stat.StatAttackPower,
+		Stat.StatMeleeHit,
+		Stat.StatMeleeCrit,
+		Stat.StatMeleeHaste,
+		Stat.StatExpertise,
+		Stat.StatArmorPenetration,
+		Stat.StatSpellPower,
+		Stat.StatSpellHit,
+		Stat.StatSpellCrit,
+		Stat.StatSpellHaste,
+	],
 
-			// All stats for which EP should be calculated.
-			epStats: [
-				Stat.StatIntellect,
-				Stat.StatAgility,
-				Stat.StatStrength,
-				Stat.StatAttackPower,
-				Stat.StatMeleeHit,
-				Stat.StatMeleeCrit,
-				Stat.StatMeleeHaste,
-				Stat.StatArmorPenetration,
-				Stat.StatExpertise,
-				Stat.StatSpellPower,
-				Stat.StatSpellCrit,
-				Stat.StatSpellHit,
-				Stat.StatSpellHaste,
-			],
-			epPseudoStats: [
-				PseudoStat.PseudoStatMainHandDps,
-				PseudoStat.PseudoStatOffHandDps,
-			],
-			// Reference stat against which to calculate EP. I think all classes use either spell power or attack power.
-			epReferenceStat: Stat.StatAttackPower,
-			// Which stats to display in the Character Stats section, at the bottom of the left-hand sidebar.
-			displayStats: [
-				Stat.StatHealth,
-				Stat.StatStamina,
-				Stat.StatStrength,
-				Stat.StatAgility,
-				Stat.StatIntellect,
-				Stat.StatAttackPower,
-				Stat.StatMeleeHit,
-				Stat.StatMeleeCrit,
-				Stat.StatMeleeHaste,
-				Stat.StatExpertise,
-				Stat.StatArmorPenetration,
-				Stat.StatSpellPower,
-				Stat.StatSpellHit,
-				Stat.StatSpellCrit,
-				Stat.StatSpellHaste,
-			],
+	defaults: {
+		// Default equipped gear.
+		gear: Presets.P4_PRESET_WF.gear,
+		// Default EP weights for sorting gear in the gear picker.
+		epWeights: Stats.fromMap({
+			[Stat.StatIntellect]: 1.48,
+			[Stat.StatAgility]: 1.59,
+			[Stat.StatStrength]: 1.1,
+			[Stat.StatSpellPower]: 1.13,
+			[Stat.StatSpellHit]: 0, //default EP assumes cap
+			[Stat.StatSpellCrit]: 0.91,
+			[Stat.StatSpellHaste]: 0.37,
+			[Stat.StatAttackPower]: 1.0,
+			[Stat.StatMeleeHit]: 1.38,
+			[Stat.StatMeleeCrit]: 0.81,
+			[Stat.StatMeleeHaste]: 1.61, //haste is complicated
+			[Stat.StatArmorPenetration]: 0.48,
+			[Stat.StatExpertise]: 0, //default EP assumes cap
+		}, {
+			[PseudoStat.PseudoStatMainHandDps]: 5.21,
+			[PseudoStat.PseudoStatOffHandDps]: 2.21,
+		}),
+		// Default consumes settings.
+		consumes: Presets.DefaultConsumes,
+		// Default rotation settings.
+		rotation: Presets.DefaultRotation,
+		// Default talents.
+		talents: Presets.StandardTalents.data,
+		// Default spec-specific settings.
+		specOptions: Presets.DefaultOptions,
+		// Default raid/party buffs settings.
+		raidBuffs: Presets.DefaultRaidBuffs,
+		partyBuffs: PartyBuffs.create({
+		}),
+		individualBuffs: IndividualBuffs.create({
+			blessingOfKings: true,
+			blessingOfWisdom: TristateEffect.TristateEffectImproved,
+			blessingOfMight: TristateEffect.TristateEffectImproved,
+			judgementsOfTheWise: true,
+		}),
+		debuffs: Presets.DefaultDebuffs,
+	},
 
-			defaults: {
-				// Default equipped gear.
-				gear: Presets.P4_PRESET_WF.gear,
-				// Default EP weights for sorting gear in the gear picker.
-				epWeights: Stats.fromMap({
-					[Stat.StatIntellect]: 1.48,
-					[Stat.StatAgility]: 1.59,
-					[Stat.StatStrength]: 1.1,
-					[Stat.StatSpellPower]: 1.13,
-					[Stat.StatSpellHit]: 0, //default EP assumes cap
-					[Stat.StatSpellCrit]: 0.91,
-					[Stat.StatSpellHaste]: 0.37,
-					[Stat.StatAttackPower]: 1.0,
-					[Stat.StatMeleeHit]: 1.38,
-					[Stat.StatMeleeCrit]: 0.81,
-					[Stat.StatMeleeHaste]: 1.61, //haste is complicated
-					[Stat.StatArmorPenetration]: 0.48,
-					[Stat.StatExpertise]: 0, //default EP assumes cap
-				}, {
-					[PseudoStat.PseudoStatMainHandDps]: 5.21,
-					[PseudoStat.PseudoStatOffHandDps]: 2.21,
-				}),
-				// Default consumes settings.
-				consumes: Presets.DefaultConsumes,
-				// Default rotation settings.
-				rotation: Presets.DefaultRotation,
-				// Default talents.
-				talents: Presets.StandardTalents.data,
-				// Default spec-specific settings.
-				specOptions: Presets.DefaultOptions,
-				// Default raid/party buffs settings.
-				raidBuffs: Presets.DefaultRaidBuffs,
-				partyBuffs: PartyBuffs.create({
-				}),
-				individualBuffs: IndividualBuffs.create({
-					blessingOfKings: true,
-					blessingOfWisdom: TristateEffect.TristateEffectImproved,
-					blessingOfMight: TristateEffect.TristateEffectImproved,
-					judgementsOfTheWise: true,
-				}),
-				debuffs: Presets.DefaultDebuffs,
-			},
+	// IconInputs to include in the 'Player' section on the settings tab.
+	playerIconInputs: [
+		ShamanInputs.ShamanShieldInput,
+		ShamanInputs.ShamanImbueMH,
+		ShamanInputs.ShamanImbueOH,
+	],
+	// Inputs to include in the 'Rotation' section on the settings tab.
+	rotationInputs: ShamanInputs.EnhancementShamanRotationConfig,
+	// Buff and Debuff inputs to include/exclude, overriding the EP-based defaults.
+	includeBuffDebuffInputs: [
+		IconInputs.ReplenishmentBuff,
+		IconInputs.MP5Buff,
+		IconInputs.SpellHasteBuff,
+		IconInputs.SpiritBuff,
+	],
+	excludeBuffDebuffInputs: [
+		IconInputs.BleedDebuff,
+	],
+	// Inputs to include in the 'Other' section on the settings tab.
+	otherInputs: {
+		inputs: [
+			ShamanInputs.SyncTypeInput,
+			OtherInputs.TankAssignment,
+			OtherInputs.InFrontOfTarget,
+		],
+	},
+	customSections: [
+		TotemsSection,
+		FireElementalSection
+	],
+	encounterPicker: {
+		// Whether to include 'Execute Duration (%)' in the 'Encounter' section of the settings tab.
+		showExecuteProportion: false,
+	},
 
-			// IconInputs to include in the 'Player' section on the settings tab.
-			playerIconInputs: [
-				ShamanInputs.ShamanShieldInput,
-				ShamanInputs.ShamanImbueMH,
-				ShamanInputs.ShamanImbueOH,
-			],
-			// Inputs to include in the 'Rotation' section on the settings tab.
-			rotationInputs: ShamanInputs.EnhancementShamanRotationConfig,
-			// Buff and Debuff inputs to include/exclude, overriding the EP-based defaults.
-			includeBuffDebuffInputs: [
-				IconInputs.ReplenishmentBuff,
-				IconInputs.MP5Buff,
-				IconInputs.SpellHasteBuff,
-				IconInputs.SpiritBuff,
-			],
-			excludeBuffDebuffInputs: [
-				IconInputs.BleedDebuff,
-			],
-			// Inputs to include in the 'Other' section on the settings tab.
-			otherInputs: {
-				inputs: [
-					ShamanInputs.SyncTypeInput,
-					OtherInputs.TankAssignment,
-					OtherInputs.InFrontOfTarget,
-				],
-			},
-			customSections: [
-				TotemsSection,
-				FireElementalSection
-			],
-			encounterPicker: {
-				// Whether to include 'Execute Duration (%)' in the 'Encounter' section of the settings tab.
-				showExecuteProportion: false,
-			},
+	presets: {
+		// Preset talents that the user can quickly select.
+		talents: [
+			Presets.StandardTalents,
+			Presets.Phase3Talents,
+		],
+		// Preset rotations that the user can quickly select.
+		rotations: [
+			Presets.ROTATION_FT_DEFAULT,
+			Presets.ROTATION_WF_DEFAULT,
+			Presets.ROTATION_PHASE_3,
+		],
+		// Preset gear configurations that the user can quickly select.
+		gear: [
+			Presets.PRERAID_PRESET,
+			Presets.P1_PRESET,
+			Presets.P2_PRESET_FT,
+			Presets.P2_PRESET_WF,
+			Presets.P3_PRESET_ALLIANCE,
+			Presets.P3_PRESET_HORDE,
+			Presets.P4_PRESET_FT,
+			Presets.P4_PRESET_WF,
+		],
+	},
 
-			presets: {
-				// Preset talents that the user can quickly select.
-				talents: [
-					Presets.StandardTalents,
-					Presets.Phase3Talents,
-				],
-				// Preset rotations that the user can quickly select.
-				rotations: [
-					Presets.ROTATION_FT_DEFAULT,
-					Presets.ROTATION_WF_DEFAULT,
-					Presets.ROTATION_PHASE_3,
-				],
-				// Preset gear configurations that the user can quickly select.
-				gear: [
-					Presets.PRERAID_PRESET,
-					Presets.P1_PRESET,
-					Presets.P2_PRESET_FT,
-					Presets.P2_PRESET_WF,
-					Presets.P3_PRESET_ALLIANCE,
-					Presets.P3_PRESET_HORDE,
-					Presets.P4_PRESET_FT,
-					Presets.P4_PRESET_WF,
-				],
-			},
+	autoRotation: (player: Player<Spec.SpecEnhancementShaman>): APLRotation => {
+		const hasT94P = player.getCurrentStats().sets.includes('Triumphant Nobundo\'s Battlegear (4pc)')
+			|| player.getCurrentStats().sets.includes('Nobundo\'s Battlegear (4pc)')
+			|| player.getCurrentStats().sets.includes('Triumphant Thrall\'s Battlegear (4pc)')
+			|| player.getCurrentStats().sets.includes('Thrall\'s Battlegear (4pc)');
+		const options = player.getSpecOptions();
 
-			autoRotation: (player: Player<Spec.SpecEnhancementShaman>): APLRotation => {
-				const hasT94P = player.getCurrentStats().sets.includes('Triumphant Nobundo\'s Battlegear (4pc)')
-					|| player.getCurrentStats().sets.includes('Nobundo\'s Battlegear (4pc)')
-					|| player.getCurrentStats().sets.includes('Triumphant Thrall\'s Battlegear (4pc)')
-					|| player.getCurrentStats().sets.includes('Thrall\'s Battlegear (4pc)');
-				const options = player.getSpecOptions();
+		if (hasT94P) {
+			console.log("has set");
+			return Presets.ROTATION_PHASE_3.rotation.rotation!;
+		} else if (options.imbueMh == ShamanImbue.FlametongueWeapon) {
+			return Presets.ROTATION_FT_DEFAULT.rotation.rotation!;
+		} else {
+			return Presets.ROTATION_WF_DEFAULT.rotation.rotation!;
+		}
+	},
+});
 
-				if (hasT94P) {
-					console.log("has set");
-					return Presets.ROTATION_PHASE_3.rotation.rotation!;
-				} else if (options.imbueMh == ShamanImbue.FlametongueWeapon) {
-					return Presets.ROTATION_FT_DEFAULT.rotation.rotation!;
-				} else {
-					return Presets.ROTATION_WF_DEFAULT.rotation.rotation!;
-				}
-			},
-		});
+export class EnhancementShamanSimUI extends IndividualSimUI<Spec.SpecEnhancementShaman> {
+	constructor(parentElem: HTMLElement, player: Player<Spec.SpecEnhancementShaman>) {
+		super(parentElem, player, SPEC_CONFIG);
 	}
 }
diff --git a/ui/feral_druid/sim.ts b/ui/feral_druid/sim.ts
index 2c5f57fa1c..fd3e77f31b 100644
--- a/ui/feral_druid/sim.ts
+++ b/ui/feral_druid/sim.ts
@@ -7,8 +7,8 @@ import { Stat, PseudoStat } from '../core/proto/common.js';
 import { TristateEffect } from '../core/proto/common.js'
 import { Stats } from '../core/proto_utils/stats.js';
 import { Player } from '../core/player.js';
-import { IndividualSimUI } from '../core/individual_sim_ui.js';
-import { EventID, TypedEvent } from '../core/typed_event.js';
+import { IndividualSimUI, registerSpecConfig } from '../core/individual_sim_ui.js';
+import { TypedEvent } from '../core/typed_event.js';
 import { Gear } from '../core/proto_utils/gear.js';
 import { ItemSlot } from '../core/proto/common.js';
 import { GemColor } from '../core/proto/common.js';
@@ -16,159 +16,160 @@ import { Profession } from '../core/proto/common.js';
 
 import * as IconInputs from '../core/components/icon_inputs.js';
 import * as OtherInputs from '../core/components/other_inputs.js';
-import * as Tooltips from '../core/constants/tooltips.js';
 
 import * as DruidInputs from './inputs.js';
 import * as Presets from './presets.js';
 import { APLRotation } from 'ui/core/proto/apl.js';
 
+const SPEC_CONFIG = registerSpecConfig(Spec.SpecFeralDruid, {
+	cssClass: 'feral-druid-sim-ui',
+	cssScheme: 'druid',
+	// List any known bugs / issues here and they'll be shown on the site.
+	knownIssues: [
+	],
+	warnings: [
+	],
+
+	// All stats for which EP should be calculated.
+	epStats: [
+		Stat.StatStrength,
+		Stat.StatAgility,
+		Stat.StatAttackPower,
+		Stat.StatMeleeHit,
+		Stat.StatMeleeCrit,
+		Stat.StatMeleeHaste,
+		Stat.StatArmorPenetration,
+		Stat.StatExpertise,
+	],
+	epPseudoStats: [
+		PseudoStat.PseudoStatMainHandDps,
+	],
+	// Reference stat against which to calculate EP. I think all classes use either spell power or attack power.
+	epReferenceStat: Stat.StatAttackPower,
+	// Which stats to display in the Character Stats section, at the bottom of the left-hand sidebar.
+	displayStats: [
+		Stat.StatHealth,
+		Stat.StatStrength,
+		Stat.StatAgility,
+		Stat.StatAttackPower,
+		Stat.StatMeleeHit,
+		Stat.StatMeleeCrit,
+		Stat.StatMeleeHaste,
+		Stat.StatArmorPenetration,
+		Stat.StatExpertise,
+		Stat.StatMana,
+	],
+
+	defaults: {
+		// Default equipped gear.
+		gear: Presets.P4_PRESET.gear,
+		// Default EP weights for sorting gear in the gear picker.
+		epWeights: Stats.fromMap({
+			[Stat.StatStrength]: 2.40,
+			[Stat.StatAgility]: 2.39,
+			[Stat.StatAttackPower]: 1,
+			[Stat.StatMeleeHit]: 2.51,
+			[Stat.StatMeleeCrit]: 2.23,
+			[Stat.StatMeleeHaste]: 1.83,
+			[Stat.StatArmorPenetration]: 2.08,
+			[Stat.StatExpertise]: 2.44,
+		}, {
+			[PseudoStat.PseudoStatMainHandDps]: 16.5,
+		}),
+		// Default consumes settings.
+		consumes: Presets.DefaultConsumes,
+		// Default rotation settings.
+		rotation: Presets.DefaultRotation,
+		// Default talents.
+		talents: Presets.StandardTalents.data,
+		// Default spec-specific settings.
+		specOptions: Presets.DefaultOptions,
+		// Default raid/party buffs settings.
+		raidBuffs: RaidBuffs.create({
+			arcaneBrilliance: true,
+			giftOfTheWild: TristateEffect.TristateEffectImproved,
+			bloodlust: true,
+			manaSpringTotem: TristateEffect.TristateEffectRegular,
+			strengthOfEarthTotem: TristateEffect.TristateEffectImproved,
+			battleShout: TristateEffect.TristateEffectImproved,
+			unleashedRage: true,
+			icyTalons: true,
+			swiftRetribution: true,
+			sanctifiedRetribution: true,
+		}),
+		partyBuffs: PartyBuffs.create({
+			heroicPresence: true,
+		}),
+		individualBuffs: IndividualBuffs.create({
+			blessingOfKings: true,
+			blessingOfMight: TristateEffect.TristateEffectImproved,
+		}),
+		debuffs: Debuffs.create({
+			judgementOfWisdom: true,
+			bloodFrenzy: true,
+			giftOfArthas: true,
+			exposeArmor: true,
+			faerieFire: TristateEffect.TristateEffectImproved,
+			sunderArmor: true,
+			curseOfWeakness: TristateEffect.TristateEffectRegular,
+			heartOfTheCrusader: true,
+		}),
+	},
+
+	// IconInputs to include in the 'Player' section on the settings tab.
+	playerIconInputs: [
+	],
+	// Inputs to include in the 'Rotation' section on the settings tab.
+	rotationInputs: DruidInputs.FeralDruidRotationConfig,
+	// Buff and Debuff inputs to include/exclude, overriding the EP-based defaults.
+	includeBuffDebuffInputs: [
+		IconInputs.IntellectBuff,
+		IconInputs.MP5Buff,
+		IconInputs.JudgementOfWisdom,
+	],
+	excludeBuffDebuffInputs: [
+	],
+	// Inputs to include in the 'Other' section on the settings tab.
+	otherInputs: {
+		inputs: [
+			DruidInputs.LatencyMs,
+			DruidInputs.AssumeBleedActive,
+			OtherInputs.TankAssignment,
+			OtherInputs.InFrontOfTarget,
+		],
+	},
+	encounterPicker: {
+		// Whether to include 'Execute Duration (%)' in the 'Encounter' section of the settings tab.
+		showExecuteProportion: false,
+	},
+
+	presets: {
+		// Preset talents that the user can quickly select.
+		talents: [
+			Presets.StandardTalents,
+		],
+		rotations: [
+			Presets.ROTATION_PRESET_LEGACY_DEFAULT,
+			Presets.APL_ROTATION_DEFAULT,
+		],
+		// Preset gear configurations that the user can quickly select.
+		gear: [
+			Presets.PRERAID_PRESET,
+			Presets.P1_PRESET,
+			Presets.P2_PRESET,
+			Presets.P3_PRESET,
+			Presets.P4_PRESET,
+		],
+	},
+	
+	autoRotation: (_player: Player<Spec.SpecFeralDruid>): APLRotation => {
+		return Presets.ROTATION_PRESET_LEGACY_DEFAULT.rotation.rotation!;
+	}
+});
+
 export class FeralDruidSimUI extends IndividualSimUI<Spec.SpecFeralDruid> {
 	constructor(parentElem: HTMLElement, player: Player<Spec.SpecFeralDruid>) {
-		super(parentElem, player, {
-			cssClass: 'feral-druid-sim-ui',
-			cssScheme: 'druid',
-			// List any known bugs / issues here and they'll be shown on the site.
-			knownIssues: [
-			],
-			warnings: [
-			],
-
-			// All stats for which EP should be calculated.
-			epStats: [
-				Stat.StatStrength,
-				Stat.StatAgility,
-				Stat.StatAttackPower,
-				Stat.StatMeleeHit,
-				Stat.StatMeleeCrit,
-				Stat.StatMeleeHaste,
-				Stat.StatArmorPenetration,
-				Stat.StatExpertise,
-			],
-			epPseudoStats: [
-				PseudoStat.PseudoStatMainHandDps,
-			],
-			// Reference stat against which to calculate EP. I think all classes use either spell power or attack power.
-			epReferenceStat: Stat.StatAttackPower,
-			// Which stats to display in the Character Stats section, at the bottom of the left-hand sidebar.
-			displayStats: [
-				Stat.StatHealth,
-				Stat.StatStrength,
-				Stat.StatAgility,
-				Stat.StatAttackPower,
-				Stat.StatMeleeHit,
-				Stat.StatMeleeCrit,
-				Stat.StatMeleeHaste,
-				Stat.StatArmorPenetration,
-				Stat.StatExpertise,
-				Stat.StatMana,
-			],
-
-			defaults: {
-				// Default equipped gear.
-				gear: Presets.P4_PRESET.gear,
-				// Default EP weights for sorting gear in the gear picker.
-				epWeights: Stats.fromMap({
-					[Stat.StatStrength]: 2.40,
-					[Stat.StatAgility]: 2.39,
-					[Stat.StatAttackPower]: 1,
-					[Stat.StatMeleeHit]: 2.51,
-					[Stat.StatMeleeCrit]: 2.23,
-					[Stat.StatMeleeHaste]: 1.83,
-					[Stat.StatArmorPenetration]: 2.08,
-					[Stat.StatExpertise]: 2.44,
-				}, {
-					[PseudoStat.PseudoStatMainHandDps]: 16.5,
-				}),
-				// Default consumes settings.
-				consumes: Presets.DefaultConsumes,
-				// Default rotation settings.
-				rotation: Presets.DefaultRotation,
-				// Default talents.
-				talents: Presets.StandardTalents.data,
-				// Default spec-specific settings.
-				specOptions: Presets.DefaultOptions,
-				// Default raid/party buffs settings.
-				raidBuffs: RaidBuffs.create({
-					arcaneBrilliance: true,
-					giftOfTheWild: TristateEffect.TristateEffectImproved,
-					bloodlust: true,
-					manaSpringTotem: TristateEffect.TristateEffectRegular,
-					strengthOfEarthTotem: TristateEffect.TristateEffectImproved,
-					battleShout: TristateEffect.TristateEffectImproved,
-					unleashedRage: true,
-					icyTalons: true,
-					swiftRetribution: true,
-					sanctifiedRetribution: true,
-				}),
-				partyBuffs: PartyBuffs.create({
-					heroicPresence: true,
-				}),
-				individualBuffs: IndividualBuffs.create({
-					blessingOfKings: true,
-					blessingOfMight: TristateEffect.TristateEffectImproved,
-				}),
-				debuffs: Debuffs.create({
-					judgementOfWisdom: true,
-					bloodFrenzy: true,
-					giftOfArthas: true,
-					exposeArmor: true,
-					faerieFire: TristateEffect.TristateEffectImproved,
-					sunderArmor: true,
-					curseOfWeakness: TristateEffect.TristateEffectRegular,
-					heartOfTheCrusader: true,
-				}),
-			},
-
-			// IconInputs to include in the 'Player' section on the settings tab.
-			playerIconInputs: [
-			],
-			// Inputs to include in the 'Rotation' section on the settings tab.
-			rotationInputs: DruidInputs.FeralDruidRotationConfig,
-			// Buff and Debuff inputs to include/exclude, overriding the EP-based defaults.
-			includeBuffDebuffInputs: [
-				IconInputs.IntellectBuff,
-				IconInputs.MP5Buff,
-				IconInputs.JudgementOfWisdom,
-			],
-			excludeBuffDebuffInputs: [
-			],
-			// Inputs to include in the 'Other' section on the settings tab.
-			otherInputs: {
-				inputs: [
-					DruidInputs.LatencyMs,
-					DruidInputs.AssumeBleedActive,
-					OtherInputs.TankAssignment,
-					OtherInputs.InFrontOfTarget,
-				],
-			},
-			encounterPicker: {
-				// Whether to include 'Execute Duration (%)' in the 'Encounter' section of the settings tab.
-				showExecuteProportion: false,
-			},
-
-			presets: {
-				// Preset talents that the user can quickly select.
-				talents: [
-					Presets.StandardTalents,
-				],
-				rotations: [
-					Presets.ROTATION_PRESET_LEGACY_DEFAULT,
-					Presets.APL_ROTATION_DEFAULT,
-				],
-				// Preset gear configurations that the user can quickly select.
-				gear: [
-					Presets.PRERAID_PRESET,
-					Presets.P1_PRESET,
-					Presets.P2_PRESET,
-					Presets.P3_PRESET,
-					Presets.P4_PRESET,
-				],
-			},
-			
-			autoRotation: (player: Player<Spec.SpecFeralDruid>): APLRotation => {
-				return Presets.ROTATION_PRESET_LEGACY_DEFAULT.rotation.rotation!;
-			}
-		});
+		super(parentElem, player, SPEC_CONFIG);
 
 		this.addOptimizeGemsAction();
 	}
diff --git a/ui/feral_tank_druid/sim.ts b/ui/feral_tank_druid/sim.ts
index e0291ed795..401b0d0afc 100644
--- a/ui/feral_tank_druid/sim.ts
+++ b/ui/feral_tank_druid/sim.ts
@@ -13,231 +13,228 @@ import {
 } from '../core/proto/apl.js';
 import { Stats } from '../core/proto_utils/stats.js';
 import { Player } from '../core/player.js';
-import { IndividualSimUI } from '../core/individual_sim_ui.js';
-import { TypedEvent } from '../core/typed_event.js';
+import { IndividualSimUI, registerSpecConfig } from '../core/individual_sim_ui.js';
 
 import {
-	DruidTalents as DruidTalents,
-	FeralTankDruid,
 	FeralTankDruid_Rotation as DruidRotation,
-	FeralTankDruid_Options as DruidOptions
 } from '../core/proto/druid.js';
 
 import * as IconInputs from '../core/components/icon_inputs.js';
 import * as OtherInputs from '../core/components/other_inputs.js';
-import * as Tooltips from '../core/constants/tooltips.js';
 import * as AplUtils from '../core/proto_utils/apl_utils.js';
 
 import * as DruidInputs from './inputs.js';
 import * as Presets from './presets.js';
 
-export class FeralTankDruidSimUI extends IndividualSimUI<Spec.SpecFeralTankDruid> {
-	constructor(parentElem: HTMLElement, player: Player<Spec.SpecFeralTankDruid>) {
-		super(parentElem, player, {
-			cssClass: 'feral-tank-druid-sim-ui',
-			cssScheme: 'druid',
-			// List any known bugs / issues here and they'll be shown on the site.
-			knownIssues: [
-			],
+const SPEC_CONFIG = registerSpecConfig(Spec.SpecFeralTankDruid, {
+	cssClass: 'feral-tank-druid-sim-ui',
+	cssScheme: 'druid',
+	// List any known bugs / issues here and they'll be shown on the site.
+	knownIssues: [
+	],
 
-			// All stats for which EP should be calculated.
-			epStats: [
-				Stat.StatStamina,
-				Stat.StatStrength,
-				Stat.StatAgility,
-				Stat.StatAttackPower,
-				Stat.StatExpertise,
-				Stat.StatMeleeHit,
-				Stat.StatMeleeCrit,
-				Stat.StatMeleeHaste,
-				Stat.StatArmor,
-				Stat.StatBonusArmor,
-				Stat.StatArmorPenetration,
-				Stat.StatDefense,
-				Stat.StatDodge,
-				Stat.StatNatureResistance,
-				Stat.StatShadowResistance,
-				Stat.StatFrostResistance,
-			],
-			epPseudoStats: [
-				PseudoStat.PseudoStatMainHandDps,
-			],
-			// Reference stat against which to calculate EP. I think all classes use either spell power or attack power.
-			epReferenceStat: Stat.StatAttackPower,
-			// Which stats to display in the Character Stats section, at the bottom of the left-hand sidebar.
-			displayStats: [
-				Stat.StatHealth,
-				Stat.StatArmor,
-				Stat.StatBonusArmor,
-				Stat.StatStamina,
-				Stat.StatStrength,
-				Stat.StatAgility,
-				Stat.StatAttackPower,
-				Stat.StatExpertise,
-				Stat.StatMeleeHit,
-				Stat.StatMeleeCrit,
-				Stat.StatMeleeHaste,
-				Stat.StatArmorPenetration,
-				Stat.StatDefense,
-				Stat.StatDodge,
-				Stat.StatSpellHit,
-				Stat.StatSpellCrit,
-				Stat.StatNatureResistance,
-				Stat.StatShadowResistance,
-				Stat.StatFrostResistance,
-			],
+	// All stats for which EP should be calculated.
+	epStats: [
+		Stat.StatStamina,
+		Stat.StatStrength,
+		Stat.StatAgility,
+		Stat.StatAttackPower,
+		Stat.StatExpertise,
+		Stat.StatMeleeHit,
+		Stat.StatMeleeCrit,
+		Stat.StatMeleeHaste,
+		Stat.StatArmor,
+		Stat.StatBonusArmor,
+		Stat.StatArmorPenetration,
+		Stat.StatDefense,
+		Stat.StatDodge,
+		Stat.StatNatureResistance,
+		Stat.StatShadowResistance,
+		Stat.StatFrostResistance,
+	],
+	epPseudoStats: [
+		PseudoStat.PseudoStatMainHandDps,
+	],
+	// Reference stat against which to calculate EP. I think all classes use either spell power or attack power.
+	epReferenceStat: Stat.StatAttackPower,
+	// Which stats to display in the Character Stats section, at the bottom of the left-hand sidebar.
+	displayStats: [
+		Stat.StatHealth,
+		Stat.StatArmor,
+		Stat.StatBonusArmor,
+		Stat.StatStamina,
+		Stat.StatStrength,
+		Stat.StatAgility,
+		Stat.StatAttackPower,
+		Stat.StatExpertise,
+		Stat.StatMeleeHit,
+		Stat.StatMeleeCrit,
+		Stat.StatMeleeHaste,
+		Stat.StatArmorPenetration,
+		Stat.StatDefense,
+		Stat.StatDodge,
+		Stat.StatSpellHit,
+		Stat.StatSpellCrit,
+		Stat.StatNatureResistance,
+		Stat.StatShadowResistance,
+		Stat.StatFrostResistance,
+	],
 
-			defaults: {
-				// Default equipped gear.
-				gear: Presets.P1_PRESET.gear,
-				// Default EP weights for sorting gear in the gear picker.
-				epWeights: Stats.fromMap({
-					[Stat.StatArmor]: 3.5665,
-					[Stat.StatBonusArmor]: 0.5187,
-					[Stat.StatStamina]: 7.3021,
-					[Stat.StatStrength]: 2.3786,
-					[Stat.StatAgility]: 4.4974,
-					[Stat.StatAttackPower]: 1,
-					[Stat.StatExpertise]: 2.6597,
-					[Stat.StatMeleeHit]: 2.9282,
-					[Stat.StatMeleeCrit]: 1.5143,
-					[Stat.StatMeleeHaste]: 2.0983,
-					[Stat.StatArmorPenetration]: 1.584,
-					[Stat.StatDefense]: 1.8171,
-					[Stat.StatDodge]: 2.0196,
-					[Stat.StatHealth]: 0.4465,
-				}, {
-					[PseudoStat.PseudoStatMainHandDps]: 0.0,
-				}),
-				// Default consumes settings.
-				consumes: Presets.DefaultConsumes,
-				// Default rotation settings.
-				rotation: Presets.DefaultSimpleRotation,
-				// Default talents.
-				talents: Presets.StandardTalents.data,
-				// Default spec-specific settings.
-				specOptions: Presets.DefaultOptions,
-				// Default raid/party buffs settings.
-				raidBuffs: RaidBuffs.create({
-					powerWordFortitude: TristateEffect.TristateEffectImproved,
-					shadowProtection: true,
-					giftOfTheWild: TristateEffect.TristateEffectImproved,
-					thorns: TristateEffect.TristateEffectImproved,
-					bloodlust: true,
-					strengthOfEarthTotem: TristateEffect.TristateEffectImproved,
-					battleShout: TristateEffect.TristateEffectImproved,
-					unleashedRage: true,
-					windfuryTotem: TristateEffect.TristateEffectImproved,
-					arcaneEmpowerment: true,
-					moonkinAura: TristateEffect.TristateEffectImproved,
-				}),
-				partyBuffs: PartyBuffs.create({
-					heroicPresence: true,
-				}),
-				individualBuffs: IndividualBuffs.create({
-					blessingOfKings: true,
-					blessingOfMight: TristateEffect.TristateEffectImproved,
-					renewedHope: true,
-				}),
-				debuffs: Debuffs.create({
-					savageCombat: true,
-					faerieFire: TristateEffect.TristateEffectImproved,
-					exposeArmor: true,
-					frostFever: TristateEffect.TristateEffectImproved,
-					masterPoisoner: true,
-					ebonPlaguebringer: true,
-					shadowMastery: true,
-				}),
-			},
+	defaults: {
+		// Default equipped gear.
+		gear: Presets.P1_PRESET.gear,
+		// Default EP weights for sorting gear in the gear picker.
+		epWeights: Stats.fromMap({
+			[Stat.StatArmor]: 3.5665,
+			[Stat.StatBonusArmor]: 0.5187,
+			[Stat.StatStamina]: 7.3021,
+			[Stat.StatStrength]: 2.3786,
+			[Stat.StatAgility]: 4.4974,
+			[Stat.StatAttackPower]: 1,
+			[Stat.StatExpertise]: 2.6597,
+			[Stat.StatMeleeHit]: 2.9282,
+			[Stat.StatMeleeCrit]: 1.5143,
+			[Stat.StatMeleeHaste]: 2.0983,
+			[Stat.StatArmorPenetration]: 1.584,
+			[Stat.StatDefense]: 1.8171,
+			[Stat.StatDodge]: 2.0196,
+			[Stat.StatHealth]: 0.4465,
+		}, {
+			[PseudoStat.PseudoStatMainHandDps]: 0.0,
+		}),
+		// Default consumes settings.
+		consumes: Presets.DefaultConsumes,
+		// Default rotation settings.
+		rotation: Presets.DefaultSimpleRotation,
+		// Default talents.
+		talents: Presets.StandardTalents.data,
+		// Default spec-specific settings.
+		specOptions: Presets.DefaultOptions,
+		// Default raid/party buffs settings.
+		raidBuffs: RaidBuffs.create({
+			powerWordFortitude: TristateEffect.TristateEffectImproved,
+			shadowProtection: true,
+			giftOfTheWild: TristateEffect.TristateEffectImproved,
+			thorns: TristateEffect.TristateEffectImproved,
+			bloodlust: true,
+			strengthOfEarthTotem: TristateEffect.TristateEffectImproved,
+			battleShout: TristateEffect.TristateEffectImproved,
+			unleashedRage: true,
+			windfuryTotem: TristateEffect.TristateEffectImproved,
+			arcaneEmpowerment: true,
+			moonkinAura: TristateEffect.TristateEffectImproved,
+		}),
+		partyBuffs: PartyBuffs.create({
+			heroicPresence: true,
+		}),
+		individualBuffs: IndividualBuffs.create({
+			blessingOfKings: true,
+			blessingOfMight: TristateEffect.TristateEffectImproved,
+			renewedHope: true,
+		}),
+		debuffs: Debuffs.create({
+			savageCombat: true,
+			faerieFire: TristateEffect.TristateEffectImproved,
+			exposeArmor: true,
+			frostFever: TristateEffect.TristateEffectImproved,
+			masterPoisoner: true,
+			ebonPlaguebringer: true,
+			shadowMastery: true,
+		}),
+	},
 
-			// IconInputs to include in the 'Player' section on the settings tab.
-			playerIconInputs: [
-			],
-			// Inputs to include in the 'Rotation' section on the settings tab.
-			rotationInputs: DruidInputs.FeralTankDruidRotationConfig,
-			// Buff and Debuff inputs to include/exclude, overriding the EP-based defaults.
-			includeBuffDebuffInputs: [
-				IconInputs.HealthBuff,
-				IconInputs.SpellCritBuff,
-				IconInputs.SpellCritDebuff,
-				IconInputs.SpellHitDebuff,
-				IconInputs.SpellDamageDebuff,
-			],
-			excludeBuffDebuffInputs: [
-			],
-			// Inputs to include in the 'Other' section on the settings tab.
-			otherInputs: {
-				inputs: [
-					OtherInputs.TankAssignment,
-					OtherInputs.IncomingHps,
-					OtherInputs.HealingCadence,
-					OtherInputs.HealingCadenceVariation,
-					OtherInputs.BurstWindow,
-					OtherInputs.InspirationUptime,
-					OtherInputs.HpPercentForDefensives,
-					DruidInputs.StartingRage,
-					OtherInputs.InFrontOfTarget,
-				],
-			},
-			encounterPicker: {
-				// Whether to include 'Execute Duration (%)' in the 'Encounter' section of the settings tab.
-				showExecuteProportion: false,
-			},
+	// IconInputs to include in the 'Player' section on the settings tab.
+	playerIconInputs: [
+	],
+	// Inputs to include in the 'Rotation' section on the settings tab.
+	rotationInputs: DruidInputs.FeralTankDruidRotationConfig,
+	// Buff and Debuff inputs to include/exclude, overriding the EP-based defaults.
+	includeBuffDebuffInputs: [
+		IconInputs.HealthBuff,
+		IconInputs.SpellCritBuff,
+		IconInputs.SpellCritDebuff,
+		IconInputs.SpellHitDebuff,
+		IconInputs.SpellDamageDebuff,
+	],
+	excludeBuffDebuffInputs: [
+	],
+	// Inputs to include in the 'Other' section on the settings tab.
+	otherInputs: {
+		inputs: [
+			OtherInputs.TankAssignment,
+			OtherInputs.IncomingHps,
+			OtherInputs.HealingCadence,
+			OtherInputs.HealingCadenceVariation,
+			OtherInputs.BurstWindow,
+			OtherInputs.InspirationUptime,
+			OtherInputs.HpPercentForDefensives,
+			DruidInputs.StartingRage,
+			OtherInputs.InFrontOfTarget,
+		],
+	},
+	encounterPicker: {
+		// Whether to include 'Execute Duration (%)' in the 'Encounter' section of the settings tab.
+		showExecuteProportion: false,
+	},
 
-			presets: {
-				// Preset talents that the user can quickly select.
-				talents: [
-					Presets.StandardTalents,
-				],
-				// Preset rotations that the user can quickly select.
-				rotations: [
-					Presets.ROTATION_PRESET_SIMPLE,
-					Presets.ROTATION_DEFAULT,
-				],
-				// Preset gear configurations that the user can quickly select.
-				gear: [
-					Presets.P1_PRESET, Presets.P2_PRESET
-				],
-			},
+	presets: {
+		// Preset talents that the user can quickly select.
+		talents: [
+			Presets.StandardTalents,
+		],
+		// Preset rotations that the user can quickly select.
+		rotations: [
+			Presets.ROTATION_PRESET_SIMPLE,
+			Presets.ROTATION_DEFAULT,
+		],
+		// Preset gear configurations that the user can quickly select.
+		gear: [
+			Presets.P1_PRESET, Presets.P2_PRESET
+		],
+	},
 
-			autoRotation: (player: Player<Spec.SpecFeralTankDruid>): APLRotation => {
-				return Presets.ROTATION_PRESET_SIMPLE.rotation.rotation!;
-			},
+	autoRotation: (_player: Player<Spec.SpecFeralTankDruid>): APLRotation => {
+		return Presets.ROTATION_PRESET_SIMPLE.rotation.rotation!;
+	},
 
-			simpleRotation: (player: Player<Spec.SpecFeralTankDruid>, simple: DruidRotation, cooldowns: Cooldowns): APLRotation => {
-				let [prepullActions, actions] = AplUtils.standardCooldownDefaults(cooldowns);
+	simpleRotation: (player: Player<Spec.SpecFeralTankDruid>, simple: DruidRotation, cooldowns: Cooldowns): APLRotation => {
+		let [prepullActions, actions] = AplUtils.standardCooldownDefaults(cooldowns);
 
-				const emergencyLacerate = APLAction.fromJsonString(`{"condition":{"and":{"vals":[{"cmp":{"op":"OpEq","lhs":{"auraNumStacks":{"sourceUnit":{"type":"CurrentTarget"},"auraId":{"spellId":48568}}},"rhs":{"const":{"val":"5"}}}},{"cmp":{"op":"OpLe","lhs":{"dotRemainingTime":{"spellId":{"spellId":48568}}},"rhs":{"const":{"val":"1.5s"}}}}]}},"castSpell":{"spellId":{"spellId":48568}}}`);
-				const demoRoar = APLAction.fromJsonString(`{"condition":{"auraShouldRefresh":{"auraId":{"spellId":48560},"maxOverlap":{"const":{"val":"1.5s"}}}},"castSpell":{"spellId":{"spellId":48560}}}`);
-				const mangle = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":48564}}}`);
-				const delayFaerieFireForMangle = APLAction.fromJsonString(`{"condition":{"and":{"vals":[{"gcdIsReady":{}},{"not":{"val":{"spellIsReady":{"spellId":{"spellId":48564}}}}},{"cmp":{"op":"OpLt","lhs":{"spellTimeToReady":{"spellId":{"spellId":48564}}},"rhs":{"const":{"val":"1.0s"}}}}]}},"wait":{"duration":{"spellTimeToReady":{"spellId":{"spellId":48564}}}}}`);
-				const faerieFire = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":16857}}}`);
-				const delayFillersForMangle = APLAction.fromJsonString(`{"condition":{"and":{"vals":[{"gcdIsReady":{}},{"not":{"val":{"spellIsReady":{"spellId":{"spellId":48564}}}}},{"cmp":{"op":"OpLt","lhs":{"spellTimeToReady":{"spellId":{"spellId":48564}}},"rhs":{"const":{"val":"1.5s"}}}}]}},"wait":{"duration":{"spellTimeToReady":{"spellId":{"spellId":48564}}}}}`);
-				const lacerate = APLAction.fromJsonString(`{"condition":{"or":{"vals":[{"cmp":{"op":"OpLt","lhs":{"auraNumStacks":{"sourceUnit":{"type":"CurrentTarget"},"auraId":{"spellId":48568}}},"rhs":{"const":{"val":"5"}}}},{"cmp":{"op":"OpLe","lhs":{"dotRemainingTime":{"spellId":{"spellId":48568}}},"rhs":{"const":{"val":"${simple.lacerateTime.toFixed(1)}s"}}}}]}},"castSpell":{"spellId":{"spellId":48568}}}`);
-				const swipe = APLAction.fromJsonString(`{"condition":{"cmp":{"op":"OpGe","lhs":{"currentRage":{}},"rhs":{"const":{"val":"${(simple.maulRageThreshold + 15).toFixed(0)}"}}}},"castSpell":{"spellId":{"spellId":48562}}}`);
-				const queueMaul = APLAction.fromJsonString(`{"condition":{"cmp":{"op":"OpGe","lhs":{"currentRage":{}},"rhs":{"const":{"val":"${simple.maulRageThreshold.toFixed(0)}"}}}},"castSpell":{"spellId":{"spellId":48480,"tag":1}}}`);
-				const waitForFaerieFire = APLAction.fromJsonString(`{"condition":{"and":{"vals":[{"gcdIsReady":{}},{"not":{"val":{"spellIsReady":{"spellId":{"spellId":16857}}}}},{"cmp":{"op":"OpLt","lhs":{"spellTimeToReady":{"spellId":{"spellId":16857}}},"rhs":{"const":{"val":"1.5s"}}}}]}},"wait":{"duration":{"spellTimeToReady":{"spellId":{"spellId":16857}}}}}`);
+		const emergencyLacerate = APLAction.fromJsonString(`{"condition":{"and":{"vals":[{"cmp":{"op":"OpEq","lhs":{"auraNumStacks":{"sourceUnit":{"type":"CurrentTarget"},"auraId":{"spellId":48568}}},"rhs":{"const":{"val":"5"}}}},{"cmp":{"op":"OpLe","lhs":{"dotRemainingTime":{"spellId":{"spellId":48568}}},"rhs":{"const":{"val":"1.5s"}}}}]}},"castSpell":{"spellId":{"spellId":48568}}}`);
+		const demoRoar = APLAction.fromJsonString(`{"condition":{"auraShouldRefresh":{"auraId":{"spellId":48560},"maxOverlap":{"const":{"val":"1.5s"}}}},"castSpell":{"spellId":{"spellId":48560}}}`);
+		const mangle = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":48564}}}`);
+		const delayFaerieFireForMangle = APLAction.fromJsonString(`{"condition":{"and":{"vals":[{"gcdIsReady":{}},{"not":{"val":{"spellIsReady":{"spellId":{"spellId":48564}}}}},{"cmp":{"op":"OpLt","lhs":{"spellTimeToReady":{"spellId":{"spellId":48564}}},"rhs":{"const":{"val":"1.0s"}}}}]}},"wait":{"duration":{"spellTimeToReady":{"spellId":{"spellId":48564}}}}}`);
+		const faerieFire = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":16857}}}`);
+		const delayFillersForMangle = APLAction.fromJsonString(`{"condition":{"and":{"vals":[{"gcdIsReady":{}},{"not":{"val":{"spellIsReady":{"spellId":{"spellId":48564}}}}},{"cmp":{"op":"OpLt","lhs":{"spellTimeToReady":{"spellId":{"spellId":48564}}},"rhs":{"const":{"val":"1.5s"}}}}]}},"wait":{"duration":{"spellTimeToReady":{"spellId":{"spellId":48564}}}}}`);
+		const lacerate = APLAction.fromJsonString(`{"condition":{"or":{"vals":[{"cmp":{"op":"OpLt","lhs":{"auraNumStacks":{"sourceUnit":{"type":"CurrentTarget"},"auraId":{"spellId":48568}}},"rhs":{"const":{"val":"5"}}}},{"cmp":{"op":"OpLe","lhs":{"dotRemainingTime":{"spellId":{"spellId":48568}}},"rhs":{"const":{"val":"${simple.lacerateTime.toFixed(1)}s"}}}}]}},"castSpell":{"spellId":{"spellId":48568}}}`);
+		const swipe = APLAction.fromJsonString(`{"condition":{"cmp":{"op":"OpGe","lhs":{"currentRage":{}},"rhs":{"const":{"val":"${(simple.maulRageThreshold + 15).toFixed(0)}"}}}},"castSpell":{"spellId":{"spellId":48562}}}`);
+		const queueMaul = APLAction.fromJsonString(`{"condition":{"cmp":{"op":"OpGe","lhs":{"currentRage":{}},"rhs":{"const":{"val":"${simple.maulRageThreshold.toFixed(0)}"}}}},"castSpell":{"spellId":{"spellId":48480,"tag":1}}}`);
+		const waitForFaerieFire = APLAction.fromJsonString(`{"condition":{"and":{"vals":[{"gcdIsReady":{}},{"not":{"val":{"spellIsReady":{"spellId":{"spellId":16857}}}}},{"cmp":{"op":"OpLt","lhs":{"spellTimeToReady":{"spellId":{"spellId":16857}}},"rhs":{"const":{"val":"1.5s"}}}}]}},"wait":{"duration":{"spellTimeToReady":{"spellId":{"spellId":16857}}}}}`);
 
-				actions.push(...[
-					emergencyLacerate,
-					simple.maintainDemoralizingRoar ? demoRoar : null,
-					mangle,
-					delayFaerieFireForMangle,
-					faerieFire,
-					delayFillersForMangle,
-					lacerate,
-					swipe,
-					queueMaul,
-					waitForFaerieFire,
-				].filter(a => a) as Array<APLAction>)
+		actions.push(...[
+			emergencyLacerate,
+			simple.maintainDemoralizingRoar ? demoRoar : null,
+			mangle,
+			delayFaerieFireForMangle,
+			faerieFire,
+			delayFillersForMangle,
+			lacerate,
+			swipe,
+			queueMaul,
+			waitForFaerieFire,
+		].filter(a => a) as Array<APLAction>)
 
-				return APLRotation.create({
-					prepullActions: prepullActions,
-					priorityList: actions.map(action => APLListItem.create({
-						action: action,
-					}))
-				});
-			},
+		return APLRotation.create({
+			prepullActions: prepullActions,
+			priorityList: actions.map(action => APLListItem.create({
+				action: action,
+			}))
 		});
+	},
+});
+
+export class FeralTankDruidSimUI extends IndividualSimUI<Spec.SpecFeralTankDruid> {
+	constructor(parentElem: HTMLElement, player: Player<Spec.SpecFeralTankDruid>) {
+		super(parentElem, player, SPEC_CONFIG);
 	}
 }
diff --git a/ui/healing_priest/sim.ts b/ui/healing_priest/sim.ts
index 089dec9e3b..118f3d81fd 100644
--- a/ui/healing_priest/sim.ts
+++ b/ui/healing_priest/sim.ts
@@ -3,135 +3,132 @@ import { Spec } from '../core/proto/common.js';
 import { Stat } from '../core/proto/common.js';
 import { Stats } from '../core/proto_utils/stats.js';
 import { Player } from '../core/player.js';
-import { IndividualSimUI } from '../core/individual_sim_ui.js';
+import { IndividualSimUI, registerSpecConfig } from '../core/individual_sim_ui.js';
 import {
 	APLRotation,
 } from '../core/proto/apl.js';
 
-import * as IconInputs from '../core/components/icon_inputs.js';
-import * as OtherInputs from '../core/components/other_inputs.js';
-import * as Mechanics from '../core/constants/mechanics.js';
-import * as Tooltips from '../core/constants/tooltips.js';
-
 import * as HealingPriestInputs from './inputs.js';
 import * as Presets from './presets.js';
 
-export class HealingPriestSimUI extends IndividualSimUI<Spec.SpecHealingPriest> {
-	constructor(parentElem: HTMLElement, player: Player<Spec.SpecHealingPriest>) {
-		super(parentElem, player, {
-			cssClass: 'healing-priest-sim-ui',
-			cssScheme: 'priest',
-			// List any known bugs / issues here and they'll be shown on the site.
-			knownIssues: [
-				'Talents that apply to, "friendly targets at or below 50% health" are not implemented.',
-				'Prayer of Mending always bounces the maximum number of times.',
-			],
+const SPEC_CONFIG = registerSpecConfig(Spec.SpecHealingPriest, {
+	cssClass: 'healing-priest-sim-ui',
+	cssScheme: 'priest',
+	// List any known bugs / issues here and they'll be shown on the site.
+	knownIssues: [
+		'Talents that apply to, "friendly targets at or below 50% health" are not implemented.',
+		'Prayer of Mending always bounces the maximum number of times.',
+	],
 
-			// All stats for which EP should be calculated.
-			epStats: [
-				Stat.StatIntellect,
-				Stat.StatSpirit,
-				Stat.StatSpellPower,
-				Stat.StatSpellCrit,
-				Stat.StatSpellHaste,
-				Stat.StatMP5,
-			],
-			// Reference stat against which to calculate EP. I think all classes use either spell power or attack power.
-			epReferenceStat: Stat.StatSpellPower,
-			// Which stats to display in the Character Stats section, at the bottom of the left-hand sidebar.
-			displayStats: [
-				Stat.StatHealth,
-				Stat.StatMana,
-				Stat.StatStamina,
-				Stat.StatIntellect,
-				Stat.StatSpirit,
-				Stat.StatSpellPower,
-				Stat.StatSpellCrit,
-				Stat.StatSpellHaste,
-				Stat.StatMP5,
-			],
+	// All stats for which EP should be calculated.
+	epStats: [
+		Stat.StatIntellect,
+		Stat.StatSpirit,
+		Stat.StatSpellPower,
+		Stat.StatSpellCrit,
+		Stat.StatSpellHaste,
+		Stat.StatMP5,
+	],
+	// Reference stat against which to calculate EP. I think all classes use either spell power or attack power.
+	epReferenceStat: Stat.StatSpellPower,
+	// Which stats to display in the Character Stats section, at the bottom of the left-hand sidebar.
+	displayStats: [
+		Stat.StatHealth,
+		Stat.StatMana,
+		Stat.StatStamina,
+		Stat.StatIntellect,
+		Stat.StatSpirit,
+		Stat.StatSpellPower,
+		Stat.StatSpellCrit,
+		Stat.StatSpellHaste,
+		Stat.StatMP5,
+	],
 
-			defaults: {
-				// Default equipped gear.
-				gear: Presets.DISC_P1_PRESET.gear,
-				// Default EP weights for sorting gear in the gear picker.
-				epWeights: Stats.fromMap({
-					[Stat.StatIntellect]: 2.73,
-					[Stat.StatSpirit]: 1.63,
-					[Stat.StatSpellPower]: 1,
-					[Stat.StatSpellCrit]: 0.75,
-					[Stat.StatSpellHaste]: 0.28,
-					[Stat.StatMP5]: 2.05,
-				}),
-				// Default consumes settings.
-				consumes: Presets.DefaultConsumes,
-				// Default rotation settings.
-				rotation: Presets.DiscDefaultRotation,
-				// Default talents.
-				talents: Presets.DiscTalents.data,
-				// Default spec-specific settings.
-				specOptions: Presets.DefaultOptions,
-				// Default raid/party buffs settings.
-				raidBuffs: Presets.DefaultRaidBuffs,
-				partyBuffs: PartyBuffs.create({}),
-				individualBuffs: Presets.DefaultIndividualBuffs,
-				debuffs: Presets.DefaultDebuffs,
-			},
+	defaults: {
+		// Default equipped gear.
+		gear: Presets.DISC_P1_PRESET.gear,
+		// Default EP weights for sorting gear in the gear picker.
+		epWeights: Stats.fromMap({
+			[Stat.StatIntellect]: 2.73,
+			[Stat.StatSpirit]: 1.63,
+			[Stat.StatSpellPower]: 1,
+			[Stat.StatSpellCrit]: 0.75,
+			[Stat.StatSpellHaste]: 0.28,
+			[Stat.StatMP5]: 2.05,
+		}),
+		// Default consumes settings.
+		consumes: Presets.DefaultConsumes,
+		// Default rotation settings.
+		rotation: Presets.DiscDefaultRotation,
+		// Default talents.
+		talents: Presets.DiscTalents.data,
+		// Default spec-specific settings.
+		specOptions: Presets.DefaultOptions,
+		// Default raid/party buffs settings.
+		raidBuffs: Presets.DefaultRaidBuffs,
+		partyBuffs: PartyBuffs.create({}),
+		individualBuffs: Presets.DefaultIndividualBuffs,
+		debuffs: Presets.DefaultDebuffs,
+	},
 
-			// IconInputs to include in the 'Player' section on the settings tab.
-			playerIconInputs: [
-				HealingPriestInputs.SelfPowerInfusion,
-				HealingPriestInputs.InnerFire,
-				HealingPriestInputs.Shadowfiend,
-			],
-			// Inputs to include in the 'Rotation' section on the settings tab.
-			rotationInputs: HealingPriestInputs.HealingPriestRotationConfig,
-			// Buff and Debuff inputs to include/exclude, overriding the EP-based defaults.
-			includeBuffDebuffInputs: [
-			],
-			excludeBuffDebuffInputs: [
-			],
-			// Inputs to include in the 'Other' section on the settings tab.
-			otherInputs: {
-				inputs: [
-					HealingPriestInputs.RapturesPerMinute,
-				],
-			},
-			encounterPicker: {
-				// Whether to include 'Execute Duration (%)' in the 'Encounter' section of the settings tab.
-				showExecuteProportion: false,
-			},
+	// IconInputs to include in the 'Player' section on the settings tab.
+	playerIconInputs: [
+		HealingPriestInputs.SelfPowerInfusion,
+		HealingPriestInputs.InnerFire,
+		HealingPriestInputs.Shadowfiend,
+	],
+	// Inputs to include in the 'Rotation' section on the settings tab.
+	rotationInputs: HealingPriestInputs.HealingPriestRotationConfig,
+	// Buff and Debuff inputs to include/exclude, overriding the EP-based defaults.
+	includeBuffDebuffInputs: [
+	],
+	excludeBuffDebuffInputs: [
+	],
+	// Inputs to include in the 'Other' section on the settings tab.
+	otherInputs: {
+		inputs: [
+			HealingPriestInputs.RapturesPerMinute,
+		],
+	},
+	encounterPicker: {
+		// Whether to include 'Execute Duration (%)' in the 'Encounter' section of the settings tab.
+		showExecuteProportion: false,
+	},
 
-			presets: {
-				// Preset talents that the user can quickly select.
-				talents: [
-					Presets.DiscTalents,
-					Presets.HolyTalents,
-				],
-				// Preset rotations that the user can quickly select.
-				rotations: [
-					Presets.ROTATION_PRESET_DISC,
-					Presets.ROTATION_PRESET_HOLY,
-				],
-				// Preset gear configurations that the user can quickly select.
-				gear: [
-					Presets.DISC_PRERAID_PRESET,
-					Presets.DISC_P1_PRESET,
-					Presets.DISC_P2_PRESET,
-					Presets.HOLY_PRERAID_PRESET,
-					Presets.HOLY_P1_PRESET,
-					Presets.HOLY_P2_PRESET,
-				],
-			},
+	presets: {
+		// Preset talents that the user can quickly select.
+		talents: [
+			Presets.DiscTalents,
+			Presets.HolyTalents,
+		],
+		// Preset rotations that the user can quickly select.
+		rotations: [
+			Presets.ROTATION_PRESET_DISC,
+			Presets.ROTATION_PRESET_HOLY,
+		],
+		// Preset gear configurations that the user can quickly select.
+		gear: [
+			Presets.DISC_PRERAID_PRESET,
+			Presets.DISC_P1_PRESET,
+			Presets.DISC_P2_PRESET,
+			Presets.HOLY_PRERAID_PRESET,
+			Presets.HOLY_P1_PRESET,
+			Presets.HOLY_P2_PRESET,
+		],
+	},
 
-			autoRotation: (player: Player<Spec.SpecHealingPriest>): APLRotation => {
-				const talentTree = player.getTalentTree();
-				if (talentTree == 0) {
-					return Presets.ROTATION_PRESET_DISC.rotation.rotation!;
-				} else {
-					return Presets.ROTATION_PRESET_HOLY.rotation.rotation!;
-				}
-			},
-		});
+	autoRotation: (player: Player<Spec.SpecHealingPriest>): APLRotation => {
+		const talentTree = player.getTalentTree();
+		if (talentTree == 0) {
+			return Presets.ROTATION_PRESET_DISC.rotation.rotation!;
+		} else {
+			return Presets.ROTATION_PRESET_HOLY.rotation.rotation!;
+		}
+	},
+});
+
+export class HealingPriestSimUI extends IndividualSimUI<Spec.SpecHealingPriest> {
+	constructor(parentElem: HTMLElement, player: Player<Spec.SpecHealingPriest>) {
+		super(parentElem, player, SPEC_CONFIG);
 	}
 }
diff --git a/ui/holy_paladin/sim.ts b/ui/holy_paladin/sim.ts
index 1f709639c7..e3b1c74793 100644
--- a/ui/holy_paladin/sim.ts
+++ b/ui/holy_paladin/sim.ts
@@ -3,161 +3,162 @@ import { PartyBuffs } from '../core/proto/common.js';
 import { IndividualBuffs } from '../core/proto/common.js';
 import { Debuffs } from '../core/proto/common.js';
 import { Spec } from '../core/proto/common.js';
-import { Stat, PseudoStat } from '../core/proto/common.js';
+import { Stat } from '../core/proto/common.js';
 import { TristateEffect } from '../core/proto/common.js'
 import {
 	APLRotation,
 } from '../core/proto/apl.js';
 import { Stats } from '../core/proto_utils/stats.js';
 import { Player } from '../core/player.js';
-import { IndividualSimUI } from '../core/individual_sim_ui.js';
-import { EventID, TypedEvent } from '../core/typed_event.js';
+import { IndividualSimUI, registerSpecConfig } from '../core/individual_sim_ui.js';
 
-import * as IconInputs from '../core/components/icon_inputs.js';
 import * as OtherInputs from '../core/components/other_inputs.js';
-import * as Mechanics from '../core/constants/mechanics.js';
 
 import * as HolyPaladinInputs from './inputs.js';
 import * as Presets from './presets.js';
 
-export class HolyPaladinSimUI extends IndividualSimUI<Spec.SpecHolyPaladin> {
-	constructor(parentElem: HTMLElement, player: Player<Spec.SpecHolyPaladin>) {
-		super(parentElem, player, {
-			cssClass: 'holy-paladin-sim-ui',
-			cssScheme: 'paladin',
-			// List any known bugs / issues here and they'll be shown on the site.
-			knownIssues: [
-			],
+const SPEC_CONFIG = registerSpecConfig(Spec.SpecHolyPaladin, {
+	cssClass: 'holy-paladin-sim-ui',
+	cssScheme: 'paladin',
+	// List any known bugs / issues here and they'll be shown on the site.
+	knownIssues: [
+	],
+
+	// All stats for which EP should be calculated.
+	epStats: [
+		Stat.StatIntellect,
+		Stat.StatSpirit,
+		Stat.StatSpellPower,
+		Stat.StatSpellCrit,
+		Stat.StatSpellHaste,
+		Stat.StatMP5,
+	],
+	// Reference stat against which to calculate EP. I think all classes use either spell power or attack power.
+	epReferenceStat: Stat.StatSpellPower,
+	// Which stats to display in the Character Stats section, at the bottom of the left-hand sidebar.
+	displayStats: [
+		Stat.StatHealth,
+		Stat.StatMana,
+		Stat.StatStamina,
+		Stat.StatIntellect,
+		Stat.StatSpirit,
+		Stat.StatSpellPower,
+		Stat.StatSpellCrit,
+		Stat.StatSpellHaste,
+		Stat.StatMP5,
+	],
+	defaults: {
+		// Default equipped gear.
+		gear: Presets.P1_PRESET.gear,
+		// Default EP weights for sorting gear in the gear picker.
+		epWeights: Stats.fromMap({
+			[Stat.StatIntellect]: 0.38,
+			[Stat.StatSpirit]: 0.34,
+			[Stat.StatSpellPower]: 1,
+			[Stat.StatSpellCrit]: 0.69,
+			[Stat.StatSpellHaste]: 0.77,
+			[Stat.StatMP5]: 0.00,
+		}),
+		// Default consumes settings.
+		consumes: Presets.DefaultConsumes,
+		// Default rotation settings.
+		rotation: Presets.DefaultRotation,
+		// Default talents.
+		talents: Presets.StandardTalents.data,
+		// Default spec-specific settings.
+		specOptions: Presets.DefaultOptions,
+		// Default raid/party buffs settings.
+		raidBuffs: RaidBuffs.create({
+			giftOfTheWild: TristateEffect.TristateEffectImproved,
+			powerWordFortitude: TristateEffect.TristateEffectImproved,
+			strengthOfEarthTotem: TristateEffect.TristateEffectImproved,
+			arcaneBrilliance: true,
+			unleashedRage: true,
+			leaderOfThePack: TristateEffect.TristateEffectRegular,
+			icyTalons: true,
+			totemOfWrath: true,
+			demonicPact: 500,
+			swiftRetribution: true,
+			moonkinAura: TristateEffect.TristateEffectRegular,
+			sanctifiedRetribution: true,
+			manaSpringTotem: TristateEffect.TristateEffectRegular,
+			bloodlust: true,
+			thorns: TristateEffect.TristateEffectImproved,
+			devotionAura: TristateEffect.TristateEffectImproved,
+			shadowProtection: true,
+		}),
+		partyBuffs: PartyBuffs.create({
+		}),
+		individualBuffs: IndividualBuffs.create({
+			blessingOfKings: true,
+			blessingOfSanctuary: true,
+			blessingOfWisdom: TristateEffect.TristateEffectImproved,
+			blessingOfMight: TristateEffect.TristateEffectImproved,
+		}),
+		debuffs: Debuffs.create({
+			judgementOfWisdom: true,
+			judgementOfLight: true,
+			misery: true,
+			faerieFire: TristateEffect.TristateEffectImproved,
+			ebonPlaguebringer: true,
+			totemOfWrath: true,
+			shadowMastery: true,
+			bloodFrenzy: true,
+			mangle: true,
+			exposeArmor: true,
+			sunderArmor: true,
+			vindication: true,
+			thunderClap: TristateEffect.TristateEffectImproved,
+			insectSwarm: true,
+		}),
+	},
 
-			// All stats for which EP should be calculated.
-			epStats: [
-				Stat.StatIntellect,
-				Stat.StatSpirit,
-				Stat.StatSpellPower,
-				Stat.StatSpellCrit,
-				Stat.StatSpellHaste,
-				Stat.StatMP5,
-			],
-			// Reference stat against which to calculate EP. I think all classes use either spell power or attack power.
-			epReferenceStat: Stat.StatSpellPower,
-			// Which stats to display in the Character Stats section, at the bottom of the left-hand sidebar.
-			displayStats: [
-				Stat.StatHealth,
-				Stat.StatMana,
-				Stat.StatStamina,
-				Stat.StatIntellect,
-				Stat.StatSpirit,
-				Stat.StatSpellPower,
-				Stat.StatSpellCrit,
-				Stat.StatSpellHaste,
-				Stat.StatMP5,
-			],
-			defaults: {
-				// Default equipped gear.
-				gear: Presets.P1_PRESET.gear,
-				// Default EP weights for sorting gear in the gear picker.
-				epWeights: Stats.fromMap({
-					[Stat.StatIntellect]: 0.38,
-					[Stat.StatSpirit]: 0.34,
-					[Stat.StatSpellPower]: 1,
-					[Stat.StatSpellCrit]: 0.69,
-					[Stat.StatSpellHaste]: 0.77,
-					[Stat.StatMP5]: 0.00,
-				}),
-				// Default consumes settings.
-				consumes: Presets.DefaultConsumes,
-				// Default rotation settings.
-				rotation: Presets.DefaultRotation,
-				// Default talents.
-				talents: Presets.StandardTalents.data,
-				// Default spec-specific settings.
-				specOptions: Presets.DefaultOptions,
-				// Default raid/party buffs settings.
-				raidBuffs: RaidBuffs.create({
-					giftOfTheWild: TristateEffect.TristateEffectImproved,
-					powerWordFortitude: TristateEffect.TristateEffectImproved,
-					strengthOfEarthTotem: TristateEffect.TristateEffectImproved,
-					arcaneBrilliance: true,
-					unleashedRage: true,
-					leaderOfThePack: TristateEffect.TristateEffectRegular,
-					icyTalons: true,
-					totemOfWrath: true,
-					demonicPact: 500,
-					swiftRetribution: true,
-					moonkinAura: TristateEffect.TristateEffectRegular,
-					sanctifiedRetribution: true,
-					manaSpringTotem: TristateEffect.TristateEffectRegular,
-					bloodlust: true,
-					thorns: TristateEffect.TristateEffectImproved,
-					devotionAura: TristateEffect.TristateEffectImproved,
-					shadowProtection: true,
-				}),
-				partyBuffs: PartyBuffs.create({
-				}),
-				individualBuffs: IndividualBuffs.create({
-					blessingOfKings: true,
-					blessingOfSanctuary: true,
-					blessingOfWisdom: TristateEffect.TristateEffectImproved,
-					blessingOfMight: TristateEffect.TristateEffectImproved,
-				}),
-				debuffs: Debuffs.create({
-					judgementOfWisdom: true,
-					judgementOfLight: true,
-					misery: true,
-					faerieFire: TristateEffect.TristateEffectImproved,
-					ebonPlaguebringer: true,
-					totemOfWrath: true,
-					shadowMastery: true,
-					bloodFrenzy: true,
-					mangle: true,
-					exposeArmor: true,
-					sunderArmor: true,
-					vindication: true,
-					thunderClap: TristateEffect.TristateEffectImproved,
-					insectSwarm: true,
-				}),
-			},
+	// IconInputs to include in the 'Player' section on the settings tab.
+	playerIconInputs: [
+	],
+	// Inputs to include in the 'Rotation' section on the settings tab.
+	rotationInputs: HolyPaladinInputs.HolyPaladinRotationConfig,
+	// Buff and Debuff inputs to include/exclude, overriding the EP-based defaults.
+	includeBuffDebuffInputs: [
+	],
+	excludeBuffDebuffInputs: [
+	],
+	// Inputs to include in the 'Other' section on the settings tab.
+	otherInputs: {
+		inputs: [
+			OtherInputs.TankAssignment,
+			OtherInputs.InspirationUptime,
+			HolyPaladinInputs.AuraSelection,
+			HolyPaladinInputs.JudgementSelection,
+		],
+	},
+	encounterPicker: {
+		// Whether to include 'Execute Duration (%)' in the 'Encounter' section of the settings tab.
+		showExecuteProportion: false,
+	},
 
-			// IconInputs to include in the 'Player' section on the settings tab.
-			playerIconInputs: [
-			],
-			// Inputs to include in the 'Rotation' section on the settings tab.
-			rotationInputs: HolyPaladinInputs.HolyPaladinRotationConfig,
-			// Buff and Debuff inputs to include/exclude, overriding the EP-based defaults.
-			includeBuffDebuffInputs: [
-			],
-			excludeBuffDebuffInputs: [
-			],
-			// Inputs to include in the 'Other' section on the settings tab.
-			otherInputs: {
-				inputs: [
-					OtherInputs.TankAssignment,
-					OtherInputs.InspirationUptime,
-					HolyPaladinInputs.AuraSelection,
-					HolyPaladinInputs.JudgementSelection,
-				],
-			},
-			encounterPicker: {
-				// Whether to include 'Execute Duration (%)' in the 'Encounter' section of the settings tab.
-				showExecuteProportion: false,
-			},
+	presets: {
+		// Preset talents that the user can quickly select.
+		talents: [
+			Presets.StandardTalents,
+		],
+		rotations: [
+		],
+		// Preset gear configurations that the user can quickly select.
+		gear: [
+			Presets.PRERAID_PRESET,
+			Presets.P1_PRESET,
+			Presets.P2_PRESET,
+		],
+	},
 
-			presets: {
-				// Preset talents that the user can quickly select.
-				talents: [
-					Presets.StandardTalents,
-				],
-				// Preset gear configurations that the user can quickly select.
-				gear: [
-					Presets.PRERAID_PRESET,
-					Presets.P1_PRESET,
-					Presets.P2_PRESET,
-				],
-			},
+	autoRotation: (_player: Player<Spec.SpecHolyPaladin>): APLRotation => {
+		return APLRotation.create();
+	},
+});
 
-			autoRotation: (_player: Player<Spec.SpecHolyPaladin>): APLRotation => {
-				return APLRotation.create();
-			},
-		});
+export class HolyPaladinSimUI extends IndividualSimUI<Spec.SpecHolyPaladin> {
+	constructor(parentElem: HTMLElement, player: Player<Spec.SpecHolyPaladin>) {
+		super(parentElem, player, SPEC_CONFIG);
 	}
 }
diff --git a/ui/hunter/sim.ts b/ui/hunter/sim.ts
index 3e8837c459..4b9755859e 100644
--- a/ui/hunter/sim.ts
+++ b/ui/hunter/sim.ts
@@ -19,16 +19,14 @@ import {
 import { Player } from '../core/player.js';
 import { Stats } from '../core/proto_utils/stats.js';
 import { getTalentPoints } from '../core/proto_utils/utils.js';
-import { IndividualSimUI } from '../core/individual_sim_ui.js';
-import { EventID, TypedEvent } from '../core/typed_event.js';
+import { IndividualSimUI, registerSpecConfig } from '../core/individual_sim_ui.js';
+import { TypedEvent } from '../core/typed_event.js';
 import { getPetTalentsConfig } from '../core/talents/hunter_pet.js';
 import { protoToTalentString } from '../core/talents/factory.js';
 
 import {
-	Hunter,
 	Hunter_Rotation as HunterRotation,
 	Hunter_Rotation_StingType as StingType,
-	Hunter_Options as HunterOptions,
 	Hunter_Options_PetType as PetType,
 	HunterPetTalents,
 	Hunter_Rotation_RotationType,
@@ -37,337 +35,338 @@ import {
 import * as IconInputs from '../core/components/icon_inputs.js';
 import * as OtherInputs from '../core/components/other_inputs.js';
 import * as Mechanics from '../core/constants/mechanics.js';
-import * as Tooltips from '../core/constants/tooltips.js';
 import * as AplUtils from '../core/proto_utils/apl_utils.js';
 
 import * as HunterInputs from './inputs.js';
 import * as Presets from './presets.js';
 
-export class HunterSimUI extends IndividualSimUI<Spec.SpecHunter> {
-	constructor(parentElem: HTMLElement, player: Player<Spec.SpecHunter>) {
-		super(parentElem, player, {
-			cssClass: 'hunter-sim-ui',
-			cssScheme: 'hunter',
-			// List any known bugs / issues here and they'll be shown on the site.
-			knownIssues: [
-			],
-			warnings: [
-				// Warning when using exotic pet without BM talented.
-				(simUI: IndividualSimUI<Spec.SpecHunter>) => {
-					return {
-						updateOn: TypedEvent.onAny([simUI.player.talentsChangeEmitter, simUI.player.specOptionsChangeEmitter]),
-						getContent: () => {
-							const petIsExotic = [
-								PetType.Chimaera,
-								PetType.CoreHound,
-								PetType.Devilsaur,
-								PetType.Silithid,
-								PetType.SpiritBeast,
-								PetType.Worm,
-							].includes(simUI.player.getSpecOptions().petType);
+const SPEC_CONFIG = registerSpecConfig(Spec.SpecHunter, {
+	cssClass: 'hunter-sim-ui',
+	cssScheme: 'hunter',
+	// List any known bugs / issues here and they'll be shown on the site.
+	knownIssues: [
+	],
+	warnings: [
+		// Warning when using exotic pet without BM talented.
+		(simUI: IndividualSimUI<Spec.SpecHunter>) => {
+			return {
+				updateOn: TypedEvent.onAny([simUI.player.talentsChangeEmitter, simUI.player.specOptionsChangeEmitter]),
+				getContent: () => {
+					const petIsExotic = [
+						PetType.Chimaera,
+						PetType.CoreHound,
+						PetType.Devilsaur,
+						PetType.Silithid,
+						PetType.SpiritBeast,
+						PetType.Worm,
+					].includes(simUI.player.getSpecOptions().petType);
 
-							const isBM = simUI.player.getTalents().beastMastery;
+					const isBM = simUI.player.getTalents().beastMastery;
 
-							if (petIsExotic && !isBM) {
-								return 'Cannot use exotic pets without the Beast Mastery talent.';
-							} else {
-								return '';
-							}
-						},
-					};
+					if (petIsExotic && !isBM) {
+						return 'Cannot use exotic pets without the Beast Mastery talent.';
+					} else {
+						return '';
+					}
 				},
-				// Warning when too many Pet talent points are used without BM talented.
-				(simUI: IndividualSimUI<Spec.SpecHunter>) => {
-					return {
-						updateOn: TypedEvent.onAny([simUI.player.talentsChangeEmitter, simUI.player.specOptionsChangeEmitter]),
-						getContent: () => {
-							const specOptions = simUI.player.getSpecOptions();
-							const petTalents = specOptions.petTalents || HunterPetTalents.create();
-							const petTalentString = protoToTalentString(petTalents, getPetTalentsConfig(specOptions.petType));
-							const talentPoints = getTalentPoints(petTalentString);
+			};
+		},
+		// Warning when too many Pet talent points are used without BM talented.
+		(simUI: IndividualSimUI<Spec.SpecHunter>) => {
+			return {
+				updateOn: TypedEvent.onAny([simUI.player.talentsChangeEmitter, simUI.player.specOptionsChangeEmitter]),
+				getContent: () => {
+					const specOptions = simUI.player.getSpecOptions();
+					const petTalents = specOptions.petTalents || HunterPetTalents.create();
+					const petTalentString = protoToTalentString(petTalents, getPetTalentsConfig(specOptions.petType));
+					const talentPoints = getTalentPoints(petTalentString);
 
-							const isBM = simUI.player.getTalents().beastMastery;
-							const maxPoints = isBM ? 20 : 16;
+					const isBM = simUI.player.getTalents().beastMastery;
+					const maxPoints = isBM ? 20 : 16;
 
-							if (talentPoints == 0) {
-								// Just return here, so we don't show a warning during page load.
-								return '';
-							} else if (talentPoints < maxPoints) {
-								return 'Unspent pet talent points.';
-							} else if (talentPoints > maxPoints) {
-								return 'More than 16 points spent in pet talents, but Beast Mastery is not talented.';
-							} else {
-								return '';
-							}
-						},
-					};
+					if (talentPoints == 0) {
+						// Just return here, so we don't show a warning during page load.
+						return '';
+					} else if (talentPoints < maxPoints) {
+						return 'Unspent pet talent points.';
+					} else if (talentPoints > maxPoints) {
+						return 'More than 16 points spent in pet talents, but Beast Mastery is not talented.';
+					} else {
+						return '';
+					}
 				},
-			],
+			};
+		},
+	],
 
-			// All stats for which EP should be calculated.
-			epStats: [
-				Stat.StatStamina,
-				Stat.StatIntellect,
-				Stat.StatAgility,
-				Stat.StatRangedAttackPower,
-				Stat.StatMeleeHit,
-				Stat.StatMeleeCrit,
-				Stat.StatMeleeHaste,
-				Stat.StatArmorPenetration,
-				Stat.StatMP5,
-			],
-			epPseudoStats: [
-				PseudoStat.PseudoStatRangedDps,
-			],
-			// Reference stat against which to calculate EP.
-			epReferenceStat: Stat.StatRangedAttackPower,
-			// Which stats to display in the Character Stats section, at the bottom of the left-hand sidebar.
-			displayStats: [
-				Stat.StatHealth,
-				Stat.StatStamina,
-				Stat.StatAgility,
-				Stat.StatIntellect,
-				Stat.StatRangedAttackPower,
-				Stat.StatMeleeHit,
-				Stat.StatMeleeCrit,
-				Stat.StatMeleeHaste,
-				Stat.StatArmorPenetration,
-				Stat.StatMP5,
-			],
-			modifyDisplayStats: (player: Player<Spec.SpecHunter>) => {
-				let stats = new Stats();
-				stats = stats.addStat(Stat.StatMeleeCrit, player.getTalents().lethalShots * 1 * Mechanics.MELEE_CRIT_RATING_PER_CRIT_CHANCE);
+	// All stats for which EP should be calculated.
+	epStats: [
+		Stat.StatStamina,
+		Stat.StatIntellect,
+		Stat.StatAgility,
+		Stat.StatRangedAttackPower,
+		Stat.StatMeleeHit,
+		Stat.StatMeleeCrit,
+		Stat.StatMeleeHaste,
+		Stat.StatArmorPenetration,
+		Stat.StatMP5,
+	],
+	epPseudoStats: [
+		PseudoStat.PseudoStatRangedDps,
+	],
+	// Reference stat against which to calculate EP.
+	epReferenceStat: Stat.StatRangedAttackPower,
+	// Which stats to display in the Character Stats section, at the bottom of the left-hand sidebar.
+	displayStats: [
+		Stat.StatHealth,
+		Stat.StatStamina,
+		Stat.StatAgility,
+		Stat.StatIntellect,
+		Stat.StatRangedAttackPower,
+		Stat.StatMeleeHit,
+		Stat.StatMeleeCrit,
+		Stat.StatMeleeHaste,
+		Stat.StatArmorPenetration,
+		Stat.StatMP5,
+	],
+	modifyDisplayStats: (player: Player<Spec.SpecHunter>) => {
+		let stats = new Stats();
+		stats = stats.addStat(Stat.StatMeleeCrit, player.getTalents().lethalShots * 1 * Mechanics.MELEE_CRIT_RATING_PER_CRIT_CHANCE);
 
-				const rangedWeapon = player.getEquippedItem(ItemSlot.ItemSlotRanged);
-				if (rangedWeapon?.enchant?.effectId == 3608) {
-					stats = stats.addStat(Stat.StatMeleeCrit, 40);
-				}
-				if (player.getRace() == Race.RaceDwarf && rangedWeapon?.item.rangedWeaponType == RangedWeaponType.RangedWeaponTypeGun) {
-					stats = stats.addStat(Stat.StatMeleeCrit, 1 * Mechanics.MELEE_CRIT_RATING_PER_CRIT_CHANCE);
-				}
-				if (player.getRace() == Race.RaceTroll && rangedWeapon?.item.rangedWeaponType == RangedWeaponType.RangedWeaponTypeBow) {
-					stats = stats.addStat(Stat.StatMeleeCrit, 1 * Mechanics.MELEE_CRIT_RATING_PER_CRIT_CHANCE);
-				}
+		const rangedWeapon = player.getEquippedItem(ItemSlot.ItemSlotRanged);
+		if (rangedWeapon?.enchant?.effectId == 3608) {
+			stats = stats.addStat(Stat.StatMeleeCrit, 40);
+		}
+		if (player.getRace() == Race.RaceDwarf && rangedWeapon?.item.rangedWeaponType == RangedWeaponType.RangedWeaponTypeGun) {
+			stats = stats.addStat(Stat.StatMeleeCrit, 1 * Mechanics.MELEE_CRIT_RATING_PER_CRIT_CHANCE);
+		}
+		if (player.getRace() == Race.RaceTroll && rangedWeapon?.item.rangedWeaponType == RangedWeaponType.RangedWeaponTypeBow) {
+			stats = stats.addStat(Stat.StatMeleeCrit, 1 * Mechanics.MELEE_CRIT_RATING_PER_CRIT_CHANCE);
+		}
 
-				return {
-					talents: stats,
-				};
-			},
+		return {
+			talents: stats,
+		};
+	},
 
-			defaults: {
-				// Default equipped gear.
-				gear: Presets.SV_P1_PRESET.gear,
-				// Default EP weights for sorting gear in the gear picker.
-				epWeights: Stats.fromMap({
-					[Stat.StatStamina]: 0.5,
-					[Stat.StatAgility]: 2.65,
-					[Stat.StatIntellect]: 1.1,
-					[Stat.StatRangedAttackPower]: 1.0,
-					[Stat.StatMeleeHit]: 2,
-					[Stat.StatMeleeCrit]: 1.5,
-					[Stat.StatMeleeHaste]: 1.39,
-					[Stat.StatArmorPenetration]: 1.32,
-				}, {
-					[PseudoStat.PseudoStatRangedDps]: 6.32,
-				}),
-				// Default consumes settings.
-				consumes: Presets.DefaultConsumes,
-				// Default rotation settings.
-				rotation: Presets.DefaultRotation,
-				// Default talents.
-				talents: Presets.SurvivalTalents.data,
-				// Default spec-specific settings.
-				specOptions: Presets.DefaultOptions,
-				// Default raid/party buffs settings.
-				raidBuffs: RaidBuffs.create({
-					arcaneBrilliance: true,
-					powerWordFortitude: TristateEffect.TristateEffectImproved,
-					giftOfTheWild: TristateEffect.TristateEffectImproved,
-					bloodlust: true,
-					strengthOfEarthTotem: TristateEffect.TristateEffectImproved,
-					windfuryTotem: TristateEffect.TristateEffectImproved,
-					battleShout: TristateEffect.TristateEffectImproved,
-					leaderOfThePack: TristateEffect.TristateEffectImproved,
-					sanctifiedRetribution: true,
-					unleashedRage: true,
-					moonkinAura: TristateEffect.TristateEffectImproved,
-				}),
-				partyBuffs: PartyBuffs.create({
-				}),
-				individualBuffs: IndividualBuffs.create({
-					blessingOfKings: true,
-					blessingOfWisdom: 2,
-					blessingOfMight: 2,
-					vampiricTouch: true,
-				}),
-				debuffs: Debuffs.create({
-					sunderArmor: true,
-					faerieFire: TristateEffect.TristateEffectImproved,
-					judgementOfWisdom: true,
-					curseOfElements: true,
-					heartOfTheCrusader: true,
-					savageCombat: true,
-				}),
-			},
+	defaults: {
+		// Default equipped gear.
+		gear: Presets.SV_P1_PRESET.gear,
+		// Default EP weights for sorting gear in the gear picker.
+		epWeights: Stats.fromMap({
+			[Stat.StatStamina]: 0.5,
+			[Stat.StatAgility]: 2.65,
+			[Stat.StatIntellect]: 1.1,
+			[Stat.StatRangedAttackPower]: 1.0,
+			[Stat.StatMeleeHit]: 2,
+			[Stat.StatMeleeCrit]: 1.5,
+			[Stat.StatMeleeHaste]: 1.39,
+			[Stat.StatArmorPenetration]: 1.32,
+		}, {
+			[PseudoStat.PseudoStatRangedDps]: 6.32,
+		}),
+		// Default consumes settings.
+		consumes: Presets.DefaultConsumes,
+		// Default rotation settings.
+		rotation: Presets.DefaultRotation,
+		// Default talents.
+		talents: Presets.SurvivalTalents.data,
+		// Default spec-specific settings.
+		specOptions: Presets.DefaultOptions,
+		// Default raid/party buffs settings.
+		raidBuffs: RaidBuffs.create({
+			arcaneBrilliance: true,
+			powerWordFortitude: TristateEffect.TristateEffectImproved,
+			giftOfTheWild: TristateEffect.TristateEffectImproved,
+			bloodlust: true,
+			strengthOfEarthTotem: TristateEffect.TristateEffectImproved,
+			windfuryTotem: TristateEffect.TristateEffectImproved,
+			battleShout: TristateEffect.TristateEffectImproved,
+			leaderOfThePack: TristateEffect.TristateEffectImproved,
+			sanctifiedRetribution: true,
+			unleashedRage: true,
+			moonkinAura: TristateEffect.TristateEffectImproved,
+		}),
+		partyBuffs: PartyBuffs.create({
+		}),
+		individualBuffs: IndividualBuffs.create({
+			blessingOfKings: true,
+			blessingOfWisdom: 2,
+			blessingOfMight: 2,
+			vampiricTouch: true,
+		}),
+		debuffs: Debuffs.create({
+			sunderArmor: true,
+			faerieFire: TristateEffect.TristateEffectImproved,
+			judgementOfWisdom: true,
+			curseOfElements: true,
+			heartOfTheCrusader: true,
+			savageCombat: true,
+		}),
+	},
 
-			// IconInputs to include in the 'Player' section on the settings tab.
-			playerIconInputs: [
-				HunterInputs.PetTypeInput,
-				HunterInputs.WeaponAmmo,
-				HunterInputs.UseHuntersMark,
-			],
-			// Inputs to include in the 'Rotation' section on the settings tab.
-			rotationInputs: HunterInputs.HunterRotationConfig,
-			petConsumeInputs: [
-				IconInputs.SpicedMammothTreats,
-			],
-			// Buff and Debuff inputs to include/exclude, overriding the EP-based defaults.
-			includeBuffDebuffInputs: [
-				IconInputs.StaminaBuff,
-				IconInputs.SpellDamageDebuff,
-			],
-			excludeBuffDebuffInputs: [
-			],
-			// Inputs to include in the 'Other' section on the settings tab.
-			otherInputs: {
-				inputs: [
-					HunterInputs.PetUptime,
-					HunterInputs.TimeToTrapWeaveMs,
-					HunterInputs.SniperTrainingUptime,
-					OtherInputs.TankAssignment,
-					OtherInputs.InFrontOfTarget,
-				],
-			},
-			encounterPicker: {
-				// Whether to include 'Execute Duration (%)' in the 'Encounter' section of the settings tab.
-				showExecuteProportion: false,
-			},
+	// IconInputs to include in the 'Player' section on the settings tab.
+	playerIconInputs: [
+		HunterInputs.PetTypeInput,
+		HunterInputs.WeaponAmmo,
+		HunterInputs.UseHuntersMark,
+	],
+	// Inputs to include in the 'Rotation' section on the settings tab.
+	rotationInputs: HunterInputs.HunterRotationConfig,
+	petConsumeInputs: [
+		IconInputs.SpicedMammothTreats,
+	],
+	// Buff and Debuff inputs to include/exclude, overriding the EP-based defaults.
+	includeBuffDebuffInputs: [
+		IconInputs.StaminaBuff,
+		IconInputs.SpellDamageDebuff,
+	],
+	excludeBuffDebuffInputs: [
+	],
+	// Inputs to include in the 'Other' section on the settings tab.
+	otherInputs: {
+		inputs: [
+			HunterInputs.PetUptime,
+			HunterInputs.TimeToTrapWeaveMs,
+			HunterInputs.SniperTrainingUptime,
+			OtherInputs.TankAssignment,
+			OtherInputs.InFrontOfTarget,
+		],
+	},
+	encounterPicker: {
+		// Whether to include 'Execute Duration (%)' in the 'Encounter' section of the settings tab.
+		showExecuteProportion: false,
+	},
 
-			presets: {
-				// Preset talents that the user can quickly select.
-				talents: [
-					Presets.BeastMasteryTalents,
-					Presets.MarksmanTalents,
-					Presets.SurvivalTalents,
-				],
-				// Preset rotations that the user can quickly select.
-				rotations: [
-					Presets.ROTATION_PRESET_SIMPLE_DEFAULT,
-					Presets.ROTATION_PRESET_BM,
-					Presets.ROTATION_PRESET_MM,
-					Presets.ROTATION_PRESET_MM_ADVANCED,
-					Presets.ROTATION_PRESET_SV,
-					Presets.ROTATION_PRESET_SV_ADVANCED,
-					Presets.ROTATION_PRESET_AOE,
-				],
-				// Preset gear configurations that the user can quickly select.
-				gear: [
-					Presets.MM_PRERAID_PRESET,
-					Presets.MM_P1_PRESET,
-					Presets.MM_P2_PRESET,
-					Presets.MM_P3_PRESET,
-					Presets.MM_P4_PRESET,
-					Presets.MM_P5_PRESET,
-					Presets.SV_PRERAID_PRESET,
-					Presets.SV_P1_PRESET,
-					Presets.SV_P2_PRESET,
-					Presets.SV_P3_PRESET,
-					Presets.SV_P4_PRESET,
-					Presets.SV_P5_PRESET,
-				],
-			},
+	presets: {
+		// Preset talents that the user can quickly select.
+		talents: [
+			Presets.BeastMasteryTalents,
+			Presets.MarksmanTalents,
+			Presets.SurvivalTalents,
+		],
+		// Preset rotations that the user can quickly select.
+		rotations: [
+			Presets.ROTATION_PRESET_SIMPLE_DEFAULT,
+			Presets.ROTATION_PRESET_BM,
+			Presets.ROTATION_PRESET_MM,
+			Presets.ROTATION_PRESET_MM_ADVANCED,
+			Presets.ROTATION_PRESET_SV,
+			Presets.ROTATION_PRESET_SV_ADVANCED,
+			Presets.ROTATION_PRESET_AOE,
+		],
+		// Preset gear configurations that the user can quickly select.
+		gear: [
+			Presets.MM_PRERAID_PRESET,
+			Presets.MM_P1_PRESET,
+			Presets.MM_P2_PRESET,
+			Presets.MM_P3_PRESET,
+			Presets.MM_P4_PRESET,
+			Presets.MM_P5_PRESET,
+			Presets.SV_PRERAID_PRESET,
+			Presets.SV_P1_PRESET,
+			Presets.SV_P2_PRESET,
+			Presets.SV_P3_PRESET,
+			Presets.SV_P4_PRESET,
+			Presets.SV_P5_PRESET,
+		],
+	},
 
-			autoRotation: (player: Player<Spec.SpecHunter>): APLRotation => {
-				const talentTree = player.getTalentTree();
-				const numTargets = player.sim.encounter.targets.length;
-				if (numTargets >= 4) {
-					return Presets.ROTATION_PRESET_AOE.rotation.rotation!;
-				} else if (talentTree == 0) {
-					return Presets.ROTATION_PRESET_BM.rotation.rotation!;
-				} else if (talentTree == 1) {
-					return Presets.ROTATION_PRESET_MM.rotation.rotation!;
-				} else {
-					return Presets.ROTATION_PRESET_SV.rotation.rotation!;
-				}
-			},
+	autoRotation: (player: Player<Spec.SpecHunter>): APLRotation => {
+		const talentTree = player.getTalentTree();
+		const numTargets = player.sim.encounter.targets.length;
+		if (numTargets >= 4) {
+			return Presets.ROTATION_PRESET_AOE.rotation.rotation!;
+		} else if (talentTree == 0) {
+			return Presets.ROTATION_PRESET_BM.rotation.rotation!;
+		} else if (talentTree == 1) {
+			return Presets.ROTATION_PRESET_MM.rotation.rotation!;
+		} else {
+			return Presets.ROTATION_PRESET_SV.rotation.rotation!;
+		}
+	},
 
-			simpleRotation: (player: Player<Spec.SpecHunter>, simple: HunterRotation, cooldowns: Cooldowns): APLRotation => {
-				let [prepullActions, actions] = AplUtils.standardCooldownDefaults(cooldowns);
+	simpleRotation: (player: Player<Spec.SpecHunter>, simple: HunterRotation, cooldowns: Cooldowns): APLRotation => {
+		let [prepullActions, actions] = AplUtils.standardCooldownDefaults(cooldowns);
 
-				const serpentSting = APLAction.fromJsonString(`{"condition":{"cmp":{"op":"OpGt","lhs":{"remainingTime":{}},"rhs":{"const":{"val":"6s"}}}},"multidot":{"spellId":{"spellId":49001},"maxDots":${simple.multiDotSerpentSting ? 3 : 1},"maxOverlap":{"const":{"val":"0ms"}}}}`);
-				const scorpidSting = APLAction.fromJsonString(`{"condition":{"auraShouldRefresh":{"auraId":{"spellId":3043},"maxOverlap":{"const":{"val":"0ms"}}}},"castSpell":{"spellId":{"spellId":3043}}}`);
-				const trapWeave = APLAction.fromJsonString(`{"condition":{"not":{"val":{"dotIsActive":{"spellId":{"spellId":49067}}}}},"castSpell":{"spellId":{"tag":1,"spellId":49067}}}`);
-				const volley = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":58434}}}`);
-				const killShot = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":61006}}}`);
-				const aimedShot = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":49050}}}`);
-				const multiShot = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":49048}}}`);
-				const steadyShot = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":49052}}}`);
-				const silencingShot = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":34490}}}`);
-				const chimeraShot = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":53209}}}`);
-				const blackArrow = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":63672}}}`);
-				const explosiveShot4 = APLAction.fromJsonString(`{"condition":{"not":{"val":{"dotIsActive":{"spellId":{"spellId":60053}}}}},"castSpell":{"spellId":{"spellId":60053}}}`);
-				const explosiveShot3 = APLAction.fromJsonString(`{"condition":{"dotIsActive":{"spellId":{"spellId":60053}}},"castSpell":{"spellId":{"spellId":60052}}}`);
-				//const arcaneShot = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":49045}}}`);
+		const serpentSting = APLAction.fromJsonString(`{"condition":{"cmp":{"op":"OpGt","lhs":{"remainingTime":{}},"rhs":{"const":{"val":"6s"}}}},"multidot":{"spellId":{"spellId":49001},"maxDots":${simple.multiDotSerpentSting ? 3 : 1},"maxOverlap":{"const":{"val":"0ms"}}}}`);
+		const scorpidSting = APLAction.fromJsonString(`{"condition":{"auraShouldRefresh":{"auraId":{"spellId":3043},"maxOverlap":{"const":{"val":"0ms"}}}},"castSpell":{"spellId":{"spellId":3043}}}`);
+		const trapWeave = APLAction.fromJsonString(`{"condition":{"not":{"val":{"dotIsActive":{"spellId":{"spellId":49067}}}}},"castSpell":{"spellId":{"tag":1,"spellId":49067}}}`);
+		const volley = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":58434}}}`);
+		const killShot = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":61006}}}`);
+		const aimedShot = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":49050}}}`);
+		const multiShot = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":49048}}}`);
+		const steadyShot = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":49052}}}`);
+		const silencingShot = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":34490}}}`);
+		const chimeraShot = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":53209}}}`);
+		const blackArrow = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":63672}}}`);
+		const explosiveShot4 = APLAction.fromJsonString(`{"condition":{"not":{"val":{"dotIsActive":{"spellId":{"spellId":60053}}}}},"castSpell":{"spellId":{"spellId":60053}}}`);
+		const explosiveShot3 = APLAction.fromJsonString(`{"condition":{"dotIsActive":{"spellId":{"spellId":60053}}},"castSpell":{"spellId":{"spellId":60052}}}`);
+		//const arcaneShot = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":49045}}}`);
 
-				if (simple.viperStartManaPercent != 0) {
-					actions.push(APLAction.fromJsonString(`{"condition":{"and":{"vals":[{"not":{"val":{"auraIsActive":{"auraId":{"spellId":34074}}}}},{"cmp":{"op":"OpLt","lhs":{"currentManaPercent":{}},"rhs":{"const":{"val":"${(simple.viperStartManaPercent * 100).toFixed(0)}%"}}}}]}},"castSpell":{"spellId":{"spellId":34074}}}`));
-				}
-				if (simple.viperStopManaPercent != 0) {
-					actions.push(APLAction.fromJsonString(`{"condition":{"and":{"vals":[{"not":{"val":{"auraIsActive":{"auraId":{"spellId":61847}}}}},{"cmp":{"op":"OpGt","lhs":{"currentManaPercent":{}},"rhs":{"const":{"val":"${(simple.viperStopManaPercent * 100).toFixed(0)}%"}}}}]}},"castSpell":{"spellId":{"spellId":61847}}}`));
-				}
+		if (simple.viperStartManaPercent != 0) {
+			actions.push(APLAction.fromJsonString(`{"condition":{"and":{"vals":[{"not":{"val":{"auraIsActive":{"auraId":{"spellId":34074}}}}},{"cmp":{"op":"OpLt","lhs":{"currentManaPercent":{}},"rhs":{"const":{"val":"${(simple.viperStartManaPercent * 100).toFixed(0)}%"}}}}]}},"castSpell":{"spellId":{"spellId":34074}}}`));
+		}
+		if (simple.viperStopManaPercent != 0) {
+			actions.push(APLAction.fromJsonString(`{"condition":{"and":{"vals":[{"not":{"val":{"auraIsActive":{"auraId":{"spellId":61847}}}}},{"cmp":{"op":"OpGt","lhs":{"currentManaPercent":{}},"rhs":{"const":{"val":"${(simple.viperStopManaPercent * 100).toFixed(0)}%"}}}}]}},"castSpell":{"spellId":{"spellId":61847}}}`));
+		}
 
-				const talentTree = player.getTalentTree();
-				if (simple.type == Hunter_Rotation_RotationType.Aoe) {
-					actions.push(...[
-						simple.sting == StingType.ScorpidSting ? scorpidSting : null,
-						simple.sting == StingType.SerpentSting ? serpentSting : null,
-						simple.trapWeave ? trapWeave : null,
-						volley,
-					].filter(a => a) as Array<APLAction>)
-				} else if (talentTree == 0) { // BM
-					actions.push(...[
-						killShot,
-						simple.trapWeave ? trapWeave : null,
-						simple.sting == StingType.ScorpidSting ? scorpidSting : null,
-						simple.sting == StingType.SerpentSting ? serpentSting : null,
-						aimedShot,
-						multiShot,
-						steadyShot,
-					].filter(a => a) as Array<APLAction>)
-				} else if (talentTree == 1) { // MM
-					actions.push(...[
-						silencingShot,
-						killShot,
-						simple.sting == StingType.ScorpidSting ? scorpidSting : null,
-						simple.sting == StingType.SerpentSting ? serpentSting : null,
-						simple.trapWeave ? trapWeave : null,
-						chimeraShot,
-						aimedShot,
-						multiShot,
-						steadyShot,
-					].filter(a => a) as Array<APLAction>)
-				} else if (talentTree == 2) { // SV
-					actions.push(...[
-						killShot,
-						explosiveShot4,
-						simple.allowExplosiveShotDownrank ? explosiveShot3 : null,
-						simple.trapWeave ? trapWeave : null,
-						simple.sting == StingType.ScorpidSting ? scorpidSting : null,
-						simple.sting == StingType.SerpentSting ? serpentSting : null,
-						blackArrow,
-						aimedShot,
-						multiShot,
-						steadyShot,
-					].filter(a => a) as Array<APLAction>)
-				}
+		const talentTree = player.getTalentTree();
+		if (simple.type == Hunter_Rotation_RotationType.Aoe) {
+			actions.push(...[
+				simple.sting == StingType.ScorpidSting ? scorpidSting : null,
+				simple.sting == StingType.SerpentSting ? serpentSting : null,
+				simple.trapWeave ? trapWeave : null,
+				volley,
+			].filter(a => a) as Array<APLAction>)
+		} else if (talentTree == 0) { // BM
+			actions.push(...[
+				killShot,
+				simple.trapWeave ? trapWeave : null,
+				simple.sting == StingType.ScorpidSting ? scorpidSting : null,
+				simple.sting == StingType.SerpentSting ? serpentSting : null,
+				aimedShot,
+				multiShot,
+				steadyShot,
+			].filter(a => a) as Array<APLAction>)
+		} else if (talentTree == 1) { // MM
+			actions.push(...[
+				silencingShot,
+				killShot,
+				simple.sting == StingType.ScorpidSting ? scorpidSting : null,
+				simple.sting == StingType.SerpentSting ? serpentSting : null,
+				simple.trapWeave ? trapWeave : null,
+				chimeraShot,
+				aimedShot,
+				multiShot,
+				steadyShot,
+			].filter(a => a) as Array<APLAction>)
+		} else if (talentTree == 2) { // SV
+			actions.push(...[
+				killShot,
+				explosiveShot4,
+				simple.allowExplosiveShotDownrank ? explosiveShot3 : null,
+				simple.trapWeave ? trapWeave : null,
+				simple.sting == StingType.ScorpidSting ? scorpidSting : null,
+				simple.sting == StingType.SerpentSting ? serpentSting : null,
+				blackArrow,
+				aimedShot,
+				multiShot,
+				steadyShot,
+			].filter(a => a) as Array<APLAction>)
+		}
 
-				return APLRotation.create({
-					prepullActions: prepullActions,
-					priorityList: actions.map(action => APLListItem.create({
-						action: action,
-					}))
-				});
-			},
+		return APLRotation.create({
+			prepullActions: prepullActions,
+			priorityList: actions.map(action => APLListItem.create({
+				action: action,
+			}))
 		});
+	},
+});
+
+export class HunterSimUI extends IndividualSimUI<Spec.SpecHunter> {
+	constructor(parentElem: HTMLElement, player: Player<Spec.SpecHunter>) {
+		super(parentElem, player, SPEC_CONFIG);
 	}
 }
diff --git a/ui/mage/sim.ts b/ui/mage/sim.ts
index 8eb6d279bb..878f912486 100644
--- a/ui/mage/sim.ts
+++ b/ui/mage/sim.ts
@@ -2,7 +2,7 @@ import {Cooldowns, Debuffs, IndividualBuffs, PartyBuffs, RaidBuffs, Spec, Stat,
 import {APLAction, APLListItem, APLPrepullAction, APLRotation} from '../core/proto/apl.js';
 import {Stats} from '../core/proto_utils/stats.js';
 import {Player} from '../core/player.js';
-import {IndividualSimUI} from '../core/individual_sim_ui.js';
+import {IndividualSimUI, registerSpecConfig} from '../core/individual_sim_ui.js';
 import {
 	Mage_Rotation as MageRotation,
 	Mage_Rotation_PrimaryFireSpell as PrimaryFireSpell,
@@ -15,285 +15,287 @@ import * as AplUtils from '../core/proto_utils/apl_utils.js';
 import * as MageInputs from './inputs.js';
 import * as Presets from './presets.js';
 
-export class MageSimUI extends IndividualSimUI<Spec.SpecMage> {
-	constructor(parentElem: HTMLElement, player: Player<Spec.SpecMage>) {
-		super(parentElem, player, {
-			cssClass: 'mage-sim-ui',
-			cssScheme: 'mage',
-			// List any known bugs / issues here and they'll be shown on the site.
-			knownIssues: [
-			],
+const SPEC_CONFIG = registerSpecConfig(Spec.SpecMage, {
+	cssClass: 'mage-sim-ui',
+	cssScheme: 'mage',
+	// List any known bugs / issues here and they'll be shown on the site.
+	knownIssues: [
+	],
 
-			// All stats for which EP should be calculated.
-			epStats: [
-				Stat.StatIntellect,
-				Stat.StatSpirit,
-				Stat.StatSpellPower,
-				Stat.StatSpellHit,
-				Stat.StatSpellCrit,
-				Stat.StatSpellHaste,
-				Stat.StatMP5,
-			],
-			// Reference stat against which to calculate EP. I think all classes use either spell power or attack power.
-			epReferenceStat: Stat.StatSpellPower,
-			// Which stats to display in the Character Stats section, at the bottom of the left-hand sidebar.
-			displayStats: [
-				Stat.StatHealth,
-				Stat.StatMana,
-				Stat.StatStamina,
-				Stat.StatIntellect,
-				Stat.StatSpirit,
-				Stat.StatSpellPower,
-				Stat.StatSpellHit,
-				Stat.StatSpellCrit,
-				Stat.StatSpellHaste,
-				Stat.StatMP5,
-			],
-			modifyDisplayStats: (player: Player<Spec.SpecMage>) => {
-				let stats = new Stats();
+	// All stats for which EP should be calculated.
+	epStats: [
+		Stat.StatIntellect,
+		Stat.StatSpirit,
+		Stat.StatSpellPower,
+		Stat.StatSpellHit,
+		Stat.StatSpellCrit,
+		Stat.StatSpellHaste,
+		Stat.StatMP5,
+	],
+	// Reference stat against which to calculate EP. I think all classes use either spell power or attack power.
+	epReferenceStat: Stat.StatSpellPower,
+	// Which stats to display in the Character Stats section, at the bottom of the left-hand sidebar.
+	displayStats: [
+		Stat.StatHealth,
+		Stat.StatMana,
+		Stat.StatStamina,
+		Stat.StatIntellect,
+		Stat.StatSpirit,
+		Stat.StatSpellPower,
+		Stat.StatSpellHit,
+		Stat.StatSpellCrit,
+		Stat.StatSpellHaste,
+		Stat.StatMP5,
+	],
+	modifyDisplayStats: (player: Player<Spec.SpecMage>) => {
+		let stats = new Stats();
 
-				if (player.getTalentTree() === 0) {
-					stats = stats.addStat(Stat.StatSpellHit, player.getTalents().arcaneFocus * 1 * Mechanics.SPELL_HIT_RATING_PER_HIT_CHANCE);
-				}
+		if (player.getTalentTree() === 0) {
+			stats = stats.addStat(Stat.StatSpellHit, player.getTalents().arcaneFocus * 1 * Mechanics.SPELL_HIT_RATING_PER_HIT_CHANCE);
+		}
 
-				return {
-					talents: stats,
-				};
-			},
+		return {
+			talents: stats,
+		};
+	},
 
-			defaults: {
-				// Default equipped gear.
-				gear: Presets.FIRE_P3_PRESET_HORDE.gear,
-				// Default EP weights for sorting gear in the gear picker.
-				epWeights: Stats.fromMap({
-					[Stat.StatIntellect]: 0.48,
-					[Stat.StatSpirit]: 0.42,
-					[Stat.StatSpellPower]: 1,
-					[Stat.StatSpellHit]: 0.38,
-					[Stat.StatSpellCrit]: 0.58,
-					[Stat.StatSpellHaste]: 0.94,
-					[Stat.StatMP5]: 0.09,
-				}),
-				// Default consumes settings.
-				consumes: Presets.DefaultFireConsumes,
-				// Default rotation settings.
-				rotation: Presets.DefaultSimpleRotation,
-				// Default talents.
-				talents: Presets.Phase3FireTalents.data,
-				// Default spec-specific settings.
-				specOptions: Presets.DefaultFireOptions,
-				other: Presets.OtherDefaults,
-				// Default raid/party buffs settings.
-				raidBuffs: RaidBuffs.create({
-					giftOfTheWild: TristateEffect.TristateEffectImproved,
-					bloodlust: true,
-					manaSpringTotem: TristateEffect.TristateEffectImproved,
-					wrathOfAirTotem: true,
-					divineSpirit: true,
-					swiftRetribution: true,
-					sanctifiedRetribution: true,
-					demonicPact: 500,
-					moonkinAura: TristateEffect.TristateEffectImproved,
-					arcaneBrilliance: true,
-				}),
-				partyBuffs: PartyBuffs.create({
-					manaTideTotems: 1,
-				}),
-				individualBuffs: IndividualBuffs.create({
-					blessingOfKings: true,
-					blessingOfWisdom: TristateEffect.TristateEffectImproved,
-					innervates: 0,
-					vampiricTouch: true,
-					focusMagic: true,
-				}),
-				debuffs: Debuffs.create({
-					judgementOfWisdom: true,
-					misery: true,
-					ebonPlaguebringer: true,
-					shadowMastery: true,
-					heartOfTheCrusader: true,
-				}),
-			},
+	defaults: {
+		// Default equipped gear.
+		gear: Presets.FIRE_P3_PRESET_HORDE.gear,
+		// Default EP weights for sorting gear in the gear picker.
+		epWeights: Stats.fromMap({
+			[Stat.StatIntellect]: 0.48,
+			[Stat.StatSpirit]: 0.42,
+			[Stat.StatSpellPower]: 1,
+			[Stat.StatSpellHit]: 0.38,
+			[Stat.StatSpellCrit]: 0.58,
+			[Stat.StatSpellHaste]: 0.94,
+			[Stat.StatMP5]: 0.09,
+		}),
+		// Default consumes settings.
+		consumes: Presets.DefaultFireConsumes,
+		// Default rotation settings.
+		rotation: Presets.DefaultSimpleRotation,
+		// Default talents.
+		talents: Presets.Phase3FireTalents.data,
+		// Default spec-specific settings.
+		specOptions: Presets.DefaultFireOptions,
+		other: Presets.OtherDefaults,
+		// Default raid/party buffs settings.
+		raidBuffs: RaidBuffs.create({
+			giftOfTheWild: TristateEffect.TristateEffectImproved,
+			bloodlust: true,
+			manaSpringTotem: TristateEffect.TristateEffectImproved,
+			wrathOfAirTotem: true,
+			divineSpirit: true,
+			swiftRetribution: true,
+			sanctifiedRetribution: true,
+			demonicPact: 500,
+			moonkinAura: TristateEffect.TristateEffectImproved,
+			arcaneBrilliance: true,
+		}),
+		partyBuffs: PartyBuffs.create({
+			manaTideTotems: 1,
+		}),
+		individualBuffs: IndividualBuffs.create({
+			blessingOfKings: true,
+			blessingOfWisdom: TristateEffect.TristateEffectImproved,
+			innervates: 0,
+			vampiricTouch: true,
+			focusMagic: true,
+		}),
+		debuffs: Debuffs.create({
+			judgementOfWisdom: true,
+			misery: true,
+			ebonPlaguebringer: true,
+			shadowMastery: true,
+			heartOfTheCrusader: true,
+		}),
+	},
 
-			// IconInputs to include in the 'Player' section on the settings tab.
-			playerIconInputs: [
-				MageInputs.Armor,
-			],
-			// Inputs to include in the 'Rotation' section on the settings tab.
-			rotationInputs: MageInputs.MageRotationConfig,
-			// Buff and Debuff inputs to include/exclude, overriding the EP-based defaults.
-			includeBuffDebuffInputs: [
-				//Should add hymn of hope, revitalize, and 
-			],
-			excludeBuffDebuffInputs: [
-			],
-			// Inputs to include in the 'Other' section on the settings tab.
-			otherInputs: {
-				inputs: [
-					MageInputs.FocusMagicUptime,
-					MageInputs.WaterElementalDisobeyChance,
-					OtherInputs.ReactionTime,
-					OtherInputs.DistanceFromTarget,
-					OtherInputs.TankAssignment,
-					OtherInputs.nibelungAverageCasts,
-				],
-			},
-			encounterPicker: {
-				// Whether to include 'Execute Duration (%)' in the 'Encounter' section of the settings tab.
-				showExecuteProportion: true,
-			},
+	// IconInputs to include in the 'Player' section on the settings tab.
+	playerIconInputs: [
+		MageInputs.Armor,
+	],
+	// Inputs to include in the 'Rotation' section on the settings tab.
+	rotationInputs: MageInputs.MageRotationConfig,
+	// Buff and Debuff inputs to include/exclude, overriding the EP-based defaults.
+	includeBuffDebuffInputs: [
+		//Should add hymn of hope, revitalize, and 
+	],
+	excludeBuffDebuffInputs: [
+	],
+	// Inputs to include in the 'Other' section on the settings tab.
+	otherInputs: {
+		inputs: [
+			MageInputs.FocusMagicUptime,
+			MageInputs.WaterElementalDisobeyChance,
+			OtherInputs.ReactionTime,
+			OtherInputs.DistanceFromTarget,
+			OtherInputs.TankAssignment,
+			OtherInputs.nibelungAverageCasts,
+		],
+	},
+	encounterPicker: {
+		// Whether to include 'Execute Duration (%)' in the 'Encounter' section of the settings tab.
+		showExecuteProportion: true,
+	},
 
-			presets: {
-				// Preset rotations that the user can quickly select.
-				rotations: [
-					Presets.ROTATION_PRESET_SIMPLE,
-					Presets.ARCANE_ROTATION_PRESET_DEFAULT,
-					Presets.FIRE_ROTATION_PRESET_DEFAULT,
-					Presets.FROSTFIRE_ROTATION_PRESET_DEFAULT,
-					Presets.FROST_ROTATION_PRESET_DEFAULT,
-					Presets.ARCANE_ROTATION_PRESET_AOE,
-					Presets.FIRE_ROTATION_PRESET_AOE,
-					Presets.FROST_ROTATION_PRESET_AOE,
-				],
-				// Preset talents that the user can quickly select.
-				talents: [
-					Presets.ArcaneTalents,
-					Presets.FireTalents,
-					Presets.FrostfireTalents,
-					Presets.FrostTalents,
-					Presets.Phase3FireTalents,
-				],
-				// Preset gear configurations that the user can quickly select.
-				gear: [
-					Presets.ARCANE_PRERAID_PRESET,
-					Presets.FIRE_PRERAID_PRESET,
-					Presets.ARCANE_P1_PRESET,
-					Presets.FIRE_P1_PRESET,
-					Presets.FROST_P1_PRESET,
-					Presets.ARCANE_P2_PRESET,
-					Presets.FIRE_P2_PRESET,
-					Presets.FROST_P2_PRESET,
-					Presets.FFB_P2_PRESET,
-					Presets.ARCANE_P3_PRESET_ALLIANCE,
-					Presets.ARCANE_P3_PRESET_HORDE,
-					Presets.FROST_P3_PRESET_ALLIANCE,
-					Presets.FROST_P3_PRESET_HORDE,
-					Presets.FIRE_P3_PRESET_ALLIANCE,
-					Presets.FIRE_P3_PRESET_HORDE,
-					Presets.FFB_P3_PRESET_ALLIANCE,
-					Presets.FFB_P3_PRESET_HORDE,
-					Presets.FIRE_P4_PRESET_HORDE,
-					Presets.FIRE_P4_PRESET_ALLIANCE,
-					Presets.FFB_P4_PRESET_HORDE,
-					Presets.FFB_P4_PRESET_ALLIANCE,
-					Presets.ARCANE_P4_PRESET_HORDE,
-					Presets.ARCANE_P4_PRESET_ALLIANCE,
-				],
-			},
+	presets: {
+		// Preset rotations that the user can quickly select.
+		rotations: [
+			Presets.ROTATION_PRESET_SIMPLE,
+			Presets.ARCANE_ROTATION_PRESET_DEFAULT,
+			Presets.FIRE_ROTATION_PRESET_DEFAULT,
+			Presets.FROSTFIRE_ROTATION_PRESET_DEFAULT,
+			Presets.FROST_ROTATION_PRESET_DEFAULT,
+			Presets.ARCANE_ROTATION_PRESET_AOE,
+			Presets.FIRE_ROTATION_PRESET_AOE,
+			Presets.FROST_ROTATION_PRESET_AOE,
+		],
+		// Preset talents that the user can quickly select.
+		talents: [
+			Presets.ArcaneTalents,
+			Presets.FireTalents,
+			Presets.FrostfireTalents,
+			Presets.FrostTalents,
+			Presets.Phase3FireTalents,
+		],
+		// Preset gear configurations that the user can quickly select.
+		gear: [
+			Presets.ARCANE_PRERAID_PRESET,
+			Presets.FIRE_PRERAID_PRESET,
+			Presets.ARCANE_P1_PRESET,
+			Presets.FIRE_P1_PRESET,
+			Presets.FROST_P1_PRESET,
+			Presets.ARCANE_P2_PRESET,
+			Presets.FIRE_P2_PRESET,
+			Presets.FROST_P2_PRESET,
+			Presets.FFB_P2_PRESET,
+			Presets.ARCANE_P3_PRESET_ALLIANCE,
+			Presets.ARCANE_P3_PRESET_HORDE,
+			Presets.FROST_P3_PRESET_ALLIANCE,
+			Presets.FROST_P3_PRESET_HORDE,
+			Presets.FIRE_P3_PRESET_ALLIANCE,
+			Presets.FIRE_P3_PRESET_HORDE,
+			Presets.FFB_P3_PRESET_ALLIANCE,
+			Presets.FFB_P3_PRESET_HORDE,
+			Presets.FIRE_P4_PRESET_HORDE,
+			Presets.FIRE_P4_PRESET_ALLIANCE,
+			Presets.FFB_P4_PRESET_HORDE,
+			Presets.FFB_P4_PRESET_ALLIANCE,
+			Presets.ARCANE_P4_PRESET_HORDE,
+			Presets.ARCANE_P4_PRESET_ALLIANCE,
+		],
+	},
 
-			autoRotation: (player: Player<Spec.SpecMage>): APLRotation => {
-				const talentTree = player.getTalentTree();
-				const numTargets = player.sim.encounter.targets.length;
-				if (numTargets > 3) {
-					if (talentTree == 0) {
-						return Presets.ARCANE_ROTATION_PRESET_AOE.rotation.rotation!;
-					} else if (talentTree == 1) {
-						return Presets.FIRE_ROTATION_PRESET_AOE.rotation.rotation!;
-					} else {
-						return Presets.FROST_ROTATION_PRESET_AOE.rotation.rotation!;
-					}
-				} else if (talentTree == 0) {
-					return Presets.ARCANE_ROTATION_PRESET_DEFAULT.rotation.rotation!;
-				} else if (talentTree == 1) {
-					if (player.getTalents().iceShards > 0) {
-						return Presets.FROSTFIRE_ROTATION_PRESET_DEFAULT.rotation.rotation!;
-					}
-					return Presets.FIRE_ROTATION_PRESET_DEFAULT.rotation.rotation!;
-				} else {
-					return Presets.FROST_ROTATION_PRESET_DEFAULT.rotation.rotation!;
-				}
-			},
+	autoRotation: (player: Player<Spec.SpecMage>): APLRotation => {
+		const talentTree = player.getTalentTree();
+		const numTargets = player.sim.encounter.targets.length;
+		if (numTargets > 3) {
+			if (talentTree == 0) {
+				return Presets.ARCANE_ROTATION_PRESET_AOE.rotation.rotation!;
+			} else if (talentTree == 1) {
+				return Presets.FIRE_ROTATION_PRESET_AOE.rotation.rotation!;
+			} else {
+				return Presets.FROST_ROTATION_PRESET_AOE.rotation.rotation!;
+			}
+		} else if (talentTree == 0) {
+			return Presets.ARCANE_ROTATION_PRESET_DEFAULT.rotation.rotation!;
+		} else if (talentTree == 1) {
+			if (player.getTalents().iceShards > 0) {
+				return Presets.FROSTFIRE_ROTATION_PRESET_DEFAULT.rotation.rotation!;
+			}
+			return Presets.FIRE_ROTATION_PRESET_DEFAULT.rotation.rotation!;
+		} else {
+			return Presets.FROST_ROTATION_PRESET_DEFAULT.rotation.rotation!;
+		}
+	},
 
-			simpleRotation: (player: Player<Spec.SpecMage>, simple: MageRotation, cooldowns: Cooldowns): APLRotation => {
-				let [prepullActions, actions] = AplUtils.standardCooldownDefaults(cooldowns);
+	simpleRotation: (player: Player<Spec.SpecMage>, simple: MageRotation, cooldowns: Cooldowns): APLRotation => {
+		let [prepullActions, actions] = AplUtils.standardCooldownDefaults(cooldowns);
 
-				const prepullMirrorImage = APLPrepullAction.fromJsonString(`{"action":{"castSpell":{"spellId":{"spellId":55342}}},"doAtValue":{"const":{"val":"-2s"}}}`);
+		const prepullMirrorImage = APLPrepullAction.fromJsonString(`{"action":{"castSpell":{"spellId":{"spellId":55342}}},"doAtValue":{"const":{"val":"-2s"}}}`);
 
-				const berserking = APLAction.fromJsonString(`{"condition":{"not":{"val":{"auraIsActive":{"auraId":{"spellId":12472}}}}},"castSpell":{"spellId":{"spellId":26297}}}`);
-				const hyperspeedAcceleration = APLAction.fromJsonString(`{"condition":{"not":{"val":{"auraIsActive":{"auraId":{"spellId":12472}}}}},"castSpell":{"spellId":{"spellId":54758}}}`);
-				const combatPot = APLAction.fromJsonString(`{"condition":{"not":{"val":{"auraIsActive":{"auraId":{"spellId":12472}}}}},"castSpell":{"spellId":{"otherId":"OtherActionPotion"}}}`);
-				const evocation = APLAction.fromJsonString(`{"condition":{"cmp":{"op":"OpLe","lhs":{"currentManaPercent":{}},"rhs":{"const":{"val":"25%"}}}},"castSpell":{"spellId":{"spellId":12051}}}`);
+		const berserking = APLAction.fromJsonString(`{"condition":{"not":{"val":{"auraIsActive":{"auraId":{"spellId":12472}}}}},"castSpell":{"spellId":{"spellId":26297}}}`);
+		const hyperspeedAcceleration = APLAction.fromJsonString(`{"condition":{"not":{"val":{"auraIsActive":{"auraId":{"spellId":12472}}}}},"castSpell":{"spellId":{"spellId":54758}}}`);
+		const combatPot = APLAction.fromJsonString(`{"condition":{"not":{"val":{"auraIsActive":{"auraId":{"spellId":12472}}}}},"castSpell":{"spellId":{"otherId":"OtherActionPotion"}}}`);
+		const evocation = APLAction.fromJsonString(`{"condition":{"cmp":{"op":"OpLe","lhs":{"currentManaPercent":{}},"rhs":{"const":{"val":"25%"}}}},"castSpell":{"spellId":{"spellId":12051}}}`);
 
-				const arcaneBlastBelowStacks = APLAction.fromJsonString(`{"condition":{"or":{"vals":[{"cmp":{"op":"OpLt","lhs":{"auraNumStacks":{"auraId":{"spellId":36032}}},"rhs":{"const":{"val":"4"}}}},{"and":{"vals":[{"cmp":{"op":"OpLt","lhs":{"auraNumStacks":{"auraId":{"spellId":36032}}},"rhs":{"const":{"val":"3"}}}},{"cmp":{"op":"OpLt","lhs":{"currentManaPercent":{}},"rhs":{"const":{"val":"${(simple.only3ArcaneBlastStacksBelowManaPercent * 100).toFixed(0)}%"}}}}]}}]}},"castSpell":{"spellId":{"spellId":42897}}}`);
-				const arcaneMissilesWithMissileBarrageBelowMana = APLAction.fromJsonString(`{"condition":{"and":{"vals":[{"auraIsActiveWithReactionTime":{"auraId":{"spellId":44401}}},{"cmp":{"op":"OpLt","lhs":{"currentManaPercent":{}},"rhs":{"const":{"val":"${(simple.missileBarrageBelowManaPercent * 100).toFixed(0)}%"}}}}]}},"castSpell":{"spellId":{"spellId":42846}}}`);
-				const arcaneMisslesWithMissileBarrage = APLAction.fromJsonString(`{"condition":{"auraIsActiveWithReactionTime":{"auraId":{"spellId":44401}}},"castSpell":{"spellId":{"spellId":42846}}}`);
-				const arcaneBlastAboveMana = APLAction.fromJsonString(`{"condition":{"cmp":{"op":"OpGt","lhs":{"currentManaPercent":{}},"rhs":{"const":{"val":"${(simple.blastWithoutMissileBarrageAboveManaPercent * 100).toFixed(0)}%"}}}},"castSpell":{"spellId":{"spellId":42897}}}`);
-				const arcaneMissiles = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":42846}}}`);
-				const arcaneBarrage = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":44781}}}`);
+		const arcaneBlastBelowStacks = APLAction.fromJsonString(`{"condition":{"or":{"vals":[{"cmp":{"op":"OpLt","lhs":{"auraNumStacks":{"auraId":{"spellId":36032}}},"rhs":{"const":{"val":"4"}}}},{"and":{"vals":[{"cmp":{"op":"OpLt","lhs":{"auraNumStacks":{"auraId":{"spellId":36032}}},"rhs":{"const":{"val":"3"}}}},{"cmp":{"op":"OpLt","lhs":{"currentManaPercent":{}},"rhs":{"const":{"val":"${(simple.only3ArcaneBlastStacksBelowManaPercent * 100).toFixed(0)}%"}}}}]}}]}},"castSpell":{"spellId":{"spellId":42897}}}`);
+		const arcaneMissilesWithMissileBarrageBelowMana = APLAction.fromJsonString(`{"condition":{"and":{"vals":[{"auraIsActiveWithReactionTime":{"auraId":{"spellId":44401}}},{"cmp":{"op":"OpLt","lhs":{"currentManaPercent":{}},"rhs":{"const":{"val":"${(simple.missileBarrageBelowManaPercent * 100).toFixed(0)}%"}}}}]}},"castSpell":{"spellId":{"spellId":42846}}}`);
+		const arcaneMisslesWithMissileBarrage = APLAction.fromJsonString(`{"condition":{"auraIsActiveWithReactionTime":{"auraId":{"spellId":44401}}},"castSpell":{"spellId":{"spellId":42846}}}`);
+		const arcaneBlastAboveMana = APLAction.fromJsonString(`{"condition":{"cmp":{"op":"OpGt","lhs":{"currentManaPercent":{}},"rhs":{"const":{"val":"${(simple.blastWithoutMissileBarrageAboveManaPercent * 100).toFixed(0)}%"}}}},"castSpell":{"spellId":{"spellId":42897}}}`);
+		const arcaneMissiles = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":42846}}}`);
+		const arcaneBarrage = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":44781}}}`);
 
-				const maintainImpScorch = APLAction.fromJsonString(`{"condition":{"auraShouldRefresh":{"auraId":{"spellId":12873},"maxOverlap":{"const":{"val":"4s"}}}},"castSpell":{"spellId":{"spellId":42859}}}`);
-				const pyroWithHotStreak = APLAction.fromJsonString(`{"condition":{"auraIsActiveWithReactionTime":{"auraId":{"spellId":44448}}},"castSpell":{"spellId":{"spellId":42891}}}`);
-				const livingBomb = APLAction.fromJsonString(`{"condition":{"and":{"vals":[{"cmp":{"op":"OpGt","lhs":{"remainingTime":{}},"rhs":{"const":{"val":"12s"}}}}]}},"multidot":{"spellId":{"spellId":55360},"maxDots":10,"maxOverlap":{"const":{"val":"0ms"}}}}`);
-				const cheekyFireBlastFinisher = APLAction.fromJsonString(`{"condition":{"cmp":{"op":"OpLe","lhs":{"remainingTime":{}},"rhs":{"spellCastTime":{"spellId":{"spellId":42859}}}}},"castSpell":{"spellId":{"spellId":42873}}}`);
-				const scorchFinisher = APLAction.fromJsonString(`{"condition":{"cmp":{"op":"OpLe","lhs":{"remainingTime":{}},"rhs":{"const":{"val":"4s"}}}},"castSpell":{"spellId":{"spellId":42859}}}`);
-				const fireball = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":42833}}}`);
-				const frostfireBolt = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":47610}}}`);
-				const scorch = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":42859}}}`);
+		const maintainImpScorch = APLAction.fromJsonString(`{"condition":{"auraShouldRefresh":{"auraId":{"spellId":12873},"maxOverlap":{"const":{"val":"4s"}}}},"castSpell":{"spellId":{"spellId":42859}}}`);
+		const pyroWithHotStreak = APLAction.fromJsonString(`{"condition":{"auraIsActiveWithReactionTime":{"auraId":{"spellId":44448}}},"castSpell":{"spellId":{"spellId":42891}}}`);
+		const livingBomb = APLAction.fromJsonString(`{"condition":{"and":{"vals":[{"cmp":{"op":"OpGt","lhs":{"remainingTime":{}},"rhs":{"const":{"val":"12s"}}}}]}},"multidot":{"spellId":{"spellId":55360},"maxDots":10,"maxOverlap":{"const":{"val":"0ms"}}}}`);
+		const cheekyFireBlastFinisher = APLAction.fromJsonString(`{"condition":{"cmp":{"op":"OpLe","lhs":{"remainingTime":{}},"rhs":{"spellCastTime":{"spellId":{"spellId":42859}}}}},"castSpell":{"spellId":{"spellId":42873}}}`);
+		const scorchFinisher = APLAction.fromJsonString(`{"condition":{"cmp":{"op":"OpLe","lhs":{"remainingTime":{}},"rhs":{"const":{"val":"4s"}}}},"castSpell":{"spellId":{"spellId":42859}}}`);
+		const fireball = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":42833}}}`);
+		const frostfireBolt = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":47610}}}`);
+		const scorch = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":42859}}}`);
 
-				const deepFreeze = APLAction.fromJsonString(`{"condition":{"auraIsActive":{"auraId":{"spellId":44545}}},"castSpell":{"spellId":{"spellId":44572}}}`);
-				const frostfireBoltWithBrainFreeze = APLAction.fromJsonString(`{"condition":{"auraIsActiveWithReactionTime":{"auraId":{"spellId":44549}}},"castSpell":{"spellId":{"spellId":47610}}}`);
-				const frostbolt = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":42842}}}`);
-				const iceLance = APLAction.fromJsonString(`{"condition":{"cmp":{"op":"OpEq","lhs":{"auraNumStacks":{"auraId":{"spellId":44545}}},"rhs":{"const":{"val":"1"}}}},"castSpell":{"spellId":{"spellId":42914}}}`);
+		const deepFreeze = APLAction.fromJsonString(`{"condition":{"auraIsActive":{"auraId":{"spellId":44545}}},"castSpell":{"spellId":{"spellId":44572}}}`);
+		const frostfireBoltWithBrainFreeze = APLAction.fromJsonString(`{"condition":{"auraIsActiveWithReactionTime":{"auraId":{"spellId":44549}}},"castSpell":{"spellId":{"spellId":47610}}}`);
+		const frostbolt = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":42842}}}`);
+		const iceLance = APLAction.fromJsonString(`{"condition":{"cmp":{"op":"OpEq","lhs":{"auraNumStacks":{"auraId":{"spellId":44545}}},"rhs":{"const":{"val":"1"}}}},"castSpell":{"spellId":{"spellId":42914}}}`);
 
-				prepullActions.push(prepullMirrorImage);
-				if (player.getTalents().improvedScorch > 0 && simple.maintainImprovedScorch) {
-					actions.push(maintainImpScorch);
-				}
+		prepullActions.push(prepullMirrorImage);
+		if (player.getTalents().improvedScorch > 0 && simple.maintainImprovedScorch) {
+			actions.push(maintainImpScorch);
+		}
 
-				const talentTree = player.getTalentTree();
-				if (talentTree == 0) { // Arcane
-					actions.push(...[
-						berserking,
-						hyperspeedAcceleration,
-						combatPot,
-						simple.missileBarrageBelowManaPercent > 0 ? arcaneMissilesWithMissileBarrageBelowMana : null,
-						arcaneBlastBelowStacks,
-						arcaneMisslesWithMissileBarrage,
-						evocation,
-						arcaneBlastAboveMana,
-						simple.useArcaneBarrage ? arcaneBarrage : null,
-						arcaneMissiles,
-					].filter(a => a) as Array<APLAction>)
-				} else if (talentTree == 1) { // Fire
-					actions.push(...[
-						pyroWithHotStreak,
-						livingBomb,
-						cheekyFireBlastFinisher,
-						scorchFinisher,
-						simple.primaryFireSpell == PrimaryFireSpell.Fireball
-							? fireball
-							: (simple.primaryFireSpell == PrimaryFireSpell.FrostfireBolt
-								? frostfireBolt : scorch),
-					].filter(a => a) as Array<APLAction>)
-				} else if (talentTree == 2) { // Frost
-					actions.push(...[
-						berserking,
-						hyperspeedAcceleration,
-						evocation,
-						deepFreeze,
-						frostfireBoltWithBrainFreeze,
-						simple.useIceLance ? iceLance : null,
-						frostbolt,
-					].filter(a => a) as Array<APLAction>)
-				}
+		const talentTree = player.getTalentTree();
+		if (talentTree == 0) { // Arcane
+			actions.push(...[
+				berserking,
+				hyperspeedAcceleration,
+				combatPot,
+				simple.missileBarrageBelowManaPercent > 0 ? arcaneMissilesWithMissileBarrageBelowMana : null,
+				arcaneBlastBelowStacks,
+				arcaneMisslesWithMissileBarrage,
+				evocation,
+				arcaneBlastAboveMana,
+				simple.useArcaneBarrage ? arcaneBarrage : null,
+				arcaneMissiles,
+			].filter(a => a) as Array<APLAction>)
+		} else if (talentTree == 1) { // Fire
+			actions.push(...[
+				pyroWithHotStreak,
+				livingBomb,
+				cheekyFireBlastFinisher,
+				scorchFinisher,
+				simple.primaryFireSpell == PrimaryFireSpell.Fireball
+					? fireball
+					: (simple.primaryFireSpell == PrimaryFireSpell.FrostfireBolt
+						? frostfireBolt : scorch),
+			].filter(a => a) as Array<APLAction>)
+		} else if (talentTree == 2) { // Frost
+			actions.push(...[
+				berserking,
+				hyperspeedAcceleration,
+				evocation,
+				deepFreeze,
+				frostfireBoltWithBrainFreeze,
+				simple.useIceLance ? iceLance : null,
+				frostbolt,
+			].filter(a => a) as Array<APLAction>)
+		}
 
-				return APLRotation.create({
-					prepullActions: prepullActions,
-					priorityList: actions.map(action => APLListItem.create({
-						action: action,
-					}))
-				});
-			},
+		return APLRotation.create({
+			prepullActions: prepullActions,
+			priorityList: actions.map(action => APLListItem.create({
+				action: action,
+			}))
 		});
+	},
+});
+
+export class MageSimUI extends IndividualSimUI<Spec.SpecMage> {
+	constructor(parentElem: HTMLElement, player: Player<Spec.SpecMage>) {
+		super(parentElem, player, SPEC_CONFIG);
 	}
 }
diff --git a/ui/protection_paladin/sim.ts b/ui/protection_paladin/sim.ts
index 091b7c2231..02aef11e11 100644
--- a/ui/protection_paladin/sim.ts
+++ b/ui/protection_paladin/sim.ts
@@ -6,14 +6,12 @@ import { Spec } from '../core/proto/common.js';
 import { Stat, PseudoStat } from '../core/proto/common.js';
 import { TristateEffect } from '../core/proto/common.js'
 import {
-	APLAction,
-	APLListItem,
 	APLRotation,
 } from '../core/proto/apl.js';
 import { Stats } from '../core/proto_utils/stats.js';
 import { Player } from '../core/player.js';
-import { IndividualSimUI } from '../core/individual_sim_ui.js';
-import { EventID, TypedEvent } from '../core/typed_event.js';
+import { IndividualSimUI, registerSpecConfig } from '../core/individual_sim_ui.js';
+import { TypedEvent } from '../core/typed_event.js';
 
 import * as IconInputs from '../core/components/icon_inputs.js';
 import * as OtherInputs from '../core/components/other_inputs.js';
@@ -24,219 +22,221 @@ import { PaladinMajorGlyph, PaladinSeal } from '../core/proto/paladin.js';
 import * as ProtectionPaladinInputs from './inputs.js';
 import * as Presets from './presets.js';
 
-export class ProtectionPaladinSimUI extends IndividualSimUI<Spec.SpecProtectionPaladin> {
-	constructor(parentElem: HTMLElement, player: Player<Spec.SpecProtectionPaladin>) {
-		super(parentElem, player, {
-			cssClass: 'protection-paladin-sim-ui',
-			cssScheme: 'paladin',
-			// List any known bugs / issues here and they'll be shown on the site.
-			knownIssues: [
-			],
+const SPEC_CONFIG = registerSpecConfig(Spec.SpecProtectionPaladin, {
+	cssClass: 'protection-paladin-sim-ui',
+	cssScheme: 'paladin',
+	// List any known bugs / issues here and they'll be shown on the site.
+	knownIssues: [
+	],
+
+	// All stats for which EP should be calculated.
+	epStats: [
+		Stat.StatStamina,
+		Stat.StatStrength,
+		Stat.StatAgility,
+		Stat.StatAttackPower,
+		Stat.StatMeleeHit,
+		Stat.StatSpellHit,
+		Stat.StatMeleeCrit,
+		Stat.StatExpertise,
+		Stat.StatMeleeHaste,
+		Stat.StatArmorPenetration,
+		Stat.StatSpellPower,
+		Stat.StatArmor,
+		Stat.StatBonusArmor,
+		Stat.StatDefense,
+		Stat.StatBlock,
+		Stat.StatBlockValue,
+		Stat.StatDodge,
+		Stat.StatParry,
+		Stat.StatResilience,
+		Stat.StatNatureResistance,
+		Stat.StatShadowResistance,
+		Stat.StatFrostResistance,
+	],
+	epPseudoStats: [
+		PseudoStat.PseudoStatMainHandDps,
+	],
+	// Reference stat against which to calculate EP. I think all classes use either spell power or attack power.
+	epReferenceStat: Stat.StatSpellPower,
+	// Which stats to display in the Character Stats section, at the bottom of the left-hand sidebar.
+	displayStats: [
+		Stat.StatHealth,
+		Stat.StatArmor,
+		Stat.StatBonusArmor,
+		Stat.StatStamina,
+		Stat.StatStrength,
+		Stat.StatAgility,
+		Stat.StatAttackPower,
+		Stat.StatMeleeHit,
+		Stat.StatMeleeCrit,
+		Stat.StatMeleeHaste,
+		Stat.StatExpertise,
+		Stat.StatArmorPenetration,
+		Stat.StatSpellPower,
+		Stat.StatSpellHit,
+		Stat.StatDefense,
+		Stat.StatBlock,
+		Stat.StatBlockValue,
+		Stat.StatDodge,
+		Stat.StatParry,
+		Stat.StatResilience,
+		Stat.StatNatureResistance,
+		Stat.StatShadowResistance,
+		Stat.StatFrostResistance,
+	],
+	modifyDisplayStats: (player: Player<Spec.SpecProtectionPaladin>) => {
+		let stats = new Stats();
 
-			// All stats for which EP should be calculated.
-			epStats: [
-				Stat.StatStamina,
-				Stat.StatStrength,
-				Stat.StatAgility,
-				Stat.StatAttackPower,
-				Stat.StatMeleeHit,
-				Stat.StatSpellHit,
-				Stat.StatMeleeCrit,
-				Stat.StatExpertise,
-				Stat.StatMeleeHaste,
-				Stat.StatArmorPenetration,
-				Stat.StatSpellPower,
-				Stat.StatArmor,
-				Stat.StatBonusArmor,
-				Stat.StatDefense,
-				Stat.StatBlock,
-				Stat.StatBlockValue,
-				Stat.StatDodge,
-				Stat.StatParry,
-				Stat.StatResilience,
-				Stat.StatNatureResistance,
-				Stat.StatShadowResistance,
-				Stat.StatFrostResistance,
-			],
-			epPseudoStats: [
-				PseudoStat.PseudoStatMainHandDps,
-			],
-			// Reference stat against which to calculate EP. I think all classes use either spell power or attack power.
-			epReferenceStat: Stat.StatSpellPower,
-			// Which stats to display in the Character Stats section, at the bottom of the left-hand sidebar.
-			displayStats: [
-				Stat.StatHealth,
-				Stat.StatArmor,
-				Stat.StatBonusArmor,
-				Stat.StatStamina,
-				Stat.StatStrength,
-				Stat.StatAgility,
-				Stat.StatAttackPower,
-				Stat.StatMeleeHit,
-				Stat.StatMeleeCrit,
-				Stat.StatMeleeHaste,
-				Stat.StatExpertise,
-				Stat.StatArmorPenetration,
-				Stat.StatSpellPower,
-				Stat.StatSpellHit,
-				Stat.StatDefense,
-				Stat.StatBlock,
-				Stat.StatBlockValue,
-				Stat.StatDodge,
-				Stat.StatParry,
-				Stat.StatResilience,
-				Stat.StatNatureResistance,
-				Stat.StatShadowResistance,
-				Stat.StatFrostResistance,
-			],
-			modifyDisplayStats: (player: Player<Spec.SpecProtectionPaladin>) => {
-				let stats = new Stats();
+		TypedEvent.freezeAllAndDo(() => {
+			if (player.getMajorGlyphs().includes(PaladinMajorGlyph.GlyphOfSealOfVengeance) && (player.getSpecOptions().seal == PaladinSeal.Vengeance)) {
+				stats = stats.addStat(Stat.StatExpertise, 10 * Mechanics.EXPERTISE_PER_QUARTER_PERCENT_REDUCTION);
+			}
+		})
 
-				TypedEvent.freezeAllAndDo(() => {
-					if (player.getMajorGlyphs().includes(PaladinMajorGlyph.GlyphOfSealOfVengeance) && (player.getSpecOptions().seal == PaladinSeal.Vengeance)) {
-						stats = stats.addStat(Stat.StatExpertise, 10 * Mechanics.EXPERTISE_PER_QUARTER_PERCENT_REDUCTION);
-					}
-				})
+		return {
+			talents: stats,
+		};
+	},
+	defaults: {
+		// Default equipped gear.
+		gear: Presets.P3_PRESET.gear,
+		// Default EP weights for sorting gear in the gear picker.
+		epWeights: Stats.fromMap({
+			[Stat.StatArmor]: 0.07,
+			[Stat.StatBonusArmor]: 0.06,
+			[Stat.StatStamina]: 1.14,
+			[Stat.StatStrength]: 1.00,
+			[Stat.StatAgility]: 0.62,
+			[Stat.StatAttackPower]: 0.26,
+			[Stat.StatExpertise]: 0.69,
+			[Stat.StatMeleeHit]: 0.79,
+			[Stat.StatMeleeCrit]: 0.30,
+			[Stat.StatMeleeHaste]: 0.17,
+			[Stat.StatArmorPenetration]: 0.04,
+			[Stat.StatSpellPower]: 0.13,
+			[Stat.StatBlock]: 0.52,
+			[Stat.StatBlockValue]: 0.28,
+			[Stat.StatDodge]: 0.46,
+			[Stat.StatParry]: 0.61,
+			[Stat.StatDefense]: 0.54,
+		}, {
+			[PseudoStat.PseudoStatMainHandDps]: 3.33,
+		}),
+		// Default consumes settings.
+		consumes: Presets.DefaultConsumes,
+		// Default rotation settings.
+		rotation: Presets.DefaultRotation,
+		// Default talents.
+		talents: Presets.GenericAoeTalents.data,
+		// Default spec-specific settings.
+		specOptions: Presets.DefaultOptions,
+		// Default raid/party buffs settings.
+		raidBuffs: RaidBuffs.create({
+			giftOfTheWild: TristateEffect.TristateEffectImproved,
+			powerWordFortitude: TristateEffect.TristateEffectImproved,
+			strengthOfEarthTotem: TristateEffect.TristateEffectImproved,
+			arcaneBrilliance: true,
+			unleashedRage: true,
+			leaderOfThePack: TristateEffect.TristateEffectRegular,
+			icyTalons: true,
+			totemOfWrath: true,
+			demonicPact: 500,
+			swiftRetribution: true,
+			moonkinAura: TristateEffect.TristateEffectRegular,
+			sanctifiedRetribution: true,
+			manaSpringTotem: TristateEffect.TristateEffectRegular,
+			bloodlust: true,
+			thorns: TristateEffect.TristateEffectImproved,
+			devotionAura: TristateEffect.TristateEffectImproved,
+			shadowProtection: true,
+		}),
+		partyBuffs: PartyBuffs.create({
+		}),
+		individualBuffs: IndividualBuffs.create({
+			blessingOfKings: true,
+			blessingOfSanctuary: true,
+			blessingOfWisdom: TristateEffect.TristateEffectImproved,
+			blessingOfMight: TristateEffect.TristateEffectImproved,
+		}),
+		debuffs: Debuffs.create({
+			judgementOfWisdom: true,
+			judgementOfLight: true,
+			misery: true,
+			faerieFire: TristateEffect.TristateEffectImproved,
+			ebonPlaguebringer: true,
+			totemOfWrath: true,
+			shadowMastery: true,
+			bloodFrenzy: true,
+			mangle: true,
+			exposeArmor: true,
+			sunderArmor: true,
+			vindication: true,
+			thunderClap: TristateEffect.TristateEffectImproved,
+			insectSwarm: true,
+		}),
+	},
 
-				return {
-					talents: stats,
-				};
-			},
-			defaults: {
-				// Default equipped gear.
-				gear: Presets.P3_PRESET.gear,
-				// Default EP weights for sorting gear in the gear picker.
-				epWeights: Stats.fromMap({
-					[Stat.StatArmor]: 0.07,
-					[Stat.StatBonusArmor]: 0.06,
-					[Stat.StatStamina]: 1.14,
-					[Stat.StatStrength]: 1.00,
-					[Stat.StatAgility]: 0.62,
-					[Stat.StatAttackPower]: 0.26,
-					[Stat.StatExpertise]: 0.69,
-					[Stat.StatMeleeHit]: 0.79,
-					[Stat.StatMeleeCrit]: 0.30,
-					[Stat.StatMeleeHaste]: 0.17,
-					[Stat.StatArmorPenetration]: 0.04,
-					[Stat.StatSpellPower]: 0.13,
-					[Stat.StatBlock]: 0.52,
-					[Stat.StatBlockValue]: 0.28,
-					[Stat.StatDodge]: 0.46,
-					[Stat.StatParry]: 0.61,
-					[Stat.StatDefense]: 0.54,
-				}, {
-					[PseudoStat.PseudoStatMainHandDps]: 3.33,
-				}),
-				// Default consumes settings.
-				consumes: Presets.DefaultConsumes,
-				// Default rotation settings.
-				rotation: Presets.DefaultRotation,
-				// Default talents.
-				talents: Presets.GenericAoeTalents.data,
-				// Default spec-specific settings.
-				specOptions: Presets.DefaultOptions,
-				// Default raid/party buffs settings.
-				raidBuffs: RaidBuffs.create({
-					giftOfTheWild: TristateEffect.TristateEffectImproved,
-					powerWordFortitude: TristateEffect.TristateEffectImproved,
-					strengthOfEarthTotem: TristateEffect.TristateEffectImproved,
-					arcaneBrilliance: true,
-					unleashedRage: true,
-					leaderOfThePack: TristateEffect.TristateEffectRegular,
-					icyTalons: true,
-					totemOfWrath: true,
-					demonicPact: 500,
-					swiftRetribution: true,
-					moonkinAura: TristateEffect.TristateEffectRegular,
-					sanctifiedRetribution: true,
-					manaSpringTotem: TristateEffect.TristateEffectRegular,
-					bloodlust: true,
-					thorns: TristateEffect.TristateEffectImproved,
-					devotionAura: TristateEffect.TristateEffectImproved,
-					shadowProtection: true,
-				}),
-				partyBuffs: PartyBuffs.create({
-				}),
-				individualBuffs: IndividualBuffs.create({
-					blessingOfKings: true,
-					blessingOfSanctuary: true,
-					blessingOfWisdom: TristateEffect.TristateEffectImproved,
-					blessingOfMight: TristateEffect.TristateEffectImproved,
-				}),
-				debuffs: Debuffs.create({
-					judgementOfWisdom: true,
-					judgementOfLight: true,
-					misery: true,
-					faerieFire: TristateEffect.TristateEffectImproved,
-					ebonPlaguebringer: true,
-					totemOfWrath: true,
-					shadowMastery: true,
-					bloodFrenzy: true,
-					mangle: true,
-					exposeArmor: true,
-					sunderArmor: true,
-					vindication: true,
-					thunderClap: TristateEffect.TristateEffectImproved,
-					insectSwarm: true,
-				}),
-			},
+	// IconInputs to include in the 'Player' section on the settings tab.
+	playerIconInputs: [
+	],
+	// Inputs to include in the 'Rotation' section on the settings tab.
+	rotationInputs: ProtectionPaladinInputs.ProtectionPaladinRotationConfig,
+	// Buff and Debuff inputs to include/exclude, overriding the EP-based defaults.
+	includeBuffDebuffInputs: [
+		IconInputs.HealthBuff,
+	],
+	excludeBuffDebuffInputs: [
+	],
+	// Inputs to include in the 'Other' section on the settings tab.
+	otherInputs: {
+		inputs: [
+			OtherInputs.TankAssignment,
+			OtherInputs.IncomingHps,
+			OtherInputs.HealingCadence,
+			OtherInputs.HealingCadenceVariation,
+			OtherInputs.BurstWindow,
+			OtherInputs.HpPercentForDefensives,
+			OtherInputs.InspirationUptime,
+			ProtectionPaladinInputs.AuraSelection,
+			ProtectionPaladinInputs.UseAvengingWrath,
+			ProtectionPaladinInputs.JudgementSelection,
+			ProtectionPaladinInputs.StartingSealSelection,
+			OtherInputs.InFrontOfTarget,
+		],
+	},
+	encounterPicker: {
+		// Whether to include 'Execute Duration (%)' in the 'Encounter' section of the settings tab.
+		showExecuteProportion: false,
+	},
 
-			// IconInputs to include in the 'Player' section on the settings tab.
-			playerIconInputs: [
-			],
-			// Inputs to include in the 'Rotation' section on the settings tab.
-			rotationInputs: ProtectionPaladinInputs.ProtectionPaladinRotationConfig,
-			// Buff and Debuff inputs to include/exclude, overriding the EP-based defaults.
-			includeBuffDebuffInputs: [
-				IconInputs.HealthBuff,
-			],
-			excludeBuffDebuffInputs: [
-			],
-			// Inputs to include in the 'Other' section on the settings tab.
-			otherInputs: {
-				inputs: [
-					OtherInputs.TankAssignment,
-					OtherInputs.IncomingHps,
-					OtherInputs.HealingCadence,
-					OtherInputs.HealingCadenceVariation,
-					OtherInputs.BurstWindow,
-					OtherInputs.HpPercentForDefensives,
-					OtherInputs.InspirationUptime,
-					ProtectionPaladinInputs.AuraSelection,
-					ProtectionPaladinInputs.UseAvengingWrath,
-					ProtectionPaladinInputs.JudgementSelection,
-					ProtectionPaladinInputs.StartingSealSelection,
-					OtherInputs.InFrontOfTarget,
-				],
-			},
-			encounterPicker: {
-				// Whether to include 'Execute Duration (%)' in the 'Encounter' section of the settings tab.
-				showExecuteProportion: false,
-			},
+	presets: {
+		// Preset talents that the user can quickly select.
+		talents: [
+			Presets.GenericAoeTalents,
+		],
+		// Preset rotations that the user can quickly select.
+		rotations: [
+			Presets.ROTATION_DEFAULT,
+		],
+		// Preset gear configurations that the user can quickly select.
+		gear: [
+			Presets.PRERAID_PRESET,
+			Presets.P4_PRERAID_PRESET,
+			Presets.P1_PRESET,
+			Presets.P2_PRESET,
+			Presets.P3_PRESET,
+			Presets.P4_PRESET,
+		],
+	},
 
-			presets: {
-				// Preset talents that the user can quickly select.
-				talents: [
-					Presets.GenericAoeTalents,
-				],
-				// Preset rotations that the user can quickly select.
-				rotations: [
-					Presets.ROTATION_DEFAULT,
-				],
-				// Preset gear configurations that the user can quickly select.
-				gear: [
-					Presets.PRERAID_PRESET,
-					Presets.P4_PRERAID_PRESET,
-					Presets.P1_PRESET,
-					Presets.P2_PRESET,
-					Presets.P3_PRESET,
-					Presets.P4_PRESET,
-				],
-			},
+	autoRotation: (_player: Player<Spec.SpecProtectionPaladin>): APLRotation => {
+		return Presets.ROTATION_DEFAULT.rotation.rotation!;
+	},
+});
 
-			autoRotation: (player: Player<Spec.SpecProtectionPaladin>): APLRotation => {
-				return Presets.ROTATION_DEFAULT.rotation.rotation!;
-			},
-		});
+export class ProtectionPaladinSimUI extends IndividualSimUI<Spec.SpecProtectionPaladin> {
+	constructor(parentElem: HTMLElement, player: Player<Spec.SpecProtectionPaladin>) {
+		super(parentElem, player, SPEC_CONFIG);
 	}
 }
diff --git a/ui/protection_warrior/sim.ts b/ui/protection_warrior/sim.ts
index f86d495166..45634c4ea4 100644
--- a/ui/protection_warrior/sim.ts
+++ b/ui/protection_warrior/sim.ts
@@ -18,240 +18,240 @@ import {
 } from '../core/proto/apl.js';
 import { Stats } from '../core/proto_utils/stats.js';
 import { Player } from '../core/player.js';
-import { IndividualSimUI } from '../core/individual_sim_ui.js';
+import { IndividualSimUI, registerSpecConfig } from '../core/individual_sim_ui.js';
 
-import { ProtectionWarrior, ProtectionWarrior_Rotation as ProtectionWarriorRotation, WarriorTalents as WarriorTalents, ProtectionWarrior_Options as ProtectionWarriorOptions } from '../core/proto/warrior.js';
+import { ProtectionWarrior_Rotation as ProtectionWarriorRotation } from '../core/proto/warrior.js';
 
 import * as IconInputs from '../core/components/icon_inputs.js';
 import * as OtherInputs from '../core/components/other_inputs.js';
-import * as Tooltips from '../core/constants/tooltips.js';
 import * as AplUtils from '../core/proto_utils/apl_utils.js';
 
 import * as ProtectionWarriorInputs from './inputs.js';
 import * as Presets from './presets.js';
 
-export class ProtectionWarriorSimUI extends IndividualSimUI<Spec.SpecProtectionWarrior> {
-	constructor(parentElem: HTMLElement, player: Player<Spec.SpecProtectionWarrior>) {
-		super(parentElem, player, {
-			cssClass: 'protection-warrior-sim-ui',
-			cssScheme: 'warrior',
-			// List any known bugs / issues here and they'll be shown on the site.
-			knownIssues: [
-			],
+const SPEC_CONFIG = registerSpecConfig(Spec.SpecProtectionWarrior, {
+	cssClass: 'protection-warrior-sim-ui',
+	cssScheme: 'warrior',
+	// List any known bugs / issues here and they'll be shown on the site.
+	knownIssues: [
+	],
 
-			// All stats for which EP should be calculated.
-			epStats: [
-				Stat.StatStamina,
-				Stat.StatStrength,
-				Stat.StatAgility,
-				Stat.StatAttackPower,
-				Stat.StatExpertise,
-				Stat.StatMeleeHit,
-				Stat.StatMeleeCrit,
-				Stat.StatMeleeHaste,
-				Stat.StatArmor,
-				Stat.StatBonusArmor,
-				Stat.StatArmorPenetration,
-				Stat.StatDefense,
-				Stat.StatBlock,
-				Stat.StatBlockValue,
-				Stat.StatDodge,
-				Stat.StatParry,
-				Stat.StatResilience,
-				Stat.StatNatureResistance,
-				Stat.StatShadowResistance,
-				Stat.StatFrostResistance,
-			],
-			epPseudoStats: [
-				PseudoStat.PseudoStatMainHandDps,
-			],
-			// Reference stat against which to calculate EP. I think all classes use either spell power or attack power.
-			epReferenceStat: Stat.StatAttackPower,
-			// Which stats to display in the Character Stats section, at the bottom of the left-hand sidebar.
-			displayStats: [
-				Stat.StatHealth,
-				Stat.StatArmor,
-				Stat.StatBonusArmor,
-				Stat.StatStamina,
-				Stat.StatStrength,
-				Stat.StatAgility,
-				Stat.StatAttackPower,
-				Stat.StatExpertise,
-				Stat.StatMeleeHit,
-				Stat.StatMeleeCrit,
-				Stat.StatMeleeHaste,
-				Stat.StatArmorPenetration,
-				Stat.StatDefense,
-				Stat.StatBlock,
-				Stat.StatBlockValue,
-				Stat.StatDodge,
-				Stat.StatParry,
-				Stat.StatResilience,
-				Stat.StatNatureResistance,
-				Stat.StatShadowResistance,
-				Stat.StatFrostResistance,
-			],
+	// All stats for which EP should be calculated.
+	epStats: [
+		Stat.StatStamina,
+		Stat.StatStrength,
+		Stat.StatAgility,
+		Stat.StatAttackPower,
+		Stat.StatExpertise,
+		Stat.StatMeleeHit,
+		Stat.StatMeleeCrit,
+		Stat.StatMeleeHaste,
+		Stat.StatArmor,
+		Stat.StatBonusArmor,
+		Stat.StatArmorPenetration,
+		Stat.StatDefense,
+		Stat.StatBlock,
+		Stat.StatBlockValue,
+		Stat.StatDodge,
+		Stat.StatParry,
+		Stat.StatResilience,
+		Stat.StatNatureResistance,
+		Stat.StatShadowResistance,
+		Stat.StatFrostResistance,
+	],
+	epPseudoStats: [
+		PseudoStat.PseudoStatMainHandDps,
+	],
+	// Reference stat against which to calculate EP. I think all classes use either spell power or attack power.
+	epReferenceStat: Stat.StatAttackPower,
+	// Which stats to display in the Character Stats section, at the bottom of the left-hand sidebar.
+	displayStats: [
+		Stat.StatHealth,
+		Stat.StatArmor,
+		Stat.StatBonusArmor,
+		Stat.StatStamina,
+		Stat.StatStrength,
+		Stat.StatAgility,
+		Stat.StatAttackPower,
+		Stat.StatExpertise,
+		Stat.StatMeleeHit,
+		Stat.StatMeleeCrit,
+		Stat.StatMeleeHaste,
+		Stat.StatArmorPenetration,
+		Stat.StatDefense,
+		Stat.StatBlock,
+		Stat.StatBlockValue,
+		Stat.StatDodge,
+		Stat.StatParry,
+		Stat.StatResilience,
+		Stat.StatNatureResistance,
+		Stat.StatShadowResistance,
+		Stat.StatFrostResistance,
+	],
 
-			defaults: {
-				// Default equipped gear.
-				gear: Presets.P3_PRESET.gear,
-				// Default EP weights for sorting gear in the gear picker.
-				epWeights: Stats.fromMap({
-					[Stat.StatArmor]: 0.174,
-					[Stat.StatBonusArmor]: 0.155,
-					[Stat.StatStamina]: 2.336,
-					[Stat.StatStrength]: 1.555,
-					[Stat.StatAgility]: 2.771,
-					[Stat.StatAttackPower]: 0.32,
-					[Stat.StatExpertise]: 1.44,
-					[Stat.StatMeleeHit]: 1.432,
-					[Stat.StatMeleeCrit]: 0.925,
-					[Stat.StatMeleeHaste]: 0.431,
-					[Stat.StatArmorPenetration]: 1.055,
-					[Stat.StatBlock]: 1.320,
-					[Stat.StatBlockValue]: 1.373,
-					[Stat.StatDodge]: 2.606,
-					[Stat.StatParry]: 2.649,
-					[Stat.StatDefense]: 3.305,
-				}, {
-					[PseudoStat.PseudoStatMainHandDps]: 6.081,
-				}),
-				// Default consumes settings.
-				consumes: Presets.DefaultConsumes,
-				// Default rotation settings.
-				rotation: Presets.DefaultRotation,
-				// Default talents.
-				talents: Presets.StandardTalents.data,
-				// Default spec-specific settings.
-				specOptions: Presets.DefaultOptions,
-				// Default raid/party buffs settings.
-				raidBuffs: RaidBuffs.create({
-					giftOfTheWild: TristateEffect.TristateEffectImproved,
-					powerWordFortitude: TristateEffect.TristateEffectImproved,
-					abominationsMight: true,
-					swiftRetribution: true,
-					bloodlust: true,
-					strengthOfEarthTotem: TristateEffect.TristateEffectImproved,
-					leaderOfThePack: TristateEffect.TristateEffectImproved,
-					sanctifiedRetribution: true,
-					devotionAura: TristateEffect.TristateEffectImproved,
-					stoneskinTotem: TristateEffect.TristateEffectImproved,
-					icyTalons: true,
-					retributionAura: true,
-					thorns: TristateEffect.TristateEffectImproved,
-					shadowProtection: true,
-				}),
-				partyBuffs: PartyBuffs.create({
-				}),
-				individualBuffs: IndividualBuffs.create({
-					blessingOfKings: true,
-					blessingOfMight: TristateEffect.TristateEffectImproved,
-					blessingOfSanctuary: true,
-				}),
-				debuffs: Debuffs.create({
-					sunderArmor: true,
-					mangle: true,
-					vindication: true,
-					faerieFire: TristateEffect.TristateEffectImproved,
-					insectSwarm: true,
-					bloodFrenzy: true,
-					judgementOfLight: true,
-					heartOfTheCrusader: true,
-					frostFever: TristateEffect.TristateEffectImproved,
-				}),
-			},
+	defaults: {
+		// Default equipped gear.
+		gear: Presets.P3_PRESET.gear,
+		// Default EP weights for sorting gear in the gear picker.
+		epWeights: Stats.fromMap({
+			[Stat.StatArmor]: 0.174,
+			[Stat.StatBonusArmor]: 0.155,
+			[Stat.StatStamina]: 2.336,
+			[Stat.StatStrength]: 1.555,
+			[Stat.StatAgility]: 2.771,
+			[Stat.StatAttackPower]: 0.32,
+			[Stat.StatExpertise]: 1.44,
+			[Stat.StatMeleeHit]: 1.432,
+			[Stat.StatMeleeCrit]: 0.925,
+			[Stat.StatMeleeHaste]: 0.431,
+			[Stat.StatArmorPenetration]: 1.055,
+			[Stat.StatBlock]: 1.320,
+			[Stat.StatBlockValue]: 1.373,
+			[Stat.StatDodge]: 2.606,
+			[Stat.StatParry]: 2.649,
+			[Stat.StatDefense]: 3.305,
+		}, {
+			[PseudoStat.PseudoStatMainHandDps]: 6.081,
+		}),
+		// Default consumes settings.
+		consumes: Presets.DefaultConsumes,
+		// Default rotation settings.
+		rotation: Presets.DefaultRotation,
+		// Default talents.
+		talents: Presets.StandardTalents.data,
+		// Default spec-specific settings.
+		specOptions: Presets.DefaultOptions,
+		// Default raid/party buffs settings.
+		raidBuffs: RaidBuffs.create({
+			giftOfTheWild: TristateEffect.TristateEffectImproved,
+			powerWordFortitude: TristateEffect.TristateEffectImproved,
+			abominationsMight: true,
+			swiftRetribution: true,
+			bloodlust: true,
+			strengthOfEarthTotem: TristateEffect.TristateEffectImproved,
+			leaderOfThePack: TristateEffect.TristateEffectImproved,
+			sanctifiedRetribution: true,
+			devotionAura: TristateEffect.TristateEffectImproved,
+			stoneskinTotem: TristateEffect.TristateEffectImproved,
+			icyTalons: true,
+			retributionAura: true,
+			thorns: TristateEffect.TristateEffectImproved,
+			shadowProtection: true,
+		}),
+		partyBuffs: PartyBuffs.create({
+		}),
+		individualBuffs: IndividualBuffs.create({
+			blessingOfKings: true,
+			blessingOfMight: TristateEffect.TristateEffectImproved,
+			blessingOfSanctuary: true,
+		}),
+		debuffs: Debuffs.create({
+			sunderArmor: true,
+			mangle: true,
+			vindication: true,
+			faerieFire: TristateEffect.TristateEffectImproved,
+			insectSwarm: true,
+			bloodFrenzy: true,
+			judgementOfLight: true,
+			heartOfTheCrusader: true,
+			frostFever: TristateEffect.TristateEffectImproved,
+		}),
+	},
 
-			// IconInputs to include in the 'Player' section on the settings tab.
-			playerIconInputs: [
-				ProtectionWarriorInputs.ShoutPicker,
-				ProtectionWarriorInputs.ShatteringThrow,
-			],
-			// Inputs to include in the 'Rotation' section on the settings tab.
-			rotationInputs: ProtectionWarriorInputs.ProtectionWarriorRotationConfig,
-			// Buff and Debuff inputs to include/exclude, overriding the EP-based defaults.
-			includeBuffDebuffInputs: [
-				IconInputs.HealthBuff,
-			],
-			excludeBuffDebuffInputs: [
-			],
-			// Inputs to include in the 'Other' section on the settings tab.
-			otherInputs: {
-				inputs: [
-					OtherInputs.TankAssignment,
-					OtherInputs.IncomingHps,
-					OtherInputs.HealingCadence,
-					OtherInputs.HealingCadenceVariation,
-					OtherInputs.BurstWindow,
-					OtherInputs.HpPercentForDefensives,
-					OtherInputs.InspirationUptime,
-					ProtectionWarriorInputs.StartingRage,
-					OtherInputs.InFrontOfTarget,
-				],
-			},
-			encounterPicker: {
-				// Whether to include 'Execute Duration (%)' in the 'Encounter' section of the settings tab.
-				showExecuteProportion: false,
-			},
+	// IconInputs to include in the 'Player' section on the settings tab.
+	playerIconInputs: [
+		ProtectionWarriorInputs.ShoutPicker,
+		ProtectionWarriorInputs.ShatteringThrow,
+	],
+	// Inputs to include in the 'Rotation' section on the settings tab.
+	rotationInputs: ProtectionWarriorInputs.ProtectionWarriorRotationConfig,
+	// Buff and Debuff inputs to include/exclude, overriding the EP-based defaults.
+	includeBuffDebuffInputs: [
+		IconInputs.HealthBuff,
+	],
+	excludeBuffDebuffInputs: [
+	],
+	// Inputs to include in the 'Other' section on the settings tab.
+	otherInputs: {
+		inputs: [
+			OtherInputs.TankAssignment,
+			OtherInputs.IncomingHps,
+			OtherInputs.HealingCadence,
+			OtherInputs.HealingCadenceVariation,
+			OtherInputs.BurstWindow,
+			OtherInputs.HpPercentForDefensives,
+			OtherInputs.InspirationUptime,
+			ProtectionWarriorInputs.StartingRage,
+			OtherInputs.InFrontOfTarget,
+		],
+	},
+	encounterPicker: {
+		// Whether to include 'Execute Duration (%)' in the 'Encounter' section of the settings tab.
+		showExecuteProportion: false,
+	},
 
-			presets: {
-				// Preset talents that the user can quickly select.
-				talents: [
-					Presets.StandardTalents,
-					Presets.UATalents,
-				],
-				// Preset rotations that the user can quickly select.
-				rotations: [
-					Presets.ROTATION_DEFAULT,
-					Presets.ROTATION_PRESET_SIMPLE,
-				],
-				// Preset gear configurations that the user can quickly select.
-				gear: [
-					Presets.PRERAID_BALANCED_PRESET,
-					Presets.P4_PRERAID_PRESET,
-					Presets.P1_BALANCED_PRESET,
-					Presets.P2_SURVIVAL_PRESET,
-					Presets.P3_PRESET,
-					Presets.P4_PRESET,
-				],
-			},
+	presets: {
+		// Preset talents that the user can quickly select.
+		talents: [
+			Presets.StandardTalents,
+			Presets.UATalents,
+		],
+		// Preset rotations that the user can quickly select.
+		rotations: [
+			Presets.ROTATION_DEFAULT,
+			Presets.ROTATION_PRESET_SIMPLE,
+		],
+		// Preset gear configurations that the user can quickly select.
+		gear: [
+			Presets.PRERAID_BALANCED_PRESET,
+			Presets.P4_PRERAID_PRESET,
+			Presets.P1_BALANCED_PRESET,
+			Presets.P2_SURVIVAL_PRESET,
+			Presets.P3_PRESET,
+			Presets.P4_PRESET,
+		],
+	},
 
-			autoRotation: (player: Player<Spec.SpecProtectionWarrior>): APLRotation => {
-				return Presets.ROTATION_DEFAULT.rotation.rotation!;
-			},
-			
-			simpleRotation: (player: Player<Spec.SpecProtectionWarrior>, simple: ProtectionWarriorRotation, cooldowns: Cooldowns): APLRotation => {
-				let [prepullActions, actions] = AplUtils.standardCooldownDefaults(cooldowns);
+	autoRotation: (_player: Player<Spec.SpecProtectionWarrior>): APLRotation => {
+		return Presets.ROTATION_DEFAULT.rotation.rotation!;
+	},
+	
+	simpleRotation: (player: Player<Spec.SpecProtectionWarrior>, simple: ProtectionWarriorRotation, cooldowns: Cooldowns): APLRotation => {
+		let [prepullActions, actions] = AplUtils.standardCooldownDefaults(cooldowns);
 
-				const preShout = APLPrepullAction.fromJsonString(`{"action":{"castSpell":{"spellId":{"spellId":47440}}},"doAtValue":{"const":{"val":"-10s"}}}`);
+		const preShout = APLPrepullAction.fromJsonString(`{"action":{"castSpell":{"spellId":{"spellId":47440}}},"doAtValue":{"const":{"val":"-10s"}}}`);
 
-                const heroicStrike = APLAction.fromJsonString(`{"condition":{"cmp":{"op":"OpGe","lhs":{"currentRage":{}},"rhs":{"const":{"val":"30"}}}},"castSpell":{"spellId":{"tag":1,"spellId":47450}}}`);
-                const shieldSlam = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":47488}}}`);
-                const revenge = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":57823}}}`);
-                const refreshShout = APLAction.fromJsonString(`{"condition":{"auraShouldRefresh":{"sourceUnit":{"type":"Self"},"auraId":{"spellId":47440},"maxOverlap":{"const":{"val":"3s"}}}},"castSpell":{"spellId":{"spellId":47440}}}`);
-                const refreshTclap = APLAction.fromJsonString(`{"condition":{"auraShouldRefresh":{"auraId":{"spellId":47502},"maxOverlap":{"const":{"val":"2s"}}}},"castSpell":{"spellId":{"spellId":47502}}}`);
-                const refreshDemo = APLAction.fromJsonString(`{"condition":{"auraShouldRefresh":{"auraId":{"spellId":47437},"maxOverlap":{"const":{"val":"2s"}}}},"castSpell":{"spellId":{"spellId":25203}}}`);
-                const devastate = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":47498}}}`);
+		const heroicStrike = APLAction.fromJsonString(`{"condition":{"cmp":{"op":"OpGe","lhs":{"currentRage":{}},"rhs":{"const":{"val":"30"}}}},"castSpell":{"spellId":{"tag":1,"spellId":47450}}}`);
+		const shieldSlam = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":47488}}}`);
+		const revenge = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":57823}}}`);
+		const refreshShout = APLAction.fromJsonString(`{"condition":{"auraShouldRefresh":{"sourceUnit":{"type":"Self"},"auraId":{"spellId":47440},"maxOverlap":{"const":{"val":"3s"}}}},"castSpell":{"spellId":{"spellId":47440}}}`);
+		const refreshTclap = APLAction.fromJsonString(`{"condition":{"auraShouldRefresh":{"auraId":{"spellId":47502},"maxOverlap":{"const":{"val":"2s"}}}},"castSpell":{"spellId":{"spellId":47502}}}`);
+		const refreshDemo = APLAction.fromJsonString(`{"condition":{"auraShouldRefresh":{"auraId":{"spellId":47437},"maxOverlap":{"const":{"val":"2s"}}}},"castSpell":{"spellId":{"spellId":25203}}}`);
+		const devastate = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":47498}}}`);
 
-				prepullActions.push(preShout);
+		prepullActions.push(preShout);
 
-				actions.push(...[
-					heroicStrike,
-					shieldSlam,
-					revenge,
-					refreshShout,
-					refreshTclap,
-					refreshDemo,
-					devastate,
-					].filter(a => a) as Array<APLAction>)
+		actions.push(...[
+			heroicStrike,
+			shieldSlam,
+			revenge,
+			refreshShout,
+			refreshTclap,
+			refreshDemo,
+			devastate,
+			].filter(a => a) as Array<APLAction>)
 
-				return APLRotation.create({
-					prepullActions: prepullActions,
-					priorityList: actions.map(action => APLListItem.create({
-						action: action,
-					}))
-				});
-			},
-			
+		return APLRotation.create({
+			prepullActions: prepullActions,
+			priorityList: actions.map(action => APLListItem.create({
+				action: action,
+			}))
 		});
+	},
+});
+
+export class ProtectionWarriorSimUI extends IndividualSimUI<Spec.SpecProtectionWarrior> {
+	constructor(parentElem: HTMLElement, player: Player<Spec.SpecProtectionWarrior>) {
+		super(parentElem, player, SPEC_CONFIG);
 	}
 }
diff --git a/ui/restoration_druid/sim.ts b/ui/restoration_druid/sim.ts
index 053ec3d528..3c49f2583d 100644
--- a/ui/restoration_druid/sim.ts
+++ b/ui/restoration_druid/sim.ts
@@ -5,116 +5,120 @@ import {
 } from '../core/proto/apl.js';
 import { Stats } from '../core/proto_utils/stats.js';
 import { Player } from '../core/player.js';
-import { IndividualSimUI } from '../core/individual_sim_ui.js';
+import { IndividualSimUI, registerSpecConfig } from '../core/individual_sim_ui.js';
 import * as OtherInputs from '../core/components/other_inputs.js';
 
 import * as DruidInputs from './inputs.js';
 import * as Presets from './presets.js';
 
-export class RestorationDruidSimUI extends IndividualSimUI<Spec.SpecRestorationDruid> {
-	constructor(parentElem: HTMLElement, player: Player<Spec.SpecRestorationDruid>) {
-		super(parentElem, player, {
-			cssClass: 'restoration-druid-sim-ui',
-			cssScheme: 'druid',
-			// List any known bugs / issues here and they'll be shown on the site.
-			knownIssues: [
-			],
+const SPEC_CONFIG = registerSpecConfig(Spec.SpecRestorationDruid, {
+	cssClass: 'restoration-druid-sim-ui',
+	cssScheme: 'druid',
+	// List any known bugs / issues here and they'll be shown on the site.
+	knownIssues: [
+	],
+
+	// All stats for which EP should be calculated.
+	epStats: [
+		Stat.StatIntellect,
+		Stat.StatSpirit,
+		Stat.StatSpellPower,
+		Stat.StatSpellCrit,
+		Stat.StatSpellHaste,
+		Stat.StatMP5,
+	],
+	// Reference stat against which to calculate EP. I think all classes use either spell power or attack power.
+	epReferenceStat: Stat.StatSpellPower,
+	// Which stats to display in the Character Stats section, at the bottom of the left-hand sidebar.
+	displayStats: [
+		Stat.StatHealth,
+		Stat.StatMana,
+		Stat.StatStamina,
+		Stat.StatIntellect,
+		Stat.StatSpirit,
+		Stat.StatSpellPower,
+		Stat.StatSpellCrit,
+		Stat.StatSpellHaste,
+		Stat.StatMP5,
+	],
 
-			// All stats for which EP should be calculated.
-			epStats: [
-				Stat.StatIntellect,
-				Stat.StatSpirit,
-				Stat.StatSpellPower,
-				Stat.StatSpellCrit,
-				Stat.StatSpellHaste,
-				Stat.StatMP5,
-			],
-			// Reference stat against which to calculate EP. I think all classes use either spell power or attack power.
-			epReferenceStat: Stat.StatSpellPower,
-			// Which stats to display in the Character Stats section, at the bottom of the left-hand sidebar.
-			displayStats: [
-				Stat.StatHealth,
-				Stat.StatMana,
-				Stat.StatStamina,
-				Stat.StatIntellect,
-				Stat.StatSpirit,
-				Stat.StatSpellPower,
-				Stat.StatSpellCrit,
-				Stat.StatSpellHaste,
-				Stat.StatMP5,
-			],
+	defaults: {
+		// Default equipped gear.
+		gear: Presets.P1_PRESET.gear,
+		// Default EP weights for sorting gear in the gear picker.
+		epWeights: Stats.fromMap({
+			[Stat.StatIntellect]: 0.38,
+			[Stat.StatSpirit]: 0.34,
+			[Stat.StatSpellPower]: 1,
+			[Stat.StatSpellCrit]: 0.69,
+			[Stat.StatSpellHaste]: 0.77,
+			[Stat.StatMP5]: 0.00,
+		}),
+		// Default consumes settings.
+		consumes: Presets.DefaultConsumes,
+		// Default rotation settings.
+		rotation: Presets.DefaultRotation,
+		// Default talents.
+		talents: Presets.CelestialFocusTalents.data,
+		// Default spec-specific settings.
+		specOptions: Presets.DefaultOptions,
+		// Default raid/party buffs settings.
+		raidBuffs: Presets.DefaultRaidBuffs,
 
-			defaults: {
-				// Default equipped gear.
-				gear: Presets.P1_PRESET.gear,
-				// Default EP weights for sorting gear in the gear picker.
-				epWeights: Stats.fromMap({
-					[Stat.StatIntellect]: 0.38,
-					[Stat.StatSpirit]: 0.34,
-					[Stat.StatSpellPower]: 1,
-					[Stat.StatSpellCrit]: 0.69,
-					[Stat.StatSpellHaste]: 0.77,
-					[Stat.StatMP5]: 0.00,
-				}),
-				// Default consumes settings.
-				consumes: Presets.DefaultConsumes,
-				// Default rotation settings.
-				rotation: Presets.DefaultRotation,
-				// Default talents.
-				talents: Presets.CelestialFocusTalents.data,
-				// Default spec-specific settings.
-				specOptions: Presets.DefaultOptions,
-				// Default raid/party buffs settings.
-				raidBuffs: Presets.DefaultRaidBuffs,
+		partyBuffs: Presets.DefaultPartyBuffs,
 
-				partyBuffs: Presets.DefaultPartyBuffs,
+		individualBuffs: Presets.DefaultIndividualBuffs,
 
-				individualBuffs: Presets.DefaultIndividualBuffs,
+		debuffs: Presets.DefaultDebuffs,
 
-				debuffs: Presets.DefaultDebuffs,
+		other: Presets.OtherDefaults,
+	},
 
-				other: Presets.OtherDefaults,
-			},
+	// IconInputs to include in the 'Player' section on the settings tab.
+	playerIconInputs: [
+		DruidInputs.SelfInnervate,
+	],
+	// Inputs to include in the 'Rotation' section on the settings tab.
+	rotationInputs: DruidInputs.RestorationDruidRotationConfig,
+	// Buff and Debuff inputs to include/exclude, overriding the EP-based defaults.
+	includeBuffDebuffInputs: [
+	],
+	excludeBuffDebuffInputs: [
+	],
+	// Inputs to include in the 'Other' section on the settings tab.
+	otherInputs: {
+		inputs: [
+			OtherInputs.TankAssignment,
+		],
+	},
+	encounterPicker: {
+		// Whether to include 'Execute Duration (%)' in the 'Encounter' section of the settings tab.
+		showExecuteProportion: false,
+	},
 
-			// IconInputs to include in the 'Player' section on the settings tab.
-			playerIconInputs: [
-				DruidInputs.SelfInnervate,
-			],
-			// Inputs to include in the 'Rotation' section on the settings tab.
-			rotationInputs: DruidInputs.RestorationDruidRotationConfig,
-			// Buff and Debuff inputs to include/exclude, overriding the EP-based defaults.
-			includeBuffDebuffInputs: [
-			],
-			excludeBuffDebuffInputs: [
-			],
-			// Inputs to include in the 'Other' section on the settings tab.
-			otherInputs: {
-				inputs: [
-					OtherInputs.TankAssignment,
-				],
-			},
-			encounterPicker: {
-				// Whether to include 'Execute Duration (%)' in the 'Encounter' section of the settings tab.
-				showExecuteProportion: false,
-			},
+	presets: {
+		// Preset talents that the user can quickly select.
+		talents: [
+			Presets.CelestialFocusTalents,
+			Presets.ThiccRestoTalents,
+		],
+		rotations: [
+		],
+		// Preset gear configurations that the user can quickly select.
+		gear: [
+			Presets.PRERAID_PRESET,
+			Presets.P1_PRESET,
+			Presets.P2_PRESET,
+		],
+	},
 
-			presets: {
-				// Preset talents that the user can quickly select.
-				talents: [
-					Presets.CelestialFocusTalents,
-					Presets.ThiccRestoTalents,
-				],
-				// Preset gear configurations that the user can quickly select.
-				gear: [
-					Presets.PRERAID_PRESET,
-					Presets.P1_PRESET,
-					Presets.P2_PRESET,
-				],
-			},
+	autoRotation: (_player: Player<Spec.SpecRestorationDruid>): APLRotation => {
+		return APLRotation.create();
+	},
+});
 
-			autoRotation: (_player: Player<Spec.SpecRestorationDruid>): APLRotation => {
-				return APLRotation.create();
-			},
-		});
+export class RestorationDruidSimUI extends IndividualSimUI<Spec.SpecRestorationDruid> {
+	constructor(parentElem: HTMLElement, player: Player<Spec.SpecRestorationDruid>) {
+		super(parentElem, player, SPEC_CONFIG);
 	}
 }
diff --git a/ui/restoration_shaman/sim.ts b/ui/restoration_shaman/sim.ts
index 51aa936a10..31f368684d 100644
--- a/ui/restoration_shaman/sim.ts
+++ b/ui/restoration_shaman/sim.ts
@@ -10,7 +10,7 @@ import {
 } from '../core/proto/apl.js';
 import { Player } from '../core/player.js';
 import { Stats } from '../core/proto_utils/stats.js';
-import { IndividualSimUI } from '../core/individual_sim_ui.js';
+import { IndividualSimUI, registerSpecConfig } from '../core/individual_sim_ui.js';
 import { TotemsSection } from '../core/components/totem_inputs.js';
 
 import * as OtherInputs from '../core/components/other_inputs.js';
@@ -19,133 +19,137 @@ import * as Mechanics from '../core/constants/mechanics.js';
 import * as ShamanInputs from './inputs.js';
 import * as Presets from './presets.js';
 
-export class RestorationShamanSimUI extends IndividualSimUI<Spec.SpecRestorationShaman> {
-	constructor(parentElem: HTMLElement, player: Player<Spec.SpecRestorationShaman>) {
-		super(parentElem, player, {
-			cssClass: 'restoration-shaman-sim-ui',
-			cssScheme: 'shaman',
-			// List any known bugs / issues here and they'll be shown on the site.
-			knownIssues: [
-			],
-			warnings: [
-			],
+const SPEC_CONFIG = registerSpecConfig(Spec.SpecRestorationShaman, {
+	cssClass: 'restoration-shaman-sim-ui',
+	cssScheme: 'shaman',
+	// List any known bugs / issues here and they'll be shown on the site.
+	knownIssues: [
+	],
+	warnings: [
+	],
+
+	// All stats for which EP should be calculated.
+	epStats: [
+		Stat.StatIntellect,
+		Stat.StatSpirit,
+		Stat.StatSpellPower,
+		Stat.StatSpellCrit,
+		Stat.StatSpellHaste,
+		Stat.StatMP5,
+	],
+	// Reference stat against which to calculate EP. I think all classes use either spell power or attack power.
+	epReferenceStat: Stat.StatSpellPower,
+	// Which stats to display in the Character Stats section, at the bottom of the left-hand sidebar.
+	displayStats: [
+		Stat.StatHealth,
+		Stat.StatMana,
+		Stat.StatStamina,
+		Stat.StatIntellect,
+		Stat.StatSpirit,
+		Stat.StatSpellPower,
+		Stat.StatSpellCrit,
+		Stat.StatSpellHaste,
+		Stat.StatMP5,
+	],
+	modifyDisplayStats: (player: Player<Spec.SpecRestorationShaman>) => {
+		let stats = new Stats();
+		stats = stats.addStat(Stat.StatSpellCrit, player.getTalents().tidalMastery * 1 * Mechanics.SPELL_CRIT_RATING_PER_CRIT_CHANCE);
+		return {
+			talents: stats,
+		};
+	},
 
-			// All stats for which EP should be calculated.
-			epStats: [
-				Stat.StatIntellect,
-				Stat.StatSpirit,
-				Stat.StatSpellPower,
-				Stat.StatSpellCrit,
-				Stat.StatSpellHaste,
-				Stat.StatMP5,
-			],
-			// Reference stat against which to calculate EP. I think all classes use either spell power or attack power.
-			epReferenceStat: Stat.StatSpellPower,
-			// Which stats to display in the Character Stats section, at the bottom of the left-hand sidebar.
-			displayStats: [
-				Stat.StatHealth,
-				Stat.StatMana,
-				Stat.StatStamina,
-				Stat.StatIntellect,
-				Stat.StatSpirit,
-				Stat.StatSpellPower,
-				Stat.StatSpellCrit,
-				Stat.StatSpellHaste,
-				Stat.StatMP5,
-			],
-			modifyDisplayStats: (player: Player<Spec.SpecRestorationShaman>) => {
-				let stats = new Stats();
-				stats = stats.addStat(Stat.StatSpellCrit, player.getTalents().tidalMastery * 1 * Mechanics.SPELL_CRIT_RATING_PER_CRIT_CHANCE);
-				return {
-					talents: stats,
-				};
-			},
+	defaults: {
+		// Default equipped gear.
+		gear: Presets.P1_PRESET.gear,
+		// Default EP weights for sorting gear in the gear picker.
+		epWeights: Stats.fromMap({
+			[Stat.StatIntellect]: 0.22,
+			[Stat.StatSpirit]: 0.05,
+			[Stat.StatSpellPower]: 1,
+			[Stat.StatSpellCrit]: 0.67,
+			[Stat.StatSpellHaste]: 1.29,
+			[Stat.StatMP5]: 0.08,
+		}),
+		// Default consumes settings.
+		consumes: Presets.DefaultConsumes,
+		// Default rotation settings.
+		rotation: Presets.DefaultRotation,
+		// Default talents.
+		talents: Presets.RaidHealingTalents.data,
+		// Default spec-specific settings.
+		specOptions: Presets.DefaultOptions,
+		// Default raid/party buffs settings.
+		raidBuffs: RaidBuffs.create({
+			arcaneBrilliance: true,
+			divineSpirit: true,
+			giftOfTheWild: TristateEffect.TristateEffectImproved,
+			moonkinAura: TristateEffect.TristateEffectImproved,
+			sanctifiedRetribution: true,
+		}),
+		partyBuffs: PartyBuffs.create({
+		}),
+		individualBuffs: IndividualBuffs.create({
+			blessingOfKings: true,
+			blessingOfWisdom: 2,
+			vampiricTouch: true,
+		}),
+		debuffs: Debuffs.create({
+			faerieFire: TristateEffect.TristateEffectImproved,
+			judgementOfWisdom: true,
+			misery: true,
+			curseOfElements: true,
+			shadowMastery: true,
+		}),
+	},
+	// IconInputs to include in the 'Player' section on the settings tab.
+	playerIconInputs: [
+		ShamanInputs.ShamanShieldInput,
+	],
+	// Inputs to include in the 'Rotation' section on the settings tab.
+	rotationInputs: ShamanInputs.RestorationShamanRotationConfig,
+	// Buff and Debuff inputs to include/exclude, overriding the EP-based defaults.
+	includeBuffDebuffInputs: [
+	],
+	excludeBuffDebuffInputs: [
+	],
+	// Inputs to include in the 'Other' section on the settings tab.
+	otherInputs: {
+		inputs: [
+			OtherInputs.TankAssignment
+		],
+	},
+	customSections: [
+		TotemsSection,
+	],
+	encounterPicker: {
+		// Whether to include 'Execute Duration (%)' in the 'Encounter' section of the settings tab.
+		showExecuteProportion: false,
+	},
 
-			defaults: {
-				// Default equipped gear.
-				gear: Presets.P1_PRESET.gear,
-				// Default EP weights for sorting gear in the gear picker.
-				epWeights: Stats.fromMap({
-					[Stat.StatIntellect]: 0.22,
-					[Stat.StatSpirit]: 0.05,
-					[Stat.StatSpellPower]: 1,
-					[Stat.StatSpellCrit]: 0.67,
-					[Stat.StatSpellHaste]: 1.29,
-					[Stat.StatMP5]: 0.08,
-				}),
-				// Default consumes settings.
-				consumes: Presets.DefaultConsumes,
-				// Default rotation settings.
-				rotation: Presets.DefaultRotation,
-				// Default talents.
-				talents: Presets.RaidHealingTalents.data,
-				// Default spec-specific settings.
-				specOptions: Presets.DefaultOptions,
-				// Default raid/party buffs settings.
-				raidBuffs: RaidBuffs.create({
-					arcaneBrilliance: true,
-					divineSpirit: true,
-					giftOfTheWild: TristateEffect.TristateEffectImproved,
-					moonkinAura: TristateEffect.TristateEffectImproved,
-					sanctifiedRetribution: true,
-				}),
-				partyBuffs: PartyBuffs.create({
-				}),
-				individualBuffs: IndividualBuffs.create({
-					blessingOfKings: true,
-					blessingOfWisdom: 2,
-					vampiricTouch: true,
-				}),
-				debuffs: Debuffs.create({
-					faerieFire: TristateEffect.TristateEffectImproved,
-					judgementOfWisdom: true,
-					misery: true,
-					curseOfElements: true,
-					shadowMastery: true,
-				}),
-			},
-			// IconInputs to include in the 'Player' section on the settings tab.
-			playerIconInputs: [
-				ShamanInputs.ShamanShieldInput,
-			],
-			// Inputs to include in the 'Rotation' section on the settings tab.
-			rotationInputs: ShamanInputs.RestorationShamanRotationConfig,
-			// Buff and Debuff inputs to include/exclude, overriding the EP-based defaults.
-			includeBuffDebuffInputs: [
-			],
-			excludeBuffDebuffInputs: [
-			],
-			// Inputs to include in the 'Other' section on the settings tab.
-			otherInputs: {
-				inputs: [
-					OtherInputs.TankAssignment
-				],
-			},
-			customSections: [
-				TotemsSection,
-			],
-			encounterPicker: {
-				// Whether to include 'Execute Duration (%)' in the 'Encounter' section of the settings tab.
-				showExecuteProportion: false,
-			},
+	presets: {
+		// Preset talents that the user can quickly select.
+		talents: [
+			Presets.RaidHealingTalents,
+			Presets.TankHealingTalents,
+		],
+		rotations: [
+		],
+		// Preset gear configurations that the user can quickly select.
+		gear: [
+			Presets.PRERAID_PRESET,
+			Presets.P1_PRESET,
+			Presets.P2_PRESET,
+		],
+	},
 
-			presets: {
-				// Preset talents that the user can quickly select.
-				talents: [
-					Presets.RaidHealingTalents,
-					Presets.TankHealingTalents,
-				],
-				// Preset gear configurations that the user can quickly select.
-				gear: [
-					Presets.PRERAID_PRESET,
-					Presets.P1_PRESET,
-					Presets.P2_PRESET,
-				],
-			},
+	autoRotation: (_player: Player<Spec.SpecRestorationShaman>): APLRotation => {
+		return APLRotation.create();
+	},
+});
 
-			autoRotation: (_player: Player<Spec.SpecRestorationShaman>): APLRotation => {
-				return APLRotation.create();
-			},
-		});
+export class RestorationShamanSimUI extends IndividualSimUI<Spec.SpecRestorationShaman> {
+	constructor(parentElem: HTMLElement, player: Player<Spec.SpecRestorationShaman>) {
+		super(parentElem, player, SPEC_CONFIG);
 	}
 }
diff --git a/ui/retribution_paladin/sim.ts b/ui/retribution_paladin/sim.ts
index 0fced5644e..52b1c06596 100644
--- a/ui/retribution_paladin/sim.ts
+++ b/ui/retribution_paladin/sim.ts
@@ -6,14 +6,12 @@ import { Spec } from '../core/proto/common.js';
 import { Stat, PseudoStat } from '../core/proto/common.js';
 import { TristateEffect } from '../core/proto/common.js'
 import {
-	APLAction,
-	APLListItem,
 	APLRotation,
 } from '../core/proto/apl.js';
 import { Stats } from '../core/proto_utils/stats.js';
 import { Player } from '../core/player.js';
-import { IndividualSimUI } from '../core/individual_sim_ui.js';
-import { EventID, TypedEvent } from '../core/typed_event.js';
+import { IndividualSimUI, registerSpecConfig } from '../core/individual_sim_ui.js';
+import { TypedEvent } from '../core/typed_event.js';
 
 import * as IconInputs from '../core/components/icon_inputs.js';
 import * as OtherInputs from '../core/components/other_inputs.js';
@@ -24,207 +22,209 @@ import { PaladinMajorGlyph, PaladinSeal } from '../core/proto/paladin.js';
 import * as RetributionPaladinInputs from './inputs.js';
 import * as Presets from './presets.js';
 
-export class RetributionPaladinSimUI extends IndividualSimUI<Spec.SpecRetributionPaladin> {
-	constructor(parentElem: HTMLElement, player: Player<Spec.SpecRetributionPaladin>) {
-		super(parentElem, player, {
-			cssClass: 'retribution-paladin-sim-ui',
-			cssScheme: 'paladin',
-			// List any known bugs / issues here and they'll be shown on the site.
-			knownIssues: [
-			],
+const SPEC_CONFIG = registerSpecConfig(Spec.SpecRetributionPaladin, {
+	cssClass: 'retribution-paladin-sim-ui',
+	cssScheme: 'paladin',
+	// List any known bugs / issues here and they'll be shown on the site.
+	knownIssues: [
+	],
+
+	// All stats for which EP should be calculated.
+	epStats: [
+		Stat.StatStrength,
+		Stat.StatAgility,
+		Stat.StatIntellect,
+		Stat.StatMP5,
+		Stat.StatAttackPower,
+		Stat.StatMeleeHit,
+		Stat.StatMeleeCrit,
+		Stat.StatMeleeHaste,
+		Stat.StatExpertise,
+		Stat.StatArmorPenetration,
+		Stat.StatSpellPower,
+		Stat.StatSpellCrit,
+		Stat.StatSpellHit,
+		Stat.StatSpellHaste,
+	],
+	epPseudoStats: [
+		PseudoStat.PseudoStatMainHandDps,
+	],
+	// Reference stat against which to calculate EP. I think all classes use either spell power or attack power.
+	epReferenceStat: Stat.StatAttackPower,
+	// Which stats to display in the Character Stats section, at the bottom of the left-hand sidebar.
+	displayStats: [
+		Stat.StatStrength,
+		Stat.StatAgility,
+		Stat.StatIntellect,
+		Stat.StatMP5,
+		Stat.StatAttackPower,
+		Stat.StatMeleeHit,
+		Stat.StatMeleeCrit,
+		Stat.StatMeleeHaste,
+		Stat.StatExpertise,
+		Stat.StatArmorPenetration,
+		Stat.StatSpellHaste,
+		Stat.StatSpellPower,
+		Stat.StatSpellCrit,
+		Stat.StatSpellHit,
+		Stat.StatMana,
+		Stat.StatHealth,
+	],
+	modifyDisplayStats: (player: Player<Spec.SpecRetributionPaladin>) => {
+		let stats = new Stats();
 
-			// All stats for which EP should be calculated.
-			epStats: [
-				Stat.StatStrength,
-				Stat.StatAgility,
-				Stat.StatIntellect,
-				Stat.StatMP5,
-				Stat.StatAttackPower,
-				Stat.StatMeleeHit,
-				Stat.StatMeleeCrit,
-				Stat.StatMeleeHaste,
-				Stat.StatExpertise,
-				Stat.StatArmorPenetration,
-				Stat.StatSpellPower,
-				Stat.StatSpellCrit,
-				Stat.StatSpellHit,
-				Stat.StatSpellHaste,
-			],
-			epPseudoStats: [
-				PseudoStat.PseudoStatMainHandDps,
-			],
-			// Reference stat against which to calculate EP. I think all classes use either spell power or attack power.
-			epReferenceStat: Stat.StatAttackPower,
-			// Which stats to display in the Character Stats section, at the bottom of the left-hand sidebar.
-			displayStats: [
-				Stat.StatStrength,
-				Stat.StatAgility,
-				Stat.StatIntellect,
-				Stat.StatMP5,
-				Stat.StatAttackPower,
-				Stat.StatMeleeHit,
-				Stat.StatMeleeCrit,
-				Stat.StatMeleeHaste,
-				Stat.StatExpertise,
-				Stat.StatArmorPenetration,
-				Stat.StatSpellHaste,
-				Stat.StatSpellPower,
-				Stat.StatSpellCrit,
-				Stat.StatSpellHit,
-				Stat.StatMana,
-				Stat.StatHealth,
-			],
-			modifyDisplayStats: (player: Player<Spec.SpecRetributionPaladin>) => {
-				let stats = new Stats();
+		TypedEvent.freezeAllAndDo(() => {
+			if (player.getMajorGlyphs().includes(PaladinMajorGlyph.GlyphOfSealOfVengeance) && (player.getSpecOptions().seal == PaladinSeal.Vengeance)) {
+				stats = stats.addStat(Stat.StatExpertise, 10 * Mechanics.EXPERTISE_PER_QUARTER_PERCENT_REDUCTION);
+			}
+		})
 
-				TypedEvent.freezeAllAndDo(() => {
-					if (player.getMajorGlyphs().includes(PaladinMajorGlyph.GlyphOfSealOfVengeance) && (player.getSpecOptions().seal == PaladinSeal.Vengeance)) {
-						stats = stats.addStat(Stat.StatExpertise, 10 * Mechanics.EXPERTISE_PER_QUARTER_PERCENT_REDUCTION);
-					}
-				})
+		return {
+			talents: stats,
+		};
+	},
 
-				return {
-					talents: stats,
-				};
-			},
+	defaults: {
+		// Default equipped gear.
+		gear: Presets.P1_PRESET.gear,
+		// Default EP weights for sorting gear in the gear picker.
+		epWeights: Stats.fromMap({
+			[Stat.StatStrength]: 2.53,
+			[Stat.StatAgility]: 1.13,
+			[Stat.StatIntellect]: 0.15,
+			[Stat.StatSpellPower]: 0.32,
+			[Stat.StatSpellHit]: 0.41,
+			[Stat.StatSpellCrit]: 0.01,
+			[Stat.StatSpellHaste]: 0.12,
+			[Stat.StatMP5]: 0.05,
+			[Stat.StatAttackPower]: 1,
+			[Stat.StatMeleeHit]: 1.96,
+			[Stat.StatMeleeCrit]: 1.16,
+			[Stat.StatMeleeHaste]: 1.44,
+			[Stat.StatArmorPenetration]: 0.76,
+			[Stat.StatExpertise]: 1.80,
+		}, {
+			[PseudoStat.PseudoStatMainHandDps]: 7.33,
+		}),
+		// Default consumes settings.
+		consumes: Presets.DefaultConsumes,
+		// Default rotation settings.
+		rotation: Presets.DefaultRotation,
+		// Default talents.
+		talents: Presets.AuraMasteryTalents.data,
+		// Default spec-specific settings.
+		specOptions: Presets.DefaultOptions,
+		// Default raid/party buffs settings.
+		raidBuffs: RaidBuffs.create({
+			arcaneBrilliance: true,
+			divineSpirit: true,
+			giftOfTheWild: TristateEffect.TristateEffectImproved,
+			bloodlust: true,
+			manaSpringTotem: TristateEffect.TristateEffectRegular,
+			hornOfWinter: true,
+			battleShout: TristateEffect.TristateEffectImproved,
+			sanctifiedRetribution: true,
+			swiftRetribution: true,
+			elementalOath: true,
+			rampage: true,
+			trueshotAura: true,
+			icyTalons: true,
+			totemOfWrath: true,
+			wrathOfAirTotem: true,
+			demonicPact: 500,
+		}),
+		partyBuffs: PartyBuffs.create({
+		}),
+		individualBuffs: IndividualBuffs.create({
+			judgementsOfTheWise: true,
+			blessingOfKings: true,
+			blessingOfMight: TristateEffect.TristateEffectImproved,
+		}),
+		debuffs: Debuffs.create({
+			shadowMastery: true,
+			totemOfWrath: true,
+			judgementOfWisdom: true,
+			judgementOfLight: true,
+			misery: true,
+			curseOfElements: true,
+			bloodFrenzy: true,
+			exposeArmor: true,
+			sunderArmor: true,
+			faerieFire: TristateEffect.TristateEffectImproved,
+			curseOfWeakness: TristateEffect.TristateEffectRegular,
+		}),
+	},
 
-			defaults: {
-				// Default equipped gear.
-				gear: Presets.P1_PRESET.gear,
-				// Default EP weights for sorting gear in the gear picker.
-				epWeights: Stats.fromMap({
-					[Stat.StatStrength]: 2.53,
-					[Stat.StatAgility]: 1.13,
-					[Stat.StatIntellect]: 0.15,
-					[Stat.StatSpellPower]: 0.32,
-					[Stat.StatSpellHit]: 0.41,
-					[Stat.StatSpellCrit]: 0.01,
-					[Stat.StatSpellHaste]: 0.12,
-					[Stat.StatMP5]: 0.05,
-					[Stat.StatAttackPower]: 1,
-					[Stat.StatMeleeHit]: 1.96,
-					[Stat.StatMeleeCrit]: 1.16,
-					[Stat.StatMeleeHaste]: 1.44,
-					[Stat.StatArmorPenetration]: 0.76,
-					[Stat.StatExpertise]: 1.80,
-				}, {
-					[PseudoStat.PseudoStatMainHandDps]: 7.33,
-				}),
-				// Default consumes settings.
-				consumes: Presets.DefaultConsumes,
-				// Default rotation settings.
-				rotation: Presets.DefaultRotation,
-				// Default talents.
-				talents: Presets.AuraMasteryTalents.data,
-				// Default spec-specific settings.
-				specOptions: Presets.DefaultOptions,
-				// Default raid/party buffs settings.
-				raidBuffs: RaidBuffs.create({
-					arcaneBrilliance: true,
-					divineSpirit: true,
-					giftOfTheWild: TristateEffect.TristateEffectImproved,
-					bloodlust: true,
-					manaSpringTotem: TristateEffect.TristateEffectRegular,
-					hornOfWinter: true,
-					battleShout: TristateEffect.TristateEffectImproved,
-					sanctifiedRetribution: true,
-					swiftRetribution: true,
-					elementalOath: true,
-					rampage: true,
-					trueshotAura: true,
-					icyTalons: true,
-					totemOfWrath: true,
-					wrathOfAirTotem: true,
-					demonicPact: 500,
-				}),
-				partyBuffs: PartyBuffs.create({
-				}),
-				individualBuffs: IndividualBuffs.create({
-					judgementsOfTheWise: true,
-					blessingOfKings: true,
-					blessingOfMight: TristateEffect.TristateEffectImproved,
-				}),
-				debuffs: Debuffs.create({
-					shadowMastery: true,
-					totemOfWrath: true,
-					judgementOfWisdom: true,
-					judgementOfLight: true,
-					misery: true,
-					curseOfElements: true,
-					bloodFrenzy: true,
-					exposeArmor: true,
-					sunderArmor: true,
-					faerieFire: TristateEffect.TristateEffectImproved,
-					curseOfWeakness: TristateEffect.TristateEffectRegular,
-				}),
-			},
+	// IconInputs to include in the 'Player' section on the settings tab.
+	playerIconInputs: [
+		RetributionPaladinInputs.AuraSelection,
+		RetributionPaladinInputs.JudgementSelection,
+		RetributionPaladinInputs.StartingSealSelection,
+	],
+	// Inputs to include in the 'Rotation' section on the settings tab.
+	rotationInputs: {
+		inputs: [
+			RetributionPaladinInputs.RotationSelector,
+			RetributionPaladinInputs.RetributionPaladinRotationDivinePleaSelection,
+			RetributionPaladinInputs.RetributionPaladinRotationAvoidClippingConsecration,
+			RetributionPaladinInputs.RetributionPaladinRotationHoldLastAvengingWrathUntilExecution,
+			RetributionPaladinInputs.RetributionPaladinRotationCancelChaosBane,
+			RetributionPaladinInputs.RetributionPaladinRotationDivinePleaSelectionAlternate,
+			RetributionPaladinInputs.RetributionPaladinRotationDivinePleaPercentageConfig,
+			RetributionPaladinInputs.RetributionPaladinRotationConsSlackConfig,
+			RetributionPaladinInputs.RetributionPaladinRotationExoSlackConfig,
+			RetributionPaladinInputs.RetributionPaladinRotationHolyWrathConfig,
+			RetributionPaladinInputs.RetributionPaladinSoVTargets,
+			RetributionPaladinInputs.RetributionPaladinRotationPriorityConfig,
+			RetributionPaladinInputs.RetributionPaladinCastSequenceConfig
+		]
+	},
+	// Buff and Debuff inputs to include/exclude, overriding the EP-based defaults.
+	includeBuffDebuffInputs: [
+		IconInputs.ReplenishmentBuff,
+	],
+	excludeBuffDebuffInputs: [
+	],
+	// Inputs to include in the 'Other' section on the settings tab.
+	otherInputs: {
+		inputs: [
+			OtherInputs.TankAssignment,
+			OtherInputs.InFrontOfTarget,
+		],
+	},
+	encounterPicker: {
+		// Whether to include 'Execute Duration (%)' in the 'Encounter' section of the settings tab.
+		showExecuteProportion: false,
+	},
 
-			// IconInputs to include in the 'Player' section on the settings tab.
-			playerIconInputs: [
-				RetributionPaladinInputs.AuraSelection,
-				RetributionPaladinInputs.JudgementSelection,
-				RetributionPaladinInputs.StartingSealSelection,
-			],
-			// Inputs to include in the 'Rotation' section on the settings tab.
-			rotationInputs: {
-				inputs: [
-					RetributionPaladinInputs.RotationSelector,
-					RetributionPaladinInputs.RetributionPaladinRotationDivinePleaSelection,
-					RetributionPaladinInputs.RetributionPaladinRotationAvoidClippingConsecration,
-					RetributionPaladinInputs.RetributionPaladinRotationHoldLastAvengingWrathUntilExecution,
-					RetributionPaladinInputs.RetributionPaladinRotationCancelChaosBane,
-					RetributionPaladinInputs.RetributionPaladinRotationDivinePleaSelectionAlternate,
-					RetributionPaladinInputs.RetributionPaladinRotationDivinePleaPercentageConfig,
-					RetributionPaladinInputs.RetributionPaladinRotationConsSlackConfig,
-					RetributionPaladinInputs.RetributionPaladinRotationExoSlackConfig,
-					RetributionPaladinInputs.RetributionPaladinRotationHolyWrathConfig,
-					RetributionPaladinInputs.RetributionPaladinSoVTargets,
-					RetributionPaladinInputs.RetributionPaladinRotationPriorityConfig,
-					RetributionPaladinInputs.RetributionPaladinCastSequenceConfig
-				]
-			},
-			// Buff and Debuff inputs to include/exclude, overriding the EP-based defaults.
-			includeBuffDebuffInputs: [
-				IconInputs.ReplenishmentBuff,
-			],
-			excludeBuffDebuffInputs: [
-			],
-			// Inputs to include in the 'Other' section on the settings tab.
-			otherInputs: {
-				inputs: [
-					OtherInputs.TankAssignment,
-					OtherInputs.InFrontOfTarget,
-				],
-			},
-			encounterPicker: {
-				// Whether to include 'Execute Duration (%)' in the 'Encounter' section of the settings tab.
-				showExecuteProportion: false,
-			},
+	presets: {
+		rotations: [
+			Presets.ROTATION_PRESET_LEGACY_DEFAULT,
+			Presets.ROTATION_PRESET_DEFAULT,
+		],
+		// Preset talents that the user can quickly select.
+		talents: [
+			Presets.AuraMasteryTalents,
+			Presets.DivineSacTalents,
+		],
+		// Preset gear configurations that the user can quickly select.
+		gear: [
+			Presets.PRERAID_PRESET,
+			Presets.P1_PRESET,
+			Presets.P2_PRESET,
+			Presets.P3_PRESET,
+			Presets.P4_PRESET,
+			Presets.P5_PRESET,
+		],
+	},
 
-			presets: {
-				rotations: [
-					Presets.ROTATION_PRESET_LEGACY_DEFAULT,
-					Presets.ROTATION_PRESET_DEFAULT,
-				],
-				// Preset talents that the user can quickly select.
-				talents: [
-					Presets.AuraMasteryTalents,
-					Presets.DivineSacTalents,
-				],
-				// Preset gear configurations that the user can quickly select.
-				gear: [
-					Presets.PRERAID_PRESET,
-					Presets.P1_PRESET,
-					Presets.P2_PRESET,
-					Presets.P3_PRESET,
-					Presets.P4_PRESET,
-					Presets.P5_PRESET,
-				],
-			},
+	autoRotation: (_player: Player<Spec.SpecRetributionPaladin>): APLRotation => {
+		return Presets.ROTATION_PRESET_DEFAULT.rotation.rotation!;
+	},
+});
 
-			autoRotation: (player: Player<Spec.SpecRetributionPaladin>): APLRotation => {
-				return Presets.ROTATION_PRESET_DEFAULT.rotation.rotation!;
-			},
-		});
+export class RetributionPaladinSimUI extends IndividualSimUI<Spec.SpecRetributionPaladin> {
+	constructor(parentElem: HTMLElement, player: Player<Spec.SpecRetributionPaladin>) {
+		super(parentElem, player, SPEC_CONFIG);
 	}
 }
diff --git a/ui/rogue/sim.ts b/ui/rogue/sim.ts
index 9738566ffe..5b1f5b28cd 100644
--- a/ui/rogue/sim.ts
+++ b/ui/rogue/sim.ts
@@ -11,13 +11,11 @@ import {
 	WeaponType
 } from '../core/proto/common.js';
 import {
-	APLAction,
-	APLListItem,
 	APLRotation,
 } from '../core/proto/apl.js';
 import { Player } from '../core/player.js';
 import { Stats } from '../core/proto_utils/stats.js';
-import { IndividualSimUI } from '../core/individual_sim_ui.js';
+import { IndividualSimUI, registerSpecConfig } from '../core/individual_sim_ui.js';
 
 import {
 	Rogue_Options_PoisonImbue,
@@ -36,390 +34,392 @@ import * as RogueInputs from './inputs.js';
 import * as Presets from './presets.js';
 import { DefaultOptions } from './presets.js';
 
-export class RogueSimUI extends IndividualSimUI<Spec.SpecRogue> {
-	constructor(parentElem: HTMLElement, player: Player<Spec.SpecRogue>) {
-		super(parentElem, player, {
-			cssClass: 'rogue-sim-ui',
-			cssScheme: 'rogue',
-			// List any known bugs / issues here and they'll be shown on the site.
-			knownIssues: [
-				'Rotations are not fully optimized, especially for non-standard setups.',
-			],
-			warnings: [
-				(simUI: IndividualSimUI<Spec.SpecRogue>) => {
-					return {
-						updateOn: simUI.sim.encounter.changeEmitter,
-						getContent: () => {
-							let hasNoArmor = false
-							for (const target of simUI.sim.encounter.targets) {
-								if (new Stats(target.stats).getStat(Stat.StatArmor) <= 0) {
-									hasNoArmor = true
-									break
-								}
-							}
-							if (hasNoArmor) {
-								return 'One or more targets have no armor. Check advanced encounter settings.';
-							} else {
-								return '';
-							}
-						},
-					};
+const SPEC_CONFIG = registerSpecConfig(Spec.SpecRogue, {
+	cssClass: 'rogue-sim-ui',
+	cssScheme: 'rogue',
+	// List any known bugs / issues here and they'll be shown on the site.
+	knownIssues: [
+		'Rotations are not fully optimized, especially for non-standard setups.',
+	],
+	warnings: [
+		(simUI: IndividualSimUI<Spec.SpecRogue>) => {
+			return {
+				updateOn: simUI.sim.encounter.changeEmitter,
+				getContent: () => {
+					let hasNoArmor = false
+					for (const target of simUI.sim.encounter.targets) {
+						if (new Stats(target.stats).getStat(Stat.StatArmor) <= 0) {
+							hasNoArmor = true
+							break
+						}
+					}
+					if (hasNoArmor) {
+						return 'One or more targets have no armor. Check advanced encounter settings.';
+					} else {
+						return '';
+					}
 				},
-				(simUI: IndividualSimUI<Spec.SpecRogue>) => {
-					return {
-						updateOn: simUI.player.changeEmitter,
-						getContent: () => {
-							if (
-								simUI.player.getTalents().mutilate &&
-								(simUI.player.getGear().getEquippedItem(ItemSlot.ItemSlotMainHand)?.item.weaponType != WeaponType.WeaponTypeDagger ||
-									simUI.player.getGear().getEquippedItem(ItemSlot.ItemSlotOffHand)?.item.weaponType != WeaponType.WeaponTypeDagger)
-							) {
-								return '"Mutilate" talent selected, but daggers not equipped in both hands.';
-							} else {
-								return '';
-							}
-						},
-					};
+			};
+		},
+		(simUI: IndividualSimUI<Spec.SpecRogue>) => {
+			return {
+				updateOn: simUI.player.changeEmitter,
+				getContent: () => {
+					if (
+						simUI.player.getTalents().mutilate &&
+						(simUI.player.getGear().getEquippedItem(ItemSlot.ItemSlotMainHand)?.item.weaponType != WeaponType.WeaponTypeDagger ||
+							simUI.player.getGear().getEquippedItem(ItemSlot.ItemSlotOffHand)?.item.weaponType != WeaponType.WeaponTypeDagger)
+					) {
+						return '"Mutilate" talent selected, but daggers not equipped in both hands.';
+					} else {
+						return '';
+					}
 				},
-				(simUI: IndividualSimUI<Spec.SpecRogue>) => {
-					return {
-						updateOn: simUI.player.changeEmitter,
-						getContent: () => {
-							if (simUI.player.getRotation().combatBuilder == CombatBuilder.Backstab &&
-								simUI.player.getGear().getEquippedItem(ItemSlot.ItemSlotMainHand)?.item.weaponType != WeaponType.WeaponTypeDagger) {
-								return 'Builder "Backstab" selected, but no dagger equipped.';
-							} else {
-								return '';
-							}
-						},
-					};
+			};
+		},
+		(simUI: IndividualSimUI<Spec.SpecRogue>) => {
+			return {
+				updateOn: simUI.player.changeEmitter,
+				getContent: () => {
+					if (simUI.player.getRotation().combatBuilder == CombatBuilder.Backstab &&
+						simUI.player.getGear().getEquippedItem(ItemSlot.ItemSlotMainHand)?.item.weaponType != WeaponType.WeaponTypeDagger) {
+						return 'Builder "Backstab" selected, but no dagger equipped.';
+					} else {
+						return '';
+					}
 				},
-				(simUI: IndividualSimUI<Spec.SpecRogue>) => {
-					return {
-						updateOn: simUI.player.changeEmitter,
-						getContent: () => {
-							if (simUI.player.getTalents().hackAndSlash) {
-								if (simUI.player.getGear().getEquippedItem(ItemSlot.ItemSlotMainHand)?.item.weaponType == WeaponType.WeaponTypeSword ||
-									simUI.player.getGear().getEquippedItem(ItemSlot.ItemSlotMainHand)?.item.weaponType == WeaponType.WeaponTypeAxe ||
-									simUI.player.getGear().getEquippedItem(ItemSlot.ItemSlotOffHand)?.item.weaponType == WeaponType.WeaponTypeSword ||
-									simUI.player.getGear().getEquippedItem(ItemSlot.ItemSlotOffHand)?.item.weaponType == WeaponType.WeaponTypeAxe) {
-									return '';
-								} else {
-									return '"Hack and Slash" talent selected, but swords or axes not equipped.';
-								}
-							} else {
-								return '';
-							}
-						},
-					};
+			};
+		},
+		(simUI: IndividualSimUI<Spec.SpecRogue>) => {
+			return {
+				updateOn: simUI.player.changeEmitter,
+				getContent: () => {
+					if (simUI.player.getTalents().hackAndSlash) {
+						if (simUI.player.getGear().getEquippedItem(ItemSlot.ItemSlotMainHand)?.item.weaponType == WeaponType.WeaponTypeSword ||
+							simUI.player.getGear().getEquippedItem(ItemSlot.ItemSlotMainHand)?.item.weaponType == WeaponType.WeaponTypeAxe ||
+							simUI.player.getGear().getEquippedItem(ItemSlot.ItemSlotOffHand)?.item.weaponType == WeaponType.WeaponTypeSword ||
+							simUI.player.getGear().getEquippedItem(ItemSlot.ItemSlotOffHand)?.item.weaponType == WeaponType.WeaponTypeAxe) {
+							return '';
+						} else {
+							return '"Hack and Slash" talent selected, but swords or axes not equipped.';
+						}
+					} else {
+						return '';
+					}
 				},
-				(simUI: IndividualSimUI<Spec.SpecRogue>) => {
-					return {
-						updateOn: simUI.player.changeEmitter,
-						getContent: () => {
-							if (simUI.player.getTalents().closeQuartersCombat) {
-								if (simUI.player.getGear().getEquippedItem(ItemSlot.ItemSlotMainHand)?.item.weaponType == WeaponType.WeaponTypeFist ||
-									simUI.player.getGear().getEquippedItem(ItemSlot.ItemSlotMainHand)?.item.weaponType == WeaponType.WeaponTypeDagger ||
-									simUI.player.getGear().getEquippedItem(ItemSlot.ItemSlotOffHand)?.item.weaponType == WeaponType.WeaponTypeFist ||
-									simUI.player.getGear().getEquippedItem(ItemSlot.ItemSlotOffHand)?.item.weaponType == WeaponType.WeaponTypeDagger) {
-									return '';
-								} else {
-									return '"Close Quarters Combat" talent selected, but fists or daggers not equipped.';
-								}
-							} else {
-								return '';
-							}
-						},
-					};
+			};
+		},
+		(simUI: IndividualSimUI<Spec.SpecRogue>) => {
+			return {
+				updateOn: simUI.player.changeEmitter,
+				getContent: () => {
+					if (simUI.player.getTalents().closeQuartersCombat) {
+						if (simUI.player.getGear().getEquippedItem(ItemSlot.ItemSlotMainHand)?.item.weaponType == WeaponType.WeaponTypeFist ||
+							simUI.player.getGear().getEquippedItem(ItemSlot.ItemSlotMainHand)?.item.weaponType == WeaponType.WeaponTypeDagger ||
+							simUI.player.getGear().getEquippedItem(ItemSlot.ItemSlotOffHand)?.item.weaponType == WeaponType.WeaponTypeFist ||
+							simUI.player.getGear().getEquippedItem(ItemSlot.ItemSlotOffHand)?.item.weaponType == WeaponType.WeaponTypeDagger) {
+							return '';
+						} else {
+							return '"Close Quarters Combat" talent selected, but fists or daggers not equipped.';
+						}
+					} else {
+						return '';
+					}
 				},
-				(simUI: IndividualSimUI<Spec.SpecRogue>) => {
-					return {
-						updateOn: simUI.player.changeEmitter,
-						getContent: () => {
-							if (simUI.player.getTalents().maceSpecialization) {
-								if (simUI.player.getGear().getEquippedItem(ItemSlot.ItemSlotMainHand)?.item.weaponType == WeaponType.WeaponTypeMace ||
-									simUI.player.getGear().getEquippedItem(ItemSlot.ItemSlotOffHand)?.item.weaponType == WeaponType.WeaponTypeMace) {
-									return '';
-								} else {
-									return '"Mace Specialization" talent selected, but maces not equipped.';
-								}
-							} else {
-								return '';
-							}
-						},
-					};
+			};
+		},
+		(simUI: IndividualSimUI<Spec.SpecRogue>) => {
+			return {
+				updateOn: simUI.player.changeEmitter,
+				getContent: () => {
+					if (simUI.player.getTalents().maceSpecialization) {
+						if (simUI.player.getGear().getEquippedItem(ItemSlot.ItemSlotMainHand)?.item.weaponType == WeaponType.WeaponTypeMace ||
+							simUI.player.getGear().getEquippedItem(ItemSlot.ItemSlotOffHand)?.item.weaponType == WeaponType.WeaponTypeMace) {
+							return '';
+						} else {
+							return '"Mace Specialization" talent selected, but maces not equipped.';
+						}
+					} else {
+						return '';
+					}
 				},
-				(simUI: IndividualSimUI<Spec.SpecRogue>) => {
-					return {
-						updateOn: simUI.player.changeEmitter,
-						getContent: () => {
-							if (simUI.player.getInFrontOfTarget() && (simUI.player.getRotation().combatBuilder == CombatBuilder.Backstab ||
-								simUI.player.getRotation().openWithGarrote)) {
-								return 'Option "In Front of Target" selected, but using Backstab or Garrote as builder or opener.';
-							} else {
-								return '';
-							}
-						},
-					};
+			};
+		},
+		(simUI: IndividualSimUI<Spec.SpecRogue>) => {
+			return {
+				updateOn: simUI.player.changeEmitter,
+				getContent: () => {
+					if (simUI.player.getInFrontOfTarget() && (simUI.player.getRotation().combatBuilder == CombatBuilder.Backstab ||
+						simUI.player.getRotation().openWithGarrote)) {
+						return 'Option "In Front of Target" selected, but using Backstab or Garrote as builder or opener.';
+					} else {
+						return '';
+					}
 				},
-				(simUI: IndividualSimUI<Spec.SpecRogue>) => {
-					return {
-						updateOn: simUI.player.changeEmitter,
-						getContent: () => {
-							if (simUI.player.getRotation().combatBuilder == CombatBuilder.HemorrhageCombat && !simUI.player.getTalents().hemorrhage) {
-								return 'Builder "Hemorrhage" selected, but Hemorrhage is not talented.';
-							} else {
-								return '';
-							}
-						},
-					};
+			};
+		},
+		(simUI: IndividualSimUI<Spec.SpecRogue>) => {
+			return {
+				updateOn: simUI.player.changeEmitter,
+				getContent: () => {
+					if (simUI.player.getRotation().combatBuilder == CombatBuilder.HemorrhageCombat && !simUI.player.getTalents().hemorrhage) {
+						return 'Builder "Hemorrhage" selected, but Hemorrhage is not talented.';
+					} else {
+						return '';
+					}
 				},
-				(simUI: IndividualSimUI<Spec.SpecRogue>) => {
-					return {
-						updateOn: simUI.player.changeEmitter,
-						getContent: () => {
-							if (simUI.player.getRotation().useGhostlyStrike && !simUI.player.getMajorGlyphs().includes(RogueMajorGlyph.GlyphOfGhostlyStrike)) {
-								return '"Use Ghostly Strike" selected, but missing Glyph of Ghostly Strike.';
-							} else {
-								return '';
-							}
-						},
-					};
+			};
+		},
+		(simUI: IndividualSimUI<Spec.SpecRogue>) => {
+			return {
+				updateOn: simUI.player.changeEmitter,
+				getContent: () => {
+					if (simUI.player.getRotation().useGhostlyStrike && !simUI.player.getMajorGlyphs().includes(RogueMajorGlyph.GlyphOfGhostlyStrike)) {
+						return '"Use Ghostly Strike" selected, but missing Glyph of Ghostly Strike.';
+					} else {
+						return '';
+					}
 				},
-				(simUI: IndividualSimUI<Spec.SpecRogue>) => {
-					return {
-						updateOn: simUI.player.changeEmitter,
-						getContent: () => {
-							if (simUI.player.getRotation().useFeint && !simUI.player.getMajorGlyphs().includes(RogueMajorGlyph.GlyphOfFeint)) {
-								return '"Use Feint" selected, but missing Glyph of Feint.';
-							} else {
-								return '';
-							}
-						},
-					};
+			};
+		},
+		(simUI: IndividualSimUI<Spec.SpecRogue>) => {
+			return {
+				updateOn: simUI.player.changeEmitter,
+				getContent: () => {
+					if (simUI.player.getRotation().useFeint && !simUI.player.getMajorGlyphs().includes(RogueMajorGlyph.GlyphOfFeint)) {
+						return '"Use Feint" selected, but missing Glyph of Feint.';
+					} else {
+						return '';
+					}
 				},
-				(simUI: IndividualSimUI<Spec.SpecRogue>) => {
-					return {
-						updateOn: simUI.player.changeEmitter,
-						getContent: () => {
-							if (simUI.player.getRotation().exposeArmorFrequency == 2 && !simUI.player.getMajorGlyphs().includes(RogueMajorGlyph.GlyphOfExposeArmor) && simUI.player.getTalentTree() == 1) {
-								return '"Maintain Expose Armor" selected, but missing Glyph of Expose Armor.';
-							} else {
-								return '';
-							}
-						},
-					};
+			};
+		},
+		(simUI: IndividualSimUI<Spec.SpecRogue>) => {
+			return {
+				updateOn: simUI.player.changeEmitter,
+				getContent: () => {
+					if (simUI.player.getRotation().exposeArmorFrequency == 2 && !simUI.player.getMajorGlyphs().includes(RogueMajorGlyph.GlyphOfExposeArmor) && simUI.player.getTalentTree() == 1) {
+						return '"Maintain Expose Armor" selected, but missing Glyph of Expose Armor.';
+					} else {
+						return '';
+					}
 				},
-				(simUI: IndividualSimUI<Spec.SpecRogue>) => {
-					return {
-						updateOn: simUI.player.changeEmitter,
-						getContent: () => {
-							const mhWeaponSpeed = simUI.player.getGear().getEquippedItem(ItemSlot.ItemSlotMainHand)?.item.weaponSpeed;
-							const ohWeaponSpeed = simUI.player.getGear().getEquippedItem(ItemSlot.ItemSlotOffHand)?.item.weaponSpeed;
-							const mhImbue = simUI.player.getSpecOptions().mhImbue;
-							const ohImbue = simUI.player.getSpecOptions().ohImbue;
-							if (typeof mhWeaponSpeed == 'undefined' || typeof ohWeaponSpeed == 'undefined' || !simUI.player.getSpecOptions().applyPoisonsManually) {
-								return '';
-							}
-							if (mhWeaponSpeed < ohWeaponSpeed && ohImbue == Rogue_Options_PoisonImbue.DeadlyPoison) {
-								return 'Deadly poison applied to slower (off hand) weapon.';
-							}
-							if (ohWeaponSpeed < mhWeaponSpeed && mhImbue == Rogue_Options_PoisonImbue.DeadlyPoison) {
-								return 'Deadly poison applied to slower (main hand) weapon.';
-							}
-							return '';
-						},
-					};
+			};
+		},
+		(simUI: IndividualSimUI<Spec.SpecRogue>) => {
+			return {
+				updateOn: simUI.player.changeEmitter,
+				getContent: () => {
+					const mhWeaponSpeed = simUI.player.getGear().getEquippedItem(ItemSlot.ItemSlotMainHand)?.item.weaponSpeed;
+					const ohWeaponSpeed = simUI.player.getGear().getEquippedItem(ItemSlot.ItemSlotOffHand)?.item.weaponSpeed;
+					const mhImbue = simUI.player.getSpecOptions().mhImbue;
+					const ohImbue = simUI.player.getSpecOptions().ohImbue;
+					if (typeof mhWeaponSpeed == 'undefined' || typeof ohWeaponSpeed == 'undefined' || !simUI.player.getSpecOptions().applyPoisonsManually) {
+						return '';
+					}
+					if (mhWeaponSpeed < ohWeaponSpeed && ohImbue == Rogue_Options_PoisonImbue.DeadlyPoison) {
+						return 'Deadly poison applied to slower (off hand) weapon.';
+					}
+					if (ohWeaponSpeed < mhWeaponSpeed && mhImbue == Rogue_Options_PoisonImbue.DeadlyPoison) {
+						return 'Deadly poison applied to slower (main hand) weapon.';
+					}
+					return '';
 				},
-			],
+			};
+		},
+	],
 
-			// All stats for which EP should be calculated.
-			epStats: [
-				Stat.StatAgility,
-				Stat.StatStrength,
-				Stat.StatAttackPower,
-				Stat.StatMeleeHit,
-				Stat.StatMeleeCrit,
-				Stat.StatSpellHit,
-				Stat.StatSpellCrit,
-				Stat.StatMeleeHaste,
-				Stat.StatArmorPenetration,
-				Stat.StatExpertise,
-			],
-			epPseudoStats: [
-				PseudoStat.PseudoStatMainHandDps,
-				PseudoStat.PseudoStatOffHandDps,
-			],
-			// Reference stat against which to calculate EP.
-			epReferenceStat: Stat.StatAttackPower,
-			// Which stats to display in the Character Stats section, at the bottom of the left-hand sidebar.
-			displayStats: [
-				Stat.StatHealth,
-				Stat.StatStamina,
-				Stat.StatAgility,
-				Stat.StatStrength,
-				Stat.StatAttackPower,
-				Stat.StatMeleeHit,
-				Stat.StatSpellHit,
-				Stat.StatMeleeCrit,
-				Stat.StatSpellCrit,
-				Stat.StatMeleeHaste,
-				Stat.StatArmorPenetration,
-				Stat.StatExpertise,
-			],
+	// All stats for which EP should be calculated.
+	epStats: [
+		Stat.StatAgility,
+		Stat.StatStrength,
+		Stat.StatAttackPower,
+		Stat.StatMeleeHit,
+		Stat.StatMeleeCrit,
+		Stat.StatSpellHit,
+		Stat.StatSpellCrit,
+		Stat.StatMeleeHaste,
+		Stat.StatArmorPenetration,
+		Stat.StatExpertise,
+	],
+	epPseudoStats: [
+		PseudoStat.PseudoStatMainHandDps,
+		PseudoStat.PseudoStatOffHandDps,
+	],
+	// Reference stat against which to calculate EP.
+	epReferenceStat: Stat.StatAttackPower,
+	// Which stats to display in the Character Stats section, at the bottom of the left-hand sidebar.
+	displayStats: [
+		Stat.StatHealth,
+		Stat.StatStamina,
+		Stat.StatAgility,
+		Stat.StatStrength,
+		Stat.StatAttackPower,
+		Stat.StatMeleeHit,
+		Stat.StatSpellHit,
+		Stat.StatMeleeCrit,
+		Stat.StatSpellCrit,
+		Stat.StatMeleeHaste,
+		Stat.StatArmorPenetration,
+		Stat.StatExpertise,
+	],
 
-			defaults: {
-				// Default equipped gear.
-				gear: Presets.PRERAID_PRESET_ASSASSINATION.gear,
-				// Default EP weights for sorting gear in the gear picker.
-				epWeights: Stats.fromMap({
-					[Stat.StatAgility]: 1.86,
-					[Stat.StatStrength]: 1.14,
-					[Stat.StatAttackPower]: 1,
-					[Stat.StatSpellCrit]: 0.28,
-					[Stat.StatSpellHit]: 0.08,
-					[Stat.StatMeleeHit]: 1.39,
-					[Stat.StatMeleeCrit]: 1.32,
-					[Stat.StatMeleeHaste]: 1.48,
-					[Stat.StatArmorPenetration]: 0.84,
-					[Stat.StatExpertise]: 0.98,
-				}, {
-					[PseudoStat.PseudoStatMainHandDps]: 2.94,
-					[PseudoStat.PseudoStatOffHandDps]: 2.45,
-				}),
-				// Default consumes settings.
-				consumes: Presets.DefaultConsumes,
-				// Default rotation settings.
-				rotation: Presets.DefaultRotation,
-				// Default talents.
-				talents: Presets.AssassinationTalents137.data,
-				// Default spec-specific settings.
-				specOptions: Presets.DefaultOptions,
-				// Default raid/party buffs settings.
-				raidBuffs: RaidBuffs.create({
-					giftOfTheWild: TristateEffect.TristateEffectImproved,
-					bloodlust: true,
-					strengthOfEarthTotem: TristateEffect.TristateEffectImproved,
-					icyTalons: true,
-					leaderOfThePack: TristateEffect.TristateEffectImproved,
-					abominationsMight: true,
-					swiftRetribution: true,
-					elementalOath: true,
-					sanctifiedRetribution: true,
-				}),
-				partyBuffs: PartyBuffs.create({
-				}),
-				individualBuffs: IndividualBuffs.create({
-					blessingOfKings: true,
-					blessingOfMight: TristateEffect.TristateEffectImproved,
-				}),
-				debuffs: Debuffs.create({
-					heartOfTheCrusader: true,
-					mangle: true,
-					sunderArmor: true,
-					faerieFire: TristateEffect.TristateEffectImproved,
-					shadowMastery: true,
-					earthAndMoon: true,
-					bloodFrenzy: true,
-				}),
-			},
+	defaults: {
+		// Default equipped gear.
+		gear: Presets.PRERAID_PRESET_ASSASSINATION.gear,
+		// Default EP weights for sorting gear in the gear picker.
+		epWeights: Stats.fromMap({
+			[Stat.StatAgility]: 1.86,
+			[Stat.StatStrength]: 1.14,
+			[Stat.StatAttackPower]: 1,
+			[Stat.StatSpellCrit]: 0.28,
+			[Stat.StatSpellHit]: 0.08,
+			[Stat.StatMeleeHit]: 1.39,
+			[Stat.StatMeleeCrit]: 1.32,
+			[Stat.StatMeleeHaste]: 1.48,
+			[Stat.StatArmorPenetration]: 0.84,
+			[Stat.StatExpertise]: 0.98,
+		}, {
+			[PseudoStat.PseudoStatMainHandDps]: 2.94,
+			[PseudoStat.PseudoStatOffHandDps]: 2.45,
+		}),
+		// Default consumes settings.
+		consumes: Presets.DefaultConsumes,
+		// Default rotation settings.
+		rotation: Presets.DefaultRotation,
+		// Default talents.
+		talents: Presets.AssassinationTalents137.data,
+		// Default spec-specific settings.
+		specOptions: Presets.DefaultOptions,
+		// Default raid/party buffs settings.
+		raidBuffs: RaidBuffs.create({
+			giftOfTheWild: TristateEffect.TristateEffectImproved,
+			bloodlust: true,
+			strengthOfEarthTotem: TristateEffect.TristateEffectImproved,
+			icyTalons: true,
+			leaderOfThePack: TristateEffect.TristateEffectImproved,
+			abominationsMight: true,
+			swiftRetribution: true,
+			elementalOath: true,
+			sanctifiedRetribution: true,
+		}),
+		partyBuffs: PartyBuffs.create({
+		}),
+		individualBuffs: IndividualBuffs.create({
+			blessingOfKings: true,
+			blessingOfMight: TristateEffect.TristateEffectImproved,
+		}),
+		debuffs: Debuffs.create({
+			heartOfTheCrusader: true,
+			mangle: true,
+			sunderArmor: true,
+			faerieFire: TristateEffect.TristateEffectImproved,
+			shadowMastery: true,
+			earthAndMoon: true,
+			bloodFrenzy: true,
+		}),
+	},
 
-			playerInputs: {
-				inputs: [
-					RogueInputs.ApplyPoisonsManually
-				]
-			},
-			// IconInputs to include in the 'Player' section on the settings tab.
-			playerIconInputs: [
-				RogueInputs.MainHandImbue,
-				RogueInputs.OffHandImbue,
-			],
-			// Inputs to include in the 'Rotation' section on the settings tab.
-			rotationInputs: RogueInputs.RogueRotationConfig,
-			// Buff and Debuff inputs to include/exclude, overriding the EP-based defaults.
-			includeBuffDebuffInputs: [
-				IconInputs.SpellCritBuff,
-				IconInputs.SpellCritDebuff,
-				IconInputs.SpellHitDebuff,
-				IconInputs.SpellDamageDebuff
-			],
-			excludeBuffDebuffInputs: [
-			],
-			// Inputs to include in the 'Other' section on the settings tab.
-			otherInputs: {
-				inputs: [
-					RogueInputs.StartingOverkillDuration,
-					RogueInputs.VanishBreakTime,
-					RogueInputs.AssumeBleedActive,
-					RogueInputs.HonorOfThievesCritRate,
-					OtherInputs.TankAssignment,
-					OtherInputs.InFrontOfTarget,
-				],
-			},
-			encounterPicker: {
-				// Whether to include 'Execute Duration (%)' in the 'Encounter' section of the settings tab.
-				showExecuteProportion: false,
-			},
+	playerInputs: {
+		inputs: [
+			RogueInputs.ApplyPoisonsManually
+		]
+	},
+	// IconInputs to include in the 'Player' section on the settings tab.
+	playerIconInputs: [
+		RogueInputs.MainHandImbue,
+		RogueInputs.OffHandImbue,
+	],
+	// Inputs to include in the 'Rotation' section on the settings tab.
+	rotationInputs: RogueInputs.RogueRotationConfig,
+	// Buff and Debuff inputs to include/exclude, overriding the EP-based defaults.
+	includeBuffDebuffInputs: [
+		IconInputs.SpellCritBuff,
+		IconInputs.SpellCritDebuff,
+		IconInputs.SpellHitDebuff,
+		IconInputs.SpellDamageDebuff
+	],
+	excludeBuffDebuffInputs: [
+	],
+	// Inputs to include in the 'Other' section on the settings tab.
+	otherInputs: {
+		inputs: [
+			RogueInputs.StartingOverkillDuration,
+			RogueInputs.VanishBreakTime,
+			RogueInputs.AssumeBleedActive,
+			RogueInputs.HonorOfThievesCritRate,
+			OtherInputs.TankAssignment,
+			OtherInputs.InFrontOfTarget,
+		],
+	},
+	encounterPicker: {
+		// Whether to include 'Execute Duration (%)' in the 'Encounter' section of the settings tab.
+		showExecuteProportion: false,
+	},
 
-			presets: {
-				// Preset talents that the user can quickly select.
-				talents: [
-					Presets.AssassinationTalents137,
-					Presets.AssassinationTalents182,
-					Presets.AssassinationTalentsBF,
-					Presets.CombatHackTalents,
-					Presets.CombatCQCTalents,
-					Presets.SubtletyTalents,
-					Presets.HemoSubtletyTalents,
-				],
-				// Preset rotations that the user can quickly select.
-				rotations: [
-					Presets.ROTATION_PRESET_MUTILATE,
-					Presets.ROTATION_PRESET_MUTILATE_EXPOSE,
-					Presets.ROTATION_PRESET_RUPTURE_MUTILATE,
-					Presets.ROTATION_PRESET_RUPTURE_MUTILATE_EXPOSE,
-					Presets.ROTATION_PRESET_COMBAT,
-					Presets.ROTATION_PRESET_COMBAT_EXPOSE,
-					Presets.ROTATION_PRESET_COMBAT_CLEAVE_SND,
-					Presets.ROTATION_PRESET_COMBAT_CLEAVE_SND_EXPOSE,
-					Presets.ROTATION_PRESET_AOE,
-				],
-				// Preset gear configurations that the user can quickly select.
-				gear: [
-					Presets.PRERAID_PRESET_ASSASSINATION,
-					Presets.PRERAID_PRESET_COMBAT,
-					Presets.P1_PRESET_ASSASSINATION,
-					Presets.P1_PRESET_COMBAT,
-					Presets.P1_PRESET_HEMO_SUB,
-					Presets.P2_PRESET_ASSASSINATION,
-					Presets.P2_PRESET_COMBAT,
-					Presets.P3_PRESET_ASSASSINATION,
-					Presets.P3_PRESET_COMBAT,
-					Presets.P4_PRESET_ASSASSINATION,
-					Presets.P4_PRESET_COMBAT,
-					Presets.P5_PRESET_ASSASSINATION,
-					Presets.P5_PRESET_COMBAT,
-					Presets.P2_PRESET_HEMO_SUB,
-					Presets.P3_PRESET_HEMO_SUB,
-					Presets.P3_PRESET_DANCE_SUB,
-				],
-			},
+	presets: {
+		// Preset talents that the user can quickly select.
+		talents: [
+			Presets.AssassinationTalents137,
+			Presets.AssassinationTalents182,
+			Presets.AssassinationTalentsBF,
+			Presets.CombatHackTalents,
+			Presets.CombatCQCTalents,
+			Presets.SubtletyTalents,
+			Presets.HemoSubtletyTalents,
+		],
+		// Preset rotations that the user can quickly select.
+		rotations: [
+			Presets.ROTATION_PRESET_MUTILATE,
+			Presets.ROTATION_PRESET_MUTILATE_EXPOSE,
+			Presets.ROTATION_PRESET_RUPTURE_MUTILATE,
+			Presets.ROTATION_PRESET_RUPTURE_MUTILATE_EXPOSE,
+			Presets.ROTATION_PRESET_COMBAT,
+			Presets.ROTATION_PRESET_COMBAT_EXPOSE,
+			Presets.ROTATION_PRESET_COMBAT_CLEAVE_SND,
+			Presets.ROTATION_PRESET_COMBAT_CLEAVE_SND_EXPOSE,
+			Presets.ROTATION_PRESET_AOE,
+		],
+		// Preset gear configurations that the user can quickly select.
+		gear: [
+			Presets.PRERAID_PRESET_ASSASSINATION,
+			Presets.PRERAID_PRESET_COMBAT,
+			Presets.P1_PRESET_ASSASSINATION,
+			Presets.P1_PRESET_COMBAT,
+			Presets.P1_PRESET_HEMO_SUB,
+			Presets.P2_PRESET_ASSASSINATION,
+			Presets.P2_PRESET_COMBAT,
+			Presets.P3_PRESET_ASSASSINATION,
+			Presets.P3_PRESET_COMBAT,
+			Presets.P4_PRESET_ASSASSINATION,
+			Presets.P4_PRESET_COMBAT,
+			Presets.P5_PRESET_ASSASSINATION,
+			Presets.P5_PRESET_COMBAT,
+			Presets.P2_PRESET_HEMO_SUB,
+			Presets.P3_PRESET_HEMO_SUB,
+			Presets.P3_PRESET_DANCE_SUB,
+		],
+	},
 
-			autoRotation: (player: Player<Spec.SpecRogue>): APLRotation => {
-				const talentTree = player.getTalentTree();
-				const numTargets = player.sim.encounter.targets.length;
-				if (numTargets >= 5) {
-					return Presets.ROTATION_PRESET_AOE.rotation.rotation!;
-				} else if (talentTree == 0) {
-					return Presets.ROTATION_PRESET_MUTILATE_EXPOSE.rotation.rotation!;
-				} else if (talentTree == 1) {
-					return Presets.ROTATION_PRESET_COMBAT_EXPOSE.rotation.rotation!;
-				} else {
-					// TODO: Need a sub rotation here
-					return Presets.ROTATION_PRESET_MUTILATE_EXPOSE.rotation.rotation!;
-				}
-			},
-		})
+	autoRotation: (player: Player<Spec.SpecRogue>): APLRotation => {
+		const talentTree = player.getTalentTree();
+		const numTargets = player.sim.encounter.targets.length;
+		if (numTargets >= 5) {
+			return Presets.ROTATION_PRESET_AOE.rotation.rotation!;
+		} else if (talentTree == 0) {
+			return Presets.ROTATION_PRESET_MUTILATE_EXPOSE.rotation.rotation!;
+		} else if (talentTree == 1) {
+			return Presets.ROTATION_PRESET_COMBAT_EXPOSE.rotation.rotation!;
+		} else {
+			// TODO: Need a sub rotation here
+			return Presets.ROTATION_PRESET_MUTILATE_EXPOSE.rotation.rotation!;
+		}
+	},
+});
+
+export class RogueSimUI extends IndividualSimUI<Spec.SpecRogue> {
+	constructor(parentElem: HTMLElement, player: Player<Spec.SpecRogue>) {
+		super(parentElem, player, SPEC_CONFIG);
 		this.player.changeEmitter.on((c) => {
 			const rotation = this.player.getRotation()
 			const options = this.player.getSpecOptions()
diff --git a/ui/shadow_priest/sim.ts b/ui/shadow_priest/sim.ts
index 676f209524..8a233e69c8 100644
--- a/ui/shadow_priest/sim.ts
+++ b/ui/shadow_priest/sim.ts
@@ -4,14 +4,12 @@ import {
 	Stat,
 } from '../core/proto/common.js';
 import {
-	APLAction,
-	APLListItem,
 	APLRotation,
 } from '../core/proto/apl.js';
 
 import { Stats } from '../core/proto_utils/stats.js';
 import { Player } from '../core/player.js';
-import { IndividualSimUI } from '../core/individual_sim_ui.js';
+import { IndividualSimUI, registerSpecConfig } from '../core/individual_sim_ui.js';
 import * as IconInputs from '../core/components/icon_inputs.js';
 import * as OtherInputs from '../core/components/other_inputs.js';
 import * as Mechanics from '../core/constants/mechanics.js';
@@ -19,149 +17,151 @@ import * as Mechanics from '../core/constants/mechanics.js';
 import * as ShadowPriestInputs from './inputs.js';
 import * as Presets from './presets.js';
 
-export class ShadowPriestSimUI extends IndividualSimUI<Spec.SpecShadowPriest> {
-	constructor(parentElem: HTMLElement, player: Player<Spec.SpecShadowPriest>) {
-		super(parentElem, player, {
-			cssClass: 'shadow-priest-sim-ui',
-			cssScheme: 'priest',
-			// List any known bugs / issues here and they'll be shown on the site.
-			knownIssues: [
-			],
+const SPEC_CONFIG = registerSpecConfig(Spec.SpecShadowPriest, {
+	cssClass: 'shadow-priest-sim-ui',
+	cssScheme: 'priest',
+	// List any known bugs / issues here and they'll be shown on the site.
+	knownIssues: [
+	],
+
+	// All stats for which EP should be calculated.
+	epStats: [
+		Stat.StatIntellect,
+		Stat.StatSpirit,
+		Stat.StatSpellPower,
+		Stat.StatSpellHit,
+		Stat.StatSpellCrit,
+		Stat.StatSpellHaste,
+		Stat.StatMP5,
+	],
+	// Reference stat against which to calculate EP. I think all classes use either spell power or attack power.
+	epReferenceStat: Stat.StatSpellPower,
+	// Which stats to display in the Character Stats section, at the bottom of the left-hand sidebar.
+	displayStats: [
+		Stat.StatHealth,
+		Stat.StatMana,
+		Stat.StatStamina,
+		Stat.StatIntellect,
+		Stat.StatSpirit,
+		Stat.StatSpellPower,
+		Stat.StatSpellHit,
+		Stat.StatSpellCrit,
+		Stat.StatSpellHaste,
+		Stat.StatMP5,
+	],
+	modifyDisplayStats: (player: Player<Spec.SpecShadowPriest>) => {
+		let stats = new Stats();
+		stats = stats.addStat(Stat.StatSpellHit, player.getTalents().shadowFocus * 1 * Mechanics.SPELL_HIT_RATING_PER_HIT_CHANCE);
 
-			// All stats for which EP should be calculated.
-			epStats: [
-				Stat.StatIntellect,
-				Stat.StatSpirit,
-				Stat.StatSpellPower,
-				Stat.StatSpellHit,
-				Stat.StatSpellCrit,
-				Stat.StatSpellHaste,
-				Stat.StatMP5,
-			],
-			// Reference stat against which to calculate EP. I think all classes use either spell power or attack power.
-			epReferenceStat: Stat.StatSpellPower,
-			// Which stats to display in the Character Stats section, at the bottom of the left-hand sidebar.
-			displayStats: [
-				Stat.StatHealth,
-				Stat.StatMana,
-				Stat.StatStamina,
-				Stat.StatIntellect,
-				Stat.StatSpirit,
-				Stat.StatSpellPower,
-				Stat.StatSpellHit,
-				Stat.StatSpellCrit,
-				Stat.StatSpellHaste,
-				Stat.StatMP5,
-			],
-			modifyDisplayStats: (player: Player<Spec.SpecShadowPriest>) => {
-				let stats = new Stats();
-				stats = stats.addStat(Stat.StatSpellHit, player.getTalents().shadowFocus * 1 * Mechanics.SPELL_HIT_RATING_PER_HIT_CHANCE);
+		return {
+			talents: stats,
+		};
+	},
 
-				return {
-					talents: stats,
-				};
-			},
+	defaults: {
+		// Default equipped gear.
+		gear: Presets.P4_PRESET.gear,
+		// Default EP weights for sorting gear in the gear picker.
+		epWeights: Stats.fromMap({
+			[Stat.StatIntellect]: 0.11,
+			[Stat.StatSpirit]: 0.47,
+			[Stat.StatSpellPower]: 1,
+			[Stat.StatSpellHit]: 0.87,
+			[Stat.StatSpellCrit]: 0.74,
+			[Stat.StatSpellHaste]: 1.65,
+			[Stat.StatMP5]: 0.00,
+		}),
+		// Default consumes settings.
+		consumes: Presets.DefaultConsumes,
+		// Default rotation settings.
+		rotation: Presets.DefaultRotation,
+		// Default talents.
+		talents: Presets.StandardTalents.data,
+		// Default spec-specific settings.
+		specOptions: Presets.DefaultOptions,
+		// Default raid/party buffs settings.
+		raidBuffs: Presets.DefaultRaidBuffs,
 
-			defaults: {
-				// Default equipped gear.
-				gear: Presets.P4_PRESET.gear,
-				// Default EP weights for sorting gear in the gear picker.
-				epWeights: Stats.fromMap({
-					[Stat.StatIntellect]: 0.11,
-					[Stat.StatSpirit]: 0.47,
-					[Stat.StatSpellPower]: 1,
-					[Stat.StatSpellHit]: 0.87,
-					[Stat.StatSpellCrit]: 0.74,
-					[Stat.StatSpellHaste]: 1.65,
-					[Stat.StatMP5]: 0.00,
-				}),
-				// Default consumes settings.
-				consumes: Presets.DefaultConsumes,
-				// Default rotation settings.
-				rotation: Presets.DefaultRotation,
-				// Default talents.
-				talents: Presets.StandardTalents.data,
-				// Default spec-specific settings.
-				specOptions: Presets.DefaultOptions,
-				// Default raid/party buffs settings.
-				raidBuffs: Presets.DefaultRaidBuffs,
+		partyBuffs: PartyBuffs.create({}),
 
-				partyBuffs: PartyBuffs.create({}),
+		individualBuffs: Presets.DefaultIndividualBuffs,
 
-				individualBuffs: Presets.DefaultIndividualBuffs,
+		debuffs: Presets.DefaultDebuffs,
 
-				debuffs: Presets.DefaultDebuffs,
+		other: Presets.OtherDefaults,
+	},
 
-				other: Presets.OtherDefaults,
-			},
+	// IconInputs to include in the 'Player' section on the settings tab.
+	playerIconInputs: [
+		ShadowPriestInputs.ArmorInput,
+	],
+	rotationIconInputs: [
+		ShadowPriestInputs.MindBlastInput,
+		ShadowPriestInputs.ShadowWordDeathInput,
+		ShadowPriestInputs.ShadowfiendInput,
+	],
+	// Inputs to include in the 'Rotation' section on the settings tab.
+	rotationInputs: ShadowPriestInputs.ShadowPriestRotationConfig,
+	// Buff and Debuff inputs to include/exclude, overriding the EP-based defaults.
+	includeBuffDebuffInputs: [
+		IconInputs.ReplenishmentBuff,
+		IconInputs.MeleeHasteBuff,
+		IconInputs.MeleeCritBuff,
+		IconInputs.MP5Buff,
+		IconInputs.AttackPowerPercentBuff,
+		IconInputs.AttackPowerBuff,
+		IconInputs.StaminaBuff,
+	],
+	excludeBuffDebuffInputs: [
+	],
+	// Inputs to include in the 'Other' section on the settings tab.
+	otherInputs: {
+		inputs: [
+			OtherInputs.TankAssignment,
+			OtherInputs.ChannelClipDelay,
+			OtherInputs.nibelungAverageCasts,
+		],
+	},
+	encounterPicker: {
+		// Whether to include 'Execute Duration (%)' in the 'Encounter' section of the settings tab.
+		showExecuteProportion: false,
+	},
 
-			// IconInputs to include in the 'Player' section on the settings tab.
-			playerIconInputs: [
-				ShadowPriestInputs.ArmorInput,
-			],
-			rotationIconInputs: [
-				ShadowPriestInputs.MindBlastInput,
-				ShadowPriestInputs.ShadowWordDeathInput,
-				ShadowPriestInputs.ShadowfiendInput,
-			],
-			// Inputs to include in the 'Rotation' section on the settings tab.
-			rotationInputs: ShadowPriestInputs.ShadowPriestRotationConfig,
-			// Buff and Debuff inputs to include/exclude, overriding the EP-based defaults.
-			includeBuffDebuffInputs: [
-				IconInputs.ReplenishmentBuff,
-				IconInputs.MeleeHasteBuff,
-				IconInputs.MeleeCritBuff,
-				IconInputs.MP5Buff,
-				IconInputs.AttackPowerPercentBuff,
-				IconInputs.AttackPowerBuff,
-				IconInputs.StaminaBuff,
-			],
-			excludeBuffDebuffInputs: [
-			],
-			// Inputs to include in the 'Other' section on the settings tab.
-			otherInputs: {
-				inputs: [
-					OtherInputs.TankAssignment,
-					OtherInputs.ChannelClipDelay,
-					OtherInputs.nibelungAverageCasts,
-				],
-			},
-			encounterPicker: {
-				// Whether to include 'Execute Duration (%)' in the 'Encounter' section of the settings tab.
-				showExecuteProportion: false,
-			},
+	presets: {
+		// Preset talents that the user can quickly select.
+		talents: [
+			Presets.StandardTalents,
+			Presets.EnlightenmentTalents,
+		],
+		rotations: [
+			Presets.ROTATION_PRESET_DEFAULT,
+			Presets.ROTATION_PRESET_AOE24,
+			Presets.ROTATION_PRESET_AOE4PLUS,
+		],
+		// Preset gear configurations that the user can quickly select.
+		gear: [
+			Presets.PRERAID_PRESET,
+			Presets.P1_PRESET,
+			Presets.P2_PRESET,
+			Presets.P3_PRESET,
+			Presets.P4_PRESET,
+		],
+	},
 
-			presets: {
-				// Preset talents that the user can quickly select.
-				talents: [
-					Presets.StandardTalents,
-					Presets.EnlightenmentTalents,
-				],
-				rotations: [
-					Presets.ROTATION_PRESET_DEFAULT,
-					Presets.ROTATION_PRESET_AOE24,
-					Presets.ROTATION_PRESET_AOE4PLUS,
-				],
-				// Preset gear configurations that the user can quickly select.
-				gear: [
-					Presets.PRERAID_PRESET,
-					Presets.P1_PRESET,
-					Presets.P2_PRESET,
-					Presets.P3_PRESET,
-					Presets.P4_PRESET,
-				],
-			},
+	autoRotation: (player: Player<Spec.SpecShadowPriest>): APLRotation => {
+		const numTargets = player.sim.encounter.targets.length;
+		if (numTargets > 4) {
+			return Presets.ROTATION_PRESET_AOE4PLUS.rotation.rotation!;
+		} else if (numTargets > 1) {
+			return Presets.ROTATION_PRESET_AOE24.rotation.rotation!;
+		} else {
+			return Presets.ROTATION_PRESET_DEFAULT.rotation.rotation!;
+		}
+	},
+});
 
-			autoRotation: (player: Player<Spec.SpecShadowPriest>): APLRotation => {
-				const numTargets = player.sim.encounter.targets.length;
-				if (numTargets > 4) {
-					return Presets.ROTATION_PRESET_AOE4PLUS.rotation.rotation!;
-				} else if (numTargets > 1) {
-					return Presets.ROTATION_PRESET_AOE24.rotation.rotation!;
-				} else {
-					return Presets.ROTATION_PRESET_DEFAULT.rotation.rotation!;
-				}
-			},
-		});
+export class ShadowPriestSimUI extends IndividualSimUI<Spec.SpecShadowPriest> {
+	constructor(parentElem: HTMLElement, player: Player<Spec.SpecShadowPriest>) {
+		super(parentElem, player, SPEC_CONFIG);
 	}
 }
diff --git a/ui/smite_priest/sim.ts b/ui/smite_priest/sim.ts
index 1a30f4e262..31e6687a9c 100644
--- a/ui/smite_priest/sim.ts
+++ b/ui/smite_priest/sim.ts
@@ -6,7 +6,7 @@ import {
 } from '../core/proto/apl.js';
 import { Stats } from '../core/proto_utils/stats.js';
 import { Player } from '../core/player.js';
-import { IndividualSimUI } from '../core/individual_sim_ui.js';
+import { IndividualSimUI, registerSpecConfig } from '../core/individual_sim_ui.js';
 
 import * as OtherInputs from '../core/components/other_inputs.js';
 import * as Mechanics from '../core/constants/mechanics.js';
@@ -14,120 +14,122 @@ import * as Mechanics from '../core/constants/mechanics.js';
 import * as SmitePriestInputs from './inputs.js';
 import * as Presets from './presets.js';
 
-export class SmitePriestSimUI extends IndividualSimUI<Spec.SpecSmitePriest> {
-	constructor(parentElem: HTMLElement, player: Player<Spec.SpecSmitePriest>) {
-		super(parentElem, player, {
-			cssClass: 'smite-priest-sim-ui',
-			cssScheme: 'priest',
-			// List any known bugs / issues here and they'll be shown on the site.
-			knownIssues: [
-			],
+const SPEC_CONFIG = registerSpecConfig(Spec.SpecSmitePriest, {
+	cssClass: 'smite-priest-sim-ui',
+	cssScheme: 'priest',
+	// List any known bugs / issues here and they'll be shown on the site.
+	knownIssues: [
+	],
+
+	// All stats for which EP should be calculated.
+	epStats: [
+		Stat.StatIntellect,
+		Stat.StatSpirit,
+		Stat.StatSpellPower,
+		Stat.StatSpellHit,
+		Stat.StatSpellCrit,
+		Stat.StatSpellHaste,
+		Stat.StatMP5,
+	],
+	// Reference stat against which to calculate EP. I think all classes use either spell power or attack power.
+	epReferenceStat: Stat.StatSpellPower,
+	// Which stats to display in the Character Stats section, at the bottom of the left-hand sidebar.
+	displayStats: [
+		Stat.StatHealth,
+		Stat.StatStamina,
+		Stat.StatIntellect,
+		Stat.StatSpirit,
+		Stat.StatSpellPower,
+		Stat.StatSpellHit,
+		Stat.StatSpellCrit,
+		Stat.StatSpellHaste,
+		Stat.StatMP5,
+	],
+	modifyDisplayStats: (player: Player<Spec.SpecSmitePriest>) => {
+		let stats = new Stats();
+		stats = stats.addStat(Stat.StatSpellHit, player.getTalents().shadowFocus * 1 * Mechanics.SPELL_HIT_RATING_PER_HIT_CHANCE);
 
-			// All stats for which EP should be calculated.
-			epStats: [
-				Stat.StatIntellect,
-				Stat.StatSpirit,
-				Stat.StatSpellPower,
-				Stat.StatSpellHit,
-				Stat.StatSpellCrit,
-				Stat.StatSpellHaste,
-				Stat.StatMP5,
-			],
-			// Reference stat against which to calculate EP. I think all classes use either spell power or attack power.
-			epReferenceStat: Stat.StatSpellPower,
-			// Which stats to display in the Character Stats section, at the bottom of the left-hand sidebar.
-			displayStats: [
-				Stat.StatHealth,
-				Stat.StatStamina,
-				Stat.StatIntellect,
-				Stat.StatSpirit,
-				Stat.StatSpellPower,
-				Stat.StatSpellHit,
-				Stat.StatSpellCrit,
-				Stat.StatSpellHaste,
-				Stat.StatMP5,
-			],
-			modifyDisplayStats: (player: Player<Spec.SpecSmitePriest>) => {
-				let stats = new Stats();
-				stats = stats.addStat(Stat.StatSpellHit, player.getTalents().shadowFocus * 1 * Mechanics.SPELL_HIT_RATING_PER_HIT_CHANCE);
+		return {
+			talents: stats,
+		};
+	},
 
-				return {
-					talents: stats,
-				};
-			},
+	defaults: {
+		// Default equipped gear.
+		gear: Presets.P1_PRESET.gear,
+		// Default EP weights for sorting gear in the gear picker.
+		epWeights: Stats.fromMap({
+			[Stat.StatIntellect]: 0.38,
+			[Stat.StatSpirit]: 0.38,
+			[Stat.StatSpellPower]: 1,
+			[Stat.StatSpellHit]: 1.65,
+			[Stat.StatSpellCrit]: 0.32,
+			[Stat.StatSpellHaste]: 0.78,
+			[Stat.StatMP5]: 0.35,
+		}),
+		// Default consumes settings.
+		consumes: Presets.DefaultConsumes,
+		// Default rotation settings.
+		rotation: Presets.DefaultRotation,
+		// Default talents.
+		talents: Presets.StandardTalents.data,
+		// Default spec-specific settings.
+		specOptions: Presets.DefaultOptions,
+		// Default raid/party buffs settings.
+		raidBuffs: Presets.DefaultRaidBuffs,
+		partyBuffs: PartyBuffs.create({}),
+		individualBuffs: Presets.DefaultIndividualBuffs,
+		debuffs: Presets.DefaultDebuffs,
+	},
 
-			defaults: {
-				// Default equipped gear.
-				gear: Presets.P1_PRESET.gear,
-				// Default EP weights for sorting gear in the gear picker.
-				epWeights: Stats.fromMap({
-					[Stat.StatIntellect]: 0.38,
-					[Stat.StatSpirit]: 0.38,
-					[Stat.StatSpellPower]: 1,
-					[Stat.StatSpellHit]: 1.65,
-					[Stat.StatSpellCrit]: 0.32,
-					[Stat.StatSpellHaste]: 0.78,
-					[Stat.StatMP5]: 0.35,
-				}),
-				// Default consumes settings.
-				consumes: Presets.DefaultConsumes,
-				// Default rotation settings.
-				rotation: Presets.DefaultRotation,
-				// Default talents.
-				talents: Presets.StandardTalents.data,
-				// Default spec-specific settings.
-				specOptions: Presets.DefaultOptions,
-				// Default raid/party buffs settings.
-				raidBuffs: Presets.DefaultRaidBuffs,
-				partyBuffs: PartyBuffs.create({}),
-				individualBuffs: Presets.DefaultIndividualBuffs,
-				debuffs: Presets.DefaultDebuffs,
-			},
+	// IconInputs to include in the 'Player' section on the settings tab.
+	playerIconInputs: [
+		SmitePriestInputs.SelfPowerInfusion,
+		SmitePriestInputs.InnerFire,
+		SmitePriestInputs.Shadowfiend,
+	],
+	// Inputs to include in the 'Rotation' section on the settings tab.
+	rotationInputs: SmitePriestInputs.SmitePriestRotationConfig,
+	// Buff and Debuff inputs to include/exclude, overriding the EP-based defaults.
+	includeBuffDebuffInputs: [
+	],
+	excludeBuffDebuffInputs: [
+	],
+	// Inputs to include in the 'Other' section on the settings tab.
+	otherInputs: {
+		inputs: [
+			OtherInputs.TankAssignment,
+		],
+	},
+	encounterPicker: {
+		// Whether to include 'Execute Duration (%)' in the 'Encounter' section of the settings tab.
+		showExecuteProportion: false,
+	},
 
-			// IconInputs to include in the 'Player' section on the settings tab.
-			playerIconInputs: [
-				SmitePriestInputs.SelfPowerInfusion,
-				SmitePriestInputs.InnerFire,
-				SmitePriestInputs.Shadowfiend,
-			],
-			// Inputs to include in the 'Rotation' section on the settings tab.
-			rotationInputs: SmitePriestInputs.SmitePriestRotationConfig,
-			// Buff and Debuff inputs to include/exclude, overriding the EP-based defaults.
-			includeBuffDebuffInputs: [
-			],
-			excludeBuffDebuffInputs: [
-			],
-			// Inputs to include in the 'Other' section on the settings tab.
-			otherInputs: {
-				inputs: [
-					OtherInputs.TankAssignment,
-				],
-			},
-			encounterPicker: {
-				// Whether to include 'Execute Duration (%)' in the 'Encounter' section of the settings tab.
-				showExecuteProportion: false,
-			},
+	presets: {
+		// Preset talents that the user can quickly select.
+		talents: [
+			Presets.StandardTalents,
+		],
+		// Preset rotations that the user can quickly select.
+		rotations: [
+			Presets.ROTATION_PRESET_LEGACY_DEFAULT,
+			Presets.ROTATION_PRESET_APL,
+		],
+		// Preset gear configurations that the user can quickly select.
+		gear: [
+			Presets.PRERAID_PRESET,
+			Presets.P1_PRESET,
+		],
+	},
 
-			presets: {
-				// Preset talents that the user can quickly select.
-				talents: [
-					Presets.StandardTalents,
-				],
-				// Preset rotations that the user can quickly select.
-				rotations: [
-					Presets.ROTATION_PRESET_LEGACY_DEFAULT,
-					Presets.ROTATION_PRESET_APL,
-				],
-				// Preset gear configurations that the user can quickly select.
-				gear: [
-					Presets.PRERAID_PRESET,
-					Presets.P1_PRESET,
-				],
-			},
+	autoRotation: (_player: Player<Spec.SpecSmitePriest>): APLRotation => {
+		return Presets.ROTATION_PRESET_APL.rotation.rotation!;
+	},
+});
 
-			autoRotation: (_player: Player<Spec.SpecSmitePriest>): APLRotation => {
-				return Presets.ROTATION_PRESET_APL.rotation.rotation!;
-			},
-		});
+export class SmitePriestSimUI extends IndividualSimUI<Spec.SpecSmitePriest> {
+	constructor(parentElem: HTMLElement, player: Player<Spec.SpecSmitePriest>) {
+		super(parentElem, player, SPEC_CONFIG);
 	}
 }
diff --git a/ui/tank_deathknight/sim.ts b/ui/tank_deathknight/sim.ts
index f2a89e3f44..6e41515bc9 100644
--- a/ui/tank_deathknight/sim.ts
+++ b/ui/tank_deathknight/sim.ts
@@ -6,216 +6,213 @@ import { Spec } from '../core/proto/common.js';
 import { Stat, PseudoStat } from '../core/proto/common.js';
 import { TristateEffect } from '../core/proto/common.js'
 import {
-	APLAction,
-	APLListItem,
 	APLRotation,
 } from '../core/proto/apl.js';
 import { Player } from '../core/player.js';
 import { Stats } from '../core/proto_utils/stats.js';
-import { IndividualSimUI } from '../core/individual_sim_ui.js';
-
-import { TankDeathknight, TankDeathknight_Rotation as DeathKnightRotation, DeathknightTalents as DeathKnightTalents, TankDeathknight_Options as DeathKnightOptions } from '../core/proto/deathknight.js';
+import { IndividualSimUI, registerSpecConfig } from '../core/individual_sim_ui.js';
 
 import * as IconInputs from '../core/components/icon_inputs.js';
 import * as OtherInputs from '../core/components/other_inputs.js';
-import * as Tooltips from '../core/constants/tooltips.js';
 
 import * as DeathKnightInputs from './inputs.js';
 import * as Presets from './presets.js';
 
-export class TankDeathknightSimUI extends IndividualSimUI<Spec.SpecTankDeathknight> {
-	constructor(parentElem: HTMLElement, player: Player<Spec.SpecTankDeathknight>) {
-		super(parentElem, player, {
-			cssClass: 'tank-deathknight-sim-ui',
-			cssScheme: 'death-knight',
-			// List any known bugs / issues here and they'll be shown on the site.
-			knownIssues: [
-				"<p>Defensive CDs use is very basic and wip.</p>"
-			],
+const SPEC_CONFIG = registerSpecConfig(Spec.SpecTankDeathknight, {
+	cssClass: 'tank-deathknight-sim-ui',
+	cssScheme: 'death-knight',
+	// List any known bugs / issues here and they'll be shown on the site.
+	knownIssues: [
+		"<p>Defensive CDs use is very basic and wip.</p>"
+	],
 
-			// All stats for which EP should be calculated.
-			epStats: [
-				Stat.StatStamina,
-				Stat.StatStrength,
-				Stat.StatAgility,
-				Stat.StatAttackPower,
-				Stat.StatExpertise,
-				Stat.StatMeleeHit,
-				Stat.StatMeleeCrit,
-				Stat.StatMeleeHaste,
-				Stat.StatSpellHit,
-				Stat.StatSpellCrit,
-				Stat.StatSpellHaste,
-				Stat.StatHealth,
-				Stat.StatArmor,
-				Stat.StatBonusArmor,
-				Stat.StatArmorPenetration,
-				Stat.StatDefense,
-				Stat.StatDodge,
-				Stat.StatParry,
-				Stat.StatResilience,
-				Stat.StatSpellHit,
-				Stat.StatNatureResistance,
-				Stat.StatShadowResistance,
-				Stat.StatFrostResistance,
-			],
-			epPseudoStats: [
-				PseudoStat.PseudoStatMainHandDps,
-				PseudoStat.PseudoStatOffHandDps,
-			],
-			// Reference stat against which to calculate EP. I think all classes use either spell power or attack power.
-			epReferenceStat: Stat.StatAttackPower,
-			// Which stats to display in the Character Stats section, at the bottom of the left-hand sidebar.
-			displayStats: [
-				Stat.StatHealth,
-				Stat.StatArmor,
-				Stat.StatBonusArmor,
-				Stat.StatStamina,
-				Stat.StatStrength,
-				Stat.StatAgility,
-				Stat.StatAttackPower,
-				Stat.StatExpertise,
-				Stat.StatSpellHit,
-				Stat.StatSpellCrit,
-				Stat.StatMeleeHit,
-				Stat.StatMeleeCrit,
-				Stat.StatMeleeHaste,
-				Stat.StatArmorPenetration,
-				Stat.StatDefense,
-				Stat.StatDodge,
-				Stat.StatParry,
-				Stat.StatResilience,
-				Stat.StatNatureResistance,
-				Stat.StatShadowResistance,
-				Stat.StatFrostResistance,
-			],
-			defaults: {
-				// Default equipped gear.
-				gear: Presets.P2_BLOOD_PRESET.gear,
-				// Default EP weights for sorting gear in the gear picker.
-				epWeights: Stats.fromMap({
-					[Stat.StatArmor]: 0.05,
-					[Stat.StatBonusArmor]: 0.03,
-					[Stat.StatStamina]: 1,
-					[Stat.StatStrength]: 0.33,
-					[Stat.StatAgility]: 0.6,
-					[Stat.StatAttackPower]: 0.06,
-					[Stat.StatExpertise]: 0.67,
-					[Stat.StatMeleeHit]: 0.67,
-					[Stat.StatMeleeCrit]: 0.28,
-					[Stat.StatMeleeHaste]: 0.21,
-					[Stat.StatArmorPenetration]: 0.19,
-					[Stat.StatBlock]: 0.35,
-					[Stat.StatBlockValue]: 0.59,
-					[Stat.StatDodge]: 0.7,
-					[Stat.StatParry]: 0.58,
-					[Stat.StatDefense]: 0.8,
-				}, {
-					[PseudoStat.PseudoStatMainHandDps]: 3.10,
-					[PseudoStat.PseudoStatOffHandDps]: 0.0,
-				}),
-				// Default consumes settings.
-				consumes: Presets.DefaultConsumes,
-				// Default rotation settings.
-				rotation: Presets.DefaultRotation,
-				// Default talents.
-				talents: Presets.BloodTalents.data,
-				// Default spec-specific settings.
-				specOptions: Presets.DefaultOptions,
-				// Default raid/party buffs settings.
-				raidBuffs: RaidBuffs.create({
-					retributionAura: true,
-					powerWordFortitude: TristateEffect.TristateEffectImproved,
-					giftOfTheWild: TristateEffect.TristateEffectImproved,
-					swiftRetribution: true,
-					strengthOfEarthTotem: TristateEffect.TristateEffectImproved,
-					icyTalons: true,
-					abominationsMight: true,
-					leaderOfThePack: TristateEffect.TristateEffectRegular,
-					sanctifiedRetribution: true,
-					bloodlust: true,
-					devotionAura: TristateEffect.TristateEffectImproved,
-					stoneskinTotem: TristateEffect.TristateEffectImproved,
-				}),
-				partyBuffs: PartyBuffs.create({
-				}),
-				individualBuffs: IndividualBuffs.create({
-					blessingOfKings: true,
-					blessingOfMight: TristateEffect.TristateEffectImproved,
-					blessingOfSanctuary: true,
-				}),
-				debuffs: Debuffs.create({
-					bloodFrenzy: true,
-					faerieFire: TristateEffect.TristateEffectRegular,
-					sunderArmor: true,
-					misery: true,
-					ebonPlaguebringer: true,
-					mangle: true,
-					heartOfTheCrusader: true,
-					demoralizingShout: TristateEffect.TristateEffectImproved,
-					frostFever: TristateEffect.TristateEffectImproved,
-					insectSwarm: true,
-					judgementOfLight: true,
-				}),
-			},
+	// All stats for which EP should be calculated.
+	epStats: [
+		Stat.StatStamina,
+		Stat.StatStrength,
+		Stat.StatAgility,
+		Stat.StatAttackPower,
+		Stat.StatExpertise,
+		Stat.StatMeleeHit,
+		Stat.StatMeleeCrit,
+		Stat.StatMeleeHaste,
+		Stat.StatSpellHit,
+		Stat.StatSpellCrit,
+		Stat.StatSpellHaste,
+		Stat.StatHealth,
+		Stat.StatArmor,
+		Stat.StatBonusArmor,
+		Stat.StatArmorPenetration,
+		Stat.StatDefense,
+		Stat.StatDodge,
+		Stat.StatParry,
+		Stat.StatResilience,
+		Stat.StatSpellHit,
+		Stat.StatNatureResistance,
+		Stat.StatShadowResistance,
+		Stat.StatFrostResistance,
+	],
+	epPseudoStats: [
+		PseudoStat.PseudoStatMainHandDps,
+		PseudoStat.PseudoStatOffHandDps,
+	],
+	// Reference stat against which to calculate EP. I think all classes use either spell power or attack power.
+	epReferenceStat: Stat.StatAttackPower,
+	// Which stats to display in the Character Stats section, at the bottom of the left-hand sidebar.
+	displayStats: [
+		Stat.StatHealth,
+		Stat.StatArmor,
+		Stat.StatBonusArmor,
+		Stat.StatStamina,
+		Stat.StatStrength,
+		Stat.StatAgility,
+		Stat.StatAttackPower,
+		Stat.StatExpertise,
+		Stat.StatSpellHit,
+		Stat.StatSpellCrit,
+		Stat.StatMeleeHit,
+		Stat.StatMeleeCrit,
+		Stat.StatMeleeHaste,
+		Stat.StatArmorPenetration,
+		Stat.StatDefense,
+		Stat.StatDodge,
+		Stat.StatParry,
+		Stat.StatResilience,
+		Stat.StatNatureResistance,
+		Stat.StatShadowResistance,
+		Stat.StatFrostResistance,
+	],
+	defaults: {
+		// Default equipped gear.
+		gear: Presets.P2_BLOOD_PRESET.gear,
+		// Default EP weights for sorting gear in the gear picker.
+		epWeights: Stats.fromMap({
+			[Stat.StatArmor]: 0.05,
+			[Stat.StatBonusArmor]: 0.03,
+			[Stat.StatStamina]: 1,
+			[Stat.StatStrength]: 0.33,
+			[Stat.StatAgility]: 0.6,
+			[Stat.StatAttackPower]: 0.06,
+			[Stat.StatExpertise]: 0.67,
+			[Stat.StatMeleeHit]: 0.67,
+			[Stat.StatMeleeCrit]: 0.28,
+			[Stat.StatMeleeHaste]: 0.21,
+			[Stat.StatArmorPenetration]: 0.19,
+			[Stat.StatBlock]: 0.35,
+			[Stat.StatBlockValue]: 0.59,
+			[Stat.StatDodge]: 0.7,
+			[Stat.StatParry]: 0.58,
+			[Stat.StatDefense]: 0.8,
+		}, {
+			[PseudoStat.PseudoStatMainHandDps]: 3.10,
+			[PseudoStat.PseudoStatOffHandDps]: 0.0,
+		}),
+		// Default consumes settings.
+		consumes: Presets.DefaultConsumes,
+		// Default rotation settings.
+		rotation: Presets.DefaultRotation,
+		// Default talents.
+		talents: Presets.BloodTalents.data,
+		// Default spec-specific settings.
+		specOptions: Presets.DefaultOptions,
+		// Default raid/party buffs settings.
+		raidBuffs: RaidBuffs.create({
+			retributionAura: true,
+			powerWordFortitude: TristateEffect.TristateEffectImproved,
+			giftOfTheWild: TristateEffect.TristateEffectImproved,
+			swiftRetribution: true,
+			strengthOfEarthTotem: TristateEffect.TristateEffectImproved,
+			icyTalons: true,
+			abominationsMight: true,
+			leaderOfThePack: TristateEffect.TristateEffectRegular,
+			sanctifiedRetribution: true,
+			bloodlust: true,
+			devotionAura: TristateEffect.TristateEffectImproved,
+			stoneskinTotem: TristateEffect.TristateEffectImproved,
+		}),
+		partyBuffs: PartyBuffs.create({
+		}),
+		individualBuffs: IndividualBuffs.create({
+			blessingOfKings: true,
+			blessingOfMight: TristateEffect.TristateEffectImproved,
+			blessingOfSanctuary: true,
+		}),
+		debuffs: Debuffs.create({
+			bloodFrenzy: true,
+			faerieFire: TristateEffect.TristateEffectRegular,
+			sunderArmor: true,
+			misery: true,
+			ebonPlaguebringer: true,
+			mangle: true,
+			heartOfTheCrusader: true,
+			demoralizingShout: TristateEffect.TristateEffectImproved,
+			frostFever: TristateEffect.TristateEffectImproved,
+			insectSwarm: true,
+			judgementOfLight: true,
+		}),
+	},
 
-			// IconInputs to include in the 'Player' section on the settings tab.
-			playerIconInputs: [
-			],
-			// Inputs to include in the 'Rotation' section on the settings tab.
-			rotationInputs: DeathKnightInputs.TankDeathKnightRotationConfig,
-			// Buff and Debuff inputs to include/exclude, overriding the EP-based defaults.
-			includeBuffDebuffInputs: [
-				IconInputs.SpellDamageDebuff,
-			],
-			excludeBuffDebuffInputs: [
-			],
-			// Inputs to include in the 'Other' section on the settings tab.
-			otherInputs: {
-				inputs: [
-					OtherInputs.TankAssignment,
-					OtherInputs.HpPercentForDefensives,
-					OtherInputs.IncomingHps,
-					OtherInputs.HealingCadence,
-					OtherInputs.HealingCadenceVariation,
-					OtherInputs.BurstWindow,
-					OtherInputs.InspirationUptime,
-					OtherInputs.InFrontOfTarget,
-					DeathKnightInputs.StartingRunicPower,
-				],
-			},
-			encounterPicker: {
-				// Whether to include 'Execute Duration (%)' in the 'Encounter' section of the settings tab.
-				showExecuteProportion: false,
-			},
+	// IconInputs to include in the 'Player' section on the settings tab.
+	playerIconInputs: [
+	],
+	// Inputs to include in the 'Rotation' section on the settings tab.
+	rotationInputs: DeathKnightInputs.TankDeathKnightRotationConfig,
+	// Buff and Debuff inputs to include/exclude, overriding the EP-based defaults.
+	includeBuffDebuffInputs: [
+		IconInputs.SpellDamageDebuff,
+	],
+	excludeBuffDebuffInputs: [
+	],
+	// Inputs to include in the 'Other' section on the settings tab.
+	otherInputs: {
+		inputs: [
+			OtherInputs.TankAssignment,
+			OtherInputs.HpPercentForDefensives,
+			OtherInputs.IncomingHps,
+			OtherInputs.HealingCadence,
+			OtherInputs.HealingCadenceVariation,
+			OtherInputs.BurstWindow,
+			OtherInputs.InspirationUptime,
+			OtherInputs.InFrontOfTarget,
+			DeathKnightInputs.StartingRunicPower,
+		],
+	},
+	encounterPicker: {
+		// Whether to include 'Execute Duration (%)' in the 'Encounter' section of the settings tab.
+		showExecuteProportion: false,
+	},
 
-			presets: {
-				// Preset rotations that the user can quickly select.
-				rotations: [
-					Presets.BLOOD_LEGACY_PRESET_LEGACY_DEFAULT,
-					Presets.BLOOD_IT_SPAM_ROTATION_PRESET_DEFAULT,
-					Presets.BLOOD_AGGRO_ROTATION_PRESET_DEFAULT,
-				],
-				// Preset talents that the user can quickly select.
-				talents: [
-					Presets.BloodTalents,
-					Presets.BloodAggroTalents,
-					Presets.DoubleBuffBloodTalents,
-					Presets.FrostTalents,
-					Presets.DoubleBuffFrostTalents,
-				],
-				// Preset gear configurations that the user can quickly select.
-				gear: [
-					Presets.P1_BLOOD_PRESET,
-					Presets.P1_FROST_PRESET,
-					Presets.P2_BLOOD_PRESET,
-					Presets.P2_FROST_PRESET,
-				],
-			},
+	presets: {
+		// Preset rotations that the user can quickly select.
+		rotations: [
+			Presets.BLOOD_LEGACY_PRESET_LEGACY_DEFAULT,
+			Presets.BLOOD_IT_SPAM_ROTATION_PRESET_DEFAULT,
+			Presets.BLOOD_AGGRO_ROTATION_PRESET_DEFAULT,
+		],
+		// Preset talents that the user can quickly select.
+		talents: [
+			Presets.BloodTalents,
+			Presets.BloodAggroTalents,
+			Presets.DoubleBuffBloodTalents,
+			Presets.FrostTalents,
+			Presets.DoubleBuffFrostTalents,
+		],
+		// Preset gear configurations that the user can quickly select.
+		gear: [
+			Presets.P1_BLOOD_PRESET,
+			Presets.P1_FROST_PRESET,
+			Presets.P2_BLOOD_PRESET,
+			Presets.P2_FROST_PRESET,
+		],
+	},
 
-			autoRotation: (player: Player<Spec.SpecTankDeathknight>): APLRotation => {
-				return Presets.BLOOD_IT_SPAM_ROTATION_PRESET_DEFAULT.rotation.rotation!;
-			},
-		});
+	autoRotation: (_player: Player<Spec.SpecTankDeathknight>): APLRotation => {
+		return Presets.BLOOD_IT_SPAM_ROTATION_PRESET_DEFAULT.rotation.rotation!;
+	},
+});
+
+export class TankDeathknightSimUI extends IndividualSimUI<Spec.SpecTankDeathknight> {
+	constructor(parentElem: HTMLElement, player: Player<Spec.SpecTankDeathknight>) {
+		super(parentElem, player, SPEC_CONFIG);
 	}
 }
diff --git a/ui/warlock/sim.ts b/ui/warlock/sim.ts
index cb3fc6c01f..9449b02034 100644
--- a/ui/warlock/sim.ts
+++ b/ui/warlock/sim.ts
@@ -4,186 +4,186 @@ import {
 	Stat,
 } from '../core/proto/common.js';
 import {
-	APLAction,
-	APLListItem,
 	APLRotation,
 } from '../core/proto/apl.js';
 
 import { Stats } from '../core/proto_utils/stats.js';
 import { Player } from '../core/player.js';
-import { IndividualSimUI } from '../core/individual_sim_ui.js';
+import { IndividualSimUI, registerSpecConfig } from '../core/individual_sim_ui.js';
 import * as IconInputs from '../core/components/icon_inputs.js';
 import * as OtherInputs from '../core/components/other_inputs.js';
 import * as WarlockInputs from './inputs.js';
 import * as Presets from './presets.js';
 
+const SPEC_CONFIG = registerSpecConfig(Spec.SpecWarlock, {
+	cssClass: 'warlock-sim-ui',
+	cssScheme: 'warlock',
+	// List any known bugs / issues here and they'll be shown on the site.
+	knownIssues: [
+		"Drain Soul is currently disabled for APL rotations"
+	],
+
+	// All stats for which EP should be calculated.
+	epStats: [
+		Stat.StatIntellect,
+		Stat.StatSpirit,
+		Stat.StatSpellPower,
+		Stat.StatSpellHit,
+		Stat.StatSpellCrit,
+		Stat.StatSpellHaste,
+		Stat.StatStamina,
+	],
+	// Reference stat against which to calculate EP. DPS classes use either spell power or attack power.
+	epReferenceStat: Stat.StatSpellPower,
+	// Which stats to display in the Character Stats section, at the bottom of the left-hand sidebar.
+	displayStats: [
+		Stat.StatHealth,
+		Stat.StatIntellect,
+		Stat.StatSpirit,
+		Stat.StatSpellPower,
+		Stat.StatSpellHit,
+		Stat.StatSpellCrit,
+		Stat.StatSpellHaste,
+		Stat.StatMP5,
+		Stat.StatStamina,
+	],
+
+	defaults: {
+		// Default equipped gear.
+		gear: Presets.P3_AFFLICTION_HORDE_PRESET.gear,
+
+		// Default EP weights for sorting gear in the gear picker.
+		epWeights: Stats.fromMap({
+			[Stat.StatIntellect]: 0.18,
+			[Stat.StatSpirit]: 0.54,
+			[Stat.StatSpellPower]: 1,
+			[Stat.StatSpellHit]: 0.93,
+			[Stat.StatSpellCrit]: 0.53,
+			[Stat.StatSpellHaste]: 0.81,
+			[Stat.StatStamina]: 0.01,
+		}),
+		// Default consumes settings.
+		consumes: Presets.DefaultConsumes,
+
+		// Default rotation settings.
+		rotation: Presets.AfflictionRotation,
+		// Default talents.
+		talents: Presets.AfflictionTalents.data,
+		// Default spec-specific settings.
+		specOptions: Presets.AfflictionOptions,
+
+		// Default buffs and debuffs settings.
+		raidBuffs: Presets.DefaultRaidBuffs,
+
+		partyBuffs: PartyBuffs.create({}),
+
+		individualBuffs: Presets.DefaultIndividualBuffs,
+
+		debuffs: Presets.DefaultDebuffs,
+
+		other: Presets.OtherDefaults,
+	},
+
+	// IconInputs to include in the 'Player' section on the settings tab.
+	playerIconInputs: [
+		WarlockInputs.PetInput,
+		WarlockInputs.ArmorInput,
+		WarlockInputs.WeaponImbueInput,
+	],
+	// Inputs to include in the 'Rotation' section on the settings tab.
+	rotationIconInputs: [
+		WarlockInputs.PrimarySpellInput,
+		WarlockInputs.CorruptionSpell,
+		WarlockInputs.SecondaryDotInput,
+		WarlockInputs.SpecSpellInput,
+	],
+	rotationInputs: WarlockInputs.WarlockRotationConfig,
+
+	// Buff and Debuff inputs to include/exclude, overriding the EP-based defaults.
+	includeBuffDebuffInputs: [
+		IconInputs.ReplenishmentBuff,
+		IconInputs.MajorArmorDebuff,
+		IconInputs.MinorArmorDebuff,
+		IconInputs.PhysicalDamageDebuff,
+		IconInputs.MeleeHasteBuff,
+		IconInputs.MeleeCritBuff,
+		IconInputs.MP5Buff,
+		IconInputs.AttackPowerPercentBuff,
+		IconInputs.AttackPowerBuff,
+		IconInputs.StrengthAndAgilityBuff,
+		IconInputs.StaminaBuff,
+	],
+	excludeBuffDebuffInputs: [
+	],
+	petConsumeInputs: [
+		IconInputs.SpicedMammothTreats,
+	],
+	// Inputs to include in the 'Other' section on the settings tab.
+	otherInputs: {
+		inputs: [
+			OtherInputs.DistanceFromTarget,
+			OtherInputs.TankAssignment,
+			OtherInputs.ChannelClipDelay,
+			OtherInputs.nibelungAverageCasts,
+		],
+	},
+	encounterPicker: {
+		// Whether to include 'Execute Duration (%)' in the 'Encounter' section of the settings tab.
+		showExecuteProportion: false,
+	},
+
+	presets: {
+		// Preset talents that the user can quickly select.
+		talents: [
+			Presets.AfflictionTalents,
+			Presets.DemonologyTalents,
+			Presets.DestructionTalents,
+		],
+		// Preset rotations that the user can quickly select.
+		rotations: [
+			Presets.APL_Affliction_Legacy,
+			Presets.APL_Affliction_Default,
+			Presets.APL_Demo_Legacy,
+			Presets.APL_Demo_Default,
+			Presets.APL_Destro_Legacy,
+			Presets.APL_Destro_Default,
+		],
+
+		// Preset gear configurations that the user can quickly select.
+		gear: [
+			Presets.SWP_BIS,
+			Presets.PRERAID_AFFLICTION_PRESET,
+			Presets.P1_AFFLICTION_PRESET,
+			Presets.P2_AFFLICTION_PRESET,
+			Presets.P3_AFFLICTION_ALLIANCE_PRESET,
+			Presets.P3_AFFLICTION_HORDE_PRESET,
+			Presets.P4_AFFLICTION_PRESET,
+			Presets.PRERAID_DEMODESTRO_PRESET,
+			Presets.P1_DEMODESTRO_PRESET,
+			Presets.P2_DEMODESTRO_PRESET,
+			Presets.P3_DEMO_ALLIANCE_PRESET,
+			Presets.P3_DEMO_HORDE_PRESET,
+			Presets.P4_DEMO_PRESET,
+			Presets.P3_DESTRO_ALLIANCE_PRESET,
+			Presets.P3_DESTRO_HORDE_PRESET,
+			Presets.P4_DESTRO_PRESET,
+		],
+	},
+
+	autoRotation: (player: Player<Spec.SpecWarlock>): APLRotation => {
+		const talentTree = player.getTalentTree();
+		if (talentTree == 0) {
+			return Presets.APL_Affliction_Default.rotation.rotation!;
+		} else if (talentTree == 1) {
+			return Presets.APL_Demo_Default.rotation.rotation!;
+		} else {
+			return Presets.APL_Destro_Default.rotation.rotation!;
+		}
+	},
+});
+
 export class WarlockSimUI extends IndividualSimUI<Spec.SpecWarlock> {
 	constructor(parentElem: HTMLElement, player: Player<Spec.SpecWarlock>) {
-		super(parentElem, player, {
-			cssClass: 'warlock-sim-ui',
-			cssScheme: 'warlock',
-			// List any known bugs / issues here and they'll be shown on the site.
-			knownIssues: [
-				"Drain Soul is currently disabled for APL rotations"
-			],
-
-			// All stats for which EP should be calculated.
-			epStats: [
-				Stat.StatIntellect,
-				Stat.StatSpirit,
-				Stat.StatSpellPower,
-				Stat.StatSpellHit,
-				Stat.StatSpellCrit,
-				Stat.StatSpellHaste,
-				Stat.StatStamina,
-			],
-			// Reference stat against which to calculate EP. DPS classes use either spell power or attack power.
-			epReferenceStat: Stat.StatSpellPower,
-			// Which stats to display in the Character Stats section, at the bottom of the left-hand sidebar.
-			displayStats: [
-				Stat.StatHealth,
-				Stat.StatIntellect,
-				Stat.StatSpirit,
-				Stat.StatSpellPower,
-				Stat.StatSpellHit,
-				Stat.StatSpellCrit,
-				Stat.StatSpellHaste,
-				Stat.StatMP5,
-				Stat.StatStamina,
-			],
-
-			defaults: {
-				// Default equipped gear.
-				gear: Presets.P3_AFFLICTION_HORDE_PRESET.gear,
-
-				// Default EP weights for sorting gear in the gear picker.
-				epWeights: Stats.fromMap({
-					[Stat.StatIntellect]: 0.18,
-					[Stat.StatSpirit]: 0.54,
-					[Stat.StatSpellPower]: 1,
-					[Stat.StatSpellHit]: 0.93,
-					[Stat.StatSpellCrit]: 0.53,
-					[Stat.StatSpellHaste]: 0.81,
-					[Stat.StatStamina]: 0.01,
-				}),
-				// Default consumes settings.
-				consumes: Presets.DefaultConsumes,
-
-				// Default rotation settings.
-				rotation: Presets.AfflictionRotation,
-				// Default talents.
-				talents: Presets.AfflictionTalents.data,
-				// Default spec-specific settings.
-				specOptions: Presets.AfflictionOptions,
-
-				// Default buffs and debuffs settings.
-				raidBuffs: Presets.DefaultRaidBuffs,
-
-				partyBuffs: PartyBuffs.create({}),
-
-				individualBuffs: Presets.DefaultIndividualBuffs,
-
-				debuffs: Presets.DefaultDebuffs,
-
-				other: Presets.OtherDefaults,
-			},
-
-			// IconInputs to include in the 'Player' section on the settings tab.
-			playerIconInputs: [
-				WarlockInputs.PetInput,
-				WarlockInputs.ArmorInput,
-				WarlockInputs.WeaponImbueInput,
-			],
-			// Inputs to include in the 'Rotation' section on the settings tab.
-			rotationIconInputs: [
-				WarlockInputs.PrimarySpellInput,
-				WarlockInputs.CorruptionSpell,
-				WarlockInputs.SecondaryDotInput,
-				WarlockInputs.SpecSpellInput,
-			],
-			rotationInputs: WarlockInputs.WarlockRotationConfig,
-
-			// Buff and Debuff inputs to include/exclude, overriding the EP-based defaults.
-			includeBuffDebuffInputs: [
-				IconInputs.ReplenishmentBuff,
-				IconInputs.MajorArmorDebuff,
-				IconInputs.MinorArmorDebuff,
-				IconInputs.PhysicalDamageDebuff,
-				IconInputs.MeleeHasteBuff,
-				IconInputs.MeleeCritBuff,
-				IconInputs.MP5Buff,
-				IconInputs.AttackPowerPercentBuff,
-				IconInputs.AttackPowerBuff,
-				IconInputs.StrengthAndAgilityBuff,
-				IconInputs.StaminaBuff,
-			],
-			excludeBuffDebuffInputs: [
-			],
-			petConsumeInputs: [
-				IconInputs.SpicedMammothTreats,
-			],
-			// Inputs to include in the 'Other' section on the settings tab.
-			otherInputs: {
-				inputs: [
-					OtherInputs.DistanceFromTarget,
-					OtherInputs.TankAssignment,
-					OtherInputs.ChannelClipDelay,
-					OtherInputs.nibelungAverageCasts,
-				],
-			},
-			encounterPicker: {
-				// Whether to include 'Execute Duration (%)' in the 'Encounter' section of the settings tab.
-				showExecuteProportion: false,
-			},
-
-			presets: {
-				// Preset talents that the user can quickly select.
-				talents: [
-					Presets.AfflictionTalents,
-					Presets.DemonologyTalents,
-					Presets.DestructionTalents,
-				],
-				// Preset rotations that the user can quickly select.
-				rotations: [
-					Presets.APL_Affliction_Legacy,
-					Presets.APL_Affliction_Default,
-					Presets.APL_Demo_Legacy,
-					Presets.APL_Demo_Default,
-					Presets.APL_Destro_Legacy,
-					Presets.APL_Destro_Default,
-				],
-
-				// Preset gear configurations that the user can quickly select.
-				gear: [
-					Presets.SWP_BIS,
-					Presets.PRERAID_AFFLICTION_PRESET,
-					Presets.P1_AFFLICTION_PRESET,
-					Presets.P2_AFFLICTION_PRESET,
-					Presets.P3_AFFLICTION_ALLIANCE_PRESET,
-					Presets.P3_AFFLICTION_HORDE_PRESET,
-					Presets.P4_AFFLICTION_PRESET,
-					Presets.PRERAID_DEMODESTRO_PRESET,
-					Presets.P1_DEMODESTRO_PRESET,
-					Presets.P2_DEMODESTRO_PRESET,
-					Presets.P3_DEMO_ALLIANCE_PRESET,
-					Presets.P3_DEMO_HORDE_PRESET,
-					Presets.P4_DEMO_PRESET,
-					Presets.P3_DESTRO_ALLIANCE_PRESET,
-					Presets.P3_DESTRO_HORDE_PRESET,
-					Presets.P4_DESTRO_PRESET,
-				],
-			},
-
-			autoRotation: (player: Player<Spec.SpecWarlock>): APLRotation => {
-				const talentTree = player.getTalentTree();
-				if (talentTree == 0) {
-					return Presets.APL_Affliction_Default.rotation.rotation!;
-				} else if (talentTree == 1) {
-					return Presets.APL_Demo_Default.rotation.rotation!;
-				} else {
-					return Presets.APL_Destro_Default.rotation.rotation!;
-				}
-			},
-		});
+		super(parentElem, player, SPEC_CONFIG);
 	}
 }
diff --git a/ui/warrior/sim.ts b/ui/warrior/sim.ts
index 3f6e90b60d..020f787af9 100644
--- a/ui/warrior/sim.ts
+++ b/ui/warrior/sim.ts
@@ -11,13 +11,11 @@ import {
 	TristateEffect,
 } from '../core/proto/common.js';
 import {
-	APLAction,
-	APLListItem,
 	APLRotation,
 } from '../core/proto/apl.js';
 import { Stats } from '../core/proto_utils/stats.js';
 import { Player } from '../core/player.js';
-import { IndividualSimUI } from '../core/individual_sim_ui.js';
+import { IndividualSimUI, registerSpecConfig } from '../core/individual_sim_ui.js';
 import { TypedEvent } from '../core/typed_event.js';
 import { Gear } from '../core/proto_utils/gear.js';
 
@@ -29,185 +27,187 @@ import * as Mechanics from '../core/constants/mechanics.js';
 import * as WarriorInputs from './inputs.js';
 import * as Presets from './presets.js';
 
+const SPEC_CONFIG = registerSpecConfig(Spec.SpecWarrior, {
+	cssClass: 'warrior-sim-ui',
+	cssScheme: 'warrior',
+	// List any known bugs / issues here and they'll be shown on the site.
+	knownIssues: [
+	],
+
+	// All stats for which EP should be calculated.
+	epStats: [
+		Stat.StatStrength,
+		Stat.StatAgility,
+		Stat.StatAttackPower,
+		Stat.StatExpertise,
+		Stat.StatMeleeHit,
+		Stat.StatMeleeCrit,
+		Stat.StatMeleeHaste,
+		Stat.StatArmorPenetration,
+		Stat.StatArmor,
+	],
+	epPseudoStats: [
+		PseudoStat.PseudoStatMainHandDps,
+		PseudoStat.PseudoStatOffHandDps,
+	],
+	// Reference stat against which to calculate EP. I think all classes use either spell power or attack power.
+	epReferenceStat: Stat.StatAttackPower,
+	// Which stats to display in the Character Stats section, at the bottom of the left-hand sidebar.
+	displayStats: [
+		Stat.StatHealth,
+		Stat.StatStamina,
+		Stat.StatStrength,
+		Stat.StatAgility,
+		Stat.StatAttackPower,
+		Stat.StatExpertise,
+		Stat.StatMeleeHit,
+		Stat.StatMeleeCrit,
+		Stat.StatMeleeHaste,
+		Stat.StatArmorPenetration,
+		Stat.StatArmor,
+	],
+	modifyDisplayStats: (player: Player<Spec.SpecWarrior>) => {
+		let stats = new Stats();
+		if (!player.getInFrontOfTarget()) {
+			// When behind target, dodge is the only outcome affected by Expertise.
+			stats = stats.addStat(Stat.StatExpertise, player.getTalents().weaponMastery * 4 * Mechanics.EXPERTISE_PER_QUARTER_PERCENT_REDUCTION);
+		}
+		return {
+			talents: stats,
+		};
+	},
+
+	defaults: {
+		// Default equipped gear.
+		gear: Presets.P3_FURY_PRESET_ALLIANCE.gear,
+		// Default EP weights for sorting gear in the gear picker.
+		epWeights: Stats.fromMap({
+			[Stat.StatStrength]: 2.72,
+			[Stat.StatAgility]: 1.82,
+			[Stat.StatAttackPower]: 1,
+			[Stat.StatExpertise]: 2.55,
+			[Stat.StatMeleeHit]: 0.79,
+			[Stat.StatMeleeCrit]: 2.12,
+			[Stat.StatMeleeHaste]: 1.72,
+			[Stat.StatArmorPenetration]: 2.17,
+			[Stat.StatArmor]: 0.03,
+		}, {
+			[PseudoStat.PseudoStatMainHandDps]: 6.29,
+			[PseudoStat.PseudoStatOffHandDps]: 3.58,
+		}),
+		// Default consumes settings.
+		consumes: Presets.DefaultConsumes,
+		// Default rotation settings.
+		rotation: Presets.DefaultRotation,
+		// Default talents.
+		talents: Presets.FuryTalents.data,
+		// Default spec-specific settings.
+		specOptions: Presets.DefaultOptions,
+		// Default raid/party buffs settings.
+		raidBuffs: RaidBuffs.create({
+			giftOfTheWild: TristateEffect.TristateEffectImproved,
+			swiftRetribution: true,
+			strengthOfEarthTotem: TristateEffect.TristateEffectImproved,
+			icyTalons: true,
+			abominationsMight: true,
+			leaderOfThePack: TristateEffect.TristateEffectRegular,
+			sanctifiedRetribution: true,
+			bloodlust: true,
+			devotionAura: TristateEffect.TristateEffectImproved,
+			stoneskinTotem: TristateEffect.TristateEffectImproved,
+		}),
+		partyBuffs: PartyBuffs.create({
+			heroicPresence: false,
+		}),
+		individualBuffs: IndividualBuffs.create({
+			blessingOfKings: true,
+			blessingOfMight: TristateEffect.TristateEffectImproved,
+		}),
+		debuffs: Debuffs.create({
+			bloodFrenzy: true,
+			heartOfTheCrusader: true,
+			mangle: true,
+			sunderArmor: true,
+			curseOfWeakness: TristateEffect.TristateEffectRegular,
+			faerieFire: TristateEffect.TristateEffectImproved,
+			ebonPlaguebringer: true,
+		}),
+	},
+
+	// IconInputs to include in the 'Player' section on the settings tab.
+	playerIconInputs: [
+		WarriorInputs.ShoutPicker,
+		WarriorInputs.Recklessness,
+		WarriorInputs.ShatteringThrow,
+	],
+	// Inputs to include in the 'Rotation' section on the settings tab.
+	rotationInputs: WarriorInputs.WarriorRotationConfig,
+	// Buff and Debuff inputs to include/exclude, overriding the EP-based defaults.
+	includeBuffDebuffInputs: [
+		// just for Bryntroll
+		IconInputs.SpellDamageDebuff,
+		IconInputs.SpellHitDebuff,
+	],
+	excludeBuffDebuffInputs: [
+	],
+	// Inputs to include in the 'Other' section on the settings tab.
+	otherInputs: {
+		inputs: [
+			WarriorInputs.StartingRage,
+			WarriorInputs.StanceSnapshot,
+			WarriorInputs.DisableExpertiseGemming,
+			OtherInputs.TankAssignment,
+			OtherInputs.InFrontOfTarget,
+		],
+	},
+	encounterPicker: {
+		// Whether to include 'Execute Duration (%)' in the 'Encounter' section of the settings tab.
+		showExecuteProportion: true,
+	},
+
+	presets: {
+		// Preset talents that the user can quickly select.
+		talents: [
+			Presets.ArmsTalents,
+			Presets.FuryTalents,
+		],
+		// Preset rotations that the user can quickly select.
+		rotations: [
+			Presets.ROTATION_FURY,
+			Presets.ROTATION_FURY_SUNDER,
+			Presets.ROTATION_ARMS,
+			Presets.ROTATION_ARMS_SUNDER,
+		],
+		// Preset gear configurations that the user can quickly select.
+		gear: [
+			Presets.PRERAID_FURY_PRESET,
+			Presets.P1_FURY_PRESET,
+			Presets.P2_FURY_PRESET,
+			Presets.P3_FURY_PRESET_ALLIANCE,
+			Presets.P3_FURY_PRESET_HORDE,
+			Presets.PRERAID_ARMS_PRESET,
+			Presets.P1_ARMS_PRESET,
+			Presets.P2_ARMS_PRESET,
+			Presets.P3_ARMS_2P_PRESET_ALLIANCE,
+			Presets.P3_ARMS_4P_PRESET_ALLIANCE,
+			Presets.P3_ARMS_2P_PRESET_HORDE,
+			Presets.P3_ARMS_4P_PRESET_HORDE,
+		],
+	},
+
+	autoRotation: (player: Player<Spec.SpecWarrior>): APLRotation => {
+		const talentTree = player.getTalentTree();
+		if (talentTree == 0) {
+			return Presets.ROTATION_ARMS_SUNDER.rotation.rotation!;
+		} else {
+			return Presets.ROTATION_FURY_SUNDER.rotation.rotation!;
+		}
+	},
+});
+
 export class WarriorSimUI extends IndividualSimUI<Spec.SpecWarrior> {
 	constructor(parentElem: HTMLElement, player: Player<Spec.SpecWarrior>) {
-		super(parentElem, player, {
-			cssClass: 'warrior-sim-ui',
-			cssScheme: 'warrior',
-			// List any known bugs / issues here and they'll be shown on the site.
-			knownIssues: [
-			],
-
-			// All stats for which EP should be calculated.
-			epStats: [
-				Stat.StatStrength,
-				Stat.StatAgility,
-				Stat.StatAttackPower,
-				Stat.StatExpertise,
-				Stat.StatMeleeHit,
-				Stat.StatMeleeCrit,
-				Stat.StatMeleeHaste,
-				Stat.StatArmorPenetration,
-				Stat.StatArmor,
-			],
-			epPseudoStats: [
-				PseudoStat.PseudoStatMainHandDps,
-				PseudoStat.PseudoStatOffHandDps,
-			],
-			// Reference stat against which to calculate EP. I think all classes use either spell power or attack power.
-			epReferenceStat: Stat.StatAttackPower,
-			// Which stats to display in the Character Stats section, at the bottom of the left-hand sidebar.
-			displayStats: [
-				Stat.StatHealth,
-				Stat.StatStamina,
-				Stat.StatStrength,
-				Stat.StatAgility,
-				Stat.StatAttackPower,
-				Stat.StatExpertise,
-				Stat.StatMeleeHit,
-				Stat.StatMeleeCrit,
-				Stat.StatMeleeHaste,
-				Stat.StatArmorPenetration,
-				Stat.StatArmor,
-			],
-			modifyDisplayStats: (player: Player<Spec.SpecWarrior>) => {
-				let stats = new Stats();
-				if (!player.getInFrontOfTarget()) {
-					// When behind target, dodge is the only outcome affected by Expertise.
-					stats = stats.addStat(Stat.StatExpertise, player.getTalents().weaponMastery * 4 * Mechanics.EXPERTISE_PER_QUARTER_PERCENT_REDUCTION);
-				}
-				return {
-					talents: stats,
-				};
-			},
-
-			defaults: {
-				// Default equipped gear.
-				gear: Presets.P3_FURY_PRESET_ALLIANCE.gear,
-				// Default EP weights for sorting gear in the gear picker.
-				epWeights: Stats.fromMap({
-					[Stat.StatStrength]: 2.72,
-					[Stat.StatAgility]: 1.82,
-					[Stat.StatAttackPower]: 1,
-					[Stat.StatExpertise]: 2.55,
-					[Stat.StatMeleeHit]: 0.79,
-					[Stat.StatMeleeCrit]: 2.12,
-					[Stat.StatMeleeHaste]: 1.72,
-					[Stat.StatArmorPenetration]: 2.17,
-					[Stat.StatArmor]: 0.03,
-				}, {
-					[PseudoStat.PseudoStatMainHandDps]: 6.29,
-					[PseudoStat.PseudoStatOffHandDps]: 3.58,
-				}),
-				// Default consumes settings.
-				consumes: Presets.DefaultConsumes,
-				// Default rotation settings.
-				rotation: Presets.DefaultRotation,
-				// Default talents.
-				talents: Presets.FuryTalents.data,
-				// Default spec-specific settings.
-				specOptions: Presets.DefaultOptions,
-				// Default raid/party buffs settings.
-				raidBuffs: RaidBuffs.create({
-					giftOfTheWild: TristateEffect.TristateEffectImproved,
-					swiftRetribution: true,
-					strengthOfEarthTotem: TristateEffect.TristateEffectImproved,
-					icyTalons: true,
-					abominationsMight: true,
-					leaderOfThePack: TristateEffect.TristateEffectRegular,
-					sanctifiedRetribution: true,
-					bloodlust: true,
-					devotionAura: TristateEffect.TristateEffectImproved,
-					stoneskinTotem: TristateEffect.TristateEffectImproved,
-				}),
-				partyBuffs: PartyBuffs.create({
-					heroicPresence: false,
-				}),
-				individualBuffs: IndividualBuffs.create({
-					blessingOfKings: true,
-					blessingOfMight: TristateEffect.TristateEffectImproved,
-				}),
-				debuffs: Debuffs.create({
-					bloodFrenzy: true,
-					heartOfTheCrusader: true,
-					mangle: true,
-					sunderArmor: true,
-					curseOfWeakness: TristateEffect.TristateEffectRegular,
-					faerieFire: TristateEffect.TristateEffectImproved,
-					ebonPlaguebringer: true,
-				}),
-			},
-
-			// IconInputs to include in the 'Player' section on the settings tab.
-			playerIconInputs: [
-				WarriorInputs.ShoutPicker,
-				WarriorInputs.Recklessness,
-				WarriorInputs.ShatteringThrow,
-			],
-			// Inputs to include in the 'Rotation' section on the settings tab.
-			rotationInputs: WarriorInputs.WarriorRotationConfig,
-			// Buff and Debuff inputs to include/exclude, overriding the EP-based defaults.
-			includeBuffDebuffInputs: [
-				// just for Bryntroll
-				IconInputs.SpellDamageDebuff,
-				IconInputs.SpellHitDebuff,
-			],
-			excludeBuffDebuffInputs: [
-			],
-			// Inputs to include in the 'Other' section on the settings tab.
-			otherInputs: {
-				inputs: [
-					WarriorInputs.StartingRage,
-					WarriorInputs.StanceSnapshot,
-					WarriorInputs.DisableExpertiseGemming,
-					OtherInputs.TankAssignment,
-					OtherInputs.InFrontOfTarget,
-				],
-			},
-			encounterPicker: {
-				// Whether to include 'Execute Duration (%)' in the 'Encounter' section of the settings tab.
-				showExecuteProportion: true,
-			},
-
-			presets: {
-				// Preset talents that the user can quickly select.
-				talents: [
-					Presets.ArmsTalents,
-					Presets.FuryTalents,
-				],
-				// Preset rotations that the user can quickly select.
-				rotations: [
-					Presets.ROTATION_FURY,
-					Presets.ROTATION_FURY_SUNDER,
-					Presets.ROTATION_ARMS,
-					Presets.ROTATION_ARMS_SUNDER,
-				],
-				// Preset gear configurations that the user can quickly select.
-				gear: [
-					Presets.PRERAID_FURY_PRESET,
-					Presets.P1_FURY_PRESET,
-					Presets.P2_FURY_PRESET,
-					Presets.P3_FURY_PRESET_ALLIANCE,
-					Presets.P3_FURY_PRESET_HORDE,
-					Presets.PRERAID_ARMS_PRESET,
-					Presets.P1_ARMS_PRESET,
-					Presets.P2_ARMS_PRESET,
-					Presets.P3_ARMS_2P_PRESET_ALLIANCE,
-					Presets.P3_ARMS_4P_PRESET_ALLIANCE,
-					Presets.P3_ARMS_2P_PRESET_HORDE,
-					Presets.P3_ARMS_4P_PRESET_HORDE,
-				],
-			},
-
-			autoRotation: (player: Player<Spec.SpecWarrior>): APLRotation => {
-				const talentTree = player.getTalentTree();
-				if (talentTree == 0) {
-					return Presets.ROTATION_ARMS_SUNDER.rotation.rotation!;
-				} else {
-					return Presets.ROTATION_FURY_SUNDER.rotation.rotation!;
-				}
-			},
-		});
+		super(parentElem, player, SPEC_CONFIG);
 		this.addOptimizeGemsAction();
 	}
 	addOptimizeGemsAction() {