the guild window opens: roster, ranks, notes and the message of the day

- the social window's third tab is live — the roster in both its views, the member card with its promote/demote/remove verbs, the rank editor's thirteen permissions, the guild information board, and the invite, leave, kick and note dialogs behind them
- setting your hearthstone at an innkeeper now asks before it takes and says so afterwards, and the innkeeper's gossip lines get their own icons back
- unavailable talents grey out, and every greyed icon in the client now goes through the same luminance fold the original used
- the buff tooltip's remaining-time line reads gold with the client's own hour/minute ladder, the macro editor takes a shift-click to paste a spell, trainer rows repaint after a purchase, and a long book page stops flickering its ellipsis

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sam
2026-08-16 17:38:54 +07:00
co-authored by Claude Opus 5
parent 04340a6893
commit 8d3d805ec8
78 changed files with 13155 additions and 271 deletions
@@ -29,6 +29,65 @@ fn ui_cost_enabled() -> bool {
*ON.get_or_init(|| std::env::var("WOW_UI_COST").as_deref() == Ok("1"))
}
/// `WOW_UI_GATE=1` — the extract gate's miss reporter ([`report_gate_miss`]). Off by default and
/// read once, the `[ui-cost]` meter's posture.
fn gate_log_enabled() -> bool {
static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*ON.get_or_init(|| std::env::var("WOW_UI_GATE").as_deref() == Ok("1"))
}
/// One line naming why the extract gate did not skip this frame — the input that differed, and for
/// the render list the first entry that moved. A settled UI is what makes the paint pass free; when
/// it is never settled, this says what is moving.
fn report_gate_miss(
script: &UiScript,
now: &[benilla_ui::script::ExtractedQuad],
prev: &[benilla_ui::script::ExtractedQuad],
dims_eq: bool,
text_ui_eq: bool,
portraits_eq: bool,
) {
if !dims_eq {
eprintln!("[ui-gate] miss: window/scale changed");
return;
}
if !text_ui_eq {
eprintln!("[ui-gate] miss: focused editbox text-UI changed");
return;
}
if !portraits_eq {
eprintln!("[ui-gate] miss: portrait sources changed");
return;
}
if now.len() != prev.len() {
eprintln!(
"[ui-gate] miss: render list {} -> {} entries",
prev.len(),
now.len()
);
return;
}
match now.iter().zip(prev).position(|(a, b)| a != b) {
Some(i) => {
let (a, b) = (&now[i], &prev[i]);
let (was, now_s) = (format!("{b:?}"), format!("{a:?}"));
let owner = match a.target {
benilla_ui::order::ZTarget::Frame(fh) => script.frame_name(fh),
benilla_ui::order::ZTarget::Region(_) => None,
};
eprintln!(
"[ui-gate] miss: entry {i}/{} target={:?} name={owner:?}\n was {}\n now {}",
now.len(),
a.target,
&was[..was.len().min(280)],
&now_s[..now_s.len().min(280)],
);
}
// Nothing in this pass's own inputs moved — the miss came from the capture-mode arm.
None => eprintln!("[ui-gate] miss: capture mode (the gate never skips under a capture)"),
}
}
/// Last frame's extract-gate inputs (decision 0740), held together as one `Local` — the gate
/// compares all four or none, and a Bevy system has a hard param budget this was eating.
#[derive(Default)]
@@ -375,6 +434,19 @@ pub(super) fn drive_script(
}
return;
}
// `WOW_UI_GATE=1` — why the gate MISSED. A skip is worth the whole conversion, so the
// question "what moved?" is where a per-frame cost always ends, and a `Vec` inequality does
// not answer it. Off by default, like the `[ui-cost]` meter beside it.
if gate_log_enabled() {
report_gate_miss(
&script,
&extracted,
&prev.extracted,
prev.dims == Some(dims),
text_ui == prev.text_ui,
booths.images.0 == prev.portraits,
);
}
prev.dims = Some(dims);
prev.text_ui = text_ui.clone();
prev.portraits = booths.images.0.clone();
@@ -436,6 +508,7 @@ pub(super) fn drive_script(
circular,
portrait_unit,
rotation,
desaturated,
} => {
// A live unit portrait (`SetPortraitTexture(region, unit)`): sample this token's
// source ([`crate::portrait::PortraitImages`]) — the off-screen model bake, or the
@@ -555,6 +628,7 @@ pub(super) fn drive_script(
// The engine's SetRotation is counterclockwise-positive; the quad pass spins
// clockwise-on-screen (`UiQuad::rotation`) — negate to convert.
rotation: -rotation,
desaturated,
..default()
});
}
@@ -155,6 +155,7 @@ pub(super) fn emit(
color: [hc[0], hc[1], hc[2], hc[3] * host.alpha],
additive: false,
circular: false,
desaturated: false,
clip: text_clip,
rotation: 0.0,
mask: None,
@@ -171,10 +172,11 @@ pub(super) fn emit(
// gate inside is geometric and needs no fixed-vs-measured flag. Computed before the shadow
// so the shadow pass draws the same display string (the client's shadow is a second draw of
// the same truncated CGxString).
let ellipsized = if ebox.is_none() && !matches!(host.target, ZTarget::Frame(_)) {
crate::ui_text::ellipsize_to_fit(atlas, draw_text, draw_rect, spec)
} else {
None
let ellipsized = match host.target {
ZTarget::Region(region) if ebox.is_none() => {
crate::ui_text::ellipsize_to_fit(atlas, region, draw_text, draw_rect, spec)
}
_ => None,
};
if let Some(display) = ellipsized.as_deref() {
draw_text = display;
@@ -339,6 +341,7 @@ pub(super) fn emit(
color: [1.0, 1.0, 1.0, host.alpha],
additive: false,
circular: false,
desaturated: false,
clip: text_clip,
rotation: 0.0,
mask: None,