Chat bubbles are WorldFrame child frames, as in 1.12.1 (#925)

Chat bubbles were quads drawn straight into an overlay with no widget behind them, so `WorldFrame:GetChildren()` never returned them and bubble-skinning addons such as pfUI's found nothing.

In the reference, a bubble is an anonymous Frame under WorldFrame from a reused pool: eleven regions (the nine backdrop pieces, the tail, the text in the `NAMEPLATE_FONT` global), the text's bottom on the speaker's projected point, its own fade, and an OnShow on each reuse. It fades out and hides when the speaker is out of range or was not drawn last frame.

Bubbles are now built that way, fed by the app and drawn by the frame pass; the overlay path is gone. Item links show their bare names, and a speaker behind the camera fades out.

Tests cover the child, its regions, a pfUI-style skin, reuse and the fades.

Fixes #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Sam Sung
2026-10-12 02:06:36 +07:00
committed by GitHub
co-authored by Claude Opus 5.5
parent d65fd19095
commit eec1804d37
17 changed files with 1600 additions and 674 deletions
File diff suppressed because it is too large Load Diff
+2
View File
@@ -14,6 +14,8 @@ mod ace_gate_tests;
mod away;
/// The world broadcasts: `SMSG_ZONE_UNDER_ATTACK`/`_DEFENSE_MESSAGE`/`_SERVER_MESSAGE`.
mod broadcast;
/// The chat bubbles' hand-off to their `WorldFrame` widgets.
pub(crate) mod bubbles;
mod channels;
/// The combat log's chat lines: classification, chat type and each sentence's GlobalString key.
pub(crate) mod combat;
+19
View File
@@ -0,0 +1,19 @@
//! The chat bubbles' feed: this frame's live bubbles, gated, faded and seated by
//! [`crate::chat_bubble`], handed to the widget layer, which keeps them as `WorldFrame` children.
use bevy::prelude::*;
use crate::chat_bubble::BubbleFeed;
/// Every frame, after the bubble drive and before the script paint: write the live set into the
/// VM's bubble pool, which hides and pools the frames of lines that ended.
pub(crate) fn feed_bubbles(
feed: Res<BubbleFeed>,
// `None` in a run with no UI VM.
script: Option<NonSendMut<benilla_ui::script::UiScript>>,
) {
let (Some(mut script), Some(geometry)) = (script, feed.geometry) else {
return;
};
script.sync_chat_bubbles(geometry, &feed.states);
}
-9
View File
@@ -162,15 +162,6 @@ pub(crate) struct UiQuads {
pub(crate) mod overlay_z {
/// Floating combat, XP and honor numbers, drawn by the client in the world scene under all UI.
pub(crate) const WORLD_TEXT: u64 = 0;
/// The chat bubbles: one frame level per live bubble, farthest from the camera first.
pub(crate) const BUBBLE: u64 = 1 << 8;
/// Keys per bubble level: its bg, edge, tail and text.
pub(crate) const BUBBLE_STRIDE: u64 = 4;
/// The V-plates, over every bubble; a unit never shows both, so this orders only across units.
pub(crate) const VPLATE: u64 = 1 << 16;
/// The highest bubble level below [`VPLATE`], a guard rail far past any real count (a bubble
/// needs a speaker within 20 yd).
pub(crate) const BUBBLE_MAX_LEVEL: u64 = (VPLATE - BUBBLE) / BUBBLE_STRIDE - 1;
}
/// Clear the append lane at the top of the [`UiQuadAppend`] window, before every appender.
@@ -114,8 +114,9 @@ pub(crate) fn tiling_axes(uv: &UvRect) -> (bool, bool) {
}
/// The [`UiQuad::uv_clamp`] window of an atlas crop, inset half a texel so a magnified cell does
/// not filter in its neighbour. A tiling axis is left alone, as
/// [`benilla_ui::script::inset_atlas_bleed`] does; a whole-texture axis needs no window.
/// not filter in its neighbour (in `ChatBubble-Backdrop`, a white alpha-0 column: a pale line
/// through the speech bubble). A tiling axis is left alone, or each period would land short and
/// walk the art; a whole-texture axis needs no window.
fn uv_clamp_window(uv: &UvRect, size: (u32, u32)) -> Option<[f32; 4]> {
let mut out = [1.0, 1.0, 0.0, 0.0]; // both axes off: `min > max`
let mut any = false;