#![feature(stmt_expr_attributes)]
#![allow(
clippy::needless_pass_by_ref_mut )]
#[cfg(all(feature = "be-dyn-lib", feature = "use-dyn-lib"))]
compile_error!("Can't use both \"be-dyn-lib\" and \"use-dyn-lib\" features at once");
mod admin;
mod character_states;
mod experimental_shaders;
mod widgets;
use client::{Client, Join, LendJoin, World, WorldExt};
use common::{
cmd::ServerChatCommand,
comp,
comp::{inventory::item::armor::Friction, Poise, PoiseState},
resources::Time,
};
use core::mem;
use egui::{CollapsingHeader, Color32, Grid, Pos2, ScrollArea, Slider, Ui, Window};
use egui_plot::{Line, Plot, PlotPoints};
use crate::{
admin::draw_admin_commands_window, character_states::draw_char_state_group,
experimental_shaders::draw_experimental_shaders_window, widgets::two_col_row,
};
use common::comp::{
aura::AuraKind::{Buff, ForcePvP, FriendlyFire},
Body, Fluid,
};
use egui_winit_platform::Platform;
use std::time::Duration;
#[cfg(feature = "use-dyn-lib")]
use {
common_dynlib::LoadedLib, lazy_static::lazy_static, std::ffi::CStr, std::sync::Arc,
std::sync::Mutex,
};
#[cfg(feature = "use-dyn-lib")]
lazy_static! {
static ref LIB: Arc<Mutex<Option<LoadedLib>>> =
common_dynlib::init("veloren-voxygen-egui", "egui");
}
#[cfg(feature = "use-dyn-lib")]
const MAINTAIN_EGUI_FN: &[u8] = b"maintain_egui_inner\0";
pub struct SelectedEntityInfo {
entity_id: u32,
debug_shape_id: Option<u64>,
character_state_history: Vec<String>,
}
impl SelectedEntityInfo {
fn new(entity_id: u32) -> Self {
Self {
entity_id,
debug_shape_id: None,
character_state_history: Vec::new(),
}
}
}
pub struct AdminCommandState {
give_item_qty: u32,
give_item_selected_idx: usize,
give_item_search_text: String,
kits_selected_idx: usize,
spawn_entity_qty: u32,
spawn_entity_selected_idx: usize,
spawn_entity_search_text: String,
}
impl AdminCommandState {
fn new() -> Self {
Self {
give_item_qty: 1,
give_item_selected_idx: 0,
give_item_search_text: String::new(),
kits_selected_idx: 0,
spawn_entity_qty: 1,
spawn_entity_selected_idx: 0,
spawn_entity_search_text: String::new(),
}
}
}
pub struct EguiDebugInfo {
pub frame_time: Duration,
pub ping_ms: f64,
}
pub struct EguiInnerState {
selected_entity_info: Option<SelectedEntityInfo>,
admin_command_state: AdminCommandState,
max_entity_distance: f32,
selected_entity_cylinder_height: f32,
frame_times: Vec<f32>,
windows: EguiWindows,
debug_vectors_enabled: bool,
}
#[derive(Clone, Default)]
pub struct EguiWindows {
admin_commands: bool,
egui_inspection: bool,
egui_settings: bool,
egui_memory: bool,
frame_time: bool,
ecs_entities: bool,
experimental_shaders: bool,
}
impl Default for EguiInnerState {
fn default() -> Self {
Self {
admin_command_state: AdminCommandState::new(),
selected_entity_info: None,
max_entity_distance: 100000.0,
selected_entity_cylinder_height: 10.0,
frame_times: Vec::new(),
windows: EguiWindows::default(),
debug_vectors_enabled: false,
}
}
}
pub enum EguiDebugShapeAction {
AddCylinder {
radius: f32,
height: f32,
},
RemoveShape(u64),
SetPosAndColor {
id: u64,
pos: [f32; 4],
color: [f32; 4],
},
}
pub enum EguiAction {
ChatCommand {
cmd: ServerChatCommand,
args: Vec<String>,
},
DebugShape(EguiDebugShapeAction),
SetExperimentalShader(String, bool),
SetShowDebugVector(bool),
}
#[derive(Default)]
pub struct EguiActions {
pub actions: Vec<EguiAction>,
}
#[cfg(feature = "use-dyn-lib")]
pub fn init() { lazy_static::initialize(&LIB); }
pub fn maintain(
platform: &mut Platform,
egui_state: &mut EguiInnerState,
client: &Client,
debug_info: Option<EguiDebugInfo>,
added_cylinder_shape_id: Option<u64>,
experimental_shaders: Vec<(String, bool)>,
) -> EguiActions {
#[cfg(not(feature = "use-dyn-lib"))]
{
maintain_egui_inner(
platform,
egui_state,
client,
debug_info,
added_cylinder_shape_id,
experimental_shaders,
)
}
#[cfg(feature = "use-dyn-lib")]
{
let lock = LIB.lock().unwrap();
let lib = &lock.as_ref().unwrap().lib;
let maintain_fn: common_dynlib::Symbol<
fn(
&mut Platform,
&mut EguiInnerState,
&Client,
Option<EguiDebugInfo>,
Option<u64>,
Vec<(String, bool)>,
) -> EguiActions,
> = unsafe { lib.get(MAINTAIN_EGUI_FN) }.unwrap_or_else(|e| {
panic!(
"Trying to use: {} but had error: {:?}",
CStr::from_bytes_with_nul(MAINTAIN_EGUI_FN)
.map(CStr::to_str)
.unwrap()
.unwrap(),
e
)
});
maintain_fn(
platform,
egui_state,
client,
debug_info,
added_cylinder_shape_id,
experimental_shaders,
)
}
}
#[cfg_attr(feature = "be-dyn-lib", export_name = "maintain_egui_inner")]
pub fn maintain_egui_inner(
platform: &mut Platform,
egui_state: &mut EguiInnerState,
client: &Client,
debug_info: Option<EguiDebugInfo>,
added_cylinder_shape_id: Option<u64>,
experimental_shaders: Vec<(String, bool)>,
) -> EguiActions {
platform.begin_frame();
let ctx = &platform.context();
let mut egui_actions = EguiActions::default();
let mut previous_selected_entity: Option<SelectedEntityInfo> = None;
let mut max_entity_distance = egui_state.max_entity_distance;
let mut selected_entity_cylinder_height = egui_state.selected_entity_cylinder_height;
let mut windows = egui_state.windows.clone();
let mut debug_vectors_enabled_mut = egui_state.debug_vectors_enabled;
if let Some(shape_id) = added_cylinder_shape_id {
if let Some(selected_entity) = &mut egui_state.selected_entity_info {
selected_entity.debug_shape_id = Some(shape_id);
}
}
if let Some(debug_info) = debug_info.as_ref() {
egui_state
.frame_times
.push(debug_info.frame_time.as_nanos() as f32);
if egui_state.frame_times.len() > 250 {
egui_state.frame_times.remove(0);
}
};
let start_pos = Pos2 { x: 300.0, y: 0.0 };
Window::new("Debug Control")
.default_pos(start_pos)
.default_width(200.0)
.default_height(200.0)
.show(&platform.context(), |ui| {
ui.horizontal(|ui| {
ui.label(format!(
"Ping: {:.1}ms",
debug_info.as_ref().map_or(0.0, |x| x.ping_ms)
));
});
ui.group(|ui| {
ui.vertical(|ui| {
ui.checkbox(&mut windows.admin_commands, "Admin Commands");
ui.checkbox(&mut windows.ecs_entities, "ECS Entities");
ui.checkbox(&mut windows.frame_time, "Frame Time");
ui.checkbox(&mut windows.experimental_shaders, "Experimental Shaders");
ui.checkbox(&mut debug_vectors_enabled_mut, "Show Debug Vectors");
});
});
ui.group(|ui| {
ui.vertical(|ui| {
ui.label("Show EGUI Windows");
ui.horizontal(|ui| {
ui.checkbox(&mut windows.egui_inspection, "🔍 Inspection");
ui.checkbox(&mut windows.egui_settings, "🔧 Settings");
ui.checkbox(&mut windows.egui_memory, "📝 Memory");
})
})
});
});
Window::new("🔧 Settings")
.open(&mut windows.egui_settings)
.vscroll(true)
.show(ctx, |ui| {
ctx.settings_ui(ui);
});
Window::new("🔍 Inspection")
.open(&mut windows.egui_inspection)
.vscroll(true)
.show(ctx, |ui| {
ctx.inspection_ui(ui);
});
Window::new("📝 Memory")
.open(&mut windows.egui_memory)
.resizable(false)
.show(ctx, |ui| {
ctx.memory_ui(ui);
});
Window::new("Frame Time")
.open(&mut windows.frame_time)
.default_width(200.0)
.default_height(200.0)
.show(ctx, |ui| {
Plot::new("Frame Time").show(ui, |plot_ui| {
plot_ui.line(Line::new(PlotPoints::from_iter(
egui_state
.frame_times
.iter()
.enumerate()
.map(|(i, x)| [i as f64, *x as f64]),
)))
});
});
if windows.ecs_entities {
let ecs = client.state().ecs();
let positions = client.state().ecs().read_storage::<comp::Pos>();
let client_pos = positions.get(client.entity());
Window::new("ECS Entities")
.open(&mut windows.ecs_entities)
.default_width(500.0)
.default_height(500.0)
.show(ctx, |ui| {
ui.label(format!("Entity count: {}", &ecs.entities().join().count()));
ui.add(
Slider::new(&mut max_entity_distance, 1.0..=100000.0)
.logarithmic(true)
.clamp_to_range(true)
.text("Max entity distance"),
);
ui.add(
Slider::new(&mut selected_entity_cylinder_height, 0.1..=100.0)
.logarithmic(true)
.clamp_to_range(true)
.text("Cylinder height"),
);
let scroll_area = ScrollArea::vertical().max_height(800.0);
scroll_area.show(ui, |ui| {
Grid::new("entities_grid")
.spacing([40.0, 4.0])
.max_col_width(300.0)
.striped(true)
.show(ui, |ui| {
ui.label("-");
ui.label("ID");
ui.label("Pos");
ui.label("Vel");
ui.label("Name");
ui.label("Body");
ui.label("Poise");
ui.label("Character State");
ui.label("Character Activity");
ui.end_row();
for (
entity,
body,
stats,
pos,
_ori,
vel,
poise,
character_state,
character_activity,
) in (
&ecs.entities(),
ecs.read_storage::<Body>().maybe(),
ecs.read_storage::<comp::Stats>().maybe(),
ecs.read_storage::<comp::Pos>().maybe(),
ecs.read_storage::<comp::Ori>().maybe(),
ecs.read_storage::<comp::Vel>().maybe(),
ecs.read_storage::<Poise>().maybe(),
ecs.read_storage::<comp::CharacterState>().maybe(),
ecs.read_storage::<comp::CharacterActivity>().maybe(),
)
.join()
.filter(
|(_, _, _, pos, _, _, _, _, _)| {
client_pos.map_or(true, |client_pos| {
pos.map_or(0.0, |pos| {
pos.0.distance_squared(client_pos.0)
}) < max_entity_distance
})
},
)
{
if ui.button("View").clicked() {
previous_selected_entity =
mem::take(&mut egui_state.selected_entity_info);
if pos.is_some() {
egui_actions.actions.push(EguiAction::DebugShape(
EguiDebugShapeAction::AddCylinder {
radius: 1.0,
height: egui_state.selected_entity_cylinder_height,
},
));
}
egui_state.selected_entity_info =
Some(SelectedEntityInfo::new(entity.id()));
}
ui.label(format!("{}", entity.id()));
if let Some(pos) = pos {
ui.label(format!(
"{:.0},{:.0},{:.0}",
pos.0.x, pos.0.y, pos.0.z
));
} else {
ui.label("-");
}
if let Some(vel) = vel {
ui.label(format!("{:.1}u/s", vel.0.magnitude()));
} else {
ui.label("-");
}
if let Some(stats) = stats {
ui.label(&stats.name);
} else {
ui.label("-");
}
if let Some(body) = body {
ui.label(body_species(body));
} else {
ui.label("-");
}
if let Some(poise) = poise {
poise_state_label(ui, poise);
} else {
ui.label("-");
}
if let Some(character_state) = character_state {
ui.label(character_state.to_string());
} else {
ui.label("-");
}
if let Some(character_activity) = character_activity {
ui.label(format!("{:?}", character_activity));
} else {
ui.label("-");
}
ui.end_row();
}
});
let margin = ui.visuals().clip_rect_margin;
let current_scroll = ui.clip_rect().top() - ui.min_rect().top() + margin;
let max_scroll =
ui.min_rect().height() - ui.clip_rect().height() + 2.0 * margin;
(current_scroll, max_scroll)
});
});
if let Some(selected_entity_info) = &mut egui_state.selected_entity_info {
let selected_entity = ecs.entities().entity(selected_entity_info.entity_id);
if !selected_entity.gen().is_alive() {
previous_selected_entity = mem::take(&mut egui_state.selected_entity_info);
} else {
selected_entity_window(platform, ecs, selected_entity_info, &mut egui_actions);
}
}
}
draw_admin_commands_window(
ctx,
&mut egui_state.admin_command_state,
&mut windows.admin_commands,
&mut egui_actions,
);
draw_experimental_shaders_window(
ctx,
&mut windows.experimental_shaders,
&mut egui_actions,
&experimental_shaders,
);
if let Some(previous) = previous_selected_entity {
if let Some(debug_shape_id) = previous.debug_shape_id {
egui_actions
.actions
.push(EguiAction::DebugShape(EguiDebugShapeAction::RemoveShape(
debug_shape_id,
)));
}
};
if let Some(selected_entity) = &egui_state.selected_entity_info {
if let Some(debug_shape_id) = selected_entity.debug_shape_id {
if (egui_state.selected_entity_cylinder_height - selected_entity_cylinder_height).abs()
> f32::EPSILON
{
egui_actions.actions.push(EguiAction::DebugShape(
EguiDebugShapeAction::RemoveShape(debug_shape_id),
));
egui_actions.actions.push(EguiAction::DebugShape(
EguiDebugShapeAction::AddCylinder {
radius: 1.0,
height: selected_entity_cylinder_height,
},
));
}
}
};
if debug_vectors_enabled_mut != egui_state.debug_vectors_enabled {
egui_actions
.actions
.push(EguiAction::SetShowDebugVector(debug_vectors_enabled_mut));
egui_state.debug_vectors_enabled = debug_vectors_enabled_mut;
}
egui_state.max_entity_distance = max_entity_distance;
egui_state.selected_entity_cylinder_height = selected_entity_cylinder_height;
egui_state.windows = windows;
egui_actions
}
fn selected_entity_window(
platform: &mut Platform,
ecs: &World,
selected_entity_info: &mut SelectedEntityInfo,
egui_actions: &mut EguiActions,
) {
let entity_id = selected_entity_info.entity_id;
for (
_entity,
body,
stats,
pos,
_ori,
vel,
poise,
buffs,
auras,
character_state,
character_activity,
physics_state,
alignment,
scale,
(mass, density, health, energy),
) in (
&ecs.entities(),
ecs.read_storage::<Body>().maybe(),
ecs.read_storage::<comp::Stats>().maybe(),
ecs.read_storage::<comp::Pos>().maybe(),
ecs.read_storage::<comp::Ori>().maybe(),
ecs.read_storage::<comp::Vel>().maybe(),
ecs.read_storage::<Poise>().maybe(),
ecs.read_storage::<comp::Buffs>().maybe(),
ecs.read_storage::<comp::Auras>().maybe(),
ecs.read_storage::<comp::CharacterState>().maybe(),
ecs.read_storage::<comp::CharacterActivity>().maybe(),
ecs.read_storage::<comp::PhysicsState>().maybe(),
ecs.read_storage::<comp::Alignment>().maybe(),
ecs.read_storage::<comp::Scale>().maybe(),
(
ecs.read_storage::<comp::Mass>().maybe(),
ecs.read_storage::<comp::Density>().maybe(),
ecs.read_storage::<comp::Health>().maybe(),
ecs.read_storage::<comp::Energy>().maybe(),
),
)
.join()
.filter(|(e, _, _, _, _, _, _, _, _, _, _, _, _, _, (_, _, _, _))| e.id() == entity_id)
{
let time = ecs.read_resource::<Time>();
if let Some(pos) = pos {
if let Some(shape_id) = selected_entity_info.debug_shape_id {
egui_actions.actions.push(EguiAction::DebugShape(
EguiDebugShapeAction::SetPosAndColor {
id: shape_id,
color: [1.0, 1.0, 0.0, 0.5],
pos: [pos.0.x, pos.0.y, pos.0.z + 2.0, 0.0],
},
));
}
};
Window::new("Selected Entity")
.default_width(300.0)
.default_height(200.0)
.show(&platform.context(), |ui| {
ui.vertical(|ui| {
CollapsingHeader::new("General").default_open(true).show(ui, |ui| {
Grid::new("selected_entity_general_grid")
.spacing([40.0, 4.0])
.striped(true)
.show(ui, |ui| {
two_col_row(ui, "Health", health.map_or("-".to_owned(), |x| format!("{:.1}/{:.1}", x.current(), x.maximum())));
two_col_row(ui, "Energy", energy.map_or("-".to_owned(), |x| format!("{:.1}/{:.1}", x.current(), x.maximum())));
two_col_row(ui, "Position", pos.map_or("-".to_owned(), |x| format!("({:.1},{:.1},{:.1})", x.0.x, x.0.y, x.0.z)));
two_col_row(ui, "Velocity", vel.map_or("-".to_owned(), |x| format!("({:.1},{:.1},{:.1}) ({:.1} u/s)", x.0.x, x.0.y, x.0.z, x.0.magnitude())));
two_col_row(ui, "Alignment", alignment.map_or("-".to_owned(), |x| format!("{:?}", x)));
two_col_row(ui, "Scale", scale.map_or("-".to_owned(), |x| format!("{:?}", x)));
two_col_row(ui, "Mass", mass.map_or("-".to_owned(), |x| format!("{:.1}", x.0)));
two_col_row(ui, "Density", density.map_or("-".to_owned(), |x| format!("{:.1}", x.0)));
});
});
if let Some(stats) = stats {
CollapsingHeader::new("Stats").default_open(true).show(ui, |ui| {
Grid::new("selected_entity_stats_grid")
.spacing([40.0, 4.0])
.striped(true)
.show(ui, |ui| {
two_col_row(ui, "Name", stats.name.to_string());
two_col_row(ui, "Damage Reduction", format!("{:.1}", stats.damage_reduction.modifier()));
two_col_row(ui, "Multiplicative Max Health Modifier", format!("{:.1}", stats.max_health_modifiers.mult_mod));
two_col_row(ui, "Move Speed Modifier", format!("{:.1}", stats.move_speed_modifier));
});
});
}
if let Some(body) = body {
CollapsingHeader::new("Body").default_open(false).show(ui, |ui| {
Grid::new("selected_entity_body_grid")
.spacing([40.0, 4.0])
.striped(true)
.show(ui, |ui| {
two_col_row(ui, "Type", body.to_string());
two_col_row(ui, "Species", body_species(body));
});
});
}
if let Some(pos) = pos {
CollapsingHeader::new("Pos").default_open(false).show(ui, |ui| {
Grid::new("selected_entity_pos_grid")
.spacing([40.0, 4.0])
.max_col_width(100.0)
.striped(true)
.show(ui, |ui| {
two_col_row(ui, "x", format!("{:.1}", pos.0.x));
two_col_row(ui, "y", format!("{:.1}", pos.0.y));
two_col_row(ui, "z", format!("{:.1}", pos.0.z));
});
});
}
if let Some(poise) = poise {
CollapsingHeader::new("Poise").default_open(false).show(ui, |ui| {
Grid::new("selected_entity_poise_grid")
.spacing([40.0, 4.0])
.max_col_width(100.0)
.striped(true)
.show(ui, |ui| { #[rustfmt::skip] {
ui.label("State");
poise_state_label(ui, poise);
ui.end_row();
two_col_row(ui, "Current", format!("{:.1}/{:.1}", poise.current(), poise.maximum()));
two_col_row(ui, "Base Max", format!("{:.1}", poise.base_max()));
}});
});
}
if let Some(buffs) = buffs {
CollapsingHeader::new("Buffs").default_open(false).show(ui, |ui| {
Grid::new("selected_entity_buffs_grid")
.spacing([40.0, 4.0])
.max_col_width(100.0)
.striped(true)
.show(ui, |ui| {
ui.label("Kind");
ui.label("Time");
ui.label("Source");
ui.end_row();
buffs.buffs.iter().for_each(|(_, v)| {
ui.label(format!("{:?}", v.kind));
ui.label(
v.end_time.map_or("-".to_string(), |end| {
format!("{:?}", end.0 - time.0)
}),
);
ui.label(format!("{:?}", v.source));
ui.end_row();
});
});
});
}
if let Some(auras) = auras {
CollapsingHeader::new("Auras").default_open(false).show(ui, |ui| {
Grid::new("selected_entity_auras_grid")
.spacing([40.0, 4.0])
.striped(true)
.show(ui, |ui| {
ui.label("Kind");
ui.label("Radius");
ui.label("Duration");
ui.label("Target");
ui.end_row();
auras.auras.iter().for_each(|(_, v)| {
ui.label(match v.aura_kind {
Buff { kind, .. } => format!("Buff - {:?}", kind),
FriendlyFire => "Friendly Fire".to_string(),
ForcePvP => "ForcedPvP".to_string(),
});
ui.label(format!("{:1}", v.radius));
ui.label(v.end_time.map_or("-".to_owned(), |x| format!("{:1}s", x.0 - time.0)));
ui.label(format!("{:?}", v.target));
ui.end_row();
});
});
});
}
if let Some(character_state) = character_state {
if selected_entity_info
.character_state_history
.first()
.unwrap_or(&"-".to_owned())
!= &character_state.to_string()
{
selected_entity_info
.character_state_history
.insert(0, character_state.to_string());
if selected_entity_info.character_state_history.len() > 50 {
selected_entity_info.character_state_history.pop();
}
}
CollapsingHeader::new("Character State").default_open(false).show(ui, |ui| {
draw_char_state_group(ui, selected_entity_info, character_state);
});
}
if let Some(character_activity) = character_activity {
CollapsingHeader::new("CharacterActivity").default_open(false).show(ui, |ui| {
Grid::new("selected_entity_character_activity_grid")
.spacing([40.0, 4.0])
.max_col_width(100.0)
.striped(true)
.show(ui, |ui| {
two_col_row(ui, "look_dir", format!("{:.3?}", character_activity.look_dir));
});
});
}
if let Some(physics_state) = physics_state {
CollapsingHeader::new("Physics State").default_open(false).show(ui, |ui| {
Grid::new("selected_entity_physics_state_grid")
.spacing([40.0, 4.0])
.striped(true)
.show(ui, |ui| {
two_col_row(ui, "On Ground", physics_state.on_ground.map_or("None".to_owned(), |x| format!("{:?}", x)));
two_col_row(ui, "On Ceiling", (if physics_state.on_ceiling { "True" } else { "False " }).to_string());
two_col_row(ui, "On Wall", physics_state.on_wall.map_or("-".to_owned(), |x| format!("{:.1},{:.1},{:.1}", x.x, x.y, x.z )));
two_col_row(ui, "Touching Entities", physics_state.touch_entities.len().to_string());
two_col_row(ui, "In Fluid", match physics_state.in_fluid {
Some(Fluid::Air { elevation, vel, .. }) => format!("Air (Elevation: {:.1}), vel: ({:.1},{:.1},{:.1}) ({:.1} u/s)", elevation, vel.0.x, vel.0.y, vel.0.z, vel.0.magnitude()),
Some(Fluid::Liquid { depth, kind, .. }) => format!("{:?} (Depth: {:.1})", kind, depth),
_ => "None".to_owned() });
});
two_col_row(ui, "Footwear", match physics_state.footwear{ Friction::Ski => "Ski", Friction::Skate => "Skate", Friction::Normal=>"Normal",}.to_string());
});
}
});
});
}
}
fn body_species(body: &Body) -> String {
match body {
Body::Humanoid(body) => format!("{:?}", body.species),
Body::QuadrupedSmall(body) => format!("{:?}", body.species),
Body::QuadrupedMedium(body) => format!("{:?}", body.species),
Body::BirdMedium(body) => format!("{:?}", body.species),
Body::FishMedium(body) => format!("{:?}", body.species),
Body::Dragon(body) => format!("{:?}", body.species),
Body::BirdLarge(body) => format!("{:?}", body.species),
Body::FishSmall(body) => format!("{:?}", body.species),
Body::BipedLarge(body) => format!("{:?}", body.species),
Body::BipedSmall(body) => format!("{:?}", body.species),
Body::Object(body) => format!("{:?}", body),
Body::ItemDrop(body) => format!("{:?}", body),
Body::Golem(body) => format!("{:?}", body.species),
Body::Theropod(body) => format!("{:?}", body.species),
Body::QuadrupedLow(body) => format!("{:?}", body.species),
Body::Arthropod(body) => format!("{:?}", body.species),
Body::Ship(body) => format!("{:?}", body),
Body::Crustacean(body) => format!("{:?}", body.species),
Body::Plugin(body) => format!("{:?}", body),
}
}
fn poise_state_label(ui: &mut Ui, poise: &Poise) {
match poise.poise_state() {
PoiseState::Normal => {
ui.label("Normal");
},
PoiseState::Interrupted => {
ui.colored_label(Color32::YELLOW, "Interrupted");
},
PoiseState::Stunned => {
ui.colored_label(Color32::RED, "Stunned");
},
PoiseState::Dazed => {
ui.colored_label(Color32::RED, "Dazed");
},
PoiseState::KnockedDown => {
ui.colored_label(Color32::BLUE, "Knocked Down");
},
};
}