overhaul HUD

This commit is contained in:
yuni 2024-04-15 20:58:19 +02:00
parent f85e01c6f6
commit 8533b689b2
2 changed files with 240 additions and 137 deletions

View file

@ -9,9 +9,10 @@ use std::time::SystemTime;
pub const HUD_REFRESH_TIME: f32 = 0.1; pub const HUD_REFRESH_TIME: f32 = 0.1;
pub const FONT: &str = "fonts/Yupiter-Regular.ttf"; pub const FONT: &str = "fonts/Yupiter-Regular.ttf";
pub const LOG_MAX: usize = 4; pub const LOG_MAX: usize = 16;
pub const LOG_MAX_TIME_S: f64 = 15.0; pub const LOG_MAX_TIME_S: f64 = 30.0;
pub const LOG_MAX_ROWS: usize = 30; pub const LOG_MAX_ROWS: usize = 30;
pub const MAX_CHOICES: usize = 10;
pub const AMBIENT_LIGHT: f32 = 0.0; // Space is DARK pub const AMBIENT_LIGHT: f32 = 0.0; // Space is DARK
pub const AMBIENT_LIGHT_AR: f32 = 15.0; pub const AMBIENT_LIGHT_AR: f32 = 15.0;
//pub const REPLY_NUMBERS: [char; 10] = ['❶', '❷', '❸', '❹', '❺', '❻', '❼', '❽', '❾', '⓿']; //pub const REPLY_NUMBERS: [char; 10] = ['❶', '❷', '❸', '❹', '❺', '❻', '❼', '❽', '❾', '⓿'];
@ -48,8 +49,10 @@ impl Plugin for HudPlugin {
} }
#[derive(Event)] pub struct TargetEvent(pub Option<Entity>); #[derive(Event)] pub struct TargetEvent(pub Option<Entity>);
#[derive(Component)] struct GaugesText; #[derive(Component)] struct NodeHud;
#[derive(Component)] struct ChatText; #[derive(Component)] struct NodeConsole;
#[derive(Component)] struct NodeChoiceText;
#[derive(Component)] struct NodeCurrentChatLine;
#[derive(Component)] struct Reticule; #[derive(Component)] struct Reticule;
#[derive(Component)] struct ToggleableHudElement; #[derive(Component)] struct ToggleableHudElement;
#[derive(Component)] struct OnlyHideWhenTogglingHud; #[derive(Component)] struct OnlyHideWhenTogglingHud;
@ -93,6 +96,14 @@ impl Message {
} }
return 1.0; return 1.0;
} }
pub fn format(&self) -> String {
if self.sender.is_empty() {
return self.text.clone() + "\n";
}
else {
return format!("{}: {}\n", self.sender, self.text);
}
}
} }
#[derive(Component)] #[derive(Component)]
@ -173,12 +184,33 @@ fn setup(
} else { } else {
Visibility::Hidden Visibility::Hidden
}; };
let style = TextStyle { let font_handle = asset_server.load(FONT);
font: asset_server.load(FONT), let style_conversations = TextStyle {
font_size: settings.font_size_hud, font: font_handle.clone(),
color: Color::GRAY, font_size: settings.font_size_conversations,
color: settings.hud_color_subtitles,
..default() ..default()
}; };
let style_console = TextStyle {
font: font_handle.clone(),
font_size: settings.font_size_console,
color: settings.hud_color_console,
..default()
};
let style_choices = TextStyle {
font: font_handle.clone(),
font_size: settings.font_size_choices,
color: settings.hud_color_choices,
..default()
};
let style = TextStyle {
font: font_handle,
font_size: settings.font_size_hud,
color: settings.hud_color,
..default()
};
// Add Statistics HUD
let mut bundle_fps = TextBundle::from_sections([ let mut bundle_fps = TextBundle::from_sections([
TextSection::new("", style.clone()), TextSection::new("", style.clone()),
TextSection::new("", style.clone()), TextSection::new("", style.clone()),
@ -198,73 +230,47 @@ fn setup(
]).with_style(Style { ]).with_style(Style {
position_type: PositionType::Absolute, position_type: PositionType::Absolute,
top: Val::VMin(2.0), top: Val::VMin(2.0),
right: Val::VMin(3.0), left: Val::VMin(3.0),
..default() ..default()
}).with_text_justify(JustifyText::Right); }).with_text_justify(JustifyText::Left);
bundle_fps.visibility = visibility; bundle_fps.visibility = visibility;
commands.spawn(( commands.spawn((
GaugesText, NodeHud,
ToggleableHudElement, ToggleableHudElement,
bundle_fps, bundle_fps,
)); ));
// Add Chat Box // Add Console
let bundle_chatbox = TextBundle::from_sections([ let bundle_chatbox = TextBundle::from_sections((0..LOG_MAX_ROWS).map(|_|
TextSection::new("", style.clone()), TextSection::new("", style_console.clone()))
TextSection::new("", style.clone()), ).with_style(Style {
TextSection::new("", style.clone()),
TextSection::new("", style.clone()),
TextSection::new("", style.clone()),
TextSection::new("", style.clone()),
TextSection::new("", style.clone()),
TextSection::new("", style.clone()),
TextSection::new("", style.clone()),
TextSection::new("", style.clone()),
TextSection::new("", style.clone()),
TextSection::new("", style.clone()),
TextSection::new("", style.clone()),
TextSection::new("", style.clone()),
TextSection::new("", style.clone()),
TextSection::new("", style.clone()),
TextSection::new("", style.clone()),
TextSection::new("", style.clone()),
TextSection::new("", style.clone()),
TextSection::new("", style.clone()),
TextSection::new("", style.clone()),
TextSection::new("", style.clone()),
TextSection::new("", style.clone()),
TextSection::new("", style.clone()),
TextSection::new("", style.clone()),
TextSection::new("", style.clone()),
TextSection::new("", style.clone()),
TextSection::new("", style.clone()),
TextSection::new("", style.clone()),
TextSection::new("", style.clone()),
]).with_style(Style {
position_type: PositionType::Absolute, position_type: PositionType::Absolute,
top: Val::VMin(0.0), top: Val::VMin(0.0),
left: Val::VMin(0.0), right: Val::VMin(0.0),
..default() ..default()
}).with_text_justify(JustifyText::Left); }).with_text_justify(JustifyText::Right);
commands.spawn(( commands.spawn((
ToggleableHudElement,
NodeBundle { NodeBundle {
style: Style { style: Style {
width: Val::Percent(45.0), width: Val::Percent(50.0),
align_items: AlignItems::Start, align_items: AlignItems::Start,
position_type: PositionType::Absolute, position_type: PositionType::Absolute,
top: Val::VMin(2.0), top: Val::VMin(2.0),
left: Val::VMin(3.0), right: Val::VMin(3.0),
..default() ..default()
}, },
visibility,
..default() ..default()
}, },
)).with_children(|parent| { )).with_children(|parent| {
parent.spawn(( parent.spawn((
bundle_chatbox, bundle_chatbox,
ChatText, NodeConsole,
)); ));
}); });
// Add Reticule
commands.spawn(( commands.spawn((
Reticule, Reticule,
ToggleableHudElement, ToggleableHudElement,
@ -283,6 +289,54 @@ fn setup(
}, },
)); ));
// Chat "subtitles" and choices
commands.spawn(NodeBundle {
style: Style {
width: Val::Vw(100.0),
align_items: AlignItems::Center,
flex_direction: FlexDirection::Column,
position_type: PositionType::Absolute,
bottom: Val::Vh(2.0),
left: Val::Px(0.0),
..default()
},
..default()
}).with_children(|builder| {
builder.spawn((
TextBundle {
text: Text {
sections: vec![
TextSection::new("", style_conversations),
],
justify: JustifyText::Center,
..default()
},
style: Style {
max_width: Val::Percent(50.0),
margin: UiRect {
bottom: Val::Vh(1.0),
..default()
},
..default()
},
..default()
},
NodeCurrentChatLine,
));
let choice_sections = (0..MAX_CHOICES).map(|_|
TextSection::new("", style_choices.clone()));
builder.spawn((
TextBundle {
text: Text {
sections: choice_sections.collect(),
..default()
},
..default()
},
NodeChoiceText,
));
});
// Selectagon // Selectagon
commands.spawn(( commands.spawn((
Selectagon, Selectagon,
@ -310,9 +364,12 @@ fn update_hud(
player: Query<(&actor::HitPoints, &actor::Suit, &actor::ExperiencesGForce), With<actor::Player>>, player: Query<(&actor::HitPoints, &actor::Suit, &actor::ExperiencesGForce), With<actor::Player>>,
q_camera: Query<(&Position, &LinearVelocity), With<actor::PlayerCamera>>, q_camera: Query<(&Position, &LinearVelocity), With<actor::PlayerCamera>>,
mut timer: ResMut<FPSUpdateTimer>, mut timer: ResMut<FPSUpdateTimer>,
mut query: Query<&mut Text, With<GaugesText>>,
q_choices: Query<&chat::Choice>, q_choices: Query<&chat::Choice>,
mut query_chat: Query<&mut Text, (With<ChatText>, Without<GaugesText>)>, q_chat: Query<&chat::Chat>,
mut q_node_hud: Query<&mut Text, With<NodeHud>>,
mut q_node_console: Query<&mut Text, (With<NodeConsole>, Without<NodeHud>, Without<NodeChoiceText>)>,
mut q_node_choice: Query<&mut Text, (With<NodeChoiceText>, Without<NodeHud>, Without<NodeConsole>)>,
mut q_node_currentline: Query<&mut Text, (With<NodeCurrentChatLine>, Without<NodeHud>, Without<NodeConsole>, Without<NodeChoiceText>)>,
query_all_actors: Query<&actor::Actor>, query_all_actors: Query<&actor::Actor>,
settings: Res<var::Settings>, settings: Res<var::Settings>,
q_target: Query<(&IsClickable, Option<&Position>, Option<&LinearVelocity>), With<IsTargeted>>, q_target: Query<(&IsClickable, Option<&Position>, Option<&LinearVelocity>), With<IsTargeted>>,
@ -325,7 +382,7 @@ fn update_hud(
if player.is_ok() && q_camera_result.is_ok() { if player.is_ok() && q_camera_result.is_ok() {
let (hp, suit, gforce) = player.unwrap(); let (hp, suit, gforce) = player.unwrap();
let (pos, cam_v) = q_camera_result.unwrap(); let (pos, cam_v) = q_camera_result.unwrap();
for mut text in &mut query { for mut text in &mut q_node_hud {
text.sections[0].value = format!("2524-03-12 03:02"); text.sections[0].value = format!("2524-03-12 03:02");
if let Some(fps) = diagnostics.get(&FrameTimeDiagnosticsPlugin::FPS) { if let Some(fps) = diagnostics.get(&FrameTimeDiagnosticsPlugin::FPS) {
if let Some(value) = fps.smoothed() { if let Some(value) = fps.smoothed() {
@ -341,29 +398,30 @@ fn update_hud(
if suit.oxygen > nature::OXY_H { if suit.oxygen > nature::OXY_H {
let oxy_hour = suit.oxygen / nature::OXY_H; let oxy_hour = suit.oxygen / nature::OXY_H;
text.sections[7].value = format!("{oxy_percent:.1}% [lasts {oxy_hour:.1} hours]"); text.sections[7].value = format!("{oxy_percent:.1}% [lasts {oxy_hour:.1} hours]");
text.sections[7].style.color = Color::GRAY; text.sections[7].style.color = settings.hud_color;
} else { } else {
let oxy_min = suit.oxygen / nature::OXY_M; let oxy_min = suit.oxygen / nature::OXY_M;
text.sections[7].value = format!("{oxy_percent:.1}% [lasts {oxy_min:.1} min]"); text.sections[7].value = format!("{oxy_percent:.1}% [lasts {oxy_min:.1} min]");
text.sections[7].style.color = Color::MAROON; text.sections[7].style.color = settings.hud_color_alert;
} }
//let adrenaline = lifeform.adrenaline * 990.0 + 10.0; //let adrenaline = lifeform.adrenaline * 990.0 + 10.0;
//text.sections[11].value = format!("{adrenaline:.0}pg/mL"); //text.sections[11].value = format!("{adrenaline:.0}pg/mL");
let vitals = 100.0 * hp.current / hp.max; let vitals = 100.0 * hp.current / hp.max;
text.sections[13].value = format!("{vitals:.0}%"); text.sections[13].value = format!("{vitals:.0}%");
if vitals < 50.0 { if vitals < 50.0 {
text.sections[13].style.color = Color::MAROON; text.sections[13].style.color = settings.hud_color_alert;
} else { } else {
text.sections[13].style.color = Color::GRAY; text.sections[13].style.color = settings.hud_color;
} }
let all_actors = query_all_actors.iter().len(); let all_actors = query_all_actors.iter().len();
text.sections[9].value = format!("{all_actors:.0}"); text.sections[9].value = format!("{all_actors:.0}");
let integrity = suit.integrity * 100.0; let integrity = suit.integrity * 100.0;
text.sections[11].value = format!("{integrity:.0}%");
if integrity < 50.0 { if integrity < 50.0 {
text.sections[11].style.color = Color::MAROON; text.sections[11].style.color = settings.hud_color_alert;
text.sections[11].value = format!("{integrity:.0}% [LEAKING]");
} else { } else {
text.sections[11].style.color = Color::GRAY; text.sections[11].style.color = settings.hud_color;
text.sections[11].value = format!("{integrity:.0}%");
} }
//text.sections[17].value = format!("{speed_readable}/s / {kmh:.0}km/h / {gforce:.1}g"); //text.sections[17].value = format!("{speed_readable}/s / {kmh:.0}km/h / {gforce:.1}g");
@ -426,86 +484,117 @@ fn update_hud(
} }
} }
if let Ok(mut chat) = query_chat.get_single_mut() { let chat = q_node_console.get_single_mut();
let mut row = 0; if chat.is_err() {
let bright = Color::rgb(0.8, 0.75, 0.78); error!("Couldn't find HUD UI text section");
return;
// Chat Log and System Log
let logfilter = if settings.hud_active {
|_msg: &&Message| { true }
} else {
|msg: &&Message| { match msg.level {
LogLevel::Chat => true,
LogLevel::Warning => true,
LogLevel::Info => true,
_ => false
}}
};
let mut messages: Vec<&Message> = log.logs.iter()
.filter(logfilter)
.rev()
.take(15)
.collect();
messages.reverse();
for msg in &messages {
if msg.sender.is_empty() {
chat.sections[row].value = msg.text.clone() + "\n";
}
else {
chat.sections[row].value = format!("{}: {}\n", msg.sender, msg.text);
}
let freshness = msg.get_freshness();
let clr: f32 = (freshness.powf(1.5) as f32).clamp(0.1, 1.0);
freshest_line = freshest_line.max(freshness);
chat.sections[row].style.color = Color::rgba(0.7, 0.7, 0.7, clr);
row += 1;
}
// Highlight most recent line if in a conversation
if row > 0 && (!q_choices.is_empty() || freshest_line > 0.5) {
chat.sections[row-1].style.color = bright;
}
// Add padding between chat and choices
chat.sections[row].value = "\n".to_string();
row += 1;
// Choices
let mut choices: Vec<String> = Vec::new();
let mut count = 0;
for choice in &q_choices {
if count > 9 {
break;
}
let press_this = REPLY_NUMBERS[choice.key];
let reply = &choice.text;
//let recipient = &choice.recipient;
// TODO: indicate recipients if there's more than one
choices.push(format!("{press_this} {reply}"));
count += 1;
}
if count < 4 {
for _padding in 0..(4-count) {
choices.push(" ".to_string());
}
}
for choice in choices {
chat.sections[row].value = choice + "\n";
chat.sections[row].style.color = bright;
row += 1;
}
// Blank the remaining rows
while row < LOG_MAX_ROWS {
chat.sections[row].value = "".to_string();
row += 1;
}
} }
let mut chat = chat.unwrap();
let choicebox = q_node_choice.get_single_mut();
if choicebox.is_err() {
error!("Couldn't find HUD UI text section");
return;
}
let mut choicebox = choicebox.unwrap();
let node_currentline = q_node_currentline.get_single_mut();
if node_currentline.is_err() {
error!("Couldn't find HUD UI text section");
return;
}
let mut node_currentline = node_currentline.unwrap();
let mut row = 0;
// Chat Log and System Log
let logfilter = if settings.hud_active {
|_msg: &&Message| { true }
} else {
|msg: &&Message| { match msg.level {
LogLevel::Chat => true,
LogLevel::Warning => true,
LogLevel::Info => true,
_ => false
}}
};
let messages: Vec<&Message> = log.logs.iter()
.filter(logfilter)
.rev()
.take(15)
.collect();
//messages.reverse();
for msg in &messages {
chat.sections[row].value = msg.format();
let freshness = msg.get_freshness();
let clr: f32 = (freshness.powf(1.5) as f32).clamp(0.1, 1.0);
freshest_line = freshest_line.max(freshness);
chat.sections[row].style.color.set_a(clr);
row += 1;
}
// Display the last chat line as "subtitles"
if !q_chat.is_empty() {
let messages: Vec<&Message> = log.logs.iter()
.filter(|msg: &&Message| { match msg.level {
LogLevel::Chat => true,
_ => false
}})
.rev()
.take(1)
.collect();
if messages.len() > 0 {
node_currentline.sections[0].value = messages[0].format();
} else {
node_currentline.sections[0].value = "".to_string();
}
} else {
node_currentline.sections[0].value = "".to_string();
}
// Blank the remaining rows
while row < LOG_MAX_ROWS {
chat.sections[row].value = "".to_string();
row += 1;
}
// Choices
row = 0;
let mut choices: Vec<String> = Vec::new();
let mut count = 0;
for choice in &q_choices {
if count > 9 {
break;
}
let press_this = REPLY_NUMBERS[choice.key];
let reply = &choice.text;
//let recipient = &choice.recipient;
// TODO: indicate recipients if there's more than one
choices.push(format!("{press_this} {reply}"));
count += 1;
}
for choice in choices {
if row >= MAX_CHOICES {
break;
}
choicebox.sections[row].value = choice + "\n";
row += 1;
}
while row < MAX_CHOICES {
choicebox.sections[row].value = if row < 4 {
" \n".to_string()
} else {
"".to_string()
};
row += 1;
}
log.needs_rerendering = false; log.needs_rerendering = false;
if q_choices.is_empty() && freshest_line < 0.2 { //if q_choices.is_empty() && freshest_line < 0.2 {
log.remove_old(); log.remove_old();
} //}
} }
} }

View file

@ -29,6 +29,13 @@ pub struct Settings {
pub zoom_sensitivity_factor: f32, pub zoom_sensitivity_factor: f32,
pub font_size_hud: f32, pub font_size_hud: f32,
pub font_size_conversations: f32, pub font_size_conversations: f32,
pub font_size_choices: f32,
pub font_size_console: f32,
pub hud_color: Color,
pub hud_color_console: Color,
pub hud_color_alert: Color,
pub hud_color_subtitles: Color,
pub hud_color_choices: Color,
pub chat_speed: f32, pub chat_speed: f32,
pub hud_active: bool, pub hud_active: bool,
pub is_zooming: bool, pub is_zooming: bool,
@ -113,8 +120,15 @@ impl Default for Settings {
fov_highspeed: 25.0, fov_highspeed: 25.0,
zoom_fov: 15.0, zoom_fov: 15.0,
zoom_sensitivity_factor: 0.25, zoom_sensitivity_factor: 0.25,
font_size_hud: 32.0, font_size_hud: 28.0,
font_size_conversations: 32.0, font_size_conversations: 32.0,
font_size_choices: 28.0,
font_size_console: 20.0,
hud_color: Color::rgb(0.2, 0.5, 0.2),
hud_color_console: Color::rgb(0.2, 0.5, 0.2),
hud_color_alert: Color::rgb(0.7, 0.3, 0.3),
hud_color_subtitles: Color::rgb(0.8, 0.8, 0.8),
hud_color_choices: Color::rgb(0.45, 0.45, 0.45),
chat_speed: DEFAULT_CHAT_SPEED * if dev_mode { 2.5 } else { 1.0 }, chat_speed: DEFAULT_CHAT_SPEED * if dev_mode { 2.5 } else { 1.0 },
hud_active: false, hud_active: false,
is_zooming: false, is_zooming: false,