fix: drop the spell body from passive talent tooltips (#538)

- A passive talent's tooltip is the talent builder's: name, rank, requirements, description and the next rank, with no weapon-class "Requires" row or other spell line (#476)
- Talent tooltip tests pin the talent builder's gold spacer and every line of an exceptional talent's spell body
- SetTalent's doc states why an exceptional talent's next-rank pass never shows: no 1.12 exceptional talent has a second rank

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
This commit is contained in:
Mathih13
2026-09-29 10:27:16 +07:00
committed by GitHub
co-authored by Claude Opus 5.5 Sam
parent 459e0e90c1
commit c574e20031
3 changed files with 109 additions and 25 deletions
+9 -5
View File
@@ -8,11 +8,14 @@
//! `TALENT_BRANCH_ARRAY[tier][column]`. The respec pair answers a class trainer's
//! `CONFIRM_TALENT_WIPE` and reads no snapshot.
//!
//! `GameTooltip:SetTalent` is the spell builder (`0x52e610`) with talent lines: the rank line
//! (`TOOLTIP_TALENT_RANK`, `0x854a2c`), the red requirement lines after it, and "Click to learn"
//! (`TOOLTIP_TALENT_LEARN`, `0x8549f8`) in green on a learnable rank. The "Next rank:" block,
//! `TOOLTIP_TALENT_NEXT_RANK` in white and the next rank's description in gold, is untraced in
//! the reference.
//! `GameTooltip:SetTalent` (`0x535170`) builds an exceptional talent with the spell builder
//! (`0x52e610`) and any other with the talent builder (`0x52b0a0`), which keeps the spell's name
//! and description and drops the rest of its body. The talent lines are the talent builder's: the
//! rank line (`TOOLTIP_TALENT_RANK`, `0x854a2c`), the red requirement lines after it, the "Next
//! rank:" block (`TOOLTIP_TALENT_NEXT_RANK`, `0x854a10`, white, over the next rank's gold
//! description) and "Click to learn" (`TOOLTIP_TALENT_LEARN`, `0x8549f8`) in green on a learnable
//! rank. The spell builder writes the same lines around the full body (`0x52f735`); its next-rank
//! pass, a second full body, never runs, as no 1.12 exceptional talent has a second rank.
use mlua::{Lua, MultiValue, Table, Value};
@@ -272,6 +275,7 @@ pub(super) fn install_tooltip_method(lua: &Lua, m: &Table) -> mlua::Result<()> {
(
t.display_spell,
TalentLines {
exceptional: t.exceptional,
rank_line,
reqs: t.req_lines.clone(),
next_spell: t.next_spell,
+84 -18
View File
@@ -1,5 +1,5 @@
//! The talent bindings over a pushed snapshot, the learn queue, and `SetTalent`: the spell tooltip
//! with the talent lines interleaved.
//! The talent bindings over a pushed snapshot, the learn queue, and `SetTalent`: the talent builder
//! for a passive talent, the spell tooltip with the talent lines interleaved for an exceptional one.
use super::common::script;
use crate::script::*;
@@ -115,29 +115,40 @@ fn seed_talent_strings(s: &mut UiScript) {
.unwrap();
}
/// `SetTalent`'s lines: name, `TOOLTIP_TALENT_RANK` (white), cost, gold description,
/// `TOOLTIP_TALENT_NEXT_RANK` and the next rank's gold description, green `TOOLTIP_TALENT_LEARN`.
/// A passive talent's full spell view, every body line filled, so a test sees which ones the talent
/// builder drops.
fn passive_body(description: &str) -> SpellTooltipView {
SpellTooltipView {
name: "Improved Fireball".into(),
cost: Some("35 Mana".into()),
range: Some("30 yd range".into()),
cast_time: Some("Instant".into()),
cooldown: Some("6 sec cooldown".into()),
requires_item: Some("Requires One-Handed Axes".into()),
requires_form: Some("Requires Battle Stance".into()),
reagents: Some("Reagents: Light Feather".into()),
chance: Some("2.62% chance to dodge".into()),
description: description.into(),
..Default::default()
}
}
/// `SetTalent` on a passive talent is the talent builder `0x52b0a0`: name, `TOOLTIP_TALENT_RANK`
/// (white), the gold description, a `" "` spacer, `TOOLTIP_TALENT_NEXT_RANK` and the next rank's
/// gold description, green `TOOLTIP_TALENT_LEARN`; no cost, range, cast, requirement, reagent or
/// chance line.
#[test]
fn set_talent_renders_the_interleaved_tooltip() {
fn set_talent_on_a_passive_talent_is_the_talent_builder() {
let mut s = script();
seed_talent_strings(&mut s);
s.set_talents(one_tab_state());
s.set_spell_tooltip(
11070,
SpellTooltipView {
name: "Improved Fireball".into(),
cast_time: Some("Instant".into()),
description: "Reduces the casting time of your Fireball spell by 0.3 sec.".into(),
..Default::default()
},
passive_body("Reduces the casting time of your Fireball spell by 0.3 sec."),
);
s.set_spell_tooltip(
11071,
SpellTooltipView {
name: "Improved Fireball".into(),
description: "Reduces the casting time of your Fireball spell by 0.4 sec.".into(),
..Default::default()
},
passive_body("Reduces the casting time of your Fireball spell by 0.4 sec."),
);
s.run(
r#"
@@ -146,13 +157,18 @@ fn set_talent_renders_the_interleaved_tooltip() {
tt:SetOwner(a, "ANCHOR_RIGHT")
tt:SetTalent(1, 1)
assert(tt:IsShown(), "SetTalent shows")
-- name, rank, Instant, desc, next-rank header, next desc, learn hint = 7 lines.
-- name, rank, desc, spacer, next-rank header, next desc, learn hint = 7 lines.
assert(tt:NumLines() == 7, "got " .. tt:NumLines())
assert(TTTextLeft1:GetText() == "Improved Fireball")
assert(TTTextLeft2:GetText() == "[RANK 3/5]", "got " .. TTTextLeft2:GetText())
assert(TTTextLeft3:GetText() == "Instant")
assert(TTTextLeft3:GetText() == "Reduces the casting time of your Fireball spell by 0.3 sec.")
assert(TTTextLeft4:GetText() == " ", "the spacer, got " .. tostring(TTTextLeft4:GetText()))
assert(TTTextLeft5:GetText() == "[NEXT_RANK]")
assert(TTTextLeft6:GetText() == "Reduces the casting time of your Fireball spell by 0.4 sec.")
assert(TTTextLeft7:GetText() == "[LEARN]")
for i = 1, 7 do
assert(getglobal("TTTextRight" .. i):GetText() == nil, "no right column on line " .. i)
end
"#,
)
.unwrap();
@@ -163,6 +179,56 @@ fn set_talent_renders_the_interleaved_tooltip() {
if t == "[LEARN]" && c[0] < 1e-6 && (c[1] - 1.0).abs() < 1e-6)
});
assert!(green, "the learn hint is green");
// The spacer is `0x530380`'s gold, not `0x5303b0`'s caller colour.
let gold_spacer = quads.iter().any(|q| {
matches!(&q.content, QuadContent::Text { text: Some(t), color: Some(c), .. }
if t == " " && (c[0] - 1.0).abs() < 1e-6 && (c[1] - 210.0 / 255.0).abs() < 1e-3 && c[2] < 1e-6)
});
assert!(gold_spacer, "the spacer is gold");
assert!(s.take_errors().is_empty());
}
/// An exceptional talent (`Talent.dbc` Flags bit 0) is the spell builder `0x52e610` instead, whose
/// cost, cast and required-item lines stay: Holy Shield's "Requires Shields".
#[test]
fn set_talent_on_an_exceptional_talent_keeps_the_spell_body() {
let mut s = script();
seed_talent_strings(&mut s);
let mut state = one_tab_state();
let holy_shield = &mut state.talents[0][1];
holy_shield.name = "Holy Shield".into();
holy_shield.exceptional = true;
holy_shield.req_lines = vec!["Requires 30 points in Protection Talents".into()];
s.set_talents(state);
s.set_spell_tooltip(
11119,
SpellTooltipView {
name: "Holy Shield".into(),
cost: Some("150 Mana".into()),
cast_time: Some("Instant cast".into()),
requires_item: Some("Requires Shields".into()),
description: "Increases chance to block by 30% for 10 sec.".into(),
..Default::default()
},
);
s.run(
r#"
local a = CreateFrame("Button", "TB3"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT3")
tt:SetOwner(a, "ANCHOR_RIGHT")
tt:SetTalent(1, 2)
local want = {
"Holy Shield", "[RANK 0/5]", "Requires 30 points in Protection Talents", "150 Mana",
"Instant cast", "Requires Shields", "Increases chance to block by 30% for 10 sec.",
}
assert(tt:NumLines() == table.getn(want), "got " .. tt:NumLines())
for i, text in ipairs(want) do
local got = getglobal("TT3TextLeft" .. i):GetText()
assert(got == text, "line " .. i .. ": " .. tostring(got))
end
"#,
)
.unwrap();
assert!(s.take_errors().is_empty());
}
+16 -2
View File
@@ -227,6 +227,10 @@ pub(super) fn spell_view_of(lua: &Lua, spell_id: u32) -> Option<SpellTooltipView
/// while locked, the next-rank block and the green learn hint.
#[derive(Clone, Debug, Default)]
pub(super) struct TalentLines {
/// `Talent.dbc` Flags bit 0: `SetTalent` hands an exceptional talent to the spell builder
/// `0x52e610` (`0x5352ab`) and any other to the talent builder `0x52b0a0`, which writes no
/// cost, range, cast, required-item, form, reagent or chance line.
pub exceptional: bool,
/// `TOOLTIP_TALENT_RANK` ("Rank %d/%d") filled from the player's strings; `None` when they
/// lack the key, and no rank row shows.
pub rank_line: Option<String>,
@@ -275,6 +279,7 @@ fn render_spell(
v.rank.clone().filter(|_| show_rank).map(|t| (t, GRAY))
};
append_line(lua, this, (v.name.clone(), name_color), right, false)?;
let talent_builder = talent.is_some_and(|t| !t.exceptional);
// The talent head: the white rank line, then the red requirements while locked.
if let Some(t) = talent {
if let Some(rank) = &t.rank_line {
@@ -284,7 +289,7 @@ fn render_spell(
append_line(lua, this, (req.clone(), RED), None, true)?;
}
}
if !aura {
if !aura && !talent_builder {
match (&v.cost, &v.range) {
(Some(c), Some(r)) => append_line(
lua,
@@ -340,7 +345,16 @@ fn render_spell(
// `0x52b2cd`) over the next rank's gold description, then the green `TOOLTIP_TALENT_LEARN`
// hint (`0x52b362`). Both are the player's own strings; without them the line is skipped.
if let Some(t) = talent {
if let Some(next) = &t.next_desc {
// The talent builder skips a rank with an empty description, header and all (`0x52b294`).
let next = t
.next_desc
.as_ref()
.filter(|n| !talent_builder || !n.is_empty());
if let Some(next) = next {
// The talent builder's gold `" "` spacer (`0x82ee00`, `0x52b2a8`) after a description.
if talent_builder && !desc.is_empty() {
append_line(lua, this, (" ".into(), GOLD), None, false)?;
}
if let Some(header) = crate::strings::global(lua, "TOOLTIP_TALENT_NEXT_RANK") {
append_line(lua, this, (header, WHITE), None, false)?;
}