every line is a key, four find their sound, and the ding prints once

- the last app-side lines become GlobalStrings keys — tooltip cells, party, petition, duel, quest, PvP and rest lines read the install's own wording, so a non-enUS chain shows its own text (2045, 2080)
- a locked chest says Requires Pick Lock, ammo and thrown weapons drop their invented slot word, a met set bonus reads Set: without the count, and a 100 s cooldown reads 1.67 min as the template prints it
- the level-up block and the free-professions line printed twice in chat and print once; a declined invite, a friend coming online, a duel request and a failed quest play the sound 1.12 gives them
- Duel cancelled. is the yellow line at the top of the screen, and a trainer refusal or a failed quest with no cached template shows nothing, as on 1.12

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-09-08 16:19:46 +07:00
co-authored by Claude Fable 5.1
parent 520b6dc2aa
commit 663851b17d
54 changed files with 3874 additions and 1958 deletions
+26 -9
View File
@@ -106,11 +106,27 @@ fn learn_talent_queues_for_the_app_drain() {
assert!(s.take_errors().is_empty());
}
/// SetTalent = the spell builder + the talent interleave: name, "Rank r/m" white, cost line,
/// gold description, "Next rank:" + the next rank's gold description, green "Click to learn".
/// A stand-in string table for the talent tail's three keys — **deliberately not the shipped
/// wording**, because what is under test is which key each line reaches and what fills it, never
/// what the sentence says (decision 2045).
fn seed_talent_strings(s: &mut UiScript) {
s.run(
r#"
TOOLTIP_TALENT_RANK = "[RANK %d/%d]"
TOOLTIP_TALENT_NEXT_RANK = "[NEXT_RANK]"
TOOLTIP_TALENT_LEARN = "[LEARN]"
"#,
)
.unwrap();
}
/// SetTalent = the spell builder + the talent interleave: name, TOOLTIP_TALENT_RANK white, cost
/// line, gold description, TOOLTIP_TALENT_NEXT_RANK + the next rank's gold description, green
/// TOOLTIP_TALENT_LEARN.
#[test]
fn set_talent_renders_the_interleaved_tooltip() {
let mut s = script();
seed_talent_strings(&mut s);
s.set_talents(one_tab_state());
s.set_spell_tooltip(
11070,
@@ -136,13 +152,13 @@ fn set_talent_renders_the_interleaved_tooltip() {
tt:SetOwner(a, "ANCHOR_RIGHT")
tt:SetTalent(1, 1)
assert(tt:IsShown(), "SetTalent shows")
-- name, Rank 3/5, Instant, desc, Next rank:, next desc, Click to learn = 7 lines.
-- name, rank, Instant, desc, 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")
assert(TTTextLeft2:GetText() == "[RANK 3/5]", "got " .. TTTextLeft2:GetText())
assert(TTTextLeft3:GetText() == "Instant")
assert(TTTextLeft5:GetText() == "Next rank:")
assert(TTTextLeft7:GetText() == "Click to learn")
assert(TTTextLeft5:GetText() == "[NEXT_RANK]")
assert(TTTextLeft7:GetText() == "[LEARN]")
"#,
)
.unwrap();
@@ -151,7 +167,7 @@ fn set_talent_renders_the_interleaved_tooltip() {
let quads = s.extract();
let green = quads.iter().any(|q| {
matches!(&q.content, QuadContent::Text { text: Some(t), color: Some(c), .. }
if t == "Click to learn" && c[0] < 1e-6 && (c[1] - 1.0).abs() < 1e-6)
if t == "[LEARN]" && c[0] < 1e-6 && (c[1] - 1.0).abs() < 1e-6)
});
assert!(green, "the learn hint is green");
assert!(s.take_errors().is_empty());
@@ -162,6 +178,7 @@ fn set_talent_renders_the_interleaved_tooltip() {
#[test]
fn set_talent_locked_reqs_and_the_ask_once_miss() {
let mut s = script();
seed_talent_strings(&mut s);
s.set_talents(one_tab_state());
// No spell view pushed for Ignite (11119): the render falls back, the ask is recorded.
s.run(
@@ -171,7 +188,7 @@ fn set_talent_locked_reqs_and_the_ask_once_miss() {
tt:SetOwner(a, "ANCHOR_RIGHT")
tt:SetTalent(1, 2)
assert(tt:IsShown())
assert(TT2TextLeft1:GetText() == "Rank 0/5", "fallback shows the rank head")
assert(TT2TextLeft1:GetText() == "[RANK 0/5]", "fallback shows the rank head")
"#,
)
.unwrap();
@@ -195,7 +212,7 @@ fn set_talent_locked_reqs_and_the_ask_once_miss() {
local tt = getglobal("TT2")
tt:SetOwner(getglobal("TB2"), "ANCHOR_RIGHT")
tt:SetTalent(1, 2)
-- name, Rank 0/5, req(red), desc = 4 lines; rank 0 has no next block, locked has no hint.
-- name, rank, req(red), desc = 4 lines; rank 0 has no next block, locked has no hint.
assert(tt:NumLines() == 4, "got " .. tt:NumLines())
assert(TT2TextLeft3:GetText() == "Requires 5 points in Improved Fireball")
"#,
@@ -9,7 +9,7 @@ use crate::script::*;
/// The shopping-compare pipeline end-to-end (0274 P4): a bag-ring hover on the main GameTooltip
/// with shift held fires `SHOW_COMPARE_TOOLTIP` once per finger slot; a ref-shaped listener
/// (PaperDollFrame.lua:621-640) seats ShoppingTooltip1/2, whose ARMED `SetInventoryItem` renders
/// the byte law's compare shape — gray "Currently Equipped", WHITE name, the compact cut (the
/// the byte law's compare shape — the gray CURRENTLY_EQUIPPED header, WHITE name, the compact cut (the
/// description never prints). Releasing shift hides the pair; a shift-up hover fires nothing
/// until the rising edge.
#[test]
@@ -135,7 +135,7 @@ fn shift_compare_fires_seats_and_renders_the_compare_shape() {
assert(table.getn(compare_calls) == 2, "two ring compares, got " .. table.getn(compare_calls))
assert(compare_calls[1] == "11:1" and compare_calls[2] == "12:2", "slot:index order")
assert(ShoppingTooltip1:IsShown() and ShoppingTooltip2:IsShown())
assert(ShoppingTooltip1TextLeft1:GetText() == "Currently Equipped")
assert(ShoppingTooltip1TextLeft1:GetText() == "[CURRENTLY_EQUIPPED]")
assert(ShoppingTooltip1TextLeft2:GetText() == "Old Loop")
assert(ShoppingTooltip2TextLeft2:GetText() == "Older Loop")
-- The compact cut at 0x52e14c: the description never prints on a compare.
@@ -167,10 +167,10 @@ fn shift_compare_fires_seats_and_renders_the_compare_shape() {
})
.unwrap_or([0.0; 4])
};
let gray = color_of("Currently Equipped");
let gray = color_of("[CURRENTLY_EQUIPPED]");
assert!(
(gray[0] - 128.0 / 255.0).abs() < 0.01 && (gray[1] - 128.0 / 255.0).abs() < 0.01,
"Currently Equipped is gray, got {gray:?}"
"CURRENTLY_EQUIPPED is gray, got {gray:?}"
);
let name = color_of("Old Loop");
assert_eq!(name, [1.0, 1.0, 1.0, 1.0], "compare name is WHITE");
@@ -493,7 +493,7 @@ fn set_merchant_compare_item_answers_one_or_nil_per_candidate_slot() {
s.eval::<bool>(
r#"local n = ShoppingTooltip1:NumLines()
for i = 1, n do
if getglobal("ShoppingTooltip1TextLeft"..i):GetText() == "Currently Equipped" then
if getglobal("ShoppingTooltip1TextLeft"..i):GetText() == "[CURRENTLY_EQUIPPED]" then
return true
end
end
@@ -37,20 +37,21 @@ fn item_line_law_and_red_requirements() {
texts,
vec![
"Ravager",
"Binds when equipped",
"Two-Hand",
"[ITEM_BIND_ON_EQUIP]",
"[INVTYPE_2HWEAPON]",
// School 0 is physical: the reference names no school word for it either.
"68 - 103 Damage",
"+ 2 - 4 Shadow Damage",
"(25.3 damage per second)",
"+ 2 - 4 [SCHOOL5] Damage",
"[DPS 25.3]",
// Display order, NOT wire order: the fixture feeds (Stamina, Strength) but the
// 0x808e88 table prints Strength first (STR,AGI,STA,INT,SPI,HP,MANA).
"+9 Strength",
"+12 Stamina",
"+10 Shadow Resistance",
"Durability 90 / 90",
"Classes: Warrior, Rogue",
"Requires Level 37",
"Chance on hit: Ravager",
"[MOD_STRENGTH +9]",
"[MOD_STAMINA +12]",
"[RESIST_SINGLE +10 [SCHOOL5]]",
"[DURABILITY 90/90]",
"[CLASSES Warrior, Rogue]",
"[MIN_LEVEL 37]",
"[ONPROC] Ravager",
"\"A wicked axe of the Scarlet Crusade.\"",
],
"the verified line order (0276)"
@@ -109,13 +110,9 @@ fn red_lines_track_player_state() {
.unwrap_or_else(|| panic!("no line starting {needle}"))
.1
};
assert_eq!(find("[MIN_LEVEL"), [1.0, 1.0, 1.0, 1.0], "60 ≥ 37 → white");
assert_eq!(
find("Requires Level"),
[1.0, 1.0, 1.0, 1.0],
"60 ≥ 37 → white"
);
assert_eq!(
find("Classes:"),
find("[CLASSES"),
[1.0, 32.0 / 255.0, 32.0 / 255.0, 1.0],
"mage → red"
);
@@ -170,26 +167,26 @@ fn verified_families_signable_locked_resists_known() {
texts,
vec![
"Sealed Charter",
"<Right Click for Details>",
"Unique",
"This Item Begins a Quest",
"Locked",
"+5 to All Resistances",
"Already known",
"[ITEM_SIGNABLE]",
"[ITEM_UNIQUE]",
"[ITEM_STARTS_QUEST]",
"[LOCKED]",
"[RESIST_ALL +5]",
"[ITEM_SPELL_KNOWN]",
"\"Sign here.\"",
],
"the verified gated families in the verified order"
);
let color = |needle: &str| lines.iter().find(|(t, _)| t == needle).unwrap().1;
let red = [1.0, 32.0 / 255.0, 32.0 / 255.0, 1.0];
assert_eq!(color("Locked"), red, "LOCKED is red");
assert_eq!(color("[LOCKED]"), red, "LOCKED is red");
assert_eq!(
color("Already known"),
color("[ITEM_SPELL_KNOWN]"),
red,
"SPELL_KNOWN is unconditional red"
);
assert_eq!(
color("<Right Click for Details>"),
color("[ITEM_SIGNABLE]"),
[0.0, 1.0, 0.0, 1.0],
"SIGNABLE is green"
);
@@ -263,7 +260,11 @@ fn proficiency_and_reputation_reds() {
.1
};
let lines = lines_of(&mut s);
assert_eq!(color(&lines, "Two-Hand"), white, "proficient slot is white");
assert_eq!(
color(&lines, "[INVTYPE_2HWEAPON]"),
white,
"proficient slot is white"
);
assert_eq!(
right_color(&mut s, "Axe"),
white,
@@ -284,7 +285,7 @@ fn proficiency_and_reputation_reds() {
.unwrap();
let lines = lines_of(&mut s);
assert_eq!(
color(&lines, "Two-Hand"),
color(&lines, "[INVTYPE_2HWEAPON]"),
white,
"slot survives a hard miss"
);
@@ -302,7 +303,11 @@ fn proficiency_and_reputation_reds() {
s.run(r#"TT:SetOwner(Slot9, "ANCHOR_RIGHT"); TT:SetItemById(871)"#)
.unwrap();
let lines = lines_of(&mut s);
assert_eq!(color(&lines, "Two-Hand"), red, "alt-usable reds the slot");
assert_eq!(
color(&lines, "[INVTYPE_2HWEAPON]"),
red,
"alt-usable reds the slot"
);
assert_eq!(
right_color(&mut s, "Axe"),
white,
@@ -326,8 +331,10 @@ fn proficiency_and_reputation_reds() {
s.run(r#"TT:SetOwner(Slot9, "ANCHOR_RIGHT"); TT:SetItemById(872)"#)
.unwrap();
let lines = lines_of(&mut s);
// `INVTYPE_WEAPONOFFHAND`, NOT `INVTYPE_SHIELD` — one enUS sentence, two keys, and only the
// weapon one belongs on an InventoryType 22.
assert_eq!(
color(&lines, "Off Hand"),
color(&lines, "[INVTYPE_WEAPONOFFHAND]"),
red,
"no Dual Wield reds the slot"
);
@@ -341,7 +348,11 @@ fn proficiency_and_reputation_reds() {
s.run(r#"TT:SetOwner(Slot9, "ANCHOR_RIGHT"); TT:SetItemById(872)"#)
.unwrap();
let lines = lines_of(&mut s);
assert_eq!(color(&lines, "Off Hand"), white, "Dual Wield clears it");
assert_eq!(
color(&lines, "[INVTYPE_WEAPONOFFHAND]"),
white,
"Dual Wield clears it"
);
// An item class with NO proficiency entry never reds (the map only ever holds classes
// the server sent masks for).
s.set_item_template(
@@ -358,7 +369,7 @@ fn proficiency_and_reputation_reds() {
.unwrap();
let lines = lines_of(&mut s);
assert_eq!(
color(&lines, "Chest"),
color(&lines, "[INVTYPE_CHEST]"),
white,
"a class with no mask entry stays white"
);
@@ -377,7 +388,7 @@ fn proficiency_and_reputation_reds() {
s.run(
r#"
TT:SetOwner(Slot9, "ANCHOR_RIGHT"); TT:SetItemById(889)
assert(TTTextLeft2:GetText() == "Finger")
assert(TTTextLeft2:GetText() == "[INVTYPE_FINGER]")
assert(TTTextRight2:GetText() == nil or TTTextRight2:GetText() == "",
"a ring never prints its Miscellaneous type")
"#,
@@ -413,7 +424,7 @@ fn required_level_one_is_hidden() {
assert(tt:NumLines() == 1, "req 1 hides like req 0, got " .. tt:NumLines())
tt:SetOwner(a, "ANCHOR_RIGHT"); tt:SetItemById(13)
assert(tt:NumLines() == 2, "req 2 prints, got " .. tt:NumLines())
assert(TTTextLeft2:GetText() == "Requires Level 2")
assert(TTTextLeft2:GetText() == "[MIN_LEVEL 2]")
"#,
)
.unwrap();
@@ -516,11 +527,11 @@ fn charter_lines_sit_between_the_name_and_the_signable_line() {
texts,
vec![
"Guild Charter",
"Guild Name: BTC",
"Guild Master: Twowarrior",
"<Right Click for Details>",
"Soulbound",
"Unique",
"[GUILD_CHARTER_TITLE BTC]",
"[GUILD_CHARTER_CREATOR Twowarrior]",
"[ITEM_SIGNABLE]",
"[ITEM_SOULBOUND]",
"[ITEM_UNIQUE]",
],
"the two guild lines sit ABOVE the green line, not below it"
);
@@ -542,10 +553,10 @@ fn charter_lines_sit_between_the_name_and_the_signable_line() {
hover(&mut s, 2),
vec![
"Guild Charter",
"Guild Name: BTC",
"<Right Click for Details>",
"Soulbound",
"Unique"
"[GUILD_CHARTER_TITLE BTC]",
"[ITEM_SIGNABLE]",
"[ITEM_SOULBOUND]",
"[ITEM_UNIQUE]"
],
"an unresolved owner withholds ITS line only — the repaint fills it"
);
@@ -553,9 +564,9 @@ fn charter_lines_sit_between_the_name_and_the_signable_line() {
hover(&mut s, 3),
vec![
"Guild Charter",
"<Right Click for Details>",
"Soulbound",
"Unique"
"[ITEM_SIGNABLE]",
"[ITEM_SOULBOUND]",
"[ITEM_UNIQUE]"
],
"no record yet: exactly the plate we shipped before, not a blank one"
);
@@ -563,11 +574,11 @@ fn charter_lines_sit_between_the_name_and_the_signable_line() {
hover(&mut s, 4),
vec![
"Guild Charter",
"Petition: Something",
"Created by Someone",
"<Right Click for Details>",
"Soulbound",
"Unique"
"[PETITION_TITLE Something]",
"[PETITION_CREATOR Someone]",
"[ITEM_SIGNABLE]",
"[ITEM_SOULBOUND]",
"[ITEM_UNIQUE]"
],
"the record's charter bit picks the key family"
);
@@ -650,7 +661,7 @@ fn instance_tail_creator_and_readable() {
let texts: Vec<&str> = lines.iter().map(|(t, _)| t.as_str()).collect();
assert_eq!(
texts,
vec!["Plain Letter", "Written by One", "<Right Click to Read>"],
vec!["Plain Letter", "[WRITTEN_BY One]", "[ITEM_READABLE]"],
"the letter: writer line + instance-gated READABLE"
);
assert_eq!(lines[1].1, [1.0, 1.0, 1.0, 1.0], "WRITTEN_BY is white");
@@ -659,14 +670,18 @@ fn instance_tail_creator_and_readable() {
s.run(r#"TT:SetOwner(getglobal("SlotL"), "ANCHOR_RIGHT"); TT:SetBagItem(0, 2)"#)
.unwrap();
let texts: Vec<String> = lines_of(&mut s).into_iter().map(|(t, _)| t).collect();
assert_eq!(texts, vec!["Plain Letter", "<Right Click to Read>"]);
assert_eq!(texts, vec!["Plain Letter", "[ITEM_READABLE]"]);
// Crafted + locked: the green-escaped Made-by; no open line while LockID gates it.
s.run(r#"TT:SetOwner(getglobal("SlotL"), "ANCHOR_RIGHT"); TT:SetBagItem(0, 3)"#)
.unwrap();
let texts: Vec<String> = lines_of(&mut s).into_iter().map(|(t, _)| t).collect();
assert_eq!(
texts,
vec!["Heavy Chest", "Locked", "|cff00ff00<Made by Geoffrey>|r"],
vec![
"Heavy Chest",
"[LOCKED]",
"|cff00ff00[CREATED_BY Geoffrey]|r"
],
"CREATED_BY carries the string's own green escape; locked chest hides OPENABLE"
);
// The instance UNLOCKED bit retires the LOCKED line AND satisfies the openable lock sub-gate.
@@ -674,14 +689,14 @@ fn instance_tail_creator_and_readable() {
.unwrap();
let lines = lines_of(&mut s);
let texts: Vec<&str> = lines.iter().map(|(t, _)| t.as_str()).collect();
assert_eq!(texts, vec!["Heavy Chest", "<Right Click to Open>"]);
assert_eq!(texts, vec!["Heavy Chest", "[ITEM_OPENABLE]"]);
assert_eq!(lines[1].1, [0.0, 1.0, 0.0, 1.0], "OPENABLE is green");
// The director's case: a lockless LOOTABLE template (a clam) is openable outright — name +
// the green line, nothing between them.
s.run(r#"TT:SetOwner(getglobal("SlotL"), "ANCHOR_RIGHT"); TT:SetBagItem(0, 5)"#)
.unwrap();
let texts: Vec<String> = lines_of(&mut s).into_iter().map(|(t, _)| t).collect();
assert_eq!(texts, vec!["Small Barnacled Clam", "<Right Click to Open>"]);
assert_eq!(texts, vec!["Small Barnacled Clam", "[ITEM_OPENABLE]"]);
// The same clam mid-cooldown takes SetBagItem's OTHER leg (p6=1) — the openable tree is
// skipped wholesale, so the green line is gone (the reference prints ITEM_COOLDOWN_TIME in
// its place; unfed here). `hasCooldown`, the binding's own return, is the same boolean.
@@ -765,23 +780,35 @@ fn a_runtime_bound_instance_overrides_the_bind_line_to_soulbound() {
let bind_line = |s: &mut UiScript| {
lines_of(s)
.into_iter()
.find(|(t, _)| t == "Soulbound" || t.starts_with("Binds when") || t == "Quest Item")
.find(|(t, _)| {
matches!(
t.as_str(),
"[ITEM_SOULBOUND]"
| "[ITEM_BIND_ON_PICKUP]"
| "[ITEM_BIND_ON_EQUIP]"
| "[ITEM_BIND_ON_USE]"
| "[ITEM_BIND_QUEST]"
)
})
.unwrap_or_else(|| panic!("no bind line at all"))
};
let (text, color) = bind_line(&mut s);
assert_eq!(text, "Soulbound", "a runtime-bound instance overrides §6");
assert_eq!(
text, "[ITEM_SOULBOUND]",
"a runtime-bound instance overrides §6"
);
assert_eq!(color, [1.0, 1.0, 1.0, 1.0], "§6 is white");
// Control 1: the SAME item, instance not bound — the template's bonding stands.
s.run(r#"TT:SetBagItem(0, 2)"#).unwrap();
assert_eq!(bind_line(&mut s).0, "Binds when equipped");
assert_eq!(bind_line(&mut s).0, "[ITEM_BIND_ON_EQUIP]");
// Control 2: a TEMPLATE hover carries no instance at all, so nothing can override.
s.run(r#"TT:SetItemById(871)"#).unwrap();
assert_eq!(bind_line(&mut s).0, "Binds when equipped");
assert_eq!(bind_line(&mut s).0, "[ITEM_BIND_ON_EQUIP]");
// Control 3: the override's other arm is ITEM_BIND_QUEST — the same text 4|5 already print.
s.run(r#"TT:SetBagItem(0, 3)"#).unwrap();
assert_eq!(bind_line(&mut s).0, "Quest Item");
assert_eq!(bind_line(&mut s).0, "[ITEM_BIND_QUEST]");
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
@@ -97,7 +97,7 @@ fn bag_item_money_law_and_fallback() {
TT:SetBagItem(0, 2)
assert(money_fired == nil, "unsellable fires no money")
local last = getglobal("TTTextLeft" .. TT:NumLines())
assert(last:GetText() == "No sell price", "ITEM_UNSELLABLE line")
assert(last:GetText() == "[ITEM_UNSELLABLE]", "ITEM_UNSELLABLE line")
"#,
)
.unwrap();
@@ -160,7 +160,7 @@ fn wrap_lines_measure_wrapped_in_one_pass_and_survive_the_reenter_loop() {
s.resolve();
};
answer(&mut s);
// name 14 + gap 2 + "1 Charge" 14 + gap 2 + description 36 + 2·pad 20 = 88.
// name 14 + gap 2 + the charges line 14 + gap 2 + description 36 + 2·pad 20 = 88.
s.run(
r#"
assert(TT:GetWidth() == 270, "wrap column + padding, got " .. TT:GetWidth())
@@ -285,17 +285,17 @@ fn real_instance_hover_renders_live_durability() {
found = nil
for i = 1, tt:NumLines() do
local t = getglobal("TTTextLeft" .. i):GetText()
if t and string.find(t, "Durability") then found = t end
if t and string.find(t, "DURABILITY") then found = t end
end
assert(found == "Durability 30 / 40", "live pair on a bag hover, got " .. tostring(found))
assert(found == "[DURABILITY 30/40]", "live pair on a bag hover, got " .. tostring(found))
-- The template/link hover of the SAME item keeps the authored full pair.
tt:SetItemById(2264)
found = nil
for i = 1, tt:NumLines() do
local t = getglobal("TTTextLeft" .. i):GetText()
if t and string.find(t, "Durability") then found = t end
if t and string.find(t, "DURABILITY") then found = t end
end
assert(found == "Durability 40 / 40", "template hover stays full, got " .. tostring(found))
assert(found == "[DURABILITY 40/40]", "template hover stays full, got " .. tostring(found))
"#,
)
.unwrap();
@@ -346,9 +346,9 @@ fn broken_instance_hover_renders_zero_durability() {
found = nil
for i = 1, tt:NumLines() do
local t = getglobal("TTTextLeft" .. i):GetText()
if t and string.find(t, "Durability") then found = t end
if t and string.find(t, "DURABILITY") then found = t end
end
assert(found == "Durability 0 / 40", "broken gear shows its true 0, got " .. tostring(found))
assert(found == "[DURABILITY 0/40]", "broken gear shows its true 0, got " .. tostring(found))
"#,
)
.unwrap();
@@ -358,7 +358,7 @@ fn broken_instance_hover_renders_zero_durability() {
let lines = super::lines_of(&mut s);
let dur = lines
.iter()
.find(|(t, _)| t.starts_with("Durability"))
.find(|(t, _)| t.starts_with("[DURABILITY"))
.expect("the durability line renders");
assert_eq!(
dur.1,
@@ -431,7 +431,8 @@ fn an_enchanted_instance_renders_its_enchant_line_before_durability() {
.unwrap_or_else(|| panic!("no {needle} line in {texts:?}"))
};
assert!(
at("+7 Nature Resistance") < at("Agility +15") && at("Agility +15") < at("Durability"),
at("[RESIST_SINGLE +7 [SCHOOL3]]") < at("Agility +15")
&& at("Agility +15") < at("[DURABILITY"),
"the enchant line sits between resistances and durability: {texts:?}"
);
assert_eq!(
@@ -564,7 +565,7 @@ fn temporary_enchant_line_carries_its_countdown_and_charges() {
EnchantView {
slot: 1,
name: "Rockbiter Weapon".into(),
remaining_ms: Some(275_000), // 4 min 35 s → "(5 min)"
remaining_ms: Some(275_000), // 4 min 35 s → the MIN arm, ceiled to 5
charges: 5,
..Default::default()
},
@@ -606,7 +607,7 @@ fn temporary_enchant_line_carries_its_countdown_and_charges() {
let lines = super::lines_of(&mut s);
let texts: Vec<&str> = lines.iter().map(|(t, _)| t.as_str()).collect();
assert!(
texts.contains(&"Rockbiter Weapon (5 min) (5 Charges)"),
texts.contains(&"[ENCH Rockbiter Weapon 5 m] ([CHARGES 5 x])"),
"one line: name, countdown, charges — got {texts:?}"
);
assert!(
@@ -623,7 +624,7 @@ fn temporary_enchant_line_carries_its_countdown_and_charges() {
/// (`0x5d8b00`, the same string `GetLootSlotInfo` returns), so the plate reads "… of the Monkey".
///
/// This is the reported bug's exact shape: through the template path (`SetItemById`) the same
/// hover printed `<Random enchantment>` until the item reached a bag.
/// hover printed the ITEM_RANDOM_ENCHANT placeholder until the item reached a bag.
#[test]
fn a_looted_roll_shows_its_lines_and_never_the_placeholder() {
let mut s = script();
@@ -689,7 +690,7 @@ fn a_looted_roll_shows_its_lines_and_never_the_placeholder() {
let lines = super::lines_of(&mut s);
let texts: Vec<&str> = lines.iter().map(|(t, _)| t.as_str()).collect();
assert!(
!texts.contains(&"<Random enchantment>"),
!texts.contains(&"[ITEM_RANDOM_ENCHANT]"),
"a block source never reaches the placeholder arm — got {texts:?}"
);
assert_eq!(
@@ -749,7 +750,7 @@ fn a_looted_item_with_no_roll_shows_neither_line_nor_placeholder() {
.map(|(t, _)| t)
.collect();
assert!(
!texts.iter().any(|t| t == "<Random enchantment>"),
!texts.iter().any(|t| t == "[ITEM_RANDOM_ENCHANT]"),
"no placeholder on a block source — got {texts:?}"
);
}
@@ -801,7 +802,7 @@ fn a_linked_roll_shows_its_lines_and_never_the_placeholder() {
.map(|(t, _)| t)
.collect();
assert!(
!texts.iter().any(|t| t == "<Random enchantment>"),
!texts.iter().any(|t| t == "[ITEM_RANDOM_ENCHANT]"),
"a link never reaches the placeholder arm — got {texts:?}"
);
assert_eq!(
@@ -815,7 +816,7 @@ fn a_linked_roll_shows_its_lines_and_never_the_placeholder() {
);
}
/// `<Random enchantment>` (§E5) — the template-only placeholder: a random-property item with NO
/// `ITEM_RANDOM_ENCHANT` (§E5) — the template-only placeholder: a random-property item with NO
/// instance to read a roll from prints it, green, and the per-slot lines and this one are mutually
/// exclusive by construction. The control is the same template hovered as a real enchanted
/// instance: the roll is known, so the placeholder gives way to the slot lines.
@@ -844,7 +845,7 @@ fn random_property_template_hover_shows_the_placeholder() {
let lines = super::lines_of(&mut s);
let placeholder = lines
.iter()
.find(|(t, _)| t == "<Random enchantment>")
.find(|(t, _)| t == "[ITEM_RANDOM_ENCHANT]")
.expect("the template hover shows the placeholder");
assert_eq!(placeholder.1, [0.0, 1.0, 0.0, 1.0], "green");
@@ -876,7 +877,7 @@ fn random_property_template_hover_shows_the_placeholder() {
let lines = super::lines_of(&mut s);
let texts: Vec<&str> = lines.iter().map(|(t, _)| t.as_str()).collect();
assert!(
!texts.contains(&"<Random enchantment>") && texts.contains(&"Stamina +7"),
!texts.contains(&"[ITEM_RANDOM_ENCHANT]") && texts.contains(&"Stamina +7"),
"an instance's known roll replaces the placeholder — got {texts:?}"
);
}
@@ -3,9 +3,120 @@
//! protocol (engine fires `OnTooltipAddMoney` — merchant open + real instance + repair off),
//! and the in-flight-template fallback.
use super::common::script;
use super::common::script as bare_script;
use crate::script::*;
/// A stand-in `GlobalStrings.lua` for the item builder — **deliberately not the shipped wording**.
///
/// Every sentence the builder draws is a key resolved off the VM's own globals (decision 2045),
/// and what these tests establish is *which key* each line reaches and what fills it, never what
/// the sentence says. Naming each value after its own key is the point: `INVTYPE_SHIELD`,
/// `INVTYPE_WEAPONOFFHAND` and `SECONDARYHANDSLOT` all read "Off Hand" in enUS, `ITEM_REQ_SKILL`
/// shares "Requires %s" with the three `LOCKED_WITH_*` keys, and an assertion on the English
/// would pass on every one of them. Read an expected line here as "this key, filled with these".
///
/// The values keep each shipped template's *hole shape* — that is what proves the fill — and
/// nothing else about it.
fn seed_item_strings(s: &mut UiScript) {
s.run(
r#"
CURRENTLY_EQUIPPED = "[CURRENTLY_EQUIPPED]"
GUILD_CHARTER_TITLE = "[GUILD_CHARTER_TITLE %s]"
GUILD_CHARTER_CREATOR = "[GUILD_CHARTER_CREATOR %s]"
PETITION_TITLE = "[PETITION_TITLE %s]"
PETITION_CREATOR = "[PETITION_CREATOR %s]"
ITEM_SIGNABLE = "[ITEM_SIGNABLE]"
ITEM_CONJURED = "[ITEM_CONJURED]"
ITEM_SOULBOUND = "[ITEM_SOULBOUND]"
ITEM_BIND_QUEST = "[ITEM_BIND_QUEST]"
ITEM_BIND_ON_PICKUP = "[ITEM_BIND_ON_PICKUP]"
ITEM_BIND_ON_EQUIP = "[ITEM_BIND_ON_EQUIP]"
ITEM_BIND_ON_USE = "[ITEM_BIND_ON_USE]"
ITEM_UNIQUE = "[ITEM_UNIQUE]"
ITEM_UNIQUE_MULTIPLE = "[ITEM_UNIQUE_MULTIPLE %d]"
ITEM_STARTS_QUEST = "[ITEM_STARTS_QUEST]"
LOCKED = "[LOCKED]"
INVTYPE_HEAD = "[INVTYPE_HEAD]"; INVTYPE_NECK = "[INVTYPE_NECK]"
INVTYPE_SHOULDER = "[INVTYPE_SHOULDER]"; INVTYPE_BODY = "[INVTYPE_BODY]"
INVTYPE_CHEST = "[INVTYPE_CHEST]"; INVTYPE_WAIST = "[INVTYPE_WAIST]"
INVTYPE_LEGS = "[INVTYPE_LEGS]"; INVTYPE_FEET = "[INVTYPE_FEET]"
INVTYPE_WRIST = "[INVTYPE_WRIST]"; INVTYPE_HAND = "[INVTYPE_HAND]"
INVTYPE_FINGER = "[INVTYPE_FINGER]"; INVTYPE_TRINKET = "[INVTYPE_TRINKET]"
INVTYPE_WEAPON = "[INVTYPE_WEAPON]"; INVTYPE_SHIELD = "[INVTYPE_SHIELD]"
INVTYPE_RANGED = "[INVTYPE_RANGED]"; INVTYPE_CLOAK = "[INVTYPE_CLOAK]"
INVTYPE_2HWEAPON = "[INVTYPE_2HWEAPON]"; INVTYPE_TABARD = "[INVTYPE_TABARD]"
INVTYPE_ROBE = "[INVTYPE_ROBE]"; INVTYPE_WEAPONMAINHAND = "[INVTYPE_WEAPONMAINHAND]"
INVTYPE_WEAPONOFFHAND = "[INVTYPE_WEAPONOFFHAND]"; INVTYPE_HOLDABLE = "[INVTYPE_HOLDABLE]"
INVTYPE_RELIC = "[INVTYPE_RELIC]"
-- INVTYPE_AMMO / _THROWN / _RANGEDRIGHT / _QUIVER are DELIBERATELY absent: 1.12's
-- GlobalStrings.lua does not ship them either, so those four types draw no slot cell.
SPELL_SCHOOL1_CAP = "[SCHOOL1]"; SPELL_SCHOOL2_CAP = "[SCHOOL2]"
SPELL_SCHOOL3_CAP = "[SCHOOL3]"; SPELL_SCHOOL4_CAP = "[SCHOOL4]"
SPELL_SCHOOL5_CAP = "[SCHOOL5]"; SPELL_SCHOOL6_CAP = "[SCHOOL6]"
DPS_TEMPLATE = "[DPS %.1f]"
ARMOR_TEMPLATE = "[ARMOR %d]"
SHIELD_BLOCK_TEMPLATE = "[BLOCK %d]"
ITEM_MOD_MANA = "[MOD_MANA %c%d]"
ITEM_MOD_HEALTH = "[MOD_HEALTH %c%d]"
ITEM_MOD_AGILITY = "[MOD_AGILITY %c%d]"
ITEM_MOD_STRENGTH = "[MOD_STRENGTH %c%d]"
ITEM_MOD_INTELLECT = "[MOD_INTELLECT %c%d]"
ITEM_MOD_SPIRIT = "[MOD_SPIRIT %c%d]"
ITEM_MOD_STAMINA = "[MOD_STAMINA %c%d]"
ITEM_RESIST_ALL = "[RESIST_ALL %c%d]"
ITEM_RESIST_SINGLE = "[RESIST_SINGLE %c%d %s]"
ITEM_RANDOM_ENCHANT = "[ITEM_RANDOM_ENCHANT]"
ITEM_ENCHANT_TIME_LEFT_DAYS = "[ENCH %s %d d]"
ITEM_ENCHANT_TIME_LEFT_DAYS_P1 = "[ENCH %s %d dd]"
ITEM_ENCHANT_TIME_LEFT_HOURS = "[ENCH %s %d h]"
ITEM_ENCHANT_TIME_LEFT_HOURS_P1 = "[ENCH %s %d hh]"
ITEM_ENCHANT_TIME_LEFT_MIN = "[ENCH %s %d m]"
ITEM_ENCHANT_TIME_LEFT_SEC = "[ENCH %s %d s]"
ITEM_SPELL_CHARGES = "[CHARGES %d]"
ITEM_SPELL_CHARGES_P1 = "[CHARGES %d x]"
DURABILITY_TEMPLATE = "[DURABILITY %d/%d]"
-- ITEM_DURATION_* ships no _P1 twin, and neither does this stand-in.
ITEM_DURATION_DAYS = "[DURATION %d d]"
ITEM_DURATION_HOURS = "[DURATION %d h]"
ITEM_DURATION_MIN = "[DURATION %d m]"
ITEM_DURATION_SEC = "[DURATION %d s]"
ITEM_CLASSES_ALLOWED = "[CLASSES %s]"
ITEM_RACES_ALLOWED = "[RACES %s]"
ITEM_MIN_LEVEL = "[MIN_LEVEL %d]"
ITEM_MIN_SKILL = "[MIN_SKILL %s %d]"
ITEM_REQ_SKILL = "[REQ_SKILL %s]"
ITEM_SPELL_KNOWN = "[ITEM_SPELL_KNOWN]"
ITEM_SPELL_TRIGGER_ONUSE = "[ONUSE]"
ITEM_SPELL_TRIGGER_ONEQUIP = "[ONEQUIP]"
ITEM_SPELL_TRIGGER_ONPROC = "[ONPROC]"
ITEM_SET_NAME = "[SET_NAME %s %d/%d]"
ITEM_SET_BONUS = "[SET_BONUS %s]"
ITEM_SET_BONUS_GRAY = "[SET_BONUS_GRAY %d %s]"
ITEM_WRITTEN_BY = "[WRITTEN_BY %s]"
ITEM_CREATED_BY = "|cff00ff00[CREATED_BY %s]|r"
ITEM_OPENABLE = "[ITEM_OPENABLE]"
ITEM_READABLE = "[ITEM_READABLE]"
ITEM_UNSELLABLE = "[ITEM_UNSELLABLE]"
"#,
)
.unwrap();
}
/// The shared fixture VM, with [`seed_item_strings`] already run — every test in this tree needs
/// it, because a builder that resolves nothing draws nothing.
fn script() -> UiScript {
let mut s = bare_script();
seed_item_strings(&mut s);
s
}
/// A full weapon view exercising most line families.
fn axe() -> ItemTemplateView {
ItemTemplateView {
@@ -5,11 +5,15 @@
use super::{lines_of, script};
use crate::script::*;
/// The §22 SET block, byte-read: a blank gold line, the gold "name (owned/total)" header, the
/// skill line (white/red) between header and the member ladder (cream when equipped / gray;
/// in-flight names wait), a second blank, then the threshold bonuses SORTED ascending — green
/// only when the skill requirement is met AND owned ≥ threshold — plus the ask-once for an
/// unseen set id.
/// The §22 SET block, byte-read: a blank gold line, the gold `ITEM_SET_NAME` header, the skill
/// line (white/red) between header and the member ladder (cream when equipped / gray; in-flight
/// names wait), a second blank, then the threshold bonuses SORTED ascending — green only when the
/// skill requirement is met AND owned ≥ threshold — plus the ask-once for an unseen set id.
///
/// **The two bonus arms are two different keys** (`0x52e056..0x52e0d6`): an ACTIVE bonus formats
/// `ITEM_SET_BONUS` = "Set: %s" with no count at all, and only an inactive one formats
/// `ITEM_SET_BONUS_GRAY` = "(%d) Set: %s". Watching the (2) bonus move between them as the skill
/// gate opens and closes is what these assertions are for.
#[test]
fn item_set_block_counts_and_colors() {
let mut s = script();
@@ -65,14 +69,16 @@ fn item_set_block_counts_and_colors() {
-- in the reference: `0x530270` bails before its `inc [esi+0x31c]`).
assert(TTTextLeft3:GetText() == " \n", "blank before the header, got "
.. string.format("%q", tostring(TTTextLeft3:GetText())))
assert(TTTextLeft4:GetText() == "Defias Leather (2/5)", "got " .. tostring(TTTextLeft4:GetText()))
assert(TTTextLeft4:GetText() == "[SET_NAME Defias Leather 2/5]", "got " .. tostring(TTTextLeft4:GetText()))
assert(TTTextLeft5:GetText() == " Defias Mark")
assert(TTTextLeft8:GetText() == " Defias Boots", "in-flight member renders no line")
assert(TTTextLeft9:GetText() == " \n", "blank before the bonuses, got "
.. string.format("%q", tostring(TTTextLeft9:GetText())))
assert(TTTextLeft10:GetText() == "(2) Set: Increases movement speed slightly.",
-- Owned 2 of 5: the (2) bonus is ACTIVE and takes the countless ITEM_SET_BONUS; the
-- (4) one is not and takes ITEM_SET_BONUS_GRAY, which is the arm that carries the count.
assert(TTTextLeft10:GetText() == "[SET_BONUS Increases movement speed slightly.]",
"thresholds sort ascending, got " .. tostring(TTTextLeft10:GetText()))
assert(TTTextLeft11:GetText() == "(4) Set: +10 Attack Power.")
assert(TTTextLeft11:GetText() == "[SET_BONUS_GRAY 4 +10 Attack Power.]")
"#,
)
.unwrap();
@@ -90,7 +96,7 @@ fn item_set_block_counts_and_colors() {
let green = [0.0, 1.0, 0.0, 1.0];
let red = [1.0, 32.0 / 255.0, 32.0 / 255.0, 1.0];
assert_eq!(
color(&lines, "Defias Leather (2/5)"),
color(&lines, "[SET_NAME Defias Leather 2/5]"),
gold,
"set name is gold"
);
@@ -105,12 +111,12 @@ fn item_set_block_counts_and_colors() {
"unowned member is gray"
);
assert_eq!(
color(&lines, "(2) Set: Increases movement speed slightly."),
color(&lines, "[SET_BONUS Increases movement speed slightly.]"),
green,
"active bonus is green"
);
assert_eq!(
color(&lines, "(4) Set: +10 Attack Power."),
color(&lines, "[SET_BONUS_GRAY 4 +10 Attack Power.]"),
gray,
"inactive bonus is gray"
);
@@ -124,16 +130,20 @@ fn item_set_block_counts_and_colors() {
s.run(
r#"
TT:SetOwner(Slot10, "ANCHOR_RIGHT"); TT:SetItemById(6303)
assert(TTTextLeft5:GetText() == "Requires Leatherworking (250)",
assert(TTTextLeft5:GetText() == "[MIN_SKILL Leatherworking 250]",
"the skill line sits between header and members")
assert(TTTextLeft6:GetText() == " Defias Mark")
"#,
)
.unwrap();
let lines = lines_of(&mut s);
assert_eq!(color(&lines, "Requires Leatherworking (250)"), red);
assert_eq!(color(&lines, "[MIN_SKILL Leatherworking 250]"), red);
// The gate also moves it onto the OTHER key: an inactive bonus is the counted spelling.
assert_eq!(
color(&lines, "(2) Set: Increases movement speed slightly."),
color(
&lines,
"[SET_BONUS_GRAY 2 Increases movement speed slightly.]"
),
gray,
"an unmet skill grays even a met threshold"
);
@@ -144,13 +154,13 @@ fn item_set_block_counts_and_colors() {
.unwrap();
let lines = lines_of(&mut s);
assert_eq!(
color(&lines, "Requires Leatherworking (250)"),
color(&lines, "[MIN_SKILL Leatherworking 250]"),
[1.0, 1.0, 1.0, 1.0]
);
assert_eq!(
color(&lines, "(2) Set: Increases movement speed slightly."),
color(&lines, "[SET_BONUS Increases movement speed slightly.]"),
green,
"meeting the skill restores the green"
"meeting the skill restores the green — and the countless key with it"
);
// An unseen set id records the ask-once.
s.set_item_template(
@@ -1,11 +1,36 @@
//! The engine unit tooltip builder (decision 0274 P3, law per 0276): the level-line composition
//! (four templates, the rank words, "??", Corpse, "Race Class (Player)"), the flag lines
//! (four `TOOLTIP_UNIT_LEVEL*` keys, the `ELITE`/`BOSS` rank table, "??", `CORPSE`, "Race Class"
//! over `PLAYER` — all seeded here as marked stand-ins, see [`seed_level_strings`]), the flag lines
//! (PvP white / Skinnable red / Civilian green), the world-mouseover drive (default anchor +
//! `UPDATE_MOUSEOVER_UNIT` recolor + the fade arm on loss), and the health-bar watcher.
use super::common::script;
use crate::script::*;
/// A stand-in string table for the level line — the four `TOOLTIP_UNIT_LEVEL*` templates and the
/// three word slots that fill them, **deliberately not the shipped wording**. What these tests
/// establish is *which key* each slot combination reaches and what fills it, never what the
/// sentence says (decision 2045, "assert the identifier, not the sentence"), and here that is not
/// a formality: `TOOLTIP_UNIT_LEVEL_CLASS`'s enUS "Level %s %s" is word-for-word
/// `FRIENDS_LEVEL_TEMPLATE`, `UNIT_TYPE_LEVEL_TEMPLATE` and `CHARACTER_SELECT_INFO`, and the bare
/// template's "Level %s" is also `ITEM_LEVEL`, `LEVEL_GAINED` and `UNIT_LEVEL_TEMPLATE`. An
/// assertion on the English would pass on all seven.
fn seed_level_strings(s: &mut UiScript) {
s.run(
r#"
TOOLTIP_UNIT_LEVEL = "[LEVEL %s]"
TOOLTIP_UNIT_LEVEL_CLASS = "[LEVEL_CLASS %s %s]"
TOOLTIP_UNIT_LEVEL_TYPE = "[LEVEL_TYPE %s %s]"
TOOLTIP_UNIT_LEVEL_CLASS_TYPE = "[LEVEL_CLASS_TYPE %s %s %s]"
CORPSE = "[CORPSE]"
PLAYER = "[PLAYER]"
ELITE = "[ELITE]"
BOSS = "[BOSS]"
"#,
)
.unwrap();
}
fn wolf() -> UnitState {
UnitState {
exists: true,
@@ -21,10 +46,11 @@ fn wolf() -> UnitState {
}
}
/// The creature law: gold name, subtitle, "Level 10 Beast (Elite)", red Skinnable.
/// The creature law: gold name, subtitle, the CLASS_TYPE level line, red Skinnable.
#[test]
fn creature_line_law() {
let mut s = script();
seed_level_strings(&mut s);
s.set_screen_size(800.0, 600.0);
let mut u = wolf();
u.subtitle = Some("Alpha".into());
@@ -41,7 +67,7 @@ fn creature_line_law() {
assert(tt:SetUnit("target") == 1, "SetUnit returns 1 on a live unit")
assert(TTTextLeft1:GetText() == "Timber Wolf")
assert(TTTextLeft2:GetText() == "Alpha")
assert(TTTextLeft3:GetText() == "Level 10 Beast (Elite)", "got " .. TTTextLeft3:GetText())
assert(TTTextLeft3:GetText() == "[LEVEL_CLASS_TYPE 10 Beast [ELITE]]", "got " .. TTTextLeft3:GetText())
assert(TTTextLeft4:GetText() == "Skinnable")
-- A RECOGNISED token naming nothing answers nil...
assert(tt:SetUnit("party4") == nil, "a recognised but absent unit answers nil")
@@ -94,6 +120,7 @@ fn unqueried_wolf() -> UnitState {
#[test]
fn a_pending_name_titles_unknownobject_and_the_answer_replaces_it() {
let mut s = script();
seed_level_strings(&mut s);
s.set_screen_size(800.0, 600.0);
s.set_unit("target", Some(unqueried_wolf()));
s.set_player_req_state(PlayerReqState {
@@ -111,7 +138,7 @@ fn a_pending_name_titles_unknownobject_and_the_answer_replaces_it() {
"the title is the GlobalString, got '" .. tostring(TTTextLeft1:GetText()) .. "'")
-- The record's other lines are absent with it: no subtitle, and the level line takes the
-- bare TOOLTIP_UNIT_LEVEL template because both the CLASS and TYPE slots are the record's.
assert(TTTextLeft2:GetText() == "Level 10", "got " .. TTTextLeft2:GetText())
assert(TTTextLeft2:GetText() == "[LEVEL 10]", "got " .. TTTextLeft2:GetText())
-- The descriptor's own lines are NOT the record's and show anyway.
assert(TTTextLeft3:GetText() == "Skinnable")
"#,
@@ -132,7 +159,7 @@ fn a_pending_name_titles_unknownobject_and_the_answer_replaces_it() {
assert(TT:SetUnit("target") == 1)
assert(TTTextLeft1:GetText() == "Timber Wolf", "the answer replaced UNKNOWNOBJECT")
assert(TTTextLeft2:GetText() == "Alpha")
assert(TTTextLeft3:GetText() == "Level 10 Beast (Elite)", "got " .. TTTextLeft3:GetText())
assert(TTTextLeft3:GetText() == "[LEVEL_CLASS_TYPE 10 Beast [ELITE]]", "got " .. TTTextLeft3:GetText())
assert(TTTextLeft4:GetText() == "Skinnable")
"#,
)
@@ -191,6 +218,7 @@ fn a_pending_name_titles_unknownobject_and_the_answer_replaces_it() {
#[test]
fn faction_line_and_civilian_gate() {
let mut s = script();
seed_level_strings(&mut s);
s.set_screen_size(800.0, 600.0);
s.set_player_req_state(PlayerReqState {
level: 30,
@@ -217,7 +245,7 @@ fn faction_line_and_civilian_gate() {
local tt = CreateFrame("GameTooltip", "TT")
tt:SetOwner(a, "ANCHOR_RIGHT")
tt:SetUnit("target")
assert(TTTextLeft2:GetText() == "Level 20", "friendly creature: no type word; got " .. TTTextLeft2:GetText())
assert(TTTextLeft2:GetText() == "[LEVEL 20]", "friendly creature: no type word; got " .. TTTextLeft2:GetText())
assert(TTTextLeft3:GetText() == "Stormwind", "faction line before PvP; got " .. TTTextLeft3:GetText())
assert(TTTextLeft4:GetText() == "PvP")
assert(TTTextLeft5 == nil or TTTextLeft5:GetText() == nil, "friendly civilian shows NO Civilian line")
@@ -243,7 +271,7 @@ fn faction_line_and_civilian_gate() {
r#"
TT:SetOwner(UF9, "ANCHOR_RIGHT")
TT:SetUnit("target")
assert(TTTextLeft2:GetText() == "Level 20 Humanoid", "got " .. TTTextLeft2:GetText())
assert(TTTextLeft2:GetText() == "[LEVEL_CLASS 20 Humanoid]", "got " .. TTTextLeft2:GetText())
assert(TTTextLeft3:GetText() == "PvP")
assert(TTTextLeft4:GetText() == "Civilian", "hostile+grey+pvp civilian warns; got " .. TTTextLeft4:GetText())
assert(TTTextLeft5:GetText() == "Leader")
@@ -280,6 +308,7 @@ fn faction_line_and_civilian_gate() {
#[test]
fn level_line_variants() {
let mut s = script();
seed_level_strings(&mut s);
s.set_screen_size(800.0, 600.0);
s.set_player_req_state(PlayerReqState {
level: 60,
@@ -304,7 +333,7 @@ fn level_line_variants() {
local tt = CreateFrame("GameTooltip", "TT")
tt:SetOwner(a, "ANCHOR_RIGHT")
tt:SetUnit("target")
assert(TTTextLeft2:GetText() == "Level 32 Human Rogue (Player)", "got " .. TTTextLeft2:GetText())
assert(TTTextLeft2:GetText() == "[LEVEL_CLASS_TYPE 32 Human Rogue [PLAYER]]", "got " .. TTTextLeft2:GetText())
assert(TTTextLeft3:GetText() == "PvP")
"#,
)
@@ -326,7 +355,7 @@ fn level_line_variants() {
r#"
TT:SetOwner(UF2, "ANCHOR_RIGHT")
TT:SetUnit("target")
assert(TTTextLeft2:GetText() == "Level 3 Corpse", "got " .. TTTextLeft2:GetText())
assert(TTTextLeft2:GetText() == "[LEVEL_CLASS 3 [CORPSE]]", "got " .. TTTextLeft2:GetText())
"#,
)
.unwrap();
@@ -347,7 +376,7 @@ fn level_line_variants() {
r#"
TT:SetOwner(UF2, "ANCHOR_RIGHT")
TT:SetUnit("target")
assert(TTTextLeft2:GetText() == "Level ?? Demon (Boss)", "got " .. TTTextLeft2:GetText())
assert(TTTextLeft2:GetText() == "[LEVEL_CLASS_TYPE ?? Demon [BOSS]]", "got " .. TTTextLeft2:GetText())
"#,
)
.unwrap();
@@ -367,7 +396,7 @@ fn level_line_variants() {
s.run(
r#"
TT:SetOwner(UF2, "ANCHOR_RIGHT"); TT:SetUnit("target")
assert(TTTextLeft2:GetText() == "Level ?? Beast", "hostile 10-up, got " .. TTTextLeft2:GetText())
assert(TTTextLeft2:GetText() == "[LEVEL_CLASS ?? Beast]", "hostile 10-up, got " .. TTTextLeft2:GetText())
"#,
)
.unwrap();
@@ -376,7 +405,7 @@ fn level_line_variants() {
s.run(
r#"
TT:SetOwner(UF2, "ANCHOR_RIGHT"); TT:SetUnit("target")
assert(TTTextLeft2:GetText() == "Level 70 Beast", "unfriendly 10-up, got " .. TTTextLeft2:GetText())
assert(TTTextLeft2:GetText() == "[LEVEL_CLASS 70 Beast]", "unfriendly 10-up, got " .. TTTextLeft2:GetText())
"#,
)
.unwrap();
@@ -385,7 +414,7 @@ fn level_line_variants() {
s.run(
r#"
TT:SetOwner(UF2, "ANCHOR_RIGHT"); TT:SetUnit("target")
assert(TTTextLeft2:GetText() == "Level 70 Orc Shaman (Player)", "hostile player, got " .. TTTextLeft2:GetText())
assert(TTTextLeft2:GetText() == "[LEVEL_CLASS_TYPE 70 Orc Shaman [PLAYER]]", "hostile player, got " .. TTTextLeft2:GetText())
"#,
)
.unwrap();