the Ironforge lava returns, and the sound crackle is cornered

- the Great Forge's lava stays drawn from every angle, and the chasm catwalk reads as a connected grate again
- music decoding and shader-compile bursts stop crowding the audio's lane, and the output buffer follows the device's real deadline
- the audio path grows its instruments: a mix tap to WAV, a starvation meter, a hardware overload listener
- trainer detail icons carry the reference's own tooltip

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-08-07 16:03:52 +07:00
co-authored by Claude Fable 5
parent 50aedb8b05
commit ba9a333aeb
37 changed files with 2835 additions and 1118 deletions
@@ -498,3 +498,234 @@ fn action_hover_delegates_by_kind() {
assert_eq!(s.take_spell_tooltip_asks(), vec![5143], "the miss asked");
assert!(s.take_errors().is_empty());
}
/// The tooltip's left-column texts in order.
fn left_lines(s: &mut UiScript) -> Vec<String> {
let n: i64 = s.eval("return TT:NumLines()").unwrap();
(1..=n)
.map(|i| {
s.eval::<String>(&format!(
"return getglobal('TTTextLeft{i}'):GetText() or ''"
))
.unwrap()
})
.collect()
}
/// One trainer service fixture with an explicit tooltip subject.
fn trainer_service(spell_id: u32, name: &str, tooltip: TrainerTooltip) -> TrainerService {
TrainerService {
spell_id,
tooltip,
name: Some(name.into()),
texture: Some("Interface\\Icons\\Temp".into()),
category: TrainerServiceCategory::Available,
skill_line: 26,
skill_line_name: "Arms".into(),
..Default::default()
}
}
/// `SetTrainerService` is a SELECTOR: it renders no line of its own and routes to whichever shared
/// builder the app-side law picked. All three arms are pinned here — the item arm, the spell arm
/// (with `altCaster` suppressing the reagents block), and a header row, which is a no-op.
///
/// The index is a VISIBLE row index, so row 1 is the "Arms" header and the services start at 2 —
/// the same interleave `SetTradeSkillItem` has, and the same way to get it wrong.
#[test]
fn set_trainer_service_selects_the_builder_and_never_renders_its_own_line() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.set_item_template(
2847,
ItemTemplateView {
name: "Copper Shortsword".into(),
quality: 1,
class: 2,
subclass: 7,
..Default::default()
},
);
s.set_spell_tooltip(
200,
SpellTooltipView {
name: "Heroic Strike".into(),
cost: Some("15 Rage".into()),
reagents: Some("Reagents: Linen Cloth".into()),
description: "A strong attack.".into(),
..Default::default()
},
);
s.set_trainer(Some(TrainerState {
services: vec![
trainer_service(2756, "Copper Shortsword", TrainerTooltip::Item(2847)),
trainer_service(
100,
"Heroic Strike",
TrainerTooltip::Spell {
spell_id: 200,
alt_caster: false,
},
),
trainer_service(
101,
"Train Growl",
TrainerTooltip::Spell {
spell_id: 200,
alt_caster: true,
},
),
],
..Default::default()
}));
s.run(
r#"
local a = CreateFrame("Button", "AB1"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
CreateFrame("GameTooltip", "TT")
-- Row 1 is the "Arms" header: a no-op, not a shifted hit onto a neighbouring service.
TT:SetOwner(a, "ANCHOR_RIGHT")
TT:SetTrainerService(1)
assert(TT:NumLines() == 0, "a header row renders nothing")
-- Row 2: the recipe -> the ITEM builder, full item tooltip (not the spell's name).
TT:SetOwner(a, "ANCHOR_RIGHT")
TT:SetTrainerService(2)
assert(TTTextLeft1:GetText() == "Copper Shortsword", "the item arm renders the item")
assert(TT:NumLines() > 1, "the ITEM builder, not a one-line stub")
"#,
)
.unwrap();
// Row 3: the class-trainer service -> the SPELL builder on the TAUGHT spell, reagents shown.
s.run(
r#"
TT:SetOwner(AB1, "ANCHOR_RIGHT")
TT:SetTrainerService(3)
"#,
)
.unwrap();
let texts = left_lines(&mut s);
assert_eq!(
texts[0], "Heroic Strike",
"the spell arm hops to the taught"
);
assert!(
texts.iter().any(|t| t == "Reagents: Linen Cloth"),
"altCaster clear -> the reagents block renders: {texts:?}"
);
assert!(
texts.iter().any(|t| t == "A strong attack."),
"the gold description is part of the shared builder's law: {texts:?}"
);
// Row 4: the same view, altCaster set -> the reagents block is gone, everything else stays.
s.run(
r#"
TT:SetOwner(AB1, "ANCHOR_RIGHT")
TT:SetTrainerService(4)
"#,
)
.unwrap();
let texts = left_lines(&mut s);
assert!(
!texts.iter().any(|t| t == "Reagents: Linen Cloth"),
"altCaster gates the reagents block: {texts:?}"
);
assert!(
texts.iter().any(|t| t == "A strong attack."),
"and gates ONLY that block: {texts:?}"
);
assert!(s.take_errors().is_empty());
}
/// `SetCraftSpell` is `SetTrainerService`'s structural twin: a selector, not the two-line
/// name/description stub it used to be. Both arms are pinned, and the assertion that matters most
/// is the negative one — a rod recipe's hover shows the ROD's item tooltip while the same row's
/// ICON is the spell's (Law D, decision 1107). Icon and tooltip disagreeing on one row is the
/// verified shape, not a bug to reconcile.
#[test]
fn set_craft_spell_selects_the_builder_like_the_trainer_hover_does() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.set_item_template(
6218,
ItemTemplateView {
name: "Runed Copper Rod".into(),
quality: 1,
..Default::default()
},
);
s.set_spell_tooltip(
7420,
SpellTooltipView {
name: "Enchant Bracer - Minor Health".into(),
cast_time: Some("Instant cast".into()),
description: "Permanently enchant bracers.".into(),
..Default::default()
},
);
let recipe = |spell_id: u32, name: &str, tooltip: CraftTooltip| CraftRecipe {
spell_id,
tooltip,
name: name.into(),
sub_name: String::new(),
difficulty: TradeSkillDifficulty::Optimal,
num_available: 1,
icon: Some("Interface\\Icons\\Spell_Holy_Heal".into()),
description: Some("the OLD two-line stub's second line".into()),
needs_item_target: false,
reagents: vec![],
tools: vec![],
};
s.set_craft(Some(CraftState {
name: "Enchanting".into(),
rank: 100,
max_rank: 150,
recipes: vec![
recipe(
7420,
"Enchant Bracer - Minor Health",
CraftTooltip::Spell(7420),
),
recipe(7421, "Runed Copper Rod", CraftTooltip::Item(6218)),
],
}));
s.run(
r#"
local a = CreateFrame("Button", "AB2"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
CreateFrame("GameTooltip", "TT")
TT:SetOwner(a, "ANCHOR_RIGHT")
TT:SetCraftSpell(1)
"#,
)
.unwrap();
let texts = left_lines(&mut s);
assert_eq!(texts[0], "Enchant Bracer - Minor Health");
assert!(
texts.iter().any(|t| t == "Instant cast"),
"the SHARED spell builder's law, not a two-line stub: {texts:?}"
);
assert!(
!texts
.iter()
.any(|t| t == "the OLD two-line stub's second line"),
"the row's own `description` field is no longer the tooltip's source: {texts:?}"
);
s.run(
r#"
TT:SetOwner(AB2, "ANCHOR_RIGHT")
TT:SetCraftSpell(2)
"#,
)
.unwrap();
let texts = left_lines(&mut s);
assert_eq!(
texts[0], "Runed Copper Rod",
"a CREATE_ITEM recipe hovers the ITEM, though its icon is the spell's: {texts:?}"
);
assert!(s.take_errors().is_empty());
}