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:
@@ -824,3 +824,69 @@ fn state_events_leave_empty_wells_untinted() {
|
||||
);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// The white-buttons regression (director-reported 2026-08-07, decision 1108): a slot that was
|
||||
/// OCCUPIED — UpdateUsable painted its icon's 1/1/1 usable tint — then goes EMPTY (the feed's
|
||||
/// character-switch diff: `set_action(None)` + `ACTIONBAR_SLOT_CHANGED`) kept the tint on the
|
||||
/// now-artless icon region and drew it as a solid WHITE square. Two laws close it, both asserted
|
||||
/// here: the empty arm HIDES the icon (ref ActionButton.lua l.168), and the engine emits nothing
|
||||
/// for a texture-less region whatever its surviving tint (`0x7706e0` — the draw gate is `+0xcc`,
|
||||
/// never the colour).
|
||||
#[test]
|
||||
fn an_occupied_slot_going_empty_leaves_no_white_plate() {
|
||||
use benilla_ui::script::ActionState;
|
||||
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_action_bar(&s);
|
||||
s.set_action(
|
||||
3,
|
||||
Some(ActionSlot {
|
||||
texture: Some("Interface\\Icons\\Spell_Nature_HealingTouch".into()),
|
||||
kind: 0x00,
|
||||
action: 5185,
|
||||
count: 0,
|
||||
}),
|
||||
);
|
||||
s.set_action_state(
|
||||
3,
|
||||
Some(ActionState {
|
||||
usable: true,
|
||||
..Default::default()
|
||||
}),
|
||||
);
|
||||
s.fire_event("PLAYER_ENTERING_WORLD", vec![]);
|
||||
// The usable pass paints the occupied icon's 1/1/1 usable tint — the tint that survives.
|
||||
s.fire_event("ACTIONBAR_UPDATE_USABLE", vec![]);
|
||||
s.resolve();
|
||||
assert!(
|
||||
s.extract().iter().any(|q| matches!(&q.content,
|
||||
QuadContent::Texture { path: Some(p), .. } if p.contains("Spell_Nature_HealingTouch"))),
|
||||
"the occupied slot draws its icon"
|
||||
);
|
||||
|
||||
// The character switch: the new character's table has nothing in slot 3.
|
||||
s.set_action(3, None);
|
||||
s.set_action_state(3, None);
|
||||
s.fire_event("ACTIONBAR_SLOT_CHANGED", vec![ScriptValue::Int(3)]);
|
||||
s.resolve();
|
||||
for q in s.extract() {
|
||||
match &q.content {
|
||||
QuadContent::Texture { path: Some(p), .. }
|
||||
if p.contains("Spell_Nature_HealingTouch") =>
|
||||
{
|
||||
panic!("the emptied slot still draws the old icon")
|
||||
}
|
||||
QuadContent::Texture {
|
||||
path: None,
|
||||
color: Some(c),
|
||||
..
|
||||
} if q.rect.is_some_and(|r| r.right - r.left <= 40.0) => {
|
||||
// Well-sized only, as in the sibling test: page-wide solids are legitimate.
|
||||
panic!("the emptied slot draws its surviving tint as a solid plate: {c:?}")
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! action-button machinery. Same file, two concerns: the strip along the bar's top is the
|
||||
//! player-progress readout; the buttons are the input surface.
|
||||
|
||||
use benilla_ui::script::{ScriptValue, UiScript, UnitState};
|
||||
use benilla_ui::script::{QuadContent, ScriptValue, UiScript, UnitState};
|
||||
|
||||
/// The XP bar's load set: the manifest prefix it actually needs — fonts, UIParent, the
|
||||
/// tooltip, `TextStatusBar.xml` (the numerals machinery `BenillaExpBar_OnLoad` wires in),
|
||||
@@ -251,6 +251,44 @@ fn the_max_level_rail_replaces_the_xp_bar_at_60() {
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// The gryphon end caps stay over the strip and the rail across ANY show/hide history —
|
||||
/// including the faithful hidden→visible tail re-stamp (0x76ae10 / 0884, which a stale-rail
|
||||
/// recovery exercises: decision 1106's whole bug walked through here). The protection is
|
||||
/// 0884's own key order: the caps ride the art frame's OVERLAY and the strip's fill its
|
||||
/// ARTWORK, and within a `(strata, level)` bucket the LAYER outranks every stamp — no
|
||||
/// re-stamp can lift a lower layer over them.
|
||||
#[test]
|
||||
fn the_gryphons_outrank_the_bars_across_hide_show_cycles() {
|
||||
let mut s = exp_bar_harness();
|
||||
s.set_player_xp(300, 400);
|
||||
s.fire_event("PLAYER_ENTERING_WORLD", vec![]);
|
||||
// The relog shape: the strip hidden (a stale max-level rail), then re-shown; the rail's
|
||||
// own arrival is ALWAYS a hidden→visible re-stamp (it is born hidden).
|
||||
s.run("BenillaExpBar:Hide() BenillaExpBar:Show()").unwrap();
|
||||
s.run("BenillaMaxLevelBar:Show()").unwrap();
|
||||
s.resolve();
|
||||
let quads = s.extract();
|
||||
let z_max = |suffix: &str| {
|
||||
quads
|
||||
.iter()
|
||||
.filter(|q| {
|
||||
matches!(&q.content, QuadContent::Texture { path: Some(p), .. }
|
||||
if p.ends_with(suffix))
|
||||
})
|
||||
.map(|q| q.z)
|
||||
.max()
|
||||
};
|
||||
let fill = z_max("UI-StatusBar").expect("the strip's fill");
|
||||
let rail = z_max("UI-MainMenuBar-MaxLevel").expect("the rail's plates");
|
||||
let cap = z_max("UI-MainMenuBar-EndCap-Dwarf").expect("the gryphon end caps");
|
||||
assert!(
|
||||
fill < cap && rail < cap,
|
||||
"the end caps must paint over the re-shown bars \
|
||||
(fill z={fill:#x}, rail z={rail:#x}, cap z={cap:#x})"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// The hover numerals (decision 1082, ref TextStatusBar.lua over `MainMenuBarExpText`): entering
|
||||
/// the strip shows "curr / max" centered on the bar, leaving hides it (the lockShow refcount's
|
||||
/// 0↔1 edge — the `statusBarText` cvar that pins it on permanently reads OFF here).
|
||||
|
||||
@@ -363,6 +363,76 @@ fn filter_hides_a_state_keeping_headers() {
|
||||
);
|
||||
}
|
||||
|
||||
/// The filter rows toggle **through the real dropdown kit** — the path a mouse takes, which the test
|
||||
/// above deliberately shortcuts by faking `this`. `UIDropDownMenuButton_OnClick` runs the row's func
|
||||
/// and only THEN flips the check for a `keepShownOnClick` row, so the func must not repaint the row
|
||||
/// itself: an in-func `UIDropDownMenu_Initialize` re-derived the check from the fresh engine state and
|
||||
/// the kit's flip then inverted it straight back — the check never moved on screen, and `this.checked`
|
||||
/// stuck true, so a filter turned off could never be turned back on. Click, re-click, and re-open all
|
||||
/// have to agree.
|
||||
#[test]
|
||||
fn filter_rows_toggle_through_the_dropdown_kit() {
|
||||
let mut s = trainer_script();
|
||||
s.set_money(50);
|
||||
s.set_trainer(Some(menu()));
|
||||
s.fire_event("TRAINER_SHOW", vec![ScriptValue::Str("Sana".into())]);
|
||||
|
||||
// Open the menu the way the capsule's arrow does. Row 2 is "Unavailable" (Initialize's order),
|
||||
// checked because trainer_script() turned every state on.
|
||||
s.run("ToggleDropDownMenu(1, nil, BenillaTrainerFilterDropDown)")
|
||||
.unwrap();
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
let row_checked = |s: &mut UiScript| {
|
||||
s.eval::<bool>("return DropDownList1Button2Check:IsVisible() and true or false")
|
||||
.unwrap()
|
||||
};
|
||||
assert!(row_checked(&mut s), "Unavailable starts checked");
|
||||
assert_eq!(s.eval::<i64>("return GetNumTrainerServices()").unwrap(), 6);
|
||||
|
||||
// Click it: the check clears, the engine filter clears, and Cleave (the unavailable service)
|
||||
// drops out of the tree. Its Arms header stays.
|
||||
s.run("this = DropDownList1Button2; UIDropDownMenuButton_OnClick()")
|
||||
.unwrap();
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
assert!(!row_checked(&mut s), "the click clears the row's check");
|
||||
assert!(
|
||||
!s.eval::<bool>("return GetTrainerServiceTypeFilter('unavailable') == 1")
|
||||
.unwrap(),
|
||||
"and the engine's unavailable filter with it"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return GetNumTrainerServices()").unwrap(),
|
||||
5,
|
||||
"Cleave is hidden"
|
||||
);
|
||||
|
||||
// Click it again: back on, both on screen and in the engine — the case the old code could never
|
||||
// reach, because `this.checked` never went false.
|
||||
s.run("this = DropDownList1Button2; UIDropDownMenuButton_OnClick()")
|
||||
.unwrap();
|
||||
assert!(row_checked(&mut s), "the re-click restores the check");
|
||||
assert!(
|
||||
s.eval::<bool>("return GetTrainerServiceTypeFilter('unavailable') == 1")
|
||||
.unwrap(),
|
||||
"and re-enables the filter"
|
||||
);
|
||||
assert_eq!(s.eval::<i64>("return GetNumTrainerServices()").unwrap(), 6);
|
||||
|
||||
// Close and re-open: Initialize re-derives every row from the engine, so the menu agrees with
|
||||
// what the clicks left behind (the two states left on, "used" still on from trainer_script()).
|
||||
s.run("this = DropDownList1Button2; UIDropDownMenuButton_OnClick()")
|
||||
.unwrap();
|
||||
s.run("ToggleDropDownMenu(1, nil, BenillaTrainerFilterDropDown)")
|
||||
.unwrap(); // same owner → closes
|
||||
s.run("ToggleDropDownMenu(1, nil, BenillaTrainerFilterDropDown)")
|
||||
.unwrap(); // re-open → re-Initialize
|
||||
assert!(
|
||||
!row_checked(&mut s),
|
||||
"a re-opened menu shows the filter the clicks actually left off"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// A menu bigger than the 11 visible rows (one skill line, 15 services → a 16-row tree), so the list
|
||||
/// scrolls. The wheel spins the list even when the cursor is over a row — the engine bubbles the spin
|
||||
/// up the parent chain to the window, which drives the faux-scroll bar.
|
||||
|
||||
Reference in New Issue
Block a user