outfly/src/hud.rs
2024-04-05 19:18:49 +02:00

614 lines
20 KiB
Rust

use crate::{actor, audio, camera, chat, nature, settings};
use bevy::prelude::*;
use bevy::diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin};
use bevy_xpbd_3d::prelude::*;
use bevy::math::DVec3;
use std::collections::VecDeque;
use std::time::SystemTime;
pub const HUD_REFRESH_TIME: f32 = 0.5;
pub const FONT: &str = "fonts/Yupiter-Regular.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 = 15.0;
//pub const REPLY_NUMBERS: [char; 10] = ['❶', '❷', '❸', '❹', '❺', '❻', '❼', '❽', '❾', '⓿'];
//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.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(Component)] struct Reticule;
#[derive(Component)] struct ToggleableHudElement;
#[derive(Component)] pub struct IsClickable;
#[derive(Component)] pub struct IsTargeted;
#[derive(Resource)]
struct FPSUpdateTimer(Timer);
pub enum LogLevel {
Warning,
//Error,
Info,
//Debug,
Chat,
//Ping,
Notice,
}
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 notice(&mut self, message: String) {
self.add(message, "".to_string(), LogLevel::Notice);
}
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();
}
}
}
}
pub fn clear(&mut self) {
self.logs.clear();
}
}
fn setup(
mut commands: Commands,
settings: Res<settings::Settings>,
asset_server: Res<AssetServer>,
mut log: ResMut<Log>,
mut ambient_light: ResMut<AmbientLight>,
) {
log.notice("Resuming from suspend".to_string());
log.notice("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(
"OutFly Augmented Reality 电量 ",
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(
"",
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(
" 帧率 ",
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(
"\nAdrenaline水平 ",
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(
"\nVitals ",
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(
"\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相对的v ",
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(
"\nTarget: ",
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☢ HAZARD\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((
GaugesText,
ToggleableHudElement,
bundle_fps,
));
// 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::rgb(0.7, 0.7, 0.7),
..default()
}
),
TextSection::new(
"\n",
TextStyle {
font: asset_server.load(FONT),
font_size: settings.font_size_hud,
color: Color::WHITE,
..default()
}
),
TextSection::new(
"\n\n\n",
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),
..default()
}).with_text_justify(JustifyText::Left);
commands.spawn((
NodeBundle {
style: Style {
width: Val::Percent(50.),
align_items: AlignItems::Start,
position_type: PositionType::Absolute,
bottom: Val::Vh(10.0),
left: Val::Vw(25.0),
..default()
},
..default()
},
)).with_children(|parent| {
parent.spawn((
bundle_chatbox,
ChatText,
));
});
commands.spawn((
Reticule,
ToggleableHudElement,
NodeBundle {
style: Style {
width: Val::Px(2.0),
height: Val::Px(2.0),
position_type: PositionType::Absolute,
top: Val::Vh(50.0),
left: Val::Vw(50.0),
..default()
},
visibility: visibility,
background_color: Color::rgb(0.4, 0.4, 0.6).into(),
..default()
},
));
// 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::HitPoints, &actor::Suit, &actor::LifeForm), With<actor::Player>>,
q_camera: Query<(&Position, &LinearVelocity), With<actor::PlayerCamera>>,
mut timer: ResMut<FPSUpdateTimer>,
mut query: Query<&mut Text, With<GaugesText>>,
q_choices: Query<&chat::ChoiceAvailable>,
mut query_chat: Query<&mut Text, (With<ChatText>, Without<GaugesText>)>,
query_all_actors: Query<&actor::Actor>,
settings: Res<settings::Settings>,
q_target: Query<(&Position, &actor::Actor), With<IsTargeted>>,
) {
// TODO only when hud is actually on
if timer.0.tick(time.delta()).just_finished() || log.needs_rerendering {
let q_camera_result = q_camera.get_single();
let player = player.get_single();
if player.is_ok() && q_camera_result.is_ok() {
let (hp, suit, lifeform) = player.unwrap();
let (pos, cam_v) = q_camera_result.unwrap();
for mut text in &mut query {
text.sections[3].value = format!("2524-03-12 03:02");
if let Some(fps) = diagnostics.get(&FrameTimeDiagnosticsPlugin::FPS) {
if let Some(value) = fps.smoothed() {
// Update the value of the second section
text.sections[5].value = format!("{value:.0}");
}
}
let power = suit.power / suit.power_max * 100.0;
text.sections[1].value = format!("{power:}%");
let oxy_percent = suit.oxygen / suit.oxygen_max * 100.0;
let oxy_total = suit.oxygen * 1e6;
// the remaining oxygen hud info ignores leaking suits from low integrity
if suit.oxygen > nature::OXY_H {
let oxy_hour = suit.oxygen / nature::OXY_H;
text.sections[9].value = format!("{oxy_percent:.1}% [{oxy_total:.0}mg] [lasts {oxy_hour:.1} hours]");
} else {
let oxy_min = suit.oxygen / nature::OXY_M;
text.sections[9].value = format!("{oxy_percent:.1}% [{oxy_total:.0}mg] [lasts {oxy_min:.1} min]");
}
let adrenaline = lifeform.adrenaline * 990.0 + 10.0;
text.sections[11].value = format!("{adrenaline:.0}pg/mL");
let vitals = 100.0 * hp.current / hp.max;
text.sections[13].value = format!("{vitals:.0}%");
let all_actors = query_all_actors.iter().len();
text.sections[15].value = format!("{all_actors:.0}");
let integrity = suit.integrity * 100.0;
text.sections[17].value = format!("{integrity:.0}%");
let speed = cam_v.length();
let kmh = speed * 60.0 * 60.0 / 1000.0;
text.sections[19].value = format!("{speed:.0}m/s | {kmh:.0}km/h");
// Target display
let target: Option<DVec3>;
if let Ok((targetpos, actr)) = q_target.get_single() {
target = Some(targetpos.0);
}
else if q_target.is_empty() {
target = Some(DVec3::new(0.0, 0.0, 0.0));
}
else {
target = None;
}
if let Some(target_pos) = target {
let dist = pos.0 - target_pos;
let (x, y, z) = (dist.x, dist.y, dist.z);
text.sections[7].value = format!("{x:.0}m / {z:.0}m / {y:.0}m");
}
else {
text.sections[7].value = format!("ERROR: MULTIPLE TARGETS");
}
if q_target.is_empty() {
text.sections[21].value = "Jupiter".to_string();
}
else {
let targets: Vec<String> = q_target
.iter()
.map(|(_pos, act)| act.name.clone().unwrap_or("<unnamed>".to_string()))
.collect();
text.sections[21].value = targets.join(", ");
}
}
}
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;
}
if count < 4 {
for _padding in 0..(4-count) {
choices.push(" ".to_string());
}
}
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,
LogLevel::Info => true,
_ => false
}}
};
let logs_vec: Vec<String> = log.logs.iter()
.filter(logfilter)
.map(|s| if s.sender.is_empty() {
format!("{}", s.text)
} else {
format!("{}: {}", s.sender, s.text)
}).collect();
chat.sections[0].value = logs_vec.join("\n");
}
log.needs_rerendering = false;
log.remove_old();
}
}
fn handle_input(
mut commands: Commands,
keyboard_input: Res<ButtonInput<KeyCode>>,
mouse_input: Res<ButtonInput<MouseButton>>,
mut settings: ResMut<settings::Settings>,
mut q_hud: Query<&mut Visibility, With<ToggleableHudElement>>,
mut ew_sfx: EventWriter<audio::PlaySfxEvent>,
mut ew_togglemusic: EventWriter<audio::ToggleMusicEvent>,
mut ambient_light: ResMut<AmbientLight>,
q_objects: Query<(Entity, &Transform), (With<IsClickable>, Without<IsTargeted>)>,
q_target: Query<Entity, With<IsTargeted>>,
q_camera: Query<&Transform, With<Camera>>,
) {
if keyboard_input.just_pressed(settings.key_togglehud) {
if settings.hud_active {
for mut hudelement_visibility in q_hud.iter_mut() {
*hudelement_visibility = Visibility::Hidden;
}
settings.hud_active = false;
ambient_light.brightness = AMBIENT_LIGHT;
}
else {
for mut hudelement_visibility in q_hud.iter_mut() {
*hudelement_visibility = Visibility::Inherited;
}
settings.hud_active = true;
ambient_light.brightness = AMBIENT_LIGHT_AR;
}
ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::Switch));
ew_togglemusic.send(audio::ToggleMusicEvent());
}
if mouse_input.just_pressed(settings.key_selectobject) {
//ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::Switch));
if q_target.is_empty() {
if let (Some(entity), _dist) = camera::find_closest_target(q_objects, q_camera) {
commands.entity(entity).insert(IsTargeted);
}
}
else {
for entity in &q_target {
commands.entity(entity).remove::<IsTargeted>();
}
}
}
}