480 lines
15 KiB
Rust
480 lines
15 KiB
Rust
use crate::{settings, actor, audio};
|
|
use bevy::prelude::*;
|
|
use bevy::diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin};
|
|
use bevy::core_pipeline::bloom::{BloomCompositeMode, BloomSettings};
|
|
use std::collections::VecDeque;
|
|
use std::time::SystemTime;
|
|
|
|
pub const HUD_REFRESH_TIME: f32 = 0.5;
|
|
pub const FONT: &str = "external/NotoSansSC-Thin.ttf";
|
|
pub const LOG_MAX: usize = 20;
|
|
pub const LOG_MAX_TIME_S: u64 = 20;
|
|
pub const CHOICE_NONE: &str = "";
|
|
pub const AMBIENT_LIGHT: f32 = 0.0; // Space is DARK
|
|
pub const AMBIENT_LIGHT_AR: f32 = 20.0;
|
|
//pub const REPLY_NUMBERS: [char; 10] = ['❶', '❷', '❸', '❹', '❺', '❻', '❼', '❽', '❾', '⓿'];
|
|
pub const REPLY_NUMBERS: [char; 10] = ['①', '②', '③', '④', '⑤', '⑥', '⑦', '⑧', '⑨', '⑩'];
|
|
|
|
pub struct HudPlugin;
|
|
impl Plugin for HudPlugin {
|
|
fn build(&self, app: &mut App) {
|
|
app.add_systems(Startup, setup);
|
|
app.add_systems(Update, (update, handle_input));
|
|
app.add_systems(PostUpdate, despawn_old_choices);
|
|
app.insert_resource(Log {
|
|
logs: VecDeque::with_capacity(LOG_MAX),
|
|
needs_rerendering: true,
|
|
});
|
|
app.insert_resource(FPSUpdateTimer(
|
|
Timer::from_seconds(HUD_REFRESH_TIME, TimerMode::Repeating)));
|
|
}
|
|
}
|
|
|
|
#[derive(Component)] struct GaugesText;
|
|
#[derive(Component)] struct ChatText;
|
|
|
|
#[derive(Resource)]
|
|
struct FPSUpdateTimer(Timer);
|
|
|
|
#[derive(Component)]
|
|
pub struct ChoiceAvailable {
|
|
pub conv_id: String,
|
|
pub conv_label: String,
|
|
pub recipient: String,
|
|
pub text: String,
|
|
}
|
|
|
|
pub enum LogLevel {
|
|
Warning,
|
|
//Error,
|
|
Info,
|
|
//Debug,
|
|
Chat,
|
|
//Ping,
|
|
}
|
|
|
|
struct Message {
|
|
text: String,
|
|
sender: String,
|
|
level: LogLevel,
|
|
time: u64,
|
|
}
|
|
|
|
#[derive(Resource)]
|
|
pub struct Log {
|
|
logs: VecDeque<Message>,
|
|
needs_rerendering: bool,
|
|
}
|
|
|
|
impl Log {
|
|
pub fn info(&mut self, message: String) {
|
|
self.add(message, "System".to_string(), LogLevel::Info);
|
|
}
|
|
|
|
pub fn chat(&mut self, message: String, sender: String) {
|
|
self.add(message, sender, LogLevel::Chat);
|
|
}
|
|
|
|
pub fn warning(&mut self, message: String) {
|
|
self.add(message, "WARNING".to_string(), LogLevel::Warning);
|
|
}
|
|
|
|
pub fn add(&mut self, message: String, sender: String, level: LogLevel) {
|
|
if self.logs.len() == LOG_MAX {
|
|
self.logs.pop_front();
|
|
}
|
|
if let Ok(epoch) = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) {
|
|
self.logs.push_back(Message {
|
|
text: message,
|
|
sender: sender,
|
|
level: level,
|
|
time: epoch.as_secs(),
|
|
});
|
|
self.needs_rerendering = true;
|
|
}
|
|
}
|
|
|
|
pub fn remove_old(&mut self) {
|
|
if let Some(message) = self.logs.front() {
|
|
if let Ok(epoch) = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) {
|
|
if epoch.as_secs() - message.time > LOG_MAX_TIME_S {
|
|
self.logs.pop_front();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn setup(
|
|
mut commands: Commands,
|
|
settings: Res<settings::Settings>,
|
|
asset_server: Res<AssetServer>,
|
|
mut log: ResMut<Log>,
|
|
mut ambient_light: ResMut<AmbientLight>,
|
|
) {
|
|
log.info("Customer wake-up registered.".to_string());
|
|
log.info("Systems reactivated.".to_string());
|
|
log.warning("Oxygen Low".to_string());
|
|
let visibility = if settings.hud_active {
|
|
Visibility::Inherited
|
|
} else {
|
|
Visibility::Hidden
|
|
};
|
|
let mut bundle_fps = TextBundle::from_sections([
|
|
TextSection::new(
|
|
"帧率 ",
|
|
TextStyle {
|
|
font: asset_server.load(FONT),
|
|
font_size: settings.font_size_hud,
|
|
color: Color::GRAY,
|
|
..default()
|
|
},
|
|
),
|
|
TextSection::new(
|
|
"",
|
|
TextStyle {
|
|
font: asset_server.load(FONT),
|
|
font_size: settings.font_size_hud,
|
|
color: Color::GRAY,
|
|
..default()
|
|
}
|
|
),
|
|
TextSection::new(
|
|
"\n电量 ",
|
|
TextStyle {
|
|
font: asset_server.load(FONT),
|
|
font_size: settings.font_size_hud,
|
|
color: Color::GRAY,
|
|
..default()
|
|
},
|
|
),
|
|
TextSection::new(
|
|
"",
|
|
TextStyle {
|
|
font: asset_server.load(FONT),
|
|
font_size: settings.font_size_hud,
|
|
color: Color::GRAY,
|
|
..default()
|
|
}
|
|
),
|
|
TextSection::new(
|
|
"\n氧 OXYGEN ",
|
|
TextStyle {
|
|
font: asset_server.load(FONT),
|
|
font_size: settings.font_size_hud,
|
|
color: Color::GRAY,
|
|
..default()
|
|
},
|
|
),
|
|
TextSection::new(
|
|
"",
|
|
TextStyle {
|
|
font: asset_server.load(FONT),
|
|
font_size: settings.font_size_hud,
|
|
color: Color::MAROON,
|
|
..default()
|
|
}
|
|
),
|
|
TextSection::new(
|
|
"\nAdren水平 ",
|
|
TextStyle {
|
|
font: asset_server.load(FONT),
|
|
font_size: settings.font_size_hud,
|
|
color: Color::GRAY,
|
|
..default()
|
|
},
|
|
),
|
|
TextSection::new(
|
|
"",
|
|
TextStyle {
|
|
font: asset_server.load(FONT),
|
|
font_size: settings.font_size_hud,
|
|
color: Color::GRAY,
|
|
..default()
|
|
}
|
|
),
|
|
TextSection::new(
|
|
"\n雌激素水平 172pg/mL",
|
|
TextStyle {
|
|
font: asset_server.load(FONT),
|
|
font_size: settings.font_size_hud,
|
|
color: Color::GRAY,
|
|
..default()
|
|
},
|
|
),
|
|
TextSection::new(
|
|
"\nProximity 警告 ",
|
|
TextStyle {
|
|
font: asset_server.load(FONT),
|
|
font_size: settings.font_size_hud,
|
|
color: Color::GRAY,
|
|
..default()
|
|
},
|
|
),
|
|
TextSection::new(
|
|
"",
|
|
TextStyle {
|
|
font: asset_server.load(FONT),
|
|
font_size: settings.font_size_hud,
|
|
color: Color::GRAY,
|
|
..default()
|
|
}
|
|
),
|
|
TextSection::new(
|
|
"\nSuit Integrity ",
|
|
TextStyle {
|
|
font: asset_server.load(FONT),
|
|
font_size: settings.font_size_hud,
|
|
color: Color::GRAY,
|
|
..default()
|
|
},
|
|
),
|
|
TextSection::new(
|
|
"",
|
|
TextStyle {
|
|
font: asset_server.load(FONT),
|
|
font_size: settings.font_size_hud,
|
|
color: Color::GRAY,
|
|
..default()
|
|
}
|
|
),
|
|
TextSection::new(
|
|
"\n\n",
|
|
TextStyle {
|
|
font: asset_server.load(FONT),
|
|
font_size: settings.font_size_hud,
|
|
color: Color::GRAY,
|
|
..default()
|
|
}
|
|
),
|
|
TextSection::from_style(
|
|
TextStyle {
|
|
font: asset_server.load(FONT),
|
|
font_size: settings.font_size_hud,
|
|
color: Color::GRAY,
|
|
..default()
|
|
}
|
|
),
|
|
]).with_style(Style {
|
|
top: Val::VMin(2.0),
|
|
left: Val::VMin(3.0),
|
|
..default()
|
|
});
|
|
bundle_fps.visibility = visibility;
|
|
commands.spawn((
|
|
bundle_fps,
|
|
GaugesText,
|
|
));
|
|
|
|
// Add Chat Box
|
|
let bundle_chatbox = TextBundle::from_sections([
|
|
TextSection::new(
|
|
"Warning: System Log Uninitialized",
|
|
TextStyle {
|
|
font: asset_server.load(FONT),
|
|
font_size: settings.font_size_hud,
|
|
color: Color::GRAY,
|
|
..default()
|
|
}
|
|
),
|
|
TextSection::new(
|
|
"\n",
|
|
TextStyle {
|
|
font: asset_server.load(FONT),
|
|
font_size: settings.font_size_hud,
|
|
color: Color::WHITE,
|
|
..default()
|
|
}
|
|
),
|
|
TextSection::new(
|
|
"<Choices>",
|
|
TextStyle {
|
|
font: asset_server.load(FONT),
|
|
font_size: settings.font_size_hud,
|
|
color: Color::WHITE,
|
|
..default()
|
|
}
|
|
),
|
|
]).with_style(Style {
|
|
position_type: PositionType::Absolute,
|
|
bottom: Val::VMin(0.0),
|
|
left: Val::VMin(0.0),
|
|
//bottom: Val::VMin(40.0),
|
|
//left: Val::VMin(30.0),
|
|
..default()
|
|
}).with_text_justify(JustifyText::Left);
|
|
commands.spawn((
|
|
NodeBundle {
|
|
border_color: { BorderColor(Color::RED) },
|
|
style: Style {
|
|
width: Val::Percent(50.),
|
|
align_items: AlignItems::Start,
|
|
position_type: PositionType::Absolute,
|
|
bottom: Val::VMin(10.0),
|
|
left: Val::VMin(25.0),
|
|
..default()
|
|
},
|
|
..default()
|
|
},
|
|
)).with_children(|parent| {
|
|
parent.spawn((
|
|
bundle_chatbox,
|
|
ChatText,
|
|
));
|
|
});
|
|
|
|
// AR-related things
|
|
ambient_light.brightness = if settings.hud_active {
|
|
AMBIENT_LIGHT_AR
|
|
} else {
|
|
AMBIENT_LIGHT
|
|
};
|
|
}
|
|
|
|
fn update(
|
|
diagnostics: Res<DiagnosticsStore>,
|
|
time: Res<Time>,
|
|
mut log: ResMut<Log>,
|
|
player: Query<(&actor::Suit, &actor::LifeForm), With<actor::Player>>,
|
|
mut timer: ResMut<FPSUpdateTimer>,
|
|
mut query: Query<&mut Text, With<GaugesText>>,
|
|
q_choices: Query<&ChoiceAvailable>,
|
|
mut query_chat: Query<&mut Text, (With<ChatText>, Without<GaugesText>)>,
|
|
query_all_actors: Query<&actor::Actor>,
|
|
settings: Res<settings::Settings>,
|
|
) {
|
|
// TODO only when hud is actually on
|
|
if timer.0.tick(time.delta()).just_finished() || log.needs_rerendering {
|
|
let player = player.get_single();
|
|
if player.is_ok() {
|
|
let (suit, lifeform) = player.unwrap();
|
|
for mut text in &mut query {
|
|
if let Some(fps) = diagnostics.get(&FrameTimeDiagnosticsPlugin::FPS) {
|
|
if let Some(value) = fps.smoothed() {
|
|
// Update the value of the second section
|
|
text.sections[1].value = format!("{value:.0}");
|
|
}
|
|
}
|
|
let power = suit.power;
|
|
text.sections[3].value = format!("{power:}Wh");
|
|
let oxy_percent = suit.oxygen / suit.oxygen_max * 100.0;
|
|
let oxy_total = suit.oxygen * 1e6;
|
|
text.sections[5].value = format!("{oxy_percent:.1}% [{oxy_total:.0}mg]");
|
|
let adrenaline = lifeform.adrenaline * 990.0 + 10.0;
|
|
text.sections[7].value = format!("{adrenaline:.0}pg/mL");
|
|
let all_actors = query_all_actors.iter().len();
|
|
text.sections[10].value = format!("{all_actors:.0}");
|
|
let integrity = suit.integrity * 100.0;
|
|
text.sections[12].value = format!("{integrity:.0}%");
|
|
}
|
|
}
|
|
|
|
if let Ok(mut chat) = query_chat.get_single_mut() {
|
|
// 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[count];
|
|
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;
|
|
}
|
|
chat.sections[2].value = choices.join("\n");
|
|
|
|
// 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,
|
|
_ => false
|
|
}}
|
|
};
|
|
let logs_vec: Vec<String> = log.logs.iter()
|
|
.filter(logfilter)
|
|
.map(|s| format!("{}: {}", s.sender, s.text)).collect();
|
|
chat.sections[0].value = logs_vec.join("\n");
|
|
}
|
|
log.needs_rerendering = false;
|
|
log.remove_old();
|
|
}
|
|
}
|
|
|
|
fn handle_input(
|
|
keyboard_input: Res<ButtonInput<KeyCode>>,
|
|
mut settings: ResMut<settings::Settings>,
|
|
mut query: Query<&mut Visibility, With<GaugesText>>,
|
|
mut query_bloomsettings: Query<&mut BloomSettings>,
|
|
mut evwriter_sendmsg: EventWriter<actor::SendMessageEvent>,
|
|
mut evwriter_sfx: EventWriter<audio::PlaySfxEvent>,
|
|
mut evwriter_togglemusic: EventWriter<audio::ToggleMusicEvent>,
|
|
q_choices: Query<&ChoiceAvailable>,
|
|
mut ambient_light: ResMut<AmbientLight>,
|
|
) {
|
|
if keyboard_input.just_pressed(settings.key_togglehud) {
|
|
if let Ok(mut vis) = query.get_single_mut() {
|
|
if let Ok(mut bloomsettings) = query_bloomsettings.get_single_mut() {
|
|
if *vis == Visibility::Inherited {
|
|
*vis = Visibility::Hidden;
|
|
settings.hud_active = false;
|
|
ambient_light.brightness = AMBIENT_LIGHT;
|
|
bloomsettings.composite_mode = BloomCompositeMode::EnergyConserving;
|
|
} else {
|
|
*vis = Visibility::Inherited;
|
|
settings.hud_active = true;
|
|
ambient_light.brightness = AMBIENT_LIGHT_AR;
|
|
bloomsettings.composite_mode = BloomCompositeMode::EnergyConserving;
|
|
//bloomsettings.composite_mode = BloomCompositeMode::Additive;
|
|
}
|
|
evwriter_sfx.send(audio::PlaySfxEvent(audio::Sfx::Switch));
|
|
evwriter_togglemusic.send(audio::ToggleMusicEvent());
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut selected_choice = 1;
|
|
'outer: for key in settings.get_reply_keys() {
|
|
if keyboard_input.just_pressed(key) {
|
|
let mut count = 1;
|
|
for choice in &q_choices {
|
|
if count == selected_choice {
|
|
evwriter_sfx.send(audio::PlaySfxEvent(audio::Sfx::Click));
|
|
evwriter_sendmsg.send(actor::SendMessageEvent {
|
|
conv_id: choice.conv_id.clone(),
|
|
conv_label: choice.conv_label.clone(),
|
|
text: choice.text.clone(),
|
|
});
|
|
break 'outer;
|
|
}
|
|
count += 1;
|
|
}
|
|
}
|
|
selected_choice += 1;
|
|
}
|
|
}
|
|
|
|
fn despawn_old_choices(
|
|
mut commands: Commands,
|
|
q_conv: Query<&actor::Chat>,
|
|
q_choices: Query<(Entity, &ChoiceAvailable)>,
|
|
) {
|
|
let chats: Vec<&actor::Chat> = q_conv.iter().collect();
|
|
'outer: for (entity, choice) in &q_choices {
|
|
// Let's see if this choice still has a chat in the appropriate state
|
|
for chat in &chats {
|
|
if choice.conv_id == chat.id && choice.conv_label == chat.label {
|
|
continue 'outer;
|
|
}
|
|
}
|
|
|
|
// Despawn the choice, since no matching chat was found
|
|
commands.entity(entity).despawn();
|
|
}
|
|
}
|