From c574e2003108444412ec695c019db126ea131f38 Mon Sep 17 00:00:00 2001 From: Mathih13 Date: Tue, 29 Sep 2026 05:27:16 +0200 Subject: [PATCH] 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) Co-Authored-By: Sam --- crates/benilla-ui/src/script/talent.rs | 14 ++- crates/benilla-ui/src/script/tests/talent.rs | 102 ++++++++++++++---- crates/benilla-ui/src/script/tooltip_spell.rs | 18 +++- 3 files changed, 109 insertions(+), 25 deletions(-) diff --git a/crates/benilla-ui/src/script/talent.rs b/crates/benilla-ui/src/script/talent.rs index 0d3f3038..85809c70 100644 --- a/crates/benilla-ui/src/script/talent.rs +++ b/crates/benilla-ui/src/script/talent.rs @@ -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, diff --git a/crates/benilla-ui/src/script/tests/talent.rs b/crates/benilla-ui/src/script/tests/talent.rs index ab4c63fc..7920b405 100644 --- a/crates/benilla-ui/src/script/tests/talent.rs +++ b/crates/benilla-ui/src/script/tests/talent.rs @@ -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()); } diff --git a/crates/benilla-ui/src/script/tooltip_spell.rs b/crates/benilla-ui/src/script/tooltip_spell.rs index f1884e83..740c60e6 100644 --- a/crates/benilla-ui/src/script/tooltip_spell.rs +++ b/crates/benilla-ui/src/script/tooltip_spell.rs @@ -227,6 +227,10 @@ pub(super) fn spell_view_of(lua: &Lua, spell_id: u32) -> Option, @@ -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)?; }