1342 lines
44 KiB
Rust
1342 lines
44 KiB
Rust
// ▄████████▄ + ███ + ▄█████████ ███ +
|
|
// ███▀ ▀███ + + ███ ███▀ + ███ + +
|
|
// ███ + ███ ███ ███ █████████ ███ ███ ███ ███
|
|
// ███ +███ ███ ███ ███ ███▐██████ ███ ███ ███
|
|
// ███ + ███ ███+ ███ +███ ███ + ███ ███ + ███
|
|
// ███▄ ▄███ ███▄ ███ ███ + ███ + ███ ███▄ ███
|
|
// ▀████████▀ + ▀███████ ███▄ ███▄ ▀████ ▀███████
|
|
// + + + ███
|
|
// + ▀████████████████████████████████████████████████████▀
|
|
//
|
|
// This module manages the heads-up display and augmented reality overlays.
|
|
|
|
use crate::prelude::*;
|
|
use bevy::diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin};
|
|
use bevy::pbr::{NotShadowCaster, NotShadowReceiver};
|
|
use bevy::prelude::*;
|
|
use bevy::scene::SceneInstance;
|
|
use bevy::transform::TransformSystem;
|
|
use bevy_xpbd_3d::prelude::*;
|
|
use std::collections::VecDeque;
|
|
use std::time::SystemTime;
|
|
|
|
pub const DASHBOARD_ICON_SIZE: f32 = 64.0;
|
|
pub const HUD_REFRESH_TIME: f32 = 0.1;
|
|
pub const LOG_MAX_TIME_S: f64 = 30.0;
|
|
pub const LOG_MAX_ROWS: usize = 30;
|
|
pub const LOG_MAX: usize = LOG_MAX_ROWS;
|
|
pub const MAX_CHOICES: usize = 10;
|
|
pub const SPEEDOMETER_WIDTH: f32 = 20.0;
|
|
pub const SPEEDOMETER_HEIGHT: f32 = 10.0;
|
|
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 const REPLY_NUMBERS: [char; 10] = ['➀', '➁', '➂', '➃', '➄', '➅', '➆', '➇', '➈', '➉'];
|
|
|
|
pub const DASHBOARD_DEF: &[(Dashboard, &str)] = &[
|
|
(Dashboard::Flashlight, "highbeams"),
|
|
(Dashboard::Leak, "leak"),
|
|
(Dashboard::RotationStabiliser, "rotation_stabiliser"),
|
|
(Dashboard::CruiseControl, "cruise_control"),
|
|
(Dashboard::Radioactivity, "radioactivity"),
|
|
];
|
|
|
|
// Player avatars: [(Avatar, model name, scale, in-game name)]
|
|
pub const PLAYER_AR_AVATARS: &[(Avatar, &str, f32, &str)] = &[
|
|
(Avatar::None, "", 1.0, "No Avatar"),
|
|
(Avatar::ChefHat, "suit_ar_chefhat", 1.0, "Chef Hat"),
|
|
(Avatar::Wings, "suit_ar_wings", 1.0, "Wings"),
|
|
(Avatar::Asteroid, "asteroid2", 1.2, "Asteroid"),
|
|
];
|
|
|
|
pub struct HudPlugin;
|
|
impl Plugin for HudPlugin {
|
|
fn build(&self, app: &mut App) {
|
|
app.add_systems(Startup, setup);
|
|
app.add_systems(
|
|
Update,
|
|
(
|
|
update_hud,
|
|
update_dashboard,
|
|
update_speedometer,
|
|
update_gauges,
|
|
handle_input.run_if(in_control),
|
|
handle_target_event,
|
|
),
|
|
);
|
|
app.add_systems(
|
|
PostUpdate,
|
|
(
|
|
update_overlay_visibility,
|
|
update_avatar.run_if(on_event::<UpdateAvatarEvent>()),
|
|
update_ar_overlays
|
|
.after(camera::position_to_transform)
|
|
.in_set(sync::SyncSet::PositionToTransform),
|
|
update_poi_overlays
|
|
.after(camera::position_to_transform)
|
|
.in_set(sync::SyncSet::PositionToTransform),
|
|
update_target_selectagon
|
|
.after(PhysicsSet::Sync)
|
|
.after(camera::apply_input_to_player)
|
|
.before(TransformSystem::TransformPropagate),
|
|
),
|
|
);
|
|
app.insert_resource(AugmentedRealityState {
|
|
overlays_visible: false,
|
|
});
|
|
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,
|
|
)));
|
|
app.add_event::<TargetEvent>();
|
|
app.add_event::<UpdateAvatarEvent>();
|
|
app.add_event::<UpdateOverlayVisibility>();
|
|
}
|
|
}
|
|
|
|
#[derive(Event)]
|
|
pub struct TargetEvent(pub Option<Entity>);
|
|
#[derive(Event)]
|
|
pub struct UpdateOverlayVisibility;
|
|
#[derive(Event)]
|
|
pub struct UpdateAvatarEvent;
|
|
#[derive(Component)]
|
|
struct NodeHud;
|
|
#[derive(Component)]
|
|
struct NodeConsole;
|
|
#[derive(Component)]
|
|
struct NodeChoiceText;
|
|
#[derive(Component)]
|
|
struct NodeSpeedometerText;
|
|
#[derive(Component)]
|
|
struct NodeCurrentChatLine;
|
|
#[derive(Component)]
|
|
struct Reticule;
|
|
#[derive(Component)]
|
|
struct Speedometer;
|
|
#[derive(Component)]
|
|
struct Speedometer2;
|
|
#[derive(Component)]
|
|
struct GaugeLength(f32);
|
|
#[derive(Component)]
|
|
pub struct ToggleableHudElement;
|
|
#[derive(Component)]
|
|
pub struct ToggleableHudMapElement;
|
|
#[derive(Component)]
|
|
struct Selectagon;
|
|
#[derive(Component)]
|
|
struct PlayerAvatar;
|
|
#[derive(Component)]
|
|
pub struct IsTargeted;
|
|
#[derive(Component)]
|
|
pub struct PointOfInterestMarker(pub Entity);
|
|
|
|
#[derive(Component, Debug, Copy, Clone)]
|
|
pub enum Dashboard {
|
|
Leak,
|
|
Flashlight,
|
|
RotationStabiliser,
|
|
CruiseControl,
|
|
Radioactivity,
|
|
}
|
|
|
|
#[derive(Component, Debug, Copy, Clone)]
|
|
enum Gauge {
|
|
Health,
|
|
Power,
|
|
Oxygen,
|
|
//Integrity,
|
|
}
|
|
|
|
#[derive(Resource)]
|
|
pub struct AugmentedRealityState {
|
|
pub overlays_visible: bool,
|
|
}
|
|
|
|
#[derive(Component)]
|
|
pub struct AugmentedRealityOverlayBroadcaster;
|
|
#[derive(Component)]
|
|
pub struct AugmentedRealityOverlay {
|
|
pub owner: Entity,
|
|
pub scale: f32,
|
|
}
|
|
|
|
#[derive(Resource)]
|
|
struct FPSUpdateTimer(Timer);
|
|
|
|
pub enum Avatar {
|
|
None,
|
|
ChefHat,
|
|
Wings,
|
|
Asteroid,
|
|
}
|
|
|
|
pub enum LogLevel {
|
|
Achievement,
|
|
Always,
|
|
Warning,
|
|
//Error,
|
|
Info,
|
|
//Debug,
|
|
Chat,
|
|
//Ping,
|
|
}
|
|
|
|
struct Message {
|
|
text: String,
|
|
sender: String,
|
|
level: LogLevel,
|
|
time: f64,
|
|
}
|
|
impl Message {
|
|
pub fn get_freshness(&self) -> f64 {
|
|
if let Ok(epoch) = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) {
|
|
return (1.0 - (epoch.as_secs_f64() - self.time) / LOG_MAX_TIME_S).clamp(0.0, 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)]
|
|
pub struct IsClickable {
|
|
pub name: Option<String>,
|
|
pub pronoun: Option<String>,
|
|
pub distance: Option<f64>,
|
|
}
|
|
impl Default for IsClickable {
|
|
fn default() -> Self {
|
|
Self {
|
|
name: None,
|
|
pronoun: None,
|
|
distance: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[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, text: 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,
|
|
sender,
|
|
level,
|
|
time: epoch.as_secs_f64(),
|
|
});
|
|
self.needs_rerendering = true;
|
|
}
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
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_f64() - message.time > LOG_MAX_TIME_S {
|
|
self.logs.pop_front();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn clear(&mut self) {
|
|
self.logs.clear();
|
|
}
|
|
}
|
|
|
|
pub fn setup(
|
|
mut commands: Commands,
|
|
settings: Res<Settings>,
|
|
asset_server: Res<AssetServer>,
|
|
mut ew_updateoverlays: EventWriter<UpdateOverlayVisibility>,
|
|
) {
|
|
let visibility = if settings.hud_active {
|
|
Visibility::Inherited
|
|
} else {
|
|
Visibility::Hidden
|
|
};
|
|
let font_handle = asset_server.load(FONT);
|
|
let style_conversations = TextStyle {
|
|
font: font_handle.clone(),
|
|
font_size: settings.font_size_conversations,
|
|
color: settings.hud_color_subtitles,
|
|
..default()
|
|
};
|
|
let style_fps = TextStyle {
|
|
font: font_handle.clone(),
|
|
font_size: settings.font_size_fps,
|
|
color: settings.hud_color_fps,
|
|
..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_speedometer = TextStyle {
|
|
font: font_handle.clone(),
|
|
font_size: settings.font_size_speedometer,
|
|
color: settings.hud_color_speedometer,
|
|
..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([
|
|
TextSection::new("", style), // Target
|
|
TextSection::new("", style_fps), // Frames per second
|
|
])
|
|
.with_style(Style {
|
|
position_type: PositionType::Absolute,
|
|
top: Val::VMin(2.0),
|
|
left: Val::VMin(3.0),
|
|
..default()
|
|
})
|
|
.with_text_justify(JustifyText::Left);
|
|
bundle_fps.visibility = visibility;
|
|
commands.spawn((NodeHud, ToggleableHudElement, bundle_fps));
|
|
|
|
// Add Console
|
|
// This one is intentionally NOT a ToggleableHudElement. Instead, console entries
|
|
// are filtered based on whether the hud is active or not. LogLevel::Always is
|
|
// even shown when hud is inactive.
|
|
let bundle_chatbox = TextBundle::from_sections(
|
|
(0..LOG_MAX_ROWS).map(|_| TextSection::new("", style_console.clone())),
|
|
)
|
|
.with_style(Style {
|
|
position_type: PositionType::Absolute,
|
|
top: Val::VMin(0.0),
|
|
right: Val::VMin(0.0),
|
|
..default()
|
|
})
|
|
.with_text_justify(JustifyText::Right);
|
|
commands
|
|
.spawn((NodeBundle {
|
|
style: Style {
|
|
width: Val::Percent(50.0),
|
|
align_items: AlignItems::Start,
|
|
position_type: PositionType::Absolute,
|
|
top: Val::VMin(2.0),
|
|
right: Val::VMin(3.0),
|
|
..default()
|
|
},
|
|
..default()
|
|
},))
|
|
.with_children(|parent| {
|
|
parent.spawn((bundle_chatbox, NodeConsole));
|
|
});
|
|
|
|
// Add Reticule
|
|
let reticule_handle: Handle<Image> = asset_server.load("sprites/reticule4.png");
|
|
commands
|
|
.spawn((
|
|
NodeBundle {
|
|
style: style_centered(),
|
|
visibility,
|
|
..default()
|
|
},
|
|
ToggleableHudElement,
|
|
))
|
|
.with_children(|builder| {
|
|
builder.spawn((
|
|
ImageBundle {
|
|
image: UiImage::new(reticule_handle),
|
|
style: Style {
|
|
width: Val::VMin(5.0),
|
|
height: Val::VMin(5.0),
|
|
..Default::default()
|
|
},
|
|
..Default::default()
|
|
},
|
|
Reticule,
|
|
));
|
|
});
|
|
|
|
// HP/O2/Suit Integrity/Power Gauges
|
|
let gauges_handle: Handle<Image> = asset_server.load("sprites/gauge_horizontal.png");
|
|
let gauges = [
|
|
("sprites/gauge_heart.png", Gauge::Health),
|
|
("sprites/gauge_battery.png", Gauge::Power),
|
|
("sprites/gauge_o2.png", Gauge::Oxygen),
|
|
//("sprites/gauge_suit.png", Gauge::Integrity),
|
|
];
|
|
let icon_size = 24.0;
|
|
let gauge_bar_padding_left = 4.0;
|
|
for (i, (sprite, gauge)) in gauges.iter().enumerate() {
|
|
let bar_length = if i == 0 { 32.0 * 8.0 } else { 32.0 * 5.0 };
|
|
|
|
// The bar with variable width
|
|
commands
|
|
.spawn((
|
|
NodeBundle {
|
|
style: Style {
|
|
width: Val::Percent(30.0),
|
|
height: Val::Percent(100.0),
|
|
bottom: Val::Px(20.0 + 24.0 * i as f32),
|
|
left: Val::VMin(2.0),
|
|
align_items: AlignItems::End,
|
|
overflow: Overflow::clip(),
|
|
..default()
|
|
},
|
|
visibility,
|
|
..default()
|
|
},
|
|
ToggleableHudElement,
|
|
))
|
|
.with_children(|builder| {
|
|
builder.spawn((
|
|
NodeBundle {
|
|
style: Style {
|
|
width: Val::Px(118.0),
|
|
height: Val::Px(10.0),
|
|
bottom: Val::Px(8.0),
|
|
left: Val::Px(gauge_bar_padding_left + icon_size),
|
|
..Default::default()
|
|
},
|
|
visibility,
|
|
background_color: settings.hud_color.into(),
|
|
..Default::default()
|
|
},
|
|
gauge.clone(),
|
|
GaugeLength(bar_length),
|
|
ToggleableHudElement,
|
|
));
|
|
});
|
|
|
|
// The decorator sprites surrounding the bar
|
|
commands
|
|
.spawn((
|
|
NodeBundle {
|
|
style: Style {
|
|
width: Val::Percent(30.0),
|
|
height: Val::Percent(100.0),
|
|
bottom: Val::Px(20.0 + 24.0 * i as f32),
|
|
left: Val::VMin(2.0),
|
|
align_items: AlignItems::End,
|
|
overflow: Overflow::clip(),
|
|
..default()
|
|
},
|
|
visibility,
|
|
..default()
|
|
},
|
|
ToggleableHudElement,
|
|
))
|
|
.with_children(|builder| {
|
|
// The gauge symbol
|
|
builder.spawn((
|
|
ImageBundle {
|
|
image: UiImage::new(asset_server.load(sprite.to_string())),
|
|
style: Style {
|
|
width: Val::Px(icon_size),
|
|
height: Val::Px(icon_size),
|
|
..Default::default()
|
|
},
|
|
visibility,
|
|
..Default::default()
|
|
},
|
|
ToggleableHudElement,
|
|
));
|
|
// The gauge bar border
|
|
builder.spawn((
|
|
ImageBundle {
|
|
image: UiImage::new(gauges_handle.clone()),
|
|
style: Style {
|
|
width: Val::Px(bar_length),
|
|
height: Val::Px(10.0),
|
|
bottom: Val::Px(8.0),
|
|
left: Val::Px(gauge_bar_padding_left),
|
|
..Default::default()
|
|
},
|
|
visibility,
|
|
..Default::default()
|
|
},
|
|
ToggleableHudElement,
|
|
));
|
|
});
|
|
}
|
|
|
|
// Car-Dashboard-Style icons
|
|
let style_dashboard = Style {
|
|
width: Val::Px(DASHBOARD_ICON_SIZE),
|
|
height: Val::Px(DASHBOARD_ICON_SIZE),
|
|
..Default::default()
|
|
};
|
|
commands
|
|
.spawn((
|
|
NodeBundle {
|
|
style: Style {
|
|
width: Val::Percent(30.0),
|
|
height: Val::Percent(100.0),
|
|
bottom: Val::Px(40.0 + icon_size * gauges.len() as f32),
|
|
left: Val::VMin(4.0),
|
|
align_items: AlignItems::End,
|
|
overflow: Overflow::clip(),
|
|
..default()
|
|
},
|
|
visibility,
|
|
..default()
|
|
},
|
|
ToggleableHudElement,
|
|
))
|
|
.with_children(|builder| {
|
|
for (component, filename) in DASHBOARD_DEF {
|
|
builder.spawn((
|
|
*component,
|
|
ImageBundle {
|
|
image: UiImage::new(
|
|
asset_server.load(format!("sprites/dashboard_{}.png", filename)),
|
|
),
|
|
style: style_dashboard.clone(),
|
|
visibility: Visibility::Hidden,
|
|
..Default::default()
|
|
},
|
|
));
|
|
}
|
|
});
|
|
|
|
// Add Speedometer
|
|
let speedometer_handle: Handle<Image> = asset_server.load("sprites/speedometer.png");
|
|
commands
|
|
.spawn((
|
|
NodeBundle {
|
|
style: Style {
|
|
width: Val::VMin(0.0),
|
|
height: Val::Percent(100.0),
|
|
left: Val::Vw(100.0 - SPEEDOMETER_WIDTH),
|
|
align_items: AlignItems::End,
|
|
overflow: Overflow::clip(),
|
|
..default()
|
|
},
|
|
visibility,
|
|
..default()
|
|
},
|
|
Speedometer,
|
|
ToggleableHudElement,
|
|
))
|
|
.with_children(|builder| {
|
|
builder.spawn((ImageBundle {
|
|
image: UiImage::new(speedometer_handle),
|
|
style: Style {
|
|
width: Val::Vw(SPEEDOMETER_WIDTH),
|
|
height: Val::VMin(SPEEDOMETER_HEIGHT),
|
|
..Default::default()
|
|
},
|
|
..Default::default()
|
|
},));
|
|
});
|
|
let speedometer_handle: Handle<Image> = asset_server.load("sprites/speedometer_white.png");
|
|
commands
|
|
.spawn((
|
|
NodeBundle {
|
|
style: Style {
|
|
width: Val::VMin(0.0),
|
|
height: Val::Percent(100.0),
|
|
left: Val::Vw(100.0 - SPEEDOMETER_WIDTH),
|
|
align_items: AlignItems::End,
|
|
overflow: Overflow::clip(),
|
|
..default()
|
|
},
|
|
visibility,
|
|
..default()
|
|
},
|
|
Speedometer2,
|
|
ToggleableHudElement,
|
|
))
|
|
.with_children(|builder| {
|
|
builder.spawn((ImageBundle {
|
|
image: UiImage::new(speedometer_handle),
|
|
style: Style {
|
|
width: Val::Vw(SPEEDOMETER_WIDTH),
|
|
height: Val::VMin(SPEEDOMETER_HEIGHT),
|
|
..Default::default()
|
|
},
|
|
..Default::default()
|
|
},));
|
|
});
|
|
let mut bundle_speedometer_text = TextBundle::from_sections([
|
|
TextSection::new("", style_speedometer.clone()), // speed relative to target
|
|
TextSection::new("", style_speedometer.clone()), // speed relative to target
|
|
TextSection::new("", style_speedometer.clone()), // speed relative to orbit
|
|
])
|
|
.with_style(Style {
|
|
position_type: PositionType::Absolute,
|
|
left: Val::Vw(100.0 - SPEEDOMETER_WIDTH + 2.0),
|
|
bottom: Val::VMin(4.0),
|
|
..default()
|
|
})
|
|
.with_text_justify(JustifyText::Left);
|
|
bundle_speedometer_text.visibility = visibility;
|
|
commands.spawn((
|
|
NodeSpeedometerText,
|
|
ToggleableHudElement,
|
|
bundle_speedometer_text,
|
|
));
|
|
|
|
// 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
|
|
let mut entitycmd = commands.spawn((
|
|
Selectagon,
|
|
NotShadowCaster,
|
|
NotShadowReceiver,
|
|
SpatialBundle {
|
|
visibility: Visibility::Hidden,
|
|
..default()
|
|
},
|
|
));
|
|
load_asset("selectagon", &mut entitycmd, &*asset_server);
|
|
|
|
ew_updateoverlays.send(UpdateOverlayVisibility);
|
|
}
|
|
|
|
fn update_dashboard(
|
|
timer: ResMut<FPSUpdateTimer>,
|
|
mut q_dashboard: Query<(&mut Visibility, &Dashboard)>,
|
|
id2pos: Res<game::Id2Pos>,
|
|
q_player: Query<(&actor::Suit, &Position), With<actor::Player>>,
|
|
settings: Res<Settings>,
|
|
) {
|
|
if !settings.hud_active || !timer.0.just_finished() {
|
|
return;
|
|
}
|
|
let player = q_player.get_single();
|
|
if player.is_err() {
|
|
return;
|
|
}
|
|
let (suit, pos) = player.unwrap();
|
|
|
|
for (mut vis, icon) in &mut q_dashboard {
|
|
*vis = bool2vis(match icon {
|
|
Dashboard::Flashlight => settings.flashlight_active,
|
|
Dashboard::Leak => suit.integrity < 0.5,
|
|
Dashboard::RotationStabiliser => !settings.rotation_stabilizer_active,
|
|
Dashboard::CruiseControl => settings.cruise_control_active,
|
|
Dashboard::Radioactivity => {
|
|
if let Some(pos_jupiter) = id2pos.0.get(cmd::ID_JUPITER) {
|
|
pos_jupiter.distance(pos.0) < 140_000_000.0
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
fn update_speedometer(
|
|
timer: ResMut<FPSUpdateTimer>,
|
|
settings: Res<Settings>,
|
|
jupiter_pos: Res<game::JupiterPos>,
|
|
q_camera: Query<(&LinearVelocity, &Position), With<actor::PlayerCamera>>,
|
|
q_player: Query<&actor::ExperiencesGForce, With<actor::Player>>,
|
|
q_target: Query<&LinearVelocity, With<IsTargeted>>,
|
|
mut q_speedometer: Query<&mut Style, (With<Speedometer>, Without<Speedometer2>)>,
|
|
mut q_speedometer2: Query<&mut Style, (With<Speedometer2>, Without<Speedometer>)>,
|
|
mut q_node_speed: Query<&mut Text, With<NodeSpeedometerText>>,
|
|
) {
|
|
if !settings.hud_active || !timer.0.just_finished() {
|
|
return;
|
|
}
|
|
if let Ok((cam_v, pos)) = q_camera.get_single() {
|
|
let orbital_v = nature::orbital_velocity(pos.0 - jupiter_pos.0, nature::JUPITER_MASS);
|
|
let speed = (cam_v.0 - orbital_v).length();
|
|
|
|
let speedometer_split = 5_000.0;
|
|
if let Ok(mut speedometer) = q_speedometer.get_single_mut() {
|
|
let custom_c = speedometer_split;
|
|
let fraction = nature::inverse_lorentz_factor_custom_c(
|
|
(custom_c - speed).clamp(0.0, custom_c),
|
|
custom_c,
|
|
)
|
|
.clamp(0.0, 1.0) as f32;
|
|
let wid = (fraction * SPEEDOMETER_WIDTH).clamp(0.0, 100.0);
|
|
speedometer.width = Val::Vw(wid);
|
|
}
|
|
if let Ok(mut speedometer2) = q_speedometer2.get_single_mut() {
|
|
let custom_c = nature::C - speedometer_split;
|
|
let fraction = nature::inverse_lorentz_factor_custom_c(
|
|
(custom_c - speed + speedometer_split).clamp(0.0, custom_c),
|
|
custom_c,
|
|
)
|
|
.clamp(0.0, 1.0) as f32;
|
|
let wid = (fraction * SPEEDOMETER_WIDTH).clamp(0.0, 100.0);
|
|
speedometer2.width = Val::Vw(wid);
|
|
}
|
|
if let Ok(mut speed_text) = q_node_speed.get_single_mut() {
|
|
// G forces
|
|
speed_text.sections[0].value = if let Ok(gforce) = q_player.get_single() {
|
|
if gforce.gforce > 0.0001 {
|
|
format!("{:.1}g\n", gforce.gforce)
|
|
} else {
|
|
"".to_string()
|
|
}
|
|
} else {
|
|
"".to_string()
|
|
};
|
|
|
|
// Velocity relative to target
|
|
speed_text.sections[1].value = if let Ok(target_v) = q_target.get_single() {
|
|
let delta_v = (target_v.0 - cam_v.0).length();
|
|
if delta_v > 0.0001 {
|
|
format!("Δv {}\n", nature::readable_speed(delta_v))
|
|
} else {
|
|
"".to_string()
|
|
}
|
|
} else {
|
|
"".to_string()
|
|
};
|
|
|
|
// "Absolute velocity", or velocity relative to orbit
|
|
speed_text.sections[2].value = if speed > 0.0001 {
|
|
nature::readable_speed(speed)
|
|
} else {
|
|
"".to_string()
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
fn update_gauges(
|
|
timer: ResMut<FPSUpdateTimer>,
|
|
q_player: Query<(&actor::HitPoints, &actor::Suit, &actor::Battery), With<actor::Player>>,
|
|
mut q_gauges: Query<(&mut Style, &mut BackgroundColor, &Gauge, &GaugeLength)>,
|
|
settings: Res<Settings>,
|
|
) {
|
|
if !settings.hud_active || !timer.0.just_finished() {
|
|
return;
|
|
}
|
|
let player = q_player.get_single();
|
|
if player.is_err() {
|
|
return;
|
|
}
|
|
let (hp, suit, battery) = player.unwrap();
|
|
|
|
for (mut style, mut bg, gauge, len) in &mut q_gauges {
|
|
let value: f32 = match gauge {
|
|
Gauge::Health => hp.current / hp.max,
|
|
Gauge::Oxygen => (suit.oxygen / suit.oxygen_max).powf(0.5),
|
|
//Gauge::Integrity => suit.integrity,
|
|
Gauge::Power => battery.power / battery.capacity,
|
|
};
|
|
if value < 0.5 {
|
|
*bg = settings.hud_color_alert.into();
|
|
} else {
|
|
*bg = settings.hud_color.into();
|
|
}
|
|
style.width = Val::Px(len.0 * value);
|
|
}
|
|
}
|
|
|
|
fn update_hud(
|
|
diagnostics: Res<DiagnosticsStore>,
|
|
time: Res<Time>,
|
|
mut log: ResMut<Log>,
|
|
q_camera: Query<(&Position, &LinearVelocity), With<actor::PlayerCamera>>,
|
|
mut timer: ResMut<FPSUpdateTimer>,
|
|
q_choices: Query<&chat::Choice>,
|
|
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>,
|
|
),
|
|
>,
|
|
settings: Res<Settings>,
|
|
q_target: Query<(&IsClickable, Option<&Position>, Option<&LinearVelocity>), With<IsTargeted>>,
|
|
) {
|
|
if timer.0.tick(time.delta()).just_finished() || log.needs_rerendering {
|
|
let q_camera_result = q_camera.get_single();
|
|
let mut freshest_line: f64 = 0.0;
|
|
if settings.hud_active && q_camera_result.is_ok() {
|
|
let (pos, _) = q_camera_result.unwrap();
|
|
for mut text in &mut q_node_hud {
|
|
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}");
|
|
}
|
|
}
|
|
|
|
// Target display
|
|
let dist_scalar: f64;
|
|
let mut target_multiple = false;
|
|
let mut target_error = false;
|
|
if let Ok((
|
|
IsClickable {
|
|
distance: Some(dist),
|
|
..
|
|
},
|
|
_,
|
|
_,
|
|
)) = q_target.get_single()
|
|
{
|
|
dist_scalar = *dist;
|
|
} else {
|
|
let target: Option<DVec3>;
|
|
if let Ok((_, Some(targetpos), _)) = 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 if q_target.iter().len() > 1 {
|
|
target_multiple = true;
|
|
target = None;
|
|
} else {
|
|
target_error = true;
|
|
target = None;
|
|
}
|
|
if let Some(target_pos) = target {
|
|
let dist = pos.0 - target_pos;
|
|
dist_scalar = dist.length();
|
|
} else {
|
|
dist_scalar = 0.0;
|
|
}
|
|
}
|
|
|
|
if target_multiple {
|
|
text.sections[0].value = "ERROR: MULTIPLE TARGETS\n\n".to_string();
|
|
} else if target_error {
|
|
text.sections[0].value = "ERROR: FAILED TO AQUIRE TARGET\n\n".to_string();
|
|
} else if let Ok((clickable, _, _)) = q_target.get_single() {
|
|
let distance = if dist_scalar.is_nan() {
|
|
"UNKNOWN".to_string()
|
|
} else if dist_scalar != 0.0 {
|
|
nature::readable_distance(dist_scalar)
|
|
} else {
|
|
"ERROR".to_string()
|
|
};
|
|
let target_name = clickable.name.clone().unwrap_or("Unnamed".to_string());
|
|
let pronoun = if let Some(pronoun) = &clickable.pronoun {
|
|
format!("Pronoun: {pronoun}\n")
|
|
} else {
|
|
"".to_string()
|
|
};
|
|
text.sections[0].value =
|
|
format!("Target: {target_name}\n{pronoun}Distance: {distance}\n\n");
|
|
} else {
|
|
text.sections[0].value = "".to_string();
|
|
}
|
|
}
|
|
}
|
|
|
|
let chat = q_node_console.get_single_mut();
|
|
if chat.is_err() {
|
|
error!("Couldn't find HUD UI text section");
|
|
return;
|
|
}
|
|
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::Always => true,
|
|
LogLevel::Achievement => true,
|
|
_ => false,
|
|
}
|
|
};
|
|
let messages: Vec<&Message> = log
|
|
.logs
|
|
.iter()
|
|
.filter(logfilter)
|
|
.rev()
|
|
.take(LOG_MAX_ROWS)
|
|
.collect();
|
|
//messages.reverse();
|
|
for msg in &messages {
|
|
chat.sections[row].value = msg.format();
|
|
let freshness = msg.get_freshness();
|
|
let opacity: f32 = (freshness.powf(1.5) as f32).clamp(0.0, 1.0);
|
|
freshest_line = freshest_line.max(freshness);
|
|
chat.sections[row].style.color = match msg.level {
|
|
LogLevel::Achievement => settings.hud_color_console_achievement,
|
|
LogLevel::Warning => settings.hud_color_console_warn,
|
|
LogLevel::Info => settings.hud_color_console_system,
|
|
_ => settings.hud_color_console,
|
|
};
|
|
chat.sections[row].style.color.set_alpha(opacity);
|
|
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,
|
|
LogLevel::Info => 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;
|
|
|
|
//if q_choices.is_empty() && freshest_line < 0.2 {
|
|
log.remove_old();
|
|
//}
|
|
}
|
|
}
|
|
|
|
fn handle_input(
|
|
keyboard_input: Res<ButtonInput<KeyCode>>,
|
|
mouse_input: Res<ButtonInput<MouseButton>>,
|
|
settings: Res<Settings>,
|
|
mut ew_sfx: EventWriter<audio::PlaySfxEvent>,
|
|
mut ew_target: EventWriter<TargetEvent>,
|
|
mut ew_game: EventWriter<GameEvent>,
|
|
q_objects: Query<
|
|
(Entity, &Transform),
|
|
(
|
|
With<IsClickable>,
|
|
Without<IsTargeted>,
|
|
Without<actor::PlayerDrivesThis>,
|
|
Without<actor::Player>,
|
|
),
|
|
>,
|
|
q_camera: Query<&Transform, With<Camera>>,
|
|
) {
|
|
if keyboard_input.just_pressed(settings.key_togglehud) {
|
|
ew_game.send(GameEvent::SetAR(Turn::Toggle));
|
|
ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::Switch));
|
|
}
|
|
if settings.hud_active && mouse_input.just_pressed(settings.key_selectobject) {
|
|
if let Ok(camtrans) = q_camera.get_single() {
|
|
let objects: Vec<(Entity, &Transform)> = q_objects.iter().collect();
|
|
if let (Some(new_target), _dist) =
|
|
camera::find_closest_target::<Entity>(objects, camtrans)
|
|
{
|
|
ew_target.send(TargetEvent(Some(new_target)));
|
|
} else {
|
|
ew_target.send(TargetEvent(None));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn handle_target_event(
|
|
mut commands: Commands,
|
|
settings: Res<Settings>,
|
|
mut er_target: EventReader<TargetEvent>,
|
|
mut ew_sfx: EventWriter<audio::PlaySfxEvent>,
|
|
mut ew_achievement: EventWriter<game::AchievementEvent>,
|
|
q_target: Query<Entity, With<IsTargeted>>,
|
|
q_ids: Query<&actor::Identifier>,
|
|
) {
|
|
let mut play_sfx = false;
|
|
|
|
for TargetEvent(target) in er_target.read() {
|
|
for old_target in &q_target {
|
|
commands.entity(old_target).remove::<IsTargeted>();
|
|
play_sfx = true;
|
|
}
|
|
if let Some(entity) = target {
|
|
// TODO: This can panic if the entity despawns in the meantime
|
|
commands.entity(*entity).insert(IsTargeted);
|
|
play_sfx = true;
|
|
|
|
if let Ok(id) = q_ids.get(*entity) {
|
|
if id.0 == cmd::ID_EARTH {
|
|
ew_achievement.send(game::AchievementEvent::FindEarth);
|
|
}
|
|
}
|
|
}
|
|
if play_sfx && !settings.mute_sfx {
|
|
ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::Click));
|
|
}
|
|
break; // Only accept a single event per frame
|
|
}
|
|
}
|
|
|
|
fn update_target_selectagon(
|
|
settings: Res<Settings>,
|
|
mut q_selectagon: Query<
|
|
(&mut Transform, &mut Visibility),
|
|
(With<Selectagon>, Without<IsTargeted>, Without<Camera>),
|
|
>,
|
|
q_target: Query<&Transform, (With<IsTargeted>, Without<Camera>, Without<Selectagon>)>,
|
|
q_camera: Query<&Transform, (With<Camera>, Without<IsTargeted>, Without<Selectagon>)>,
|
|
) {
|
|
if !settings.hud_active || q_camera.is_empty() {
|
|
return;
|
|
}
|
|
let camera_trans = q_camera.get_single().unwrap();
|
|
|
|
if let Ok((mut selectagon_trans, mut selectagon_vis)) = q_selectagon.get_single_mut() {
|
|
if let Ok(target_trans) = q_target.get_single() {
|
|
match *selectagon_vis {
|
|
Visibility::Hidden => {
|
|
*selectagon_vis = Visibility::Visible;
|
|
}
|
|
_ => {}
|
|
}
|
|
selectagon_trans.translation = target_trans.translation;
|
|
selectagon_trans.scale = target_trans.scale;
|
|
selectagon_trans.look_at(camera_trans.translation, camera_trans.up());
|
|
|
|
// Enlarge Selectagon to a minimum angular diameter
|
|
let (angular_diameter, _, _) =
|
|
camera::calc_angular_diameter(&selectagon_trans, camera_trans);
|
|
let min_angular_diameter = 2.0f32.to_radians();
|
|
if angular_diameter < min_angular_diameter {
|
|
selectagon_trans.scale *= min_angular_diameter / angular_diameter;
|
|
}
|
|
} else {
|
|
match *selectagon_vis {
|
|
Visibility::Hidden => {}
|
|
_ => {
|
|
*selectagon_vis = Visibility::Hidden;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn update_ar_overlays(
|
|
q_owners: Query<
|
|
(Entity, &Transform, &Visibility),
|
|
(
|
|
With<AugmentedRealityOverlayBroadcaster>,
|
|
Without<AugmentedRealityOverlay>,
|
|
),
|
|
>,
|
|
mut q_overlays: Query<(
|
|
&mut Transform,
|
|
&mut Visibility,
|
|
&mut AugmentedRealityOverlay,
|
|
)>,
|
|
settings: ResMut<Settings>,
|
|
mut state: ResMut<AugmentedRealityState>,
|
|
) {
|
|
let (need_activate, need_clean, need_update);
|
|
if settings.hud_active {
|
|
need_activate = !state.overlays_visible;
|
|
need_clean = false;
|
|
} else {
|
|
need_activate = false;
|
|
need_clean = state.overlays_visible;
|
|
}
|
|
need_update = settings.hud_active;
|
|
state.overlays_visible = settings.hud_active;
|
|
|
|
if need_update || need_clean || need_activate {
|
|
for (mut trans, mut vis, ar) in &mut q_overlays {
|
|
for (owner_id, owner_trans, owner_vis) in &q_owners {
|
|
if owner_id == ar.owner {
|
|
*trans = *owner_trans;
|
|
if ar.scale != 1.0 {
|
|
trans.scale *= ar.scale;
|
|
}
|
|
if need_clean {
|
|
*vis = Visibility::Hidden;
|
|
} else {
|
|
*vis = *owner_vis;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn update_poi_overlays(
|
|
mut q_marker: Query<(&mut Transform, &PointOfInterestMarker)>,
|
|
q_parent: Query<&Transform, Without<PointOfInterestMarker>>,
|
|
q_camera: Query<&Transform, (With<Camera>, Without<PointOfInterestMarker>)>,
|
|
settings: ResMut<Settings>,
|
|
) {
|
|
if !settings.hud_active || !settings.map_active || q_camera.is_empty() {
|
|
return;
|
|
}
|
|
let camera_trans = q_camera.get_single().unwrap();
|
|
for (mut trans, marker) in &mut q_marker {
|
|
if let Ok(parent_trans) = q_parent.get(marker.0) {
|
|
// Enlarge POI marker to a minimum angular diameter
|
|
trans.translation = parent_trans.translation;
|
|
trans.scale = Vec3::splat(1.0);
|
|
let (angular_diameter, _, _) = camera::calc_angular_diameter(&trans, camera_trans);
|
|
let min_angular_diameter = 3.0f32.to_radians();
|
|
if angular_diameter < min_angular_diameter {
|
|
trans.scale *= min_angular_diameter / angular_diameter;
|
|
}
|
|
trans.look_at(camera_trans.translation, camera_trans.up());
|
|
}
|
|
}
|
|
}
|
|
|
|
fn update_overlay_visibility(
|
|
mut q_marker: Query<&mut Visibility, With<PointOfInterestMarker>>,
|
|
mut q_hudelement: Query<
|
|
&mut Visibility,
|
|
(With<ToggleableHudElement>, Without<PointOfInterestMarker>),
|
|
>,
|
|
mut q_selectagon: Query<
|
|
&mut Visibility,
|
|
(
|
|
With<Selectagon>,
|
|
Without<ToggleableHudElement>,
|
|
Without<PointOfInterestMarker>,
|
|
),
|
|
>,
|
|
q_target: Query<
|
|
&IsTargeted,
|
|
(
|
|
Without<Camera>,
|
|
Without<Selectagon>,
|
|
Without<PointOfInterestMarker>,
|
|
Without<ToggleableHudElement>,
|
|
),
|
|
>,
|
|
mut ambient_light: ResMut<AmbientLight>,
|
|
er_target: EventReader<UpdateOverlayVisibility>,
|
|
settings: Res<Settings>,
|
|
) {
|
|
if er_target.is_empty() {
|
|
return;
|
|
}
|
|
let check = {
|
|
|check: bool| {
|
|
if check {
|
|
Visibility::Inherited
|
|
} else {
|
|
Visibility::Hidden
|
|
}
|
|
}
|
|
};
|
|
let show_poi = check(settings.hud_active && settings.map_active);
|
|
let show_hud = check(settings.hud_active);
|
|
let show_selectagon = check(settings.hud_active && !q_target.is_empty());
|
|
for mut vis in &mut q_marker {
|
|
*vis = show_poi;
|
|
}
|
|
for mut vis in &mut q_hudelement {
|
|
*vis = show_hud;
|
|
}
|
|
for mut vis in &mut q_selectagon {
|
|
*vis = show_selectagon;
|
|
}
|
|
|
|
ambient_light.brightness = if settings.hud_active {
|
|
AMBIENT_LIGHT_AR
|
|
} else {
|
|
AMBIENT_LIGHT
|
|
};
|
|
}
|
|
|
|
fn update_avatar(
|
|
mut commands: Commands,
|
|
mut settings: ResMut<Settings>,
|
|
mut prefs: ResMut<var::Preferences>,
|
|
asset_server: Res<AssetServer>,
|
|
q_avatar: Query<(Entity, &SceneInstance), With<PlayerAvatar>>,
|
|
q_player: Query<Entity, With<actor::Player>>,
|
|
mut scene_spawner: ResMut<SceneSpawner>,
|
|
) {
|
|
if settings.ar_avatar >= PLAYER_AR_AVATARS.len() {
|
|
settings.ar_avatar = settings.ar_avatar % PLAYER_AR_AVATARS.len();
|
|
}
|
|
prefs.avatar = settings.ar_avatar;
|
|
prefs.save();
|
|
|
|
let ava = if let Some(ava) = PLAYER_AR_AVATARS.get(settings.ar_avatar) {
|
|
ava
|
|
} else {
|
|
error!("Avatar index out of bounds!");
|
|
return;
|
|
};
|
|
let model_name = ava.1;
|
|
let model_scale = ava.2;
|
|
|
|
for (entity, sceneinstance) in &q_avatar {
|
|
commands.entity(entity).despawn();
|
|
scene_spawner.despawn_instance(**sceneinstance);
|
|
}
|
|
|
|
if model_name.is_empty() {
|
|
// No avatar selected.
|
|
return;
|
|
}
|
|
|
|
if let Ok(player_entity) = q_player.get_single() {
|
|
let mut entitycmd = commands.spawn((
|
|
hud::AugmentedRealityOverlay {
|
|
owner: player_entity,
|
|
scale: model_scale,
|
|
},
|
|
world::DespawnOnPlayerDeath,
|
|
PlayerAvatar,
|
|
SpatialBundle {
|
|
visibility: bool2vis(settings.hud_active),
|
|
..default()
|
|
},
|
|
NotShadowCaster,
|
|
NotShadowReceiver,
|
|
));
|
|
load_asset(model_name, &mut entitycmd, &*asset_server);
|
|
}
|
|
}
|