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:
Sam
2026-08-22 19:04:27 +07:00
co-authored by Claude Fable 5
parent 6aa78bc468
commit cf581aa2ac
170 changed files with 20573 additions and 1578 deletions
+105 -15
View File
@@ -10,7 +10,7 @@ use bevy_egui::{egui, EguiContexts};
use super::{overlay_text, OVERLAY_FILL, OVERLAY_TEXT_DIM};
use crate::net::ObjectStore;
use crate::ui_script::{InspectMode, PointerOverUi};
use benilla_world::interact::{pick_at_cursor, PickParts, WorldObject};
use benilla_world::interact::{WorldObject, WorldPick};
use benilla_world::model_render::ModelKind;
use benilla_world::modkeys::DEV_CHORD;
use benilla_world::view::WorldCamera;
@@ -85,6 +85,16 @@ pub(super) struct InspectStores<'w, 's> {
motion: Query<'w, 's, MotionReadout>,
factions: Option<Res<'w, crate::target::ring::Factions>>,
self_store: Query<'w, 's, &'static ObjectStore, With<crate::net::SelfPlayer>>,
/// The live standings — the other half of the reaction resolve (`ring_reaction` takes the
/// reputation branch first, and a rank that came off a standing rather than a faction-template
/// comparison is the single most confusing thing about a unit's colour).
reputations: Res<'w, crate::net::Reputations>,
/// This frame's plate verdict ([`crate::vplates::VPlates`]) — the *rendered* answer, not a
/// re-derivation, so the card can never disagree with what is on screen.
plates: Res<'w, crate::vplates::VPlates>,
/// Which of the two master bits are on, so a `plate ✗` can say whether the unit lost its plate
/// on the category gate or on something else.
plate_mode: Res<'w, crate::vplates::VPlateMode>,
/// The ask-once GO template cache — the readable head a TEXT object's line reports
/// (decision 1105).
go_templates: Res<'w, crate::go_templates::GameObjectTemplates>,
@@ -111,7 +121,6 @@ pub(super) fn inspect_ui(
mut contexts: EguiContexts,
inspect: Res<InspectMode>,
mouseover: Res<MouseoverTarget>,
objects: Query<&WorldObject>,
// The pickable mesh is a child of the net entity; its descriptor store (`ObjectStore`) lives on the
// parent, so the readout hops child → parent.
parents: Query<&ChildOf>,
@@ -160,7 +169,7 @@ pub(super) fn inspect_ui(
});
// The identity card: only when hovering a picked object, pinned just off the cursor tip.
let Some(obj) = mouseover.entity.and_then(|e| objects.get(e).ok()) else {
let Some(obj) = mouseover.object.as_ref() else {
return Ok(());
};
let Some(cursor) = ctx.pointer_latest_pos() else {
@@ -174,6 +183,8 @@ pub(super) fn inspect_ui(
.and_then(|e| parents.get(e).ok())
.map(|c| c.parent());
let (factions, self_store) = (stores.factions.as_deref(), stores.self_store.single().ok());
let (reputations, plates, plate_mode) =
(&*stores.reputations, &stores.plates.0, &*stores.plate_mode);
let go_templates = &*stores.go_templates;
let (stores, kinds, collision, lit, motion, go_anims) = (
&stores.stores,
@@ -321,6 +332,70 @@ pub(super) fn inspect_ui(
s.0.unit_gender().unwrap_or(0)
)
});
// **Why this unit does or does not carry a V-plate** — the whole input set of
// `vplates::drive_vplates`'s gate on one line, plus the verdict it actually reached this frame
// (read out of [`crate::vplates::VPlates`], never re-derived, so the card cannot disagree with
// the screen). "These plates shouldn't be here" is otherwise a question no amount of looking
// can answer: the plate is drawn from a REACTION rank whose provenance (a reputation standing
// vs a faction-template comparison) is invisible, over unit flags nothing displays, and the
// two master bits are off-screen state. Reading `reaction 3 neutral(rep)` off a green-looking
// city NPC is the entire diagnosis.
let plate_line = store.filter(|_| is_unit).map(|s| {
let rank = crate::target::ring::ring_reaction(factions, reputations, Some(s), self_store);
let word = match rank {
0 => "hated",
1 => "hostile",
2 => "unfriendly",
3 => "neutral",
4 => "friendly",
5 => "honored",
6 => "revered",
_ => "exalted",
};
// Which branch produced it: a faction WITH a reputation slot colours by our standing
// (`0x605fc0` → `0x4d63a0`, before any template comparison), everything else by the
// template comparator. The two disagree constantly — a battleground emissary is `friendly`
// by template and `neutral` by standing — and only this tag says which one you are seeing.
let branch = s
.0
.unit_faction_template()
.and_then(|t| factions?.catalog().template(t))
.map(|t| {
if factions.is_some_and(|f| f.catalog().reputation_faction(t.faction).is_some()) {
"rep"
} else {
"tpl"
}
})
.unwrap_or("?");
let ctype = net_entity
.and_then(|p| guids.get(p).ok())
.and_then(|g| benilla_protocol::guid::entry(g.0))
.and_then(|e| names.creature_type(e));
let ctype = ctype.map_or_else(|| "type ?".to_string(), |t| format!("type {t}"));
let verdict = match net_entity {
Some(p) if plates.contains(&p) => "plate ✓".to_string(),
_ => {
// The category the gate would have put it in, and whether that bit is on — the
// first thing to check when a plate is missing (or present) unexpectedly.
let (category, bit) = if rank >= 4 {
("friendly", plate_mode.friends)
} else {
("enemy", plate_mode.enemies)
};
if bit {
"plate ✗".to_string()
} else {
format!("plate ✗ ({category} bit off)")
}
}
};
format!(
"{verdict} · reaction {rank} {word}({branch}) · faction {} · unitflags {:#x} · {ctype}",
s.0.unit_faction_template().unwrap_or(0),
s.0.unit_flags(),
)
});
// Player-only customization: the compositor's input, shown raw so we can
// confirm the PLAYER_BYTES decode against an in-game character.
let customization_line = store.filter(|_| is_player).map(|s| {
@@ -444,6 +519,9 @@ pub(super) fn inspect_ui(
if let Some(line) = &appearance_line {
lines.push(line.clone());
}
if let Some(line) = &plate_line {
lines.push(line.clone());
}
if let Some(line) = &customization_line {
lines.push(line.clone());
}
@@ -497,6 +575,9 @@ pub(super) fn inspect_ui(
if let Some(line) = &appearance_line {
ui.label(egui::RichText::new(line).color(OVERLAY_TEXT_DIM));
}
if let Some(line) = &plate_line {
ui.label(egui::RichText::new(line).color(OVERLAY_TEXT_DIM));
}
if let Some(line) = &customization_line {
ui.label(egui::RichText::new(line).color(OVERLAY_TEXT_DIM));
}
@@ -532,10 +613,17 @@ pub(super) fn inspect_ui(
Ok(())
}
/// The nearest [`WorldObject`] under the cursor this frame, or `None`. Consumers read `entity` and look
/// up its [`WorldObject`]; `point`/`distance` are the world-space hit.
/// The nearest identified thing under the cursor this frame, or `None`. `point`/`distance` are the
/// world-space hit.
///
/// The identity is the resource's own (`object`), not something the consumer looks up off `entity`:
/// most of the static world draws from a consolidated lane and has **no entity** to look anything
/// up on (decision 1534). `entity` is `Some` only when one owns the geometry — which is exactly
/// when the extra per-entity readouts (a unit's descriptor store, a GameObject's collision) mean
/// anything.
#[derive(Resource, Default)]
pub struct MouseoverTarget {
pub object: Option<WorldObject>,
pub entity: Option<Entity>,
pub point: Vec3,
pub distance: f32,
@@ -550,15 +638,18 @@ pub(super) fn update_mouseover(
mut target: ResMut<MouseoverTarget>,
window: Query<&Window, With<PrimaryWindow>>,
camera: Query<(&Camera, &GlobalTransform), With<WorldCamera>>,
objects: Query<Entity, With<WorldObject>>,
parts: PickParts,
objects: Query<(Entity, &WorldObject)>,
// Everything drawn, entity-owned or not — see [`WorldPick`]: most of the static world draws
// from a consolidating lane and would be invisible to a bare `PickParts` cast (1534).
pick: WorldPick,
) {
if !inspect.enabled {
if target.entity.is_some() {
if target.object.is_some() {
*target = MouseoverTarget::default();
}
return;
}
target.object = None;
target.entity = None;
// The pointer is over the dev UI (e.g. the now-overlaid debug panel), not the world — don't pick
// behind it. This replaces the old "is the cursor in the inset world viewport?" test, which no
@@ -575,12 +666,11 @@ pub(super) fn update_mouseover(
let Some(cursor) = window.cursor_position() else {
return; // cursor left the window, or we're in mouselook (hidden)
};
let identified: HashSet<Entity> = objects.iter().collect();
if let Some((entity, point, distance)) =
pick_at_cursor(cursor, camera, cam_tf, &identified, &parts)
{
target.entity = Some(entity);
target.point = point;
target.distance = distance;
let identified: HashSet<Entity> = objects.iter().map(|(e, _)| e).collect();
if let Some(hit) = pick.at_cursor(cursor, camera, cam_tf, &identified) {
target.object = hit.object;
target.entity = hit.entity;
target.point = hit.hit.point;
target.distance = hit.hit.distance;
}
}