diff --git a/src/inspector.rs b/src/inspector.rs index ab6757e..bce2492 100644 --- a/src/inspector.rs +++ b/src/inspector.rs @@ -1,11 +1,7 @@ use bevy::prelude::*; use bevy_egui::EguiContexts; -use crate::{ - database::{load_url_asset, Database}, - pokemon::*, - ui::UiAssets, -}; +use crate::{database::load_url_asset, pokemon::*, ui::UiAssets}; pub struct InspectorPlugin; @@ -13,7 +9,6 @@ impl Plugin for InspectorPlugin { fn build(&self, app: &mut App) { app.register_type::() .insert_resource(InspectorPokemon::default()) - .add_startup_system(resource_setup) .add_system(state_changed); } } @@ -24,10 +19,7 @@ pub struct InspectorPokemon { pub pokemon: Option, pub nature: Option, pub characteristic: Option, -} - -fn resource_setup(mut inspector: ResMut, pokemon: Res>) { - inspector.pokemon = pokemon.map.get("sceptile").cloned(); + pub derived_stats: DerivedStats, } fn state_changed( diff --git a/src/pokemon.rs b/src/pokemon.rs index e7c85a5..69d5bd9 100644 --- a/src/pokemon.rs +++ b/src/pokemon.rs @@ -5,6 +5,7 @@ use std::{collections::HashMap, fmt}; use crate::database::TYPE_MAP; pub type BaseValue = u8; +pub type StatValue = i32; lazy_static! { pub static ref BASESTAT_MAP: HashMap = { @@ -29,10 +30,39 @@ pub trait GetKey { fn key(&self) -> String; } -#[derive(Default, Debug, Clone)] +#[derive(Default, Debug, Clone, Reflect, FromReflect)] pub struct DerivedStats { - pub level: Option, - pub stats: HashMap, + pub level: Option, + pub hp: Option, + pub attack: Option, + pub defense: Option, + pub special_attack: Option, + pub special_defense: Option, + pub speed: Option, +} + +impl DerivedStats { + pub fn value(&self, stat: BaseStat) -> Option { + match stat { + BaseStat::Hp => self.hp, + BaseStat::Attack => self.attack, + BaseStat::Defense => self.defense, + BaseStat::SpecialAttack => self.special_attack, + BaseStat::SpecialDefense => self.special_defense, + BaseStat::Speed => self.speed, + } + } + + pub fn value_mut(&mut self, stat: BaseStat) -> &mut Option { + match stat { + BaseStat::Hp => &mut self.hp, + BaseStat::Attack => &mut self.attack, + BaseStat::Defense => &mut self.defense, + BaseStat::SpecialAttack => &mut self.special_attack, + BaseStat::SpecialDefense => &mut self.special_defense, + BaseStat::Speed => &mut self.speed, + } + } } #[derive(Default, Debug, Clone, Reflect, FromReflect, PartialEq)] @@ -126,7 +156,7 @@ impl GetKey for Pokemon { } } -#[derive(Debug, Clone, Copy, Reflect, FromReflect, Default, PartialEq)] +#[derive(Debug, Clone, Copy, Reflect, FromReflect, Default, Eq, PartialEq, Hash)] pub enum BaseStat { #[default] Hp, @@ -183,6 +213,18 @@ pub struct Nature { pub decreased: Option, } +impl Nature { + pub fn stat_mult(&self, stat: BaseStat) -> f32 { + if self.increased == Some(stat) { + 1.1 + } else if self.decreased == Some(stat) { + 0.9 + } else { + 1.0 + } + } +} + impl FromJson for Nature { fn from_json(json: &json::JsonValue) -> Option { Some(Self { @@ -232,3 +274,70 @@ impl GetKey for Characteristic { self.name.to_lowercase() } } + +pub fn calculate_possible_ivs( + base_stat: BaseStat, + base_value: BaseValue, + derived_value: StatValue, + ev: u8, + level: StatValue, + nature: Option<&Nature>, + characteristic: Option<&Characteristic>, +) -> Result, String> { + let mut possible_values = vec![]; + + let mut iv_range: Vec<_> = (0..32).collect(); + if let Some(characteristic) = characteristic { + if characteristic.stat == base_stat { + iv_range = characteristic.possible_values.clone(); + } + }; + + for iv in iv_range { + if calculate_derived_stat( + base_stat, + base_value, + iv, + ev, + level, + &nature.cloned().unwrap_or_default(), + ) == derived_value + { + possible_values.push(iv); + } + } + Ok(possible_values) +} + +fn calculate_derived_stat( + base_stat: BaseStat, + base_value: BaseValue, + iv: u8, + ev: u8, + level: StatValue, + nature: &Nature, +) -> StatValue { + match base_stat { + BaseStat::Hp => { + if base_value == 1 { + return 1; // Shedinja + } + (((2 * base_value as i32 + iv as i32) as f32 + (ev as f32 / 4.0).floor()) + * level as f32 + / 100.0) + .floor() as StatValue + + level + + 10 + } + _ => { + let nature_mult = nature.stat_mult(base_stat); + (((((2 * base_value as i32 + iv as i32) as f32 + (ev as f32 / 4.0).floor()) + * level as f32 + / 100.0) + .floor() as StatValue + + 5) as f32 + * nature_mult) + .floor() as StatValue + } + } +} diff --git a/src/ui.rs b/src/ui.rs index 87a2a19..41028c5 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,11 +1,14 @@ -use std::collections::HashMap; - use bevy::prelude::*; use bevy_egui::{egui, EguiContexts, EguiPlugin}; use lazy_static::lazy_static; +use std::collections::HashMap; use crate::{database::Database, inspector::InspectorPokemon, pokemon::*}; +const INPUT_LABEL_WIDTH: f32 = 100.0; +const INPUT_TEXT_WIDTH: f32 = 150.0; +const BAR_HEIGHT: f32 = 12.0; + lazy_static! { static ref BASE_STAT_COLOR_RANGES: Vec<(u8, egui::Color32)> = vec![ (0, egui::Color32::from_rgb(255, 0, 0)), @@ -16,6 +19,21 @@ lazy_static! { (150, egui::Color32::from_rgb(0, 255, 128)), (180, egui::Color32::from_rgb(0, 255, 255)), ]; + static ref IV_COLOR_RANGES: Vec<(u8, egui::Color32)> = vec![ + (0, egui::Color32::from_rgb(255, 0, 0)), + (16, egui::Color32::from_rgb(255, 128, 0)), + (22, egui::Color32::from_rgb(255, 255, 0)), + (27, egui::Color32::from_rgb(128, 255, 0)), + (30, egui::Color32::from_rgb(0, 255, 0)), + ]; + static ref COLUMNS: Vec = vec![ + 8.0, // Nature modifier + 25.0, // Base stat + 255.0, // Base stat bar + 60.0, // Stat name + 40.0, // Derived stat input + 30.0, // possible IV range + ]; } pub struct UiPlugin; @@ -37,10 +55,11 @@ pub struct UiAssets { pub sprite_map: HashMap, egui::TextureId)>, } -#[derive(Resource, Default, Reflect)] -#[reflect(Resource)] +#[derive(Resource, Default)] pub struct UiState { pub name: String, + pub level: String, + pub derived_stats: HashMap, } fn load_assets(mut ui_assets: ResMut, assets: Res) { @@ -75,8 +94,16 @@ fn ui_system( } egui::CentralPanel::default().show(contexts.ctx_mut(), |ui| { - const INPUT_LABEL_WIDTH: f32 = 100.0; - const INPUT_TEXT_WIDTH: f32 = 150.0; + let sprite_size = egui::Vec2::new(128., 128.); + + let pokemon = inspector.pokemon.clone(); + let pokemon = pokemon.as_ref(); + let nature = inspector.nature.clone(); + let nature = nature.as_ref(); + let characteristic = inspector.characteristic.clone(); + let characteristic = characteristic.as_ref(); + + // Pokemon ui.horizontal(|ui| { ui.horizontal(|ui| { ui.set_width(INPUT_LABEL_WIDTH); @@ -87,17 +114,16 @@ fn ui_system( ui.text_edit_singleline(&mut ui_state.name); }); }); + ui.add_space(2.); + // Nature ui.horizontal(|ui| { ui.horizontal(|ui| { ui.set_width(INPUT_LABEL_WIDTH); ui.label("nature:"); }); egui::ComboBox::new("nature", "") - .selected_text(format!( - "{}", - inspector.nature.as_ref().map_or("-", |c| &c.name) - )) + .selected_text(format!("{}", nature.map_or("-", |c| &c.name))) .show_ui(ui, |ui| { ui.selectable_value(&mut inspector.nature, None, "-"); let mut chars: Vec<_> = natures.map.values().collect(); @@ -107,17 +133,16 @@ fn ui_system( } }); }); + ui.add_space(2.); + // Characteristics ui.horizontal(|ui| { ui.horizontal(|ui| { ui.set_width(INPUT_LABEL_WIDTH); ui.label("characteristic:"); }); egui::ComboBox::new("characteristic", "") - .selected_text(format!( - "{}", - inspector.characteristic.as_ref().map_or("-", |c| &c.name) - )) + .selected_text(format!("{}", characteristic.map_or("-", |c| &c.name))) .show_ui(ui, |ui| { ui.selectable_value(&mut inspector.characteristic, None, "-"); let mut chars: Vec<_> = characteristics.map.values().collect(); @@ -132,70 +157,251 @@ fn ui_system( }); }); - if let Some(pokemon) = inspector.pokemon.as_ref() { - ui.heading(egui::RichText::new(pokemon.full_name.clone())); + ui.add_space(4.); - ui.label(format!( - "{} / {}", - pokemon.type1.as_ref().map_or("-", |t| t.name.as_str()), - pokemon.type2.as_ref().map_or("-", |t| t.name.as_str()) - )); + ui.horizontal(|ui| { + // Sprite + ui.horizontal(|ui| { + ui.set_min_size(sprite_size); + if let Some((_, rendered_texture_id)) = + pokemon.map_or(None, |pkmn| ui_assets.sprite_map.get(&pkmn.key())) + { + ui.add(egui::Image::new(*rendered_texture_id, sprite_size)); + } + }); - if let Some((_, rendered_texture_id)) = ui_assets.sprite_map.get(&pokemon.key()) { - ui.add(egui::Image::new(*rendered_texture_id, [128., 128.])); - } + // if let Some(pokemon) = pokemon { + ui.vertical(|ui| { + ui.add_space(sprite_size.y / 2. - 24.); - for stat in vec![ - BaseStat::Hp, - BaseStat::Attack, - BaseStat::Defense, - BaseStat::SpecialAttack, - BaseStat::SpecialDefense, - BaseStat::Speed, - ] { + // Name + ui.heading( + egui::RichText::new(pokemon.map_or("-", |pkmn| &pkmn.full_name)) + .color(egui::Color32::LIGHT_GRAY), + ); + + // Types + ui.label(format!( + "{} / {}", + pokemon.map_or("-", |pkmn| pkmn + .type1 + .as_ref() + .map_or("-", |t| t.name.as_str())), + pokemon.map_or("-", |pkmn| pkmn + .type2 + .as_ref() + .map_or("-", |t| t.name.as_str())) + )); + }); + // }; + }); + + ui.add_space(4.); + + ui.horizontal(|ui| { + for (i, width) in COLUMNS.iter().enumerate() { ui.horizontal(|ui| { - ui.horizontal(|ui| { - ui.set_width(60.0); - ui.label(format!("{stat}")); - }); - ui.horizontal(|ui| { - ui.horizontal(|ui| { - ui.set_width(8.0); - let symbol = inspector.nature.as_ref().map_or("", |n| { - if n.increased == Some(stat) { - "+" - } else if n.decreased == Some(stat) { - "-" - } else { - "" + ui.set_width(*width); + match i { + 4 => { + let value = &mut ui_state.level; + if ui.add(egui::TextEdit::singleline(value)).changed() { + *value = digit_filter(value); + if let Ok(value) = value.parse::() { + inspector.derived_stats.level = Some(value); } - }); - ui.label(symbol); - }); - }); - ui.horizontal(|ui| { - ui.set_width(25.0); - ui.label(pokemon.base_value(stat).to_string()); - }); - ui.horizontal(|ui| { - let base_value = pokemon.base_value(stat); - let mut color = BASE_STAT_COLOR_RANGES.first().unwrap().1; - for (treshold, c) in BASE_STAT_COLOR_RANGES.iter() { - if base_value >= *treshold { - color = c.clone(); - } else { - break; } } - - let image = - egui::Image::new(*rendered_texture_id, [base_value as f32, 12.]) - .tint(color); - ui.set_width(260.0); - ui.add(image); - }); + _ => (), + } }); } + }); + + ui.horizontal(|ui| { + for (i, width) in COLUMNS.iter().enumerate() { + ui.horizontal(|ui| { + ui.set_width(*width); + match i { + 4 => { + ui.label(egui::RichText::new("Stat")); + } + 5 => { + ui.label(egui::RichText::new("IV range")); + } + _ => (), + } + }); + } + }); + + // Stats + for stat in vec![ + BaseStat::Hp, + BaseStat::Attack, + BaseStat::Defense, + BaseStat::SpecialAttack, + BaseStat::SpecialDefense, + BaseStat::Speed, + ] { + let mut column_size = COLUMNS.iter(); + ui.horizontal(|ui| { + // Nature modifier + ui.horizontal(|ui| { + if let Some(width) = column_size.next() { + ui.set_width(*width); + } + let symbol = nature.map_or("", |n| { + if n.increased == Some(stat) { + "+" + } else if n.decreased == Some(stat) { + "-" + } else { + "" + } + }); + ui.label(symbol); + }); + + // Base stat number + ui.horizontal(|ui| { + if let Some(width) = column_size.next() { + ui.set_width(*width); + } + let base_stat = &pokemon + .map_or(String::from("-"), |pkmn| pkmn.base_value(stat).to_string()); + ui.label(egui::RichText::new(base_stat)); + }); + + // Base stat bar + ui.horizontal(|ui| { + if let Some(width) = column_size.next() { + ui.set_width(*width); + } + let base_value = pokemon.map_or(0, |pkmn| pkmn.base_value(stat)); + let bar_length = base_value as f32; + let mut color = BASE_STAT_COLOR_RANGES.first().unwrap().1; + for (treshold, c) in BASE_STAT_COLOR_RANGES.iter() { + if base_value >= *treshold { + color = c.clone(); + } else { + break; + } + } + + let image = egui::Image::new(*rendered_texture_id, [bar_length, BAR_HEIGHT]) + .tint(color); + ui.add(image); + }); + + // Stat name + ui.horizontal(|ui| { + if let Some(width) = column_size.next() { + ui.set_width(*width); + } + ui.label( + egui::RichText::new(format!("{stat}")).color(nature_color(stat, nature)), + ); + }); + + // Derived stat input + ui.horizontal(|ui| { + if let Some(width) = column_size.next() { + ui.set_width(*width); + } + if !ui_state.derived_stats.contains_key(&stat) { + ui_state.derived_stats.insert(stat, "0".to_string()); + } + let value = ui_state.derived_stats.get_mut(&stat).unwrap(); + if ui.add(egui::TextEdit::singleline(value)).changed() { + *value = digit_filter(value); + if let (Ok(value), derived) = ( + value.parse::(), + inspector.derived_stats.value_mut(stat), + ) { + *derived = Some(value); + } + } + }); + + let possible_ivs = if let (Some(pokemon), Some(level), Some(derived_stat)) = ( + pokemon, + inspector.derived_stats.level, + inspector.derived_stats.value(stat), + ) { + calculate_possible_ivs( + stat, + pokemon.base_value(stat), + derived_stat, + 0, + level, + nature, + characteristic, + ) + } else { + Ok(vec![]) + }; + + // Possible IV range + ui.horizontal(|ui| match possible_ivs { + Ok(possible_ivs) => { + if possible_ivs.len() == 0 { + ui.label(egui::RichText::new("?")); + } else if possible_ivs.len() <= 10 { + for iv in possible_ivs.iter() { + ui.label( + egui::RichText::new(format!("{}", iv)) + .color(iv_color(*iv as u8)), + ); + } + } else { + let first = possible_ivs.first().unwrap(); + let last = possible_ivs.last().unwrap(); + ui.label( + egui::RichText::new(format!("{}", first)) + .color(iv_color(*first as u8)), + ); + ui.label(egui::RichText::new("-")); + ui.label( + egui::RichText::new(format!("{}", last)) + .color(iv_color(*last as u8)), + ); + } + } + Err(reason) => { + ui.label(egui::RichText::new(reason).color(egui::Color32::RED)); + } + }); + }); } }); } + +fn nature_color(stat: BaseStat, nature: Option<&Nature>) -> egui::Color32 { + if let Some(nature) = nature { + if nature.increased == Some(stat) { + egui::Color32::from_rgb(255, 64, 192) + } else if nature.decreased == Some(stat) { + egui::Color32::from_rgb(64, 192, 255) + } else { + egui::Color32::LIGHT_GRAY + } + } else { + egui::Color32::LIGHT_GRAY + } +} + +fn iv_color(iv: u8) -> egui::Color32 { + for (treshold, color) in IV_COLOR_RANGES.iter().rev() { + if iv >= *treshold { + return color.clone(); + } + } + IV_COLOR_RANGES + .first() + .map_or(egui::Color32::LIGHT_GRAY, |(_, color)| color.clone()) +} + +fn digit_filter(string: &str) -> String { + string.chars().filter(|v| v.is_digit(10)).collect() +}