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:
@@ -209,9 +209,19 @@ impl UiScript {
|
||||
alpha_gradient: data.alpha_gradient,
|
||||
}
|
||||
} else {
|
||||
// The draw gate is the TEXTURE slot, never the colour (`0x7706e0`: `+0xcc`
|
||||
// empty -> emit NOTHING — `texture-color-composition.md` §4, VERIFIED). A
|
||||
// vertex colour is a tint on whatever texture exists; alone it is not
|
||||
// drawable content — it survives `SetTexture(nil)` by design ("a tint
|
||||
// outlives the art it was tinting") and used to leak out of here as a
|
||||
// solid plate the moment the art was cleared (the white action buttons on
|
||||
// a character switch; the 2026-07-10 grey wells were the same class).
|
||||
let has_texture = data.texture.is_some() || data.fill.is_some();
|
||||
QuadContent::Texture {
|
||||
path: data.texture,
|
||||
color: texture_color(data.fill, data.vertex_color),
|
||||
color: has_texture
|
||||
.then(|| texture_color(data.fill, data.vertex_color))
|
||||
.flatten(),
|
||||
additive: data.additive,
|
||||
tex_coords: data.tex_coords,
|
||||
circular: data.circular,
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
//! VISIBLE index from that flat position on every read, [`super::skills`]'s own by-identity
|
||||
//! persistence pattern adapted to a field this module can't change the type of).
|
||||
//!
|
||||
//! ## The tooltip channel — KNOWN GAP, not fixed here
|
||||
//! ## The tooltip channel
|
||||
//!
|
||||
//! `GameTooltip:SetTradeSkillItem(skillIndex [, reagentIndex])` is registered in
|
||||
//! [`super::tooltip_item`], beside `SetMerchantItem`, through the same id-keyed item renderer: with
|
||||
@@ -80,16 +80,10 @@
|
||||
//! the renderer's own name-only line — the recipe's name for the product channel — rather than a
|
||||
//! no-op, since that fallback already exists and reads better than a blank hover.
|
||||
//!
|
||||
//! **As of this grouping landing, `tooltip_item`'s `SetTradeSkillItem` still indexes
|
||||
//! [`TradeSkillState::recipes`] directly by the raw `skillIndex` it's called with** — it does NOT
|
||||
//! route through [`recipe_at`]'s visible-index mapping (that module is outside this change's file
|
||||
//! scope). The XML's list-row hover (`BenillaTradeSkillSkillButton_OnEnter`) is guarded here to
|
||||
//! never call it on a header row, but the product-icon and reagent-slot hovers pass
|
||||
//! `BenillaTradeSkillFrame.selectedSkill` — a genuine VISIBLE index of a selected recipe — straight
|
||||
//! through, so once any group precedes the selected recipe's own group, those two hovers will show
|
||||
//! the WRONG item. `tooltip_item::install`'s `SetTradeSkillItem` closure needs a follow-up edit to
|
||||
//! resolve through [`recipe_at`] (now `pub(crate)` for exactly this) instead of `ts.recipes.get(...)`
|
||||
//! directly.
|
||||
//! `skillIndex` is a **VISIBLE** index and is resolved through [`recipe_at`] (`pub(crate)` for
|
||||
//! exactly this), never a raw [`TradeSkillState::recipes`] position — headers interleave with rows
|
||||
//! since the TU-B grouping landed, so a raw index would show the wrong item the moment any group
|
||||
//! precedes the selected recipe's own. A header index resolves to `None` and the hover is a no-op.
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user