world books open, and anvils stop offering a cursor gear
- clicking a book in the world opens it and turns its pages: letters and world books share one reader - the cursor's object rules are the client's own 31-row strategy table now, so decorative objects stop pretending to be usable - trainer and tradeskill windows draw each row's icon by the reference's own law, and the trainer's filter dropdown toggles again - water color follows the zone's light blend, so lakes stop snapping between area tints Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -526,3 +526,51 @@ fn a_solid_colour_texel_multiplies_with_the_vertex_colour() {
|
||||
"the vertex colour outlives the texel it was multiplying"
|
||||
);
|
||||
}
|
||||
|
||||
/// The draw gate is the TEXTURE slot, never the colour (`texture-color-composition.md` §4,
|
||||
/// VERIFIED): `0x7706e0` tests `+0xcc` and emits NOTHING when it is empty, whatever the vertex
|
||||
/// colour holds. Since the tint deliberately survives `SetTexture(nil)` ("a tint outlives the art
|
||||
/// it was tinting"), a cleared region used to leak its tint out of extract as a solid plate — an
|
||||
/// occupied action button going empty on a character switch drew its surviving 1/1/1 usable-tint
|
||||
/// as a solid WHITE square (decision 1108; the 2026-07-10 grey wells were the same class).
|
||||
#[test]
|
||||
fn a_vertex_colour_without_a_texture_draws_nothing() {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.run(
|
||||
r#"
|
||||
local f = CreateFrame("Frame", "Owner")
|
||||
f:SetPoint("BOTTOMLEFT", 0, 0)
|
||||
f:SetWidth(100) f:SetHeight(50)
|
||||
icon = f:CreateTexture("Icon", "ARTWORK")
|
||||
icon:SetTexture("Interface\\Icons\\Spell_Nature_Sleep")
|
||||
icon:SetVertexColor(1, 1, 1)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
let drawn = |s: &UiScript| {
|
||||
s.extract()
|
||||
.iter()
|
||||
.filter(|q| {
|
||||
matches!(&q.content, QuadContent::Texture { path, color, .. }
|
||||
if path.is_some() || color.is_some())
|
||||
})
|
||||
.count()
|
||||
};
|
||||
assert_eq!(drawn(&s), 1, "tinted art draws");
|
||||
|
||||
// The slot empties: the art clears, the tint stays (distinct storage) — and nothing draws.
|
||||
s.run("icon:SetTexture(nil)").unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<(f32, f32, f32, f32)>("return icon:GetVertexColor()")
|
||||
.unwrap(),
|
||||
(1.0, 1.0, 1.0, 1.0),
|
||||
"the tint survives the clear"
|
||||
);
|
||||
assert_eq!(
|
||||
drawn(&s),
|
||||
0,
|
||||
"no texture at +0xcc -> emit NOTHING, never a solid plate of the surviving tint"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -260,3 +260,84 @@ fn set_hyperlink_renders_items_and_ignores_other_links() {
|
||||
.unwrap();
|
||||
assert!(s.take_errors().is_empty());
|
||||
}
|
||||
|
||||
/// One tradeskill recipe fixture — the product id is `spell_id + 10_000`, so the tooltip's own
|
||||
/// name names which recipe the channel actually landed on.
|
||||
fn ts_recipe(spell_id: u32, name: &str, group: (u32, u32, &str)) -> TradeSkillRecipe {
|
||||
TradeSkillRecipe {
|
||||
group: Some((group.0, group.1, group.2.to_string())),
|
||||
spell_id,
|
||||
name: name.into(),
|
||||
difficulty: TradeSkillDifficulty::Optimal,
|
||||
num_available: 1,
|
||||
icon: None,
|
||||
min_made: 1,
|
||||
max_made: 1,
|
||||
cooldown_secs: None,
|
||||
product_item: spell_id + 10_000,
|
||||
product_inv_type: 0,
|
||||
product_item_level: 0,
|
||||
reagents: vec![TradeSkillReagent {
|
||||
item: spell_id + 20_000,
|
||||
name: Some(format!("{name} Reagent")),
|
||||
icon: None,
|
||||
need: 1,
|
||||
have: 1,
|
||||
}],
|
||||
tools: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// `SetTradeSkillItem`'s index is a **VISIBLE** row index, not a position in `recipes` — headers
|
||||
/// interleave with rows, so the two differ the moment any group precedes the recipe. This is the
|
||||
/// exact mis-index the tradeskill module doc once carried as a known gap: with one header above it,
|
||||
/// visible row 4 is the SECOND group's recipe, and a raw `recipes[4-1]` lookup would land on the
|
||||
/// first group's second recipe instead. Both the product channel and the reagent channel are
|
||||
/// pinned, since the detail-icon and reagent-slot hovers are the two callers that pass a genuine
|
||||
/// visible index straight through. A HEADER row is a no-op, not a shifted hit.
|
||||
#[test]
|
||||
fn set_trade_skill_item_indexes_visible_rows_not_raw_recipes() {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.run(r#"local tt = CreateFrame("GameTooltip", "TT")"#)
|
||||
.unwrap();
|
||||
|
||||
// Two groups, two recipes each. Visible order: [1] "Bolts" header, [2] Alpha Bolt,
|
||||
// [3] Beta Bolt, [4] "Cloak" header, [5] Alpha Cloak, [6] Beta Cloak.
|
||||
s.set_trade_skill(Some(TradeSkillState {
|
||||
line: 197,
|
||||
line_name: "Tailoring".into(),
|
||||
rank: 57,
|
||||
max_rank: 75,
|
||||
recipes: vec![
|
||||
ts_recipe(1, "Beta Bolt", (1, 2, "Bolts")),
|
||||
ts_recipe(2, "Alpha Bolt", (1, 2, "Bolts")),
|
||||
ts_recipe(3, "Beta Cloak", (2, 1, "Cloak")),
|
||||
ts_recipe(4, "Alpha Cloak", (2, 1, "Cloak")),
|
||||
],
|
||||
repeat_count: 1,
|
||||
}));
|
||||
assert_eq!(s.eval::<i64>("return GetNumTradeSkills()").unwrap(), 6);
|
||||
|
||||
// Visible row 5 is "Alpha Cloak" (spell 4) — its product is 10_004. A raw recipes[5-1] lookup
|
||||
// would land on recipe index 4 (out of range, a silent no-op); recipes[4-1] on "Beta Cloak".
|
||||
// `TTTextLeft1` need not exist when a call renders nothing (what an out-of-range RAW index
|
||||
// does), so read it defensively — that case must surface as an assert_eq naming the row, not
|
||||
// as a Lua nil-index panic three lines away from the claim.
|
||||
let name_at = |s: &mut UiScript, row: i64, reagent: &str| -> String {
|
||||
s.run(&format!("TT:SetTradeSkillItem({row}{reagent})"))
|
||||
.unwrap();
|
||||
s.eval::<String>("return TTTextLeft1 and TTTextLeft1:GetText() or '<nothing rendered>'")
|
||||
.unwrap()
|
||||
};
|
||||
assert_eq!(name_at(&mut s, 5, ""), "Alpha Cloak");
|
||||
assert_eq!(name_at(&mut s, 6, ""), "Beta Cloak");
|
||||
assert_eq!(name_at(&mut s, 2, ""), "Alpha Bolt");
|
||||
|
||||
// The reagent channel takes the same mapping.
|
||||
assert_eq!(name_at(&mut s, 5, ", 1"), "Alpha Cloak Reagent");
|
||||
|
||||
// A header row resolves to nothing — the hover is a no-op, so the previous render stands
|
||||
// rather than a neighbouring recipe's tooltip appearing under the cursor.
|
||||
assert_eq!(name_at(&mut s, 4, ""), "Alpha Cloak Reagent");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user