wowlab_engine_content/
racials.rs1use wowlab_engine_combat::{BuffEffect, MAX_AURA_BUFF_EFFECTS};
4use wowlab_engine_domain::dbc::EffectLookup;
5use wowlab_engine_gamedata::ResolvedGameData;
6use wowlab_engine_ports::EngineError;
7use wowlab_types::{
8 constants::{ANCESTRAL_CALL_SPELL_ID, BERSERKING_SPELL_ID, BLOOD_FURY_SPELL_ID},
9 game::RaceId,
10 sim::{EffectRef, SpellIdx},
11};
12
13use crate::raid_buffs::PrecombatBuffSpec;
14
15const BLOOD_FURY_DURATION_MS: u32 = 15_000;
16const BLOOD_FURY_EFFECT: u8 = 1;
17
18const BERSERKING_DURATION_MS: u32 = 12_000;
19const BERSERKING_EFFECT: u8 = 1;
20
21const ANCESTRAL_CALL_DURATION_MS: u32 = 15_000;
22const ANCESTRAL_CALL_EFFECT: u8 = 1;
23
24fn primary_stat_effects(
25 lookup: EffectLookup<'_>,
26) -> Result<[Option<BuffEffect>; MAX_AURA_BUFF_EFFECTS], EngineError> {
27 let data = lookup.data;
28 let level = data.level();
29 let coef = lookup.coefficient();
30 let budget = wowlab_engine_domain::dbc::spell_scaling_budget(
31 data.game_tables(),
32 wowlab_engine_domain::dbc::SCALING_CLASS_PRIMARY_STAT,
33 level,
34 )
35 .ok_or_else(|| {
36 EngineError::spec_construction(format!(
37 "spell scaling budget (class {}) missing for level {level}",
38 wowlab_engine_domain::dbc::SCALING_CLASS_PRIMARY_STAT
39 ))
40 })?;
41 let mut effects = [None; MAX_AURA_BUFF_EFFECTS];
42
43 effects[0] = Some(BuffEffect::PrimaryStat(coef * budget));
44
45 Ok(effects)
46}
47
48#[must_use]
50pub(crate) fn racial_spec(race: RaceId) -> Option<PrecombatBuffSpec> {
51 match race {
52 RaceId::Orc => Some(blood_fury_spec()),
53 RaceId::Troll => Some(berserking_spec()),
54 RaceId::MagharOrc => Some(ancestral_call_spec()),
55 _ => None,
56 }
57}
58
59fn blood_fury_effects(
60 data: &ResolvedGameData,
61) -> Result<[Option<BuffEffect>; MAX_AURA_BUFF_EFFECTS], EngineError> {
62 primary_stat_effects(EffectLookup::new(
63 data,
64 EffectRef::new(SpellIdx::from_raw(BLOOD_FURY_SPELL_ID), BLOOD_FURY_EFFECT),
65 ))
66}
67
68pub(crate) const fn blood_fury_spec() -> PrecombatBuffSpec {
70 PrecombatBuffSpec::fixed(
71 BLOOD_FURY_SPELL_ID,
72 "Blood Fury",
73 BLOOD_FURY_DURATION_MS,
74 blood_fury_effects,
75 )
76}
77
78#[expect(
79 clippy::unnecessary_wraps,
80 reason = "precombat effect factories share a fallible callback signature"
81)]
82fn berserking_effects(
83 data: &ResolvedGameData,
84) -> Result<[Option<BuffEffect>; MAX_AURA_BUFF_EFFECTS], EngineError> {
85 let haste = data.base_points(SpellIdx::from_raw(BERSERKING_SPELL_ID), BERSERKING_EFFECT);
86 let mut effects = [None; MAX_AURA_BUFF_EFFECTS];
87
88 effects[0] = Some(BuffEffect::HasteMult(haste));
89
90 Ok(effects)
91}
92
93pub(crate) const fn berserking_spec() -> PrecombatBuffSpec {
95 PrecombatBuffSpec::fixed(
96 BERSERKING_SPELL_ID,
97 "Berserking",
98 BERSERKING_DURATION_MS,
99 berserking_effects,
100 )
101}
102
103fn ancestral_call_effects(
104 data: &ResolvedGameData,
105) -> Result<[Option<BuffEffect>; MAX_AURA_BUFF_EFFECTS], EngineError> {
106 primary_stat_effects(EffectLookup::new(
107 data,
108 EffectRef::new(
109 SpellIdx::from_raw(ANCESTRAL_CALL_SPELL_ID),
110 ANCESTRAL_CALL_EFFECT,
111 ),
112 ))
113}
114
115pub(crate) const fn ancestral_call_spec() -> PrecombatBuffSpec {
117 PrecombatBuffSpec::fixed(
118 ANCESTRAL_CALL_SPELL_ID,
119 "Ancestral Call",
120 ANCESTRAL_CALL_DURATION_MS,
121 ancestral_call_effects,
122 )
123}
124
125#[cfg(test)]
126mod tests;