the auction house opens, the Honor tab lands, and a day of fixes
- the auction house end to end: right-clicking an auctioneer opens Browse/Bids/Auctions on the server's reply — search, the category tree from your own DBCs, the reference's 50-a-page pager, multi-key sorting (directions corrected at the bytes), bid/buyout, the sell slot with deposit and duration, cancel with its fee popup; prices sit on the reference's own money-frame kit in lined-up columns, the twenty auction messages split refusals-red / outcomes-to-chat, and a won or sold auction reads as a proper mail receipt — the window's Lua re-worded in our own terms after the overlap ratchet caught the first cut leaning on the reference's text - the PvP honor system whole: the Honor tab and the inspect page over a real wire model, with current rank and highest-ever rank correctly two different fields - a day of reported fixes: WASD survives a /logout re-entry, addon settings save on a window close and Cmd+Q (exactly once — a mac close could fire the tail twice), clams open again, random-enchant items name themselves on every surface, the loot window's rows hold their seats with legible counts, idle players stop sinking through building floors, the shooter re-pulls between Auto Shots, portraits loosen ~19% to the client's own aspect-1.0 matrix, and big creatures thump the camera off the CameraShakes table - character select clicks like 1.12 — captions whiten on hover, buttons fire on release, Humans face straight ahead off the stage bone's real yaw, the facing is per-character, and hunter/warlock pets stand beside you at their family-ramp size with their own emitters (the imp's flames), the Voidwalker's sequence-hidden shackles culled - the smaller player-facing set: nameplate on/off rows in Options with the CanAttack category gate (emissaries stop plating), the bonus bar slides up with igBonusBarOpen, the camera's framing height glides on a shapeshift, the open letter evicts the centre seat, the far-band fog hull stops printing a pale horizon stripe at low view distance, the guide arrow is gold with per-source rim art, error text fades through its shadow too, and /convertraid converts the party - README: the auction house and PvP honor move out of "not built yet" Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -587,8 +587,8 @@ pub(super) fn drive_addons_panel(
|
||||
mouse: Res<ButtonInput<MouseButton>>,
|
||||
mut wheel: MessageReader<bevy::input::mouse::MouseWheel>,
|
||||
mut sounds: MessageWriter<GlueSound>,
|
||||
presses: Query<(&AddonsAction, &Interaction), Changed<Interaction>>,
|
||||
hovers: Query<(Entity, &AddonsAction, &Interaction)>,
|
||||
clicks: Res<crate::glue::GlueClicks>,
|
||||
hovers: Query<(Entity, &AddonsAction, Ref<Interaction>)>,
|
||||
lit: Query<(&Interaction, &Children), With<HoverLit>>,
|
||||
mut hilights: Query<&mut Visibility, With<Hilight>>,
|
||||
band: Query<(&ComputedNode, &UiGlobalTransform), With<ScrollBand>>,
|
||||
@@ -608,8 +608,17 @@ pub(super) fn drive_addons_panel(
|
||||
let mut close_and_save = false;
|
||||
let mut close_and_discard = false;
|
||||
let mut bar_pressed = false;
|
||||
for (action, interaction) in &presses {
|
||||
if *interaction != Interaction::Pressed {
|
||||
for (entity, action, interaction) in &hovers {
|
||||
// **The scroll bar warps on the PRESS**, and only it: the reference's `CSimpleSlider`
|
||||
// OnMouseDown (`0x789ca0`, 45 bytes, zero branches) warps the value from any press inside
|
||||
// the hit rect — there is no thumb hit-test in the class (wow-re `ui.md`). Every *button*
|
||||
// in this panel fires on the RELEASE, over the button that took the press (1533).
|
||||
let click = if *action == AddonsAction::ScrollBar {
|
||||
interaction.is_changed() && *interaction == Interaction::Pressed
|
||||
} else {
|
||||
clicks.hit(entity)
|
||||
};
|
||||
if !click {
|
||||
continue;
|
||||
}
|
||||
match *action {
|
||||
@@ -761,7 +770,7 @@ pub(super) fn drive_addons_panel(
|
||||
}
|
||||
|
||||
let hover = hovers.iter().find_map(|(_, a, i)| {
|
||||
if !matches!(i, Interaction::Hovered | Interaction::Pressed) {
|
||||
if !matches!(*i, Interaction::Hovered | Interaction::Pressed) {
|
||||
return None;
|
||||
}
|
||||
match a {
|
||||
|
||||
@@ -99,7 +99,8 @@ pub(super) fn drive_delete_dialog(
|
||||
keys: Res<ButtonInput<KeyCode>>,
|
||||
mut keyboard: MessageReader<KeyboardInput>,
|
||||
mut sounds: MessageWriter<GlueSound>,
|
||||
presses: Query<(&DialogAction, &Interaction), Changed<Interaction>>,
|
||||
buttons: Query<(Entity, &DialogAction)>,
|
||||
clicks: Res<crate::glue::GlueClicks>,
|
||||
mut okay: Query<(&DialogAction, &mut GlueDisabled)>,
|
||||
// The five row items of the confirm box (segments + carets), painted by the shared
|
||||
// `paint_glue_field`; plus the host pasteboard and the window handle its Wayland backend needs.
|
||||
@@ -178,8 +179,9 @@ pub(super) fn drive_delete_dialog(
|
||||
// The flows.
|
||||
let mut do_delete = false;
|
||||
let mut do_cancel = false;
|
||||
for (action, interaction) in &presses {
|
||||
if *interaction != Interaction::Pressed {
|
||||
// Both buttons fire on the RELEASE (1533) — a confirm you can slide off and cancel.
|
||||
for (entity, action) in &buttons {
|
||||
if !clicks.hit(entity) {
|
||||
continue;
|
||||
}
|
||||
match action {
|
||||
|
||||
@@ -15,11 +15,8 @@ use super::dialog::DeleteDialog;
|
||||
use super::screen::SelectAction;
|
||||
use super::{class_name, send_pick, ClientState, Roster};
|
||||
|
||||
/// The ref's drag constant (`CHARACTER_ROTATION_CONSTANT = 0.6`, CharacterSelect.lua — degrees per
|
||||
/// pixel; the facing setter is in degrees, deg→rad at the C boundary).
|
||||
pub(crate) const ROTATION_PER_PX: f32 = 0.6 * std::f32::consts::PI / 180.0;
|
||||
/// The rotate buttons' hold rate — the ref's 2°-per-frame `CHARACTER_FACING_INCREMENT`, per-second.
|
||||
const ROTATE_RATE: f32 = 120.0 * std::f32::consts::PI / 180.0;
|
||||
use crate::glue::{drag_yaw, ROTATE_RATE};
|
||||
|
||||
/// The double-click window (the ref rides the OS notion; this is the conventional interval).
|
||||
const DOUBLE_CLICK_SECS: f32 = 0.4;
|
||||
|
||||
@@ -29,7 +26,8 @@ const DOUBLE_CLICK_SECS: f32 = 0.4;
|
||||
/// sits over the buttons).
|
||||
#[allow(clippy::too_many_arguments, clippy::type_complexity)]
|
||||
pub(super) fn select_input(
|
||||
presses: Query<(&SelectAction, &Interaction), Changed<Interaction>>,
|
||||
buttons: Query<(Entity, &SelectAction)>,
|
||||
clicks: Res<crate::glue::GlueClicks>,
|
||||
keys: Res<ButtonInput<KeyCode>>,
|
||||
mut roster: ResMut<Roster>,
|
||||
pick: Res<CharPick>,
|
||||
@@ -48,8 +46,10 @@ pub(super) fn select_input(
|
||||
let now = time.elapsed_secs();
|
||||
let mut enter_world = false;
|
||||
let mut back_to_login = false;
|
||||
for (action, interaction) in &presses {
|
||||
if *interaction != Interaction::Pressed {
|
||||
// Buttons fire on the RELEASE, over the button that took the press — the reference's stock
|
||||
// `<Button>` click mask is `LeftButtonUp` alone (1533, `crate::glue::glue_clicks`).
|
||||
for (entity, action) in &buttons {
|
||||
if !clicks.hit(entity) {
|
||||
continue;
|
||||
}
|
||||
match *action {
|
||||
@@ -58,10 +58,13 @@ pub(super) fn select_input(
|
||||
// within the window is the double-click → enter world.
|
||||
let double =
|
||||
last_click.is_some_and(|(row, at)| row == i && now - at < DOUBLE_CLICK_SECS);
|
||||
let was = roster.selected();
|
||||
*last_click = Some((i, now));
|
||||
if roster.selected != Some(i) {
|
||||
roster.selected = Some(i);
|
||||
} else if double {
|
||||
// The ref selects on **every** click, the row you were already on included — which
|
||||
// is what re-squares the facing (`Roster::select_seq`). Only the double still
|
||||
// needs the "already selected" test.
|
||||
roster.select(Some(i));
|
||||
if was == Some(i) && double {
|
||||
enter_world = true;
|
||||
}
|
||||
}
|
||||
@@ -121,13 +124,13 @@ pub(super) fn select_input(
|
||||
let back = keys.just_pressed(KeyCode::ArrowUp) || keys.just_pressed(KeyCode::ArrowLeft);
|
||||
let fwd = keys.just_pressed(KeyCode::ArrowDown) || keys.just_pressed(KeyCode::ArrowRight);
|
||||
if back || fwd {
|
||||
let cur = roster.selected.unwrap_or(0);
|
||||
let cur = roster.selected().unwrap_or(0);
|
||||
let sel = if back {
|
||||
(cur + n - 1) % n
|
||||
} else {
|
||||
(cur + 1) % n
|
||||
};
|
||||
roster.selected = Some(sel);
|
||||
roster.select(Some(sel));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,15 +151,17 @@ pub(super) fn rotate_model(
|
||||
panes: Query<(&Interaction, &SelectAction)>,
|
||||
motion: Res<AccumulatedMouseMotion>,
|
||||
time: Res<Time>,
|
||||
window: Query<&Window, With<bevy::window::PrimaryWindow>>,
|
||||
mut preview: ResMut<GluePreview>,
|
||||
) {
|
||||
let window = window.single().ok();
|
||||
for (interaction, action) in &panes {
|
||||
if *interaction != Interaction::Pressed {
|
||||
continue;
|
||||
}
|
||||
match action {
|
||||
SelectAction::Scene if motion.delta.x != 0.0 => {
|
||||
preview.yaw += motion.delta.x * ROTATION_PER_PX;
|
||||
preview.yaw += drag_yaw(motion.delta.x, window);
|
||||
}
|
||||
SelectAction::RotateLeft => preview.yaw -= ROTATE_RATE * time.delta_secs(),
|
||||
SelectAction::RotateRight => preview.yaw += ROTATE_RATE * time.delta_secs(),
|
||||
|
||||
@@ -152,7 +152,17 @@ pub(crate) struct Roster {
|
||||
pub(super) chars: Vec<Character>,
|
||||
/// The selected row (the ref's `CharacterSelect.selectedIndex`, 0-based; `None` = empty list).
|
||||
/// A fresh roster clamps it into range and defaults to the first row (the ref's law).
|
||||
pub(super) selected: Option<usize>,
|
||||
/// **Write it through [`Roster::select`]**, never directly — see [`Roster::select_seq`].
|
||||
selected: Option<usize>,
|
||||
/// Bumped by every [`Roster::select`], whether or not the row changed.
|
||||
///
|
||||
/// The ref's `SelectCharacter` zeroes the select facing **unconditionally**: `0x472950`'s
|
||||
/// `mov ds:0xb4217c, 0` sits one instruction *above* the already-built discriminator, so it
|
||||
/// dominates both legs, and the merged tail re-applies it geometrically — so re-clicking the
|
||||
/// row you are already on snaps the character square again (wow-re
|
||||
/// `glue/scratch/glue-preview-facing-law.md`, 1533). A counter rather than change-detection on
|
||||
/// `selected`, because "the same index again" is a selection.
|
||||
pub(super) select_seq: u64,
|
||||
/// The guid we answered the IO thread with; `Some` = a login is requested/live.
|
||||
pub(super) pending_pick: Option<u64>,
|
||||
/// `WOW_CHAR`, when explicitly set: auto-pick this name on the FIRST roster (the dev fast
|
||||
@@ -214,11 +224,23 @@ impl Roster {
|
||||
.iter()
|
||||
.position(|c| c.name.eq_ignore_ascii_case(name))
|
||||
{
|
||||
self.selected = Some(row);
|
||||
self.select(Some(row));
|
||||
self.just_created = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Select a row — the ref's `SelectCharacter`. Every selection goes through here so the facing
|
||||
/// reset it owes cannot be forgotten at one of the seven call sites (see [`Self::select_seq`]).
|
||||
pub(super) fn select(&mut self, row: Option<usize>) {
|
||||
self.selected = row;
|
||||
self.select_seq = self.select_seq.wrapping_add(1);
|
||||
}
|
||||
|
||||
/// The selected row, if any.
|
||||
pub(super) fn selected(&self) -> Option<usize> {
|
||||
self.selected
|
||||
}
|
||||
|
||||
/// The selected character, if any.
|
||||
pub(super) fn selected_char(&self) -> Option<&Character> {
|
||||
self.selected.and_then(|i| self.chars.get(i))
|
||||
@@ -316,14 +338,15 @@ fn apply_roster_policy(
|
||||
warn!(
|
||||
"char select: created {name:?} is not on the fresh roster — selecting the last row"
|
||||
);
|
||||
roster.selected = roster.chars.len().checked_sub(1);
|
||||
let last = roster.chars.len().checked_sub(1);
|
||||
roster.select(last);
|
||||
}
|
||||
}
|
||||
if roster.chars.is_empty() {
|
||||
roster.selected = None;
|
||||
roster.select(None);
|
||||
} else {
|
||||
let sel = roster.selected.unwrap_or(0).min(roster.chars.len() - 1);
|
||||
roster.selected = Some(sel);
|
||||
let sel = roster.selected().unwrap_or(0).min(roster.chars.len() - 1);
|
||||
roster.select(Some(sel));
|
||||
}
|
||||
// `WOW_CHARSELECT_PICK=<name>` — **select** that row and stay on the screen, the
|
||||
// deliberate opposite of `WOW_CHAR`'s enter-the-world fast path. Without it the screen is
|
||||
@@ -338,7 +361,7 @@ fn apply_roster_policy(
|
||||
{
|
||||
Some(row) => {
|
||||
info!("char select: WOW_CHARSELECT_PICK={name} — selecting row {row}");
|
||||
roster.selected = Some(row);
|
||||
roster.select(Some(row));
|
||||
}
|
||||
None => warn!("char select: WOW_CHARSELECT_PICK={name} not on this account"),
|
||||
}
|
||||
@@ -493,6 +516,14 @@ fn debug_glue_roundtrip(
|
||||
/// the first — it streams a map the streamer has already torn down once — and a teardown that
|
||||
/// forgets to reset its own bookkeeping fails exactly here and nowhere else. The world-audio
|
||||
/// boundary this smoke was written for is still checked on the way through.
|
||||
///
|
||||
/// **It ends the way a player ends a session** — by closing the window, not by writing an
|
||||
/// `AppExit` of its own (decision 1528). That is not cosmetic: the close is the *latest*
|
||||
/// announcement Bevy makes (`exit_on_all_closed` in `PostUpdate`, a frame after the request), so
|
||||
/// it is the only exit that actually tests whether the shutdown tail is reachable. Exiting by a
|
||||
/// hand-written `AppExit` from `Update` skipped that question for this smoke's whole life, and the
|
||||
/// bug it would have caught — every saved variable, every addon file and the camera pose lost on
|
||||
/// every window close — shipped underneath it.
|
||||
#[allow(clippy::too_many_arguments)] // a smoke test that drives the whole round trip
|
||||
fn debug_logout_smoke(
|
||||
state: Res<State<ClientState>>,
|
||||
@@ -503,6 +534,8 @@ fn debug_logout_smoke(
|
||||
streamer: Res<benilla_world::terrain_stream::TerrainStreamer>,
|
||||
time: Res<Time>,
|
||||
mut exit: MessageWriter<AppExit>,
|
||||
mut close: MessageWriter<bevy::window::WindowCloseRequested>,
|
||||
windows: Query<Entity, With<bevy::window::PrimaryWindow>>,
|
||||
mut phase: Local<u8>,
|
||||
mut mark: Local<f32>,
|
||||
) {
|
||||
@@ -551,15 +584,34 @@ fn debug_logout_smoke(
|
||||
}
|
||||
},
|
||||
4 if *state.get() == ClientState::InWorld && player.active && now - *mark > 3.0 => {
|
||||
// **The suppressor reading is the point of the second entry, not a decoration on it**
|
||||
// (B306, decision 1542). This leg has crossed the boundary on every run since 1291 and
|
||||
// could only ever report that it *happened* — tiles, UI rebuilds, error counts — none
|
||||
// of which a character who re-entered unable to move would disturb. `scripts/smoke.sh`
|
||||
// fails on anything but `none`, and separately reports whether the run's logout was
|
||||
// the rooted kind at all: a GM probe gets vmangos's instant path, which is B306's
|
||||
// precondition missing, so a green here is not a B306 regression on its own.
|
||||
info!(
|
||||
"logout-smoke: re-entered — {} tiles resident, done",
|
||||
streamer.residency().1
|
||||
"logout-smoke: re-entered — {} tiles resident, suppressors: {}, done",
|
||||
streamer.residency().1,
|
||||
player.movement_suppressors()
|
||||
);
|
||||
*phase = 5;
|
||||
}
|
||||
5 => {
|
||||
info!("logout-smoke: done");
|
||||
exit.write(AppExit::Success);
|
||||
// The player's exit: request the window close and let `bevy_window` take it from
|
||||
// there. A run with no window (there is none in the headless harness) has no such
|
||||
// path, so it says so and falls back rather than hanging until the timeout.
|
||||
match windows.single() {
|
||||
Ok(window) => {
|
||||
close.write(bevy::window::WindowCloseRequested { window });
|
||||
}
|
||||
Err(_) => {
|
||||
warn!("logout-smoke: no primary window — exiting by AppExit instead");
|
||||
exit.write(AppExit::Success);
|
||||
}
|
||||
}
|
||||
*phase = 6;
|
||||
}
|
||||
_ => {}
|
||||
@@ -741,6 +793,9 @@ mod tests {
|
||||
},
|
||||
flags: 0,
|
||||
equipment: [benilla_protocol::CharEnumItem::default(); 19],
|
||||
pet_display_id: 0,
|
||||
pet_level: 0,
|
||||
pet_family: 0,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -142,7 +142,7 @@ pub(super) fn refresh_banner_and_buttons(
|
||||
let SelectAction::Row(i) = action else {
|
||||
continue;
|
||||
};
|
||||
let lit = roster.selected == Some(*i) || *interaction != Interaction::None;
|
||||
let lit = roster.selected() == Some(*i) || *interaction != Interaction::None;
|
||||
for child in children {
|
||||
if let Ok(mut vis) = hilights.get_mut(*child) {
|
||||
*vis = if lit {
|
||||
@@ -182,7 +182,23 @@ pub(super) fn refresh_banner_and_buttons(
|
||||
/// race's (the ref's `SetBackgroundModel` on the enum fileString — Orc before a list arrives or
|
||||
/// with an empty account, the ref's OnLoad default), the look its geared enum record. Runs every
|
||||
/// frame (cheap compares inside `GluePreview` writers — the builder keys on value change).
|
||||
pub(super) fn feed_glue_preview(roster: Res<Roster>, mut preview: ResMut<GluePreview>) {
|
||||
pub(super) fn feed_glue_preview(
|
||||
roster: Res<Roster>,
|
||||
mut preview: ResMut<GluePreview>,
|
||||
mut showing: Local<Option<u64>>,
|
||||
) {
|
||||
// **The facing belongs to the displayed character, not to the screen** (1533): turning one
|
||||
// character must not hand its angle to the next one you click, and a fresh character faces the
|
||||
// stage's own forward until you turn it. `GluePreview::yaw` is one resource shared by all three
|
||||
// glue screens, so the reset lives at the selection edge rather than in the resource.
|
||||
//
|
||||
// Keyed on the selection **counter**, not on who is shown: the ref's `SelectCharacter` zeroes
|
||||
// the facing unconditionally (`0x472950`, above the already-built discriminator), so clicking
|
||||
// the row you are already on re-squares the character too. See `Roster::select_seq`.
|
||||
if *showing != Some(roster.select_seq) {
|
||||
*showing = Some(roster.select_seq);
|
||||
preview.yaw = 0.0;
|
||||
}
|
||||
let (race, look) = match roster.selected_char() {
|
||||
Some(c) => (c.race, Some(GlueLook::Select(SelectLook::from(c)))),
|
||||
None => (2, None), // UI_Orc — the ref's initial/empty-account scene
|
||||
|
||||
Reference in New Issue
Block a user