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:
Sam
2026-08-07 16:03:35 +07:00
co-authored by Claude Fable 5
parent 19461d1e0f
commit d16d33dfca
55 changed files with 2485 additions and 358 deletions
@@ -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());
}