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:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user