- the pet gets its own unit frame beside yours, and an Attack order no longer latches as the standing command - five rooms that always read bone dry hold their water now, the Deeprun Tram's flooded stretches among them - macros fire through the client's own chat door, and the macro window fits its tabs and scrolls its icon chooser - the options window grows a Nameplates page and its first dropdown, and the name-display switches apply live in the world Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
198 lines
8.1 KiB
Rust
198 lines
8.1 KiB
Rust
//! The select screen's live refresh — everything that follows the roster or the pointer after
|
||
//! [`super::screen`] spawns the tree: the row texts + visibility, the row highlight (hover, and
|
||
//! LOCKED on the selected row — the ref's `LockHighlight`), the realm banner, the selected name
|
||
//! over the model, the buttons' enabled states, and the glue-booth feed (scene by the selected
|
||
//! character's race, the geared look).
|
||
|
||
use bevy::prelude::*;
|
||
|
||
use crate::area::AreaTableRes;
|
||
use crate::glue::widgets::{GlueDisabled, Hilight};
|
||
use crate::glue_strings::GlueStrings;
|
||
use crate::net::NetStatus;
|
||
use crate::portrait::{GlueLook, GluePreview, SelectLook};
|
||
|
||
use super::screen::{RealmBanner, RowText, SelectAction, SelectedName, MAX_ROWS};
|
||
use super::{class_name, Roster};
|
||
|
||
/// Refill the row texts + visibility, the selected name, and the realm banner whenever the roster
|
||
/// changes (a fresh enum, a selection move) — or when the screen was just (re)spawned (returning
|
||
/// from the create screen finds an unchanged roster; the fresh, empty tree must still fill).
|
||
#[allow(clippy::type_complexity, clippy::too_many_arguments)]
|
||
pub(super) fn refresh_list(
|
||
roster: Res<Roster>,
|
||
areas: Option<Res<AreaTableRes>>,
|
||
strings: Option<Res<GlueStrings>>,
|
||
status: Res<NetStatus>,
|
||
mut rows: Query<(&SelectAction, &mut Visibility), (With<Button>, Without<Hilight>)>,
|
||
mut texts: Query<(&RowText, &mut Text), Without<SelectedName>>,
|
||
mut name: Query<&mut Text, (With<SelectedName>, Without<RealmBanner>, Without<RowText>)>,
|
||
mut banner: Query<&mut Text, (With<RealmBanner>, Without<SelectedName>, Without<RowText>)>,
|
||
spawned: Query<Ref<SelectAction>>,
|
||
) {
|
||
let fresh = spawned.iter().any(|r| r.is_added());
|
||
if !roster.is_changed() && !fresh && !status.is_changed() {
|
||
return;
|
||
}
|
||
let empty = GlueStrings::default();
|
||
let strings = strings.as_deref().unwrap_or(&empty);
|
||
|
||
// Row visibility: rows with a character show; the rest hide (the ref's enumerate-then-hide).
|
||
for (action, mut vis) in &mut rows {
|
||
if let SelectAction::Row(i) = action {
|
||
*vis = if *i < roster.chars.len() {
|
||
Visibility::Inherited
|
||
} else {
|
||
Visibility::Hidden
|
||
};
|
||
}
|
||
}
|
||
// Row texts: Name; Info = `CHARACTER_SELECT_INFO` "Level %d %s" (class only — the ghost
|
||
// variant appends "(Ghost)"); Location = the zone name off AreaTable.
|
||
for (text, mut t) in &mut texts {
|
||
let (i, kind) = match text {
|
||
RowText::Name(i) => (*i, 0),
|
||
RowText::Info(i) => (*i, 1),
|
||
RowText::Location(i) => (*i, 2),
|
||
};
|
||
let new = match roster.chars.get(i) {
|
||
None => String::new(),
|
||
Some(c) => match kind {
|
||
0 => c.name.clone(),
|
||
1 => {
|
||
let key = if c.flags & benilla_protocol::CHARACTER_FLAG_GHOST != 0 {
|
||
("CHARACTER_SELECT_INFO_GHOST", "Level %d %s (Ghost)")
|
||
} else {
|
||
("CHARACTER_SELECT_INFO", "Level %d %s")
|
||
};
|
||
strings
|
||
.text(key.0, key.1)
|
||
.replacen("%d", &c.level.to_string(), 1)
|
||
.replacen("%s", class_name(c.class), 1)
|
||
}
|
||
_ => areas
|
||
.as_deref()
|
||
.and_then(|a| a.0.name(c.zone))
|
||
.unwrap_or_default()
|
||
.to_string(),
|
||
},
|
||
};
|
||
if t.0 != new {
|
||
t.0 = new;
|
||
}
|
||
}
|
||
// The selected character's name over the model (empty list → empty, the ref's
|
||
// UPDATE_SELECTED_CHARACTER arg 0).
|
||
if let Ok(mut t) = name.single_mut() {
|
||
let new = roster
|
||
.selected_char()
|
||
.map(|c| c.name.clone())
|
||
.unwrap_or_default();
|
||
if t.0 != new {
|
||
t.0 = new;
|
||
}
|
||
}
|
||
// The realm banner: "<name> (PVP)" per the realm type; "(Server down)" while unreachable
|
||
// (the ref's SERVER_DOWN suffix); a plain connecting note before the first roster.
|
||
if let Ok(mut t) = banner.single_mut() {
|
||
let new = match &roster.realm {
|
||
Some(realm) => {
|
||
let suffix = match realm.realm_type {
|
||
1 => strings.text("PVP_PARENTHESES", "(PVP)"),
|
||
6 => strings.text("RP_PARENTHESES", "(RP)"),
|
||
8 => strings.text("RPPVP_PARENTHESES", "(RPPVP)"),
|
||
_ => "",
|
||
};
|
||
let down = if status.last_reason.is_some() && roster.pending_pick.is_none() {
|
||
format!("\n({})", strings.text("SERVER_DOWN", "Server down"))
|
||
} else {
|
||
String::new()
|
||
};
|
||
format!("{} {}{down}", realm.name, suffix)
|
||
.trim_end()
|
||
.to_string()
|
||
}
|
||
None => match &status.last_reason {
|
||
Some(_) => strings.text("SERVER_DOWN", "Server down").to_string(),
|
||
None => "Connecting…".to_string(),
|
||
},
|
||
};
|
||
if t.0 != new {
|
||
t.0 = new;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Per-frame interaction visuals + button states: the row highlight (hover ∪ selected — the ref's
|
||
/// `LockHighlight` on the selected row), and the enabled states — Enter World / Delete disable on
|
||
/// an empty list (the ref's `UpdateCharacterList`), Create hides at the 10-cap or disconnected,
|
||
/// Change Realm stays disabled (decision 0465 §6).
|
||
#[allow(clippy::type_complexity)]
|
||
pub(super) fn refresh_banner_and_buttons(
|
||
roster: Res<Roster>,
|
||
mut rows: Query<(&SelectAction, &Interaction, &Children), With<Button>>,
|
||
mut hilights: Query<&mut Visibility, With<Hilight>>,
|
||
mut disables: Query<(&SelectAction, &mut GlueDisabled)>,
|
||
mut create_vis: Query<
|
||
(&SelectAction, &mut Visibility),
|
||
(With<crate::glue::widgets::GlueBtn>, Without<Hilight>),
|
||
>,
|
||
) {
|
||
for (action, interaction, children) in &mut rows {
|
||
let SelectAction::Row(i) = action else {
|
||
continue;
|
||
};
|
||
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 {
|
||
Visibility::Inherited
|
||
} else {
|
||
Visibility::Hidden
|
||
};
|
||
}
|
||
}
|
||
}
|
||
let have_chars = !roster.chars.is_empty();
|
||
for (action, mut disabled) in &mut disables {
|
||
let want = match action {
|
||
SelectAction::EnterWorld | SelectAction::Delete => !have_chars,
|
||
SelectAction::ChangeRealm => true,
|
||
_ => false,
|
||
};
|
||
if disabled.0 != want {
|
||
disabled.0 = want;
|
||
}
|
||
}
|
||
// Create New Character shows while the realm answered and a slot is free (the ref hides it
|
||
// disconnected or at MAX_CHARACTERS_PER_REALM = 10).
|
||
let show_create = roster.realm.is_some() && roster.chars.len() < MAX_ROWS;
|
||
for (action, mut vis) in &mut create_vis {
|
||
if matches!(action, SelectAction::CreateChar) {
|
||
*vis = if show_create {
|
||
Visibility::Inherited
|
||
} else {
|
||
Visibility::Hidden
|
||
};
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Feed the glue booth from the roster (decision 0465): the scene is the SELECTED character's
|
||
/// 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>) {
|
||
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
|
||
};
|
||
let scene = Some(crate::portrait::GlueScene::Race(race));
|
||
if preview.scene != scene {
|
||
preview.scene = scene;
|
||
}
|
||
if preview.look != look {
|
||
preview.look = look;
|
||
}
|
||
}
|