wowlab_engine_application/
introspection.rs1use 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
128pub 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
155pub 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
167pub 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;