Skip to main content

wowlab_engine_application/
introspection.rs

1use wowlab_engine_gamedata::ResolvedGameData;
2use wowlab_engine_ports::{
3    ContentCatalog, DynDataResolver, EngineError, HandlerParams, ResolvedEncounter,
4    ResolvedEnemyCombatStats, ResolvedEnemyEncounter, ResolvedEnemyIdentityMetadata,
5    SpecDescriptor,
6};
7use wowlab_types::{
8    constants::{CURRENT_EXPANSION_ID, DEFAULT_DURATION_S, MAX_PLAYER_LEVEL},
9    data::WeaponStats,
10    game::SpecId,
11    sim::{EncounterDefinition, Rotation, RotationAction},
12};
13
14use crate::{
15    ApplicationError, ApplicationStage, CharacterContext, GameDataInputs,
16    error::EngineResultExt as _, resolve_game_data,
17};
18
19const INTROSPECT_WAIT_SECONDS: f64 = 1.0;
20const INTROSPECT_MAX_HEALTH: f64 = 1_000_000.0;
21const INTROSPECT_CREATURE_ARMOR: f64 = 377.0;
22const INTROSPECT_ARMOR_CONSTANT: f64 = f32::from_bits(0x4475_22c1) as f64;
23const INTROSPECT_AUTO_ATTACK_DPS: f64 = f32::from_bits(0x44b6_eb33) as f64;
24const INTROSPECT_SPELL_DAMAGE: f64 = 65_851.0;
25
26fn introspection_rotation() -> Rotation {
27    let mut rotation = Rotation::empty();
28
29    rotation.name = "_introspect".to_string();
30    rotation.actions.push(RotationAction::Wait {
31        seconds: INTROSPECT_WAIT_SECONDS,
32        enabled: true,
33        condition: None,
34    });
35
36    rotation
37}
38
39fn introspection_encounter() -> Result<ResolvedEncounter, EngineError> {
40    let definition =
41        EncounterDefinition::patchwerk(DEFAULT_DURATION_S, MAX_PLAYER_LEVEL, CURRENT_EXPANSION_ID)?;
42    let enemy = definition
43        .enemies
44        .first()
45        .ok_or_else(|| EngineError::spec_construction("introspection encounter has no enemy"))?;
46    let group = definition
47        .groups
48        .get(enemy.group_id.0 as usize)
49        .ok_or_else(|| {
50            EngineError::spec_construction("introspection encounter enemy group is missing")
51        })?;
52    let identity = ResolvedEnemyIdentityMetadata::new(
53        enemy.id,
54        "Training Dummy".to_string(),
55        None,
56        None,
57        None,
58    )?;
59    let stats = ResolvedEnemyCombatStats::new(
60        enemy.id,
61        INTROSPECT_MAX_HEALTH,
62        INTROSPECT_CREATURE_ARMOR,
63        INTROSPECT_ARMOR_CONSTANT,
64        Some(
65            enemy
66                .auto_attack_dps_override
67                .unwrap_or(INTROSPECT_AUTO_ATTACK_DPS),
68        ),
69        Some(
70            enemy
71                .spell_damage_override
72                .unwrap_or(INTROSPECT_SPELL_DAMAGE),
73        ),
74    )?;
75    let resolved = ResolvedEnemyEncounter::new(enemy, &group.tags, enemy.level, identity, stats)?;
76
77    Ok(ResolvedEncounter::new(definition, vec![resolved])?)
78}
79
80fn introspection_game_data(encounter: &ResolvedEncounter) -> ResolvedGameData {
81    let primary = encounter.primary_enemy();
82
83    ResolvedGameData::from_environment(
84        u32::from(primary.level()),
85        primary.creature_armor(),
86        primary.armor_constant(),
87    )
88}
89
90pub(crate) fn make_introspect_handler(
91    descriptor: &SpecDescriptor,
92    game_data: ResolvedGameData,
93) -> Result<Box<dyn wowlab_engine_ports::SpecHandler>, EngineError> {
94    let stats = wowlab_engine_domain::stats::default_stats();
95    let encounter = introspection_encounter()?;
96    let game_data = if game_data.is_empty() {
97        introspection_game_data(&encounter)
98    } else {
99        game_data
100    };
101    let rotation = introspection_rotation();
102
103    (descriptor.handler_factory)(
104        HandlerParams {
105            game_data,
106            rotation: &rotation,
107            stats: &stats,
108            talent_selections: &[],
109            encounter: &encounter,
110            fight_duration_secs: encounter.fight_duration_secs(),
111            equipped_items: &[],
112            set_bonus_auras: &[],
113            bloodlust: false,
114            pre_pot_tempered: false,
115            flask: false,
116            augment_rune: false,
117            race: wowlab_types::game::RaceId::Human,
118            weapon_enchant_procs: &[],
119            bugs: wowlab_engine_ports::BugSettings::default(),
120            cast_latency: wowlab_engine_ports::CastLatency::default(),
121            raid_events: &[],
122            external_buffs: &[],
123        }
124        .validate()?,
125    )
126}
127
128/// Build a full introspection snapshot for a spec, including hero talents.
129pub fn introspect_spec_with_data(
130    descriptor: &SpecDescriptor,
131    game_data: ResolvedGameData,
132) -> Result<wowlab_types::game::SpecIntrospection, ApplicationError> {
133    let handler = make_introspect_handler(descriptor, game_data)
134        .in_application_stage(ApplicationStage::SpecIntrospection)?;
135    let mut introspection = handler.introspect();
136
137    introspection.hero_talents = descriptor
138        .metadata
139        .hero_talent_trees
140        .iter()
141        .map(|t| wowlab_types::game::HeroTalentTree {
142            name: t.name.to_string(),
143            spells: t
144                .spells
145                .iter()
146                .map(|&(n, id)| (n.to_string(), id))
147                .collect(),
148            auras: t.auras.iter().map(|&(n, id)| (n.to_string(), id)).collect(),
149        })
150        .collect();
151
152    Ok(introspection)
153}
154
155/// Convenience wrapper using empty game data (all durations/costs = 0).
156pub fn introspect_spec(
157    catalog: &ContentCatalog,
158    spec_id: SpecId,
159) -> Result<wowlab_types::game::SpecIntrospection, ApplicationError> {
160    let descriptor = catalog
161        .descriptor(spec_id)
162        .in_application_stage(ApplicationStage::SpecIntrospection)?;
163
164    introspect_spec_with_data(descriptor, ResolvedGameData::default())
165}
166
167/// Resolve a spec's game data and return its complete introspection snapshot.
168pub async fn introspect_spec_resolved(
169    catalog: &ContentCatalog,
170    spec_id: SpecId,
171    resolver: &DynDataResolver<'_>,
172) -> Result<wowlab_types::game::SpecIntrospection, ApplicationError> {
173    let descriptor = catalog
174        .descriptor(spec_id)
175        .in_application_stage(ApplicationStage::SpecIntrospection)?;
176    let game_data = resolve_game_data(
177        resolver,
178        GameDataInputs {
179            descriptor,
180            extra_spell_ids: &[],
181            talents: &[],
182            set_bonus_auras: &[],
183            character: CharacterContext {
184                main_hand: WeaponStats::default(),
185                race: None,
186                off_hand: None,
187                player_expansion_id: CURRENT_EXPANSION_ID,
188                player_level: MAX_PLAYER_LEVEL,
189                consumables: wowlab_engine_gamedata::ConsumableSpells::default(),
190            },
191        },
192    )
193    .await?;
194
195    introspect_spec_with_data(descriptor, game_data)
196}
197
198#[cfg(test)]
199#[path = "introspection/tests.rs"]
200mod tests;