diff --git a/assets/locales/en/translation.json b/assets/locales/en/translation.json index 5023d93d71..2207419e81 100644 --- a/assets/locales/en/translation.json +++ b/assets/locales/en/translation.json @@ -610,6 +610,11 @@ }, "pet": { "title": "Pet" + }, + "imbue": { + "title": "Imbues", + "mhImbue": "Main Hand", + "ohImbue": "Off Hand" } }, "encounter": { diff --git a/proto/common.proto b/proto/common.proto index a37349425d..20769c865b 100644 --- a/proto/common.proto +++ b/proto/common.proto @@ -573,8 +573,12 @@ message ConsumesSpec { int32 battle_elixir_id = 4; int32 guardian_elixir_id = 5; int32 food_id = 6; - int32 explosive_id = 7; - int32 conjured_id = 9; + int32 conjured_id = 7; + int32 explosive_id = 8; + bool superSapper = 9; + bool goblinSapper = 10; + int32 mhImbue_id = 11; + int32 ohImbue_id = 12; } enum MobType { diff --git a/schemas/translation.schema.json b/schemas/translation.schema.json index cfc73f691b..8119642f93 100644 --- a/schemas/translation.schema.json +++ b/schemas/translation.schema.json @@ -2521,6 +2521,26 @@ "required": [ "title" ] + }, + "imbue": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "mhImbue": { + "type": "string" + }, + "ohImbue": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "title", + "mhImbue", + "ohImbue" + ] } }, "additionalProperties": false, @@ -2530,7 +2550,8 @@ "elixirs", "food", "engineering", - "pet" + "pet", + "imbue" ] }, "encounter": { diff --git a/sim/core/armor.go b/sim/core/armor.go index 13f4819288..3a8bc1039e 100644 --- a/sim/core/armor.go +++ b/sim/core/armor.go @@ -1,5 +1,11 @@ package core +import ( + "math" + + "github.com/wowsims/tbc/sim/core/stats" +) + func (result *SpellResult) applyArmor(spell *Spell, isPeriodic bool, attackTable *AttackTable) { armorMitigationMultiplier := spell.armorMultiplier(isPeriodic, attackTable) @@ -38,7 +44,9 @@ func (at *AttackTable) getArmorDamageModifier() float64 { ignoreArmorFactor := Clamp(at.ArmorIgnoreFactor, 0.0, 1.0) // Assume target > 80 - armorConstant := float64(at.Attacker.Level)*4037.5 - 317117.5 - defenderArmor := at.Defender.Armor() * (1.0 - ignoreArmorFactor) + armorConstant := float64(at.Attacker.Level)*467.5 - 22167.5 + defenderArmor := at.Defender.Armor() - (at.Defender.Armor() * ignoreArmorFactor) + // TBC ANNI: Apply flat ArP + defenderArmor = max(defenderArmor-math.Abs(at.Attacker.stats[stats.ArmorPenetration]), 0) return 1 - defenderArmor/(defenderArmor+armorConstant) } diff --git a/sim/core/dot_test.go b/sim/core/dot_test.go index 2a236064d1..2794a47f65 100644 --- a/sim/core/dot_test.go +++ b/sim/core/dot_test.go @@ -72,7 +72,7 @@ func NewFakeElementalShaman(char *Character, _ *proto.Player) Agent { AffectedByCastSpeed: true, BonusCoefficient: 1, - OnSnapshot: func(sim *Simulation, target *Unit, dot *Dot, isRollover bool) { + OnSnapshot: func(sim *Simulation, target *Unit, dot *Dot) { dot.Snapshot(target, 100) }, OnTick: func(sim *Simulation, target *Unit, dot *Dot) { diff --git a/tools/database/dbc/util.go b/tools/database/dbc/util.go index 99c5b748de..9055f2463e 100644 --- a/tools/database/dbc/util.go +++ b/tools/database/dbc/util.go @@ -213,17 +213,17 @@ func ConvertTargetResistanceFlagToPenetrationStat(flag int) proto.Stat { func ConvertSpellDamageFlagToSchoolDamageStat(flag int) proto.Stat { switch flag { case 2: - return proto.Stat_StatHolyPower + return proto.Stat_StatHolyDamage case 4: - return proto.Stat_StatFirePower + return proto.Stat_StatFireDamage case 8: - return proto.Stat_StatNaturePower + return proto.Stat_StatNatureDamage case 16: - return proto.Stat_StatFrostPower + return proto.Stat_StatFrostDamage case 32: - return proto.Stat_StatShadowPower + return proto.Stat_StatShadowDamage case 64: - return proto.Stat_StatArcanePower + return proto.Stat_StatArcaneDamage default: return proto.Stat_StatSpellDamage } diff --git a/tools/tooltip/tooltip_parser_test.go b/tools/tooltip/tooltip_parser_test.go index ff12a5519c..e329dee99b 100644 --- a/tools/tooltip/tooltip_parser_test.go +++ b/tools/tooltip/tooltip_parser_test.go @@ -10,7 +10,7 @@ var db = dbc.GetDBC() func Test_WhenInvalidTernaryGiven_ThenProperlyApplyFixes(t *testing.T) { tp, error := ParseTooltip("$ damage every ${$16914d3/10}.2 seconds$?$w1!=0[ and movement slowed by $w1%][].", - NewTestDataProvider(CharacterConfig{SpellPower: 1000}), + NewTestDataProvider(CharacterConfig{SpellDamage: 1000}), 16914, ) diff --git a/ui/core/components/individual_sim_ui/consumes_picker.tsx b/ui/core/components/individual_sim_ui/consumes_picker.tsx index 12415bdd92..fc03d15875 100644 --- a/ui/core/components/individual_sim_ui/consumes_picker.tsx +++ b/ui/core/components/individual_sim_ui/consumes_picker.tsx @@ -43,6 +43,7 @@ export class ConsumesPicker extends Component { this.buildFoodPicker(); this.buildEngPicker(); this.buildPetPicker(); + this.buildImbuePicker(); } private buildPotionsPicker(): void { @@ -135,12 +136,14 @@ export class ConsumesPicker extends Component { const explosivesoptions = ConsumablesInputs.makeExplosivesInput(relevantStatOptions(ConsumablesInputs.EXPLOSIVE_CONFIG, this.simUI), i18n.t('settings_tab.consumables.engineering.explosives')); const explosivePicker = buildIconInput(engiConsumesElem, this.simUI.player, explosivesoptions); + const goblinSapperPicker = buildIconInput(engiConsumesElem, this.simUI.player, ConsumablesInputs.GoblinSapper); + const superSapperPicker = buildIconInput(engiConsumesElem, this.simUI.player, ConsumablesInputs.SuperSapper); - const events = this.simUI.player.professionChangeEmitter.on(() => this.updateRow(row, [explosivePicker])); + const events = this.simUI.player.professionChangeEmitter.on(() => this.updateRow(row, [explosivePicker, goblinSapperPicker, superSapperPicker])); this.addOnDisposeCallback(() => events.dispose()); // Initial update of row based on current state. - this.updateRow(row, [explosivePicker]); + this.updateRow(row, [explosivePicker, goblinSapperPicker, superSapperPicker]); } private buildPetPicker(): void { @@ -158,6 +161,26 @@ export class ConsumesPicker extends Component { } } + private buildImbuePicker(): void { + const imbuePickerRef = ref(); + const row = this.rootElem.appendChild( + +
+
+ ); + const imbuePickerElem = imbuePickerRef.value!; + + const mhImbueOptions = ConsumablesInputs.makeMHImbueInput(relevantStatOptions(ConsumablesInputs.IMBUE_CONFIG_MH, this.simUI), i18n.t('settings_tab.consumables.imbue.mhImbue')); + const ohImbueOptions = ConsumablesInputs.makeOHImbueinput(relevantStatOptions(ConsumablesInputs.IMBUE_CONFIG_OH, this.simUI), i18n.t('settings_tab.consumables.imbue.ohImbue')); + mhImbueOptions.enableWhen = (player: Player) => !player.getParty() || player.getParty()!.getBuffs().windfuryTotemRank == 0 + mhImbueOptions.changedEvent = (player: Player) => TypedEvent.onAny([player.getRaid()?.changeEmitter || player.consumesChangeEmitter]); + + buildIconInput(imbuePickerElem, this.simUI.player, mhImbueOptions); + if (isDualWieldSpec(this.simUI.player.getSpec())) { + buildIconInput(imbuePickerElem, this.simUI.player, ohImbueOptions); + } + } + private updateRow(rowElem: Element, pickers: (IconPicker, any> | IconEnumPicker, any>)[]) { rowElem.classList[!!pickers.find(p => p?.showWhen()) ? 'remove' : 'add']('hide'); } @@ -170,3 +193,7 @@ const ConsumeRow = ({ label, children }: { label: string; children: JSX.Element {children} ); +function isDualWieldSpec(spec: any): boolean { + return [Spec.SpecEnhancementShaman, Spec.SpecHunter, Spec.SpecRogue, Spec.SpecDPSWarrior, Spec.SpecProtectionWarrior].includes(spec) +} + diff --git a/ui/core/components/inputs/consumables.ts b/ui/core/components/inputs/consumables.ts index bf483fcd0f..4408a7df5c 100644 --- a/ui/core/components/inputs/consumables.ts +++ b/ui/core/components/inputs/consumables.ts @@ -7,6 +7,8 @@ import * as InputHelpers from '../input_helpers'; import { IconEnumValueConfig } from '../pickers/icon_enum_picker'; import { ActionInputConfig, ItemStatOption } from './stat_options'; import i18n from '../../../i18n/config.js'; +import { makeBooleanConsumeInput } from '../icon_inputs'; +import { playerPresets } from '../../../raid/presets'; export interface ConsumableInputConfig extends ActionInputConfig { value: T; @@ -90,24 +92,165 @@ export const CONJURED_CONFIG = [ export const makeConjuredInput = makeConsumeInputFactory({ consumesFieldName: 'conjuredId' }); -export const ExplosiveBigDaddy = { - actionId: ActionId.fromItemId(63396), - value: 89637, +/////////////////////////////////////////////////////////////////////////// +// ENGINEERING +/////////////////////////////////////////////////////////////////////////// + +export const AdamantiteGrenade = { + actionId: ActionId.fromItemId(23737), + value: 30217, + showWhen: (player: Player) => player.hasProfession(Profession.Engineering), +}; + +export const FelIronBomb = { + actionId: ActionId.fromItemId(23736), + value: 30216, showWhen: (player: Player) => player.hasProfession(Profession.Engineering), }; -export const HighpoweredBoltGun = { - actionId: ActionId.fromItemId(60223), - value: 82207, +export const GnomishFlameTurrent = { + actionId: ActionId.fromItemId(23841), + value: 30526, showWhen: (player: Player) => player.hasProfession(Profession.Engineering), }; export const EXPLOSIVE_CONFIG = [ - { config: ExplosiveBigDaddy, stats: [] }, - { config: HighpoweredBoltGun, stats: [] }, + { config: AdamantiteGrenade, stats: [] }, + { config: FelIronBomb, stats: [] }, + { config: GnomishFlameTurrent, stats: [] }, ] as ConsumableStatOption[]; export const makeExplosivesInput = makeConsumeInputFactory({ consumesFieldName: 'explosiveId' }); +export const GoblinSapper = makeBooleanConsumeInput({ + actionId: ActionId.fromItemId(10646), + fieldName: 'goblinSapper', + showWhen: (player: Player) => player.hasProfession(Profession.Engineering), +}) + +export const SuperSapper = makeBooleanConsumeInput({ + actionId: ActionId.fromItemId(23827), + fieldName: 'superSapper', + showWhen: (player: Player) => player.hasProfession(Profession.Engineering), +}) + +/////////////////////////////////////////////////////////////////////////// +// WEAPON IMBUES +/////////////////////////////////////////////////////////////////////////// + +// Oils +export const ManaOil = { + actionId: ActionId.fromItemId(20748), + value: 25123, +}; +export const BrilWizardOil = { + actionId: ActionId.fromItemId(20749), + value: 25122, +}; +export const SupWizardOil = { + actionId: ActionId.fromItemId(22522), + value: 28017, +}; +// Stones +export const AdamantiteSharpeningMH = { + actionId: ActionId.fromItemId(23529), + value: 29453, + showWhen: (player: Player) => !player.getGear().hasBluntMHWeapon() +}; +export const AdamantiteWeightMH = { + actionId: ActionId.fromItemId(28421), + value: 34340, + showWhen: (player: Player) => player.getGear().hasBluntMHWeapon() +}; +export const AdamantiteSharpeningOH = { + actionId: ActionId.fromItemId(23529), + value: 29453, + showWhen: (player: Player) => !player.getGear().hasBluntOHWeapon() +}; +export const AdamantiteWeightOH = { + actionId: ActionId.fromItemId(28421), + value: 34340, + showWhen: (player: Player) => player.getGear().hasBluntOHWeapon() +}; +// Rogue Poisons +export const RogueInstantPoison = { + actionId: ActionId.fromItemId(21927), + value: 26891, + showWhen: (player: Player) => player.getClass() == Class.ClassRogue +} +export const RogueDeadlyPoison = { + actionId: ActionId.fromItemId(22054), + value: 27186, + showWhen: (player: Player) => player.getClass() == Class.ClassRogue +} +// Shaman Imbues +export const ShamanImbueWindfury = { + actionId: ActionId.fromSpellId(25505), + value: 25505, + showWhen: (player: Player) => player.getClass() == Class.ClassShaman +} +export const ShamanImbueFlametongue = { + actionId: ActionId.fromSpellId(25489), + value: 25489, + showWhen: (player: Player) => player.getClass() == Class.ClassShaman +} + +export const ShamanImbueFrostbrand = { + actionId: ActionId.fromSpellId(25500), + value: 25500, + showWhen: (player: Player) => player.getClass() == Class.ClassShaman +} + +export const ShamanImbueRockbiter = { + actionId: ActionId.fromSpellId(25485), + value: 25485, + showWhen: (player: Player) => player.getClass() == Class.ClassShaman +} + +export const IMBUE_CONFIG_MH = [ + { config: ManaOil, stats: [Stat.StatHealingPower] }, + { config: BrilWizardOil, stats: [Stat.StatSpellDamage] }, + { config: SupWizardOil, stats: [Stat.StatSpellDamage] }, + { config: AdamantiteSharpeningMH, stats: [Stat.StatAttackPower] }, + { config: AdamantiteWeightMH, stats: [Stat.StatAttackPower] }, + { config: RogueInstantPoison, stats: [] }, + { config: RogueDeadlyPoison, stats: [] }, + { config: ShamanImbueRockbiter, stats: [] }, + { config: ShamanImbueFrostbrand, stats: [] }, + { config: ShamanImbueFlametongue, stats: [] }, + { config: ShamanImbueWindfury, stats: [] }, +] as ConsumableStatOption[]; + +export const IMBUE_CONFIG_OH = [ + { config: ManaOil, stats: [Stat.StatHealingPower] }, + { config: BrilWizardOil, stats: [Stat.StatSpellDamage] }, + { config: SupWizardOil, stats: [Stat.StatSpellDamage] }, + { config: AdamantiteSharpeningOH, stats: [Stat.StatAttackPower] }, + { config: AdamantiteWeightOH, stats: [Stat.StatAttackPower] }, + { config: RogueInstantPoison, stats: [] }, + { config: RogueDeadlyPoison, stats: [] }, + { config: ShamanImbueRockbiter, stats: [] }, + { config: ShamanImbueFrostbrand, stats: [] }, + { config: ShamanImbueFlametongue, stats: [] }, + { config: ShamanImbueWindfury, stats: [] }, +] as ConsumableStatOption[]; + +export const makeMHImbueInput = makeConsumeInputFactory({ consumesFieldName: 'mhImbueId' }); +export const makeOHImbueinput = makeConsumeInputFactory({ consumesFieldName: 'ohImbueId' }); + +/////////////////////////////////////////////////////////////////////////// +// DRUMS +/////////////////////////////////////////////////////////////////////////// + + + +/////////////////////////////////////////////////////////////////////////// +// SCROLLS +/////////////////////////////////////////////////////////////////////////// + + + +/////////////////////////////////////////////////////////////////////////// + export interface ConsumableInputOptions { consumesFieldName: keyof ConsumesSpec; setValue?: (eventID: EventID, player: Player, newValue: number) => void;