initial commit

master
hheik 2023-03-20 03:50:40 +02:00
commit d90540daa7
14 changed files with 25644 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

3731
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

20
Cargo.toml Normal file
View File

@ -0,0 +1,20 @@
[package]
name = "smart-iv-calculator"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bevy = "0.10.0"
bevy_egui = "0.20.1"
json = "0.12.4"
lazy_static = "1.4.0"
# Enable a small amount of optimization in debug mode
[profile.dev]
opt-level = 1
# Enable high optimizations for dependencies (incl. Bevy), but not for our code:
[profile.dev.package."*"]
opt-level = 3

9
README.md Normal file
View File

@ -0,0 +1,9 @@
# Dependencies
- rust / cargo
Linux users might require some `XCB` libraries to use `bevy_egui`. These packages that can be installed on Debian-based systems with the following command:
```bash
sudo apt install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev
```

BIN
assets/ui/bar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 B

211
data/characteristics.json Normal file
View File

@ -0,0 +1,211 @@
[{
"stat": "hp",
"name": "Loves to eat",
"possible_values": [
0, 5, 10, 15, 20, 25, 30
]
},
{
"stat": "hp",
"name": "Often dozes off",
"possible_values": [
1, 6, 11, 16, 21, 26, 31
]
},
{
"stat": "hp",
"name": "Often scatters things",
"possible_values": [
2, 7, 12, 17, 22, 27
]
},
{
"stat": "hp",
"name": "Scatters things often",
"possible_values": [
3, 8, 13, 18, 23, 28
]
},
{
"stat": "hp",
"name": "Likes to Relax",
"possible_values": [
4, 9, 14, 19, 24, 29
]
},
{
"stat": "attack",
"name": "Proud of its power",
"possible_values": [
0, 5, 10, 15, 20, 25, 30
]
},
{
"stat": "attack",
"name": "Likes to thrash about",
"possible_values": [
1, 6, 11, 16, 21, 26, 31
]
},
{
"stat": "attack",
"name": "A little quick tempered",
"possible_values": [
2, 7, 12, 17, 22, 27
]
},
{
"stat": "attack",
"name": "Likes to fight",
"possible_values": [
3, 8, 13, 18, 23, 28
]
},
{
"stat": "attack",
"name": "Quick Tempered",
"possible_values": [
4, 9, 14, 19, 24, 29
]
},
{
"stat": "defense",
"name": "Sturdy body",
"possible_values": [
0, 5, 10, 15, 20, 25, 30
]
},
{
"stat": "defense",
"name": "Capable of taking hits",
"possible_values": [
1, 6, 11, 16, 21, 26, 31
]
},
{
"stat": "defense",
"name": "Highly persistent",
"possible_values": [
2, 7, 12, 17, 22, 27
]
},
{
"stat": "defense",
"name": "Good endurance",
"possible_values": [
3, 8, 13, 18, 23, 28
]
},
{
"stat": "defense",
"name": "Good perseverance",
"possible_values": [
4, 9, 14, 19, 24, 29
]
},
{
"stat": "special_attack",
"name": "Highly curious",
"possible_values": [
0, 5, 10, 15, 20, 25, 30
]
},
{
"stat": "special_attack",
"name": "Mischievous",
"possible_values": [
1, 6, 11, 16, 21, 26, 31
]
},
{
"stat": "special_attack",
"name": "Thoroughly Cunning",
"possible_values": [
2, 7, 12, 17, 22, 27
]
},
{
"stat": "special_attack",
"name": "Often lost in thought",
"possible_values": [
3, 8, 13, 18, 23, 28
]
},
{
"stat": "special_attack",
"name": "Very finicky",
"possible_values": [
4, 9, 14, 19, 24, 29
]
},
{
"stat": "special_defense",
"name": "Strong willed",
"possible_values": [
0, 5, 10, 15, 20, 25, 30
]
},
{
"stat": "special_defense",
"name": "Somewhat vain",
"possible_values": [
1, 6, 11, 16, 21, 26, 31
]
},
{
"stat": "special_defense",
"name": "Strongly defiant",
"possible_values": [
2, 7, 12, 17, 22, 27
]
},
{
"stat": "special_defense",
"name": "Hates to lose",
"possible_values": [
3, 8, 13, 18, 23, 28
]
},
{
"stat": "special_defense",
"name": "Somewhat stubborn",
"possible_values": [
4, 9, 14, 19, 24, 29
]
},
{
"stat": "speed",
"name": "Likes to run",
"possible_values": [
0, 5, 10, 15, 20, 25, 30
]
},
{
"stat": "speed",
"name": "Alert to sounds",
"possible_values": [
1, 6, 11, 16, 21, 26, 31
]
},
{
"stat": "speed",
"name": "Impetuous and silly",
"possible_values": [
2, 7, 12, 17, 22, 27
]
},
{
"stat": "speed",
"name": "Somewhat of a clown",
"possible_values": [
3, 8, 13, 18, 23, 28
]
},
{
"stat": "speed",
"name": "Quick to flee",
"possible_values": [
4, 9, 14, 19, 24, 29
]
}
]

176
data/natures.json Normal file
View File

@ -0,0 +1,176 @@
[{
"name": "Hardy",
"alias": "hardy",
"id": 1,
"increased": null,
"decreased": null
},
{
"name": "Bold",
"alias": "bold",
"id": 2,
"increased": "defense",
"decreased": "attack"
},
{
"name": "Modest",
"alias": "modest",
"id": 3,
"increased": "special_attack",
"decreased": "attack"
},
{
"name": "Calm",
"alias": "calm",
"id": 4,
"increased": "special_defense",
"decreased": "attack"
},
{
"name": "Timid",
"alias": "timid",
"id": 5,
"increased": "speed",
"decreased": "attack"
},
{
"name": "Lonely",
"alias": "lonely",
"id": 6,
"increased": "attack",
"decreased": "defense"
},
{
"name": "Docile",
"alias": "docile",
"id": 7,
"increased": null,
"decreased": null
},
{
"name": "Mild",
"alias": "mild",
"id": 8,
"increased": "special_attack",
"decreased": "defense"
},
{
"name": "Gentle",
"alias": "gentle",
"id": 9,
"increased": "special_defense",
"decreased": "defense"
},
{
"name": "Hasty",
"alias": "hasty",
"id": 10,
"increased": "speed",
"decreased": "defense"
},
{
"name": "Adamant",
"alias": "adamant",
"id": 11,
"increased": "attack",
"decreased": "special_attack"
},
{
"name": "Impish",
"alias": "impish",
"id": 12,
"increased": "defense",
"decreased": "special_attack"
},
{
"name": "Bashful",
"alias": "bashful",
"id": 13,
"increased": null,
"decreased": null
},
{
"name": "Careful",
"alias": "careful",
"id": 14,
"increased": "special_defense",
"decreased": "special_attack"
},
{
"name": "Rash",
"alias": "rash",
"id": 15,
"increased": "special_attack",
"decreased": "special_defense"
},
{
"name": "Jolly",
"alias": "jolly",
"id": 16,
"increased": "speed",
"decreased": "special_attack"
},
{
"name": "Naughty",
"alias": "naughty",
"id": 17,
"increased": "attack",
"decreased": "special_defense"
},
{
"name": "Lax",
"alias": "lax",
"id": 18,
"increased": "defense",
"decreased": "special_defense"
},
{
"name": "Quirky",
"alias": "quirky",
"id": 19,
"increased": null,
"decreased": null
},
{
"name": "Naive",
"alias": "naive",
"id": 20,
"increased": "speed",
"decreased": "special_defense"
},
{
"name": "Brave",
"alias": "brave",
"id": 21,
"increased": "attack",
"decreased": "speed"
},
{
"name": "Relaxed",
"alias": "relaxed",
"id": 22,
"increased": "defense",
"decreased": "speed"
},
{
"name": "Quiet",
"alias": "quiet",
"id": 23,
"increased": "special_attack",
"decreased": "speed"
},
{
"name": "Sassy",
"alias": "sassy",
"id": 24,
"increased": "special_defense",
"decreased": "speed"
},
{
"name": "Serious",
"alias": "serious",
"id": 25,
"increased": null,
"decreased": null
}
]

20901
data/pokedex.json Normal file

File diff suppressed because it is too large Load Diff

91
data/types.json Normal file
View File

@ -0,0 +1,91 @@
[{
"id": 1,
"name": "Normal",
"alias": "normal"
},
{
"id": 2,
"name": "Fire",
"alias": "fire"
},
{
"id": 3,
"name": "Water",
"alias": "water"
},
{
"id": 4,
"name": "Electric",
"alias": "electric"
},
{
"id": 5,
"name": "Grass",
"alias": "grass"
},
{
"id": 6,
"name": "Ice",
"alias": "ice"
},
{
"id": 7,
"name": "Fighting",
"alias": "fighting"
},
{
"id": 8,
"name": "Poison",
"alias": "poison"
},
{
"id": 9,
"name": "Ground",
"alias": "ground"
},
{
"id": 10,
"name": "Flying",
"alias": "flying"
},
{
"id": 11,
"name": "Psychic",
"alias": "psychic"
},
{
"id": 12,
"name": "Bug",
"alias": "bug"
},
{
"id": 13,
"name": "Rock",
"alias": "rock"
},
{
"id": 14,
"name": "Ghost",
"alias": "ghost"
},
{
"id": 15,
"name": "Dragon",
"alias": "dragon"
},
{
"id": 16,
"name": "Dark",
"alias": "dark"
},
{
"id": 17,
"name": "Steel",
"alias": "steel"
},
{
"id": 18,
"name": "Fairy",
"alias": "fairy"
}
]

73
src/database.rs Normal file
View File

@ -0,0 +1,73 @@
use bevy::prelude::*;
use json::JsonValue;
use lazy_static::lazy_static;
use std::{
collections::HashMap,
fs,
path::{Path, PathBuf},
};
use crate::pokemon::*;
lazy_static! {
static ref DATA_DIR: PathBuf = "./data".into();
pub static ref TYPE_MAP: HashMap<u8, Type> = {
let types = parse_json_file(DATA_DIR.join("types.json"));
let mut map = HashMap::new();
for type_data in types.members() {
if let Some(type_data) = Type::from_json(type_data) {
map.insert(type_data.id, type_data);
}
}
map
};
}
pub struct DatabasePlugin;
impl Plugin for DatabasePlugin {
fn build(&self, app: &mut App) {
app.insert_resource(Database::<Pokemon>::default())
.insert_resource(Database::<Nature>::default())
.insert_resource(Database::<Characteristic>::default())
.add_startup_system(database_setup);
}
}
#[derive(Resource, Default)]
pub struct Database<T> {
pub map: HashMap<String, T>,
}
impl<T> Database<T>
where
T: FromJson + GetKey,
{
fn populate_from_json(&mut self, json: &JsonValue) {
for item in json.members() {
if let Some(item) = T::from_json(item) {
let key = item.alias();
if !self.map.contains_key(&key) {
self.map.insert(key, item);
}
}
}
}
}
fn database_setup(
mut pokemon_database: ResMut<Database<Pokemon>>,
mut nature_database: ResMut<Database<Nature>>,
mut characteristic_database: ResMut<Database<Characteristic>>,
) {
let natures = parse_json_file(DATA_DIR.join("natures.json"));
nature_database.populate_from_json(&natures);
let pokedex = parse_json_file(DATA_DIR.join("pokedex.json"));
pokemon_database.populate_from_json(&pokedex);
let characteristics = parse_json_file(DATA_DIR.join("characteristics.json"));
characteristic_database.populate_from_json(&characteristics);
}
pub fn parse_json_file<P: AsRef<Path>>(path: P) -> JsonValue {
json::parse(fs::read_to_string(path).unwrap().as_ref()).unwrap()
}

30
src/inspector.rs Normal file
View File

@ -0,0 +1,30 @@
use bevy::prelude::*;
use crate::{database::Database, pokemon::*};
pub struct InspectorPlugin;
impl Plugin for InspectorPlugin {
fn build(&self, app: &mut App) {
app.register_type::<InspectorPokemon>()
.insert_resource(InspectorPokemon::default())
.add_startup_system(resource_setup);
}
}
#[derive(Reflect, Resource, Default, Debug, Clone)]
#[reflect(Resource)]
pub struct InspectorPokemon {
pub pokemon: Option<Pokemon>,
pub nature: Option<Nature>,
pub characteristic: Option<Characteristic>,
}
fn resource_setup(
mut inspector: ResMut<InspectorPokemon>,
pokemon: Res<Database<Pokemon>>,
natures: Res<Database<Nature>>,
) {
inspector.pokemon = pokemon.map.get("sceptile").cloned();
inspector.nature = natures.map.get("adamant").cloned();
}

27
src/main.rs Normal file
View File

@ -0,0 +1,27 @@
use bevy::{prelude::*, window::WindowResolution};
pub mod database;
pub mod inspector;
pub mod pokemon;
pub mod ui;
fn main() {
App::new()
.add_plugins(
DefaultPlugins
.set(WindowPlugin {
primary_window: Some(Window {
resolution: WindowResolution::new(720., 540.),
resizable: false,
title: "Smart IV calculator".to_string(),
..default()
}),
..default()
})
.set(ImagePlugin::default_nearest()),
)
.add_plugin(database::DatabasePlugin)
.add_plugin(inspector::InspectorPlugin)
.add_plugin(ui::UiPlugin)
.run();
}

211
src/pokemon.rs Normal file
View File

@ -0,0 +1,211 @@
use bevy::reflect::{FromReflect, Reflect};
use lazy_static::lazy_static;
use std::{collections::HashMap, fmt};
use crate::database::TYPE_MAP;
pub type BaseValue = u8;
lazy_static! {
pub static ref BASESTAT_MAP: HashMap<String, BaseStat> = {
let mut map = HashMap::new();
map.insert(String::from("hp"), BaseStat::Hp);
map.insert(String::from("attack"), BaseStat::Attack);
map.insert(String::from("defense"), BaseStat::Defense);
map.insert(String::from("special_attack"), BaseStat::SpecialAttack);
map.insert(String::from("special_defense"), BaseStat::SpecialDefense);
map.insert(String::from("speed"), BaseStat::Speed);
map
};
}
pub trait FromJson {
fn from_json(json: &json::JsonValue) -> Option<Self>
where
Self: Sized;
}
pub trait GetKey {
fn alias(&self) -> String;
}
#[derive(Default, Debug, Clone)]
pub struct DerivedStats {
pub level: Option<i32>,
pub stats: HashMap<BaseStat, i32>,
}
#[derive(Default, Debug, Clone, Reflect, FromReflect)]
pub struct Pokemon {
pub id: u16,
pub national: u16,
pub name: String,
pub alias: String,
pub full_name: String,
pub image: String,
pub form: String,
pub form_alias: String,
pub gen_id: u8,
pub type1: Option<Type>,
pub type2: Option<Type>,
pub hp: BaseValue,
pub attack: BaseValue,
pub defense: BaseValue,
pub special_attack: BaseValue,
pub special_defense: BaseValue,
pub speed: BaseValue,
}
impl Pokemon {
pub fn base_value(&self, stat: BaseStat) -> BaseValue {
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,
}
}
}
impl FromJson for Pokemon {
fn from_json(json: &json::JsonValue) -> Option<Self> {
Some(Self {
id: json["id"].as_u16()?,
national: json["national"].as_u16()?,
name: json["name"].as_str()?.to_string(),
alias: json["alias"].as_str()?.to_string(),
full_name: json["full_name"].as_str()?.to_string(),
image: json["image"].as_str()?.to_string(),
form: json["form"].as_str()?.to_string(),
form_alias: json["form_alias"].as_str()?.to_string(),
gen_id: json["gen_id"].as_u8()?,
type1: json["type1"]
.as_u8()
.map_or(None, |value| TYPE_MAP.get(&value).cloned()),
type2: json["type2"]
.as_u8()
.map_or(None, |value| TYPE_MAP.get(&value).cloned()),
hp: json["hp"].as_u8()?,
attack: json["attack"].as_u8()?,
defense: json["defense"].as_u8()?,
special_attack: json["special_attack"].as_u8()?,
special_defense: json["special_defense"].as_u8()?,
speed: json["speed"].as_u8()?,
})
}
}
impl GetKey for Pokemon {
fn alias(&self) -> String {
self.alias.clone()
}
}
#[derive(Debug, Clone, Copy, Reflect, FromReflect, Default, PartialEq)]
pub enum BaseStat {
#[default]
Hp,
Attack,
Defense,
SpecialAttack,
SpecialDefense,
Speed,
}
impl fmt::Display for BaseStat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name = match self {
BaseStat::Hp => "HP",
BaseStat::Attack => "Attack",
BaseStat::Defense => "Defense",
BaseStat::SpecialAttack => "Sp.Atk",
BaseStat::SpecialDefense => "Sp.Def",
BaseStat::Speed => "Speed",
};
write!(f, "{}", name)
}
}
#[derive(Default, Debug, Clone, Reflect, FromReflect)]
pub struct Type {
pub id: u8,
pub name: String,
pub alias: String,
}
impl FromJson for Type {
fn from_json(json: &json::JsonValue) -> Option<Self> {
Some(Self {
id: json["id"].as_u8()?,
name: json["name"].as_str()?.to_string(),
alias: json["alias"].as_str()?.to_string(),
})
}
}
impl GetKey for Type {
fn alias(&self) -> String {
self.alias.clone()
}
}
#[derive(Default, Debug, Clone, Reflect, FromReflect)]
pub struct Nature {
pub id: u8,
pub name: String,
pub alias: String,
pub increased: Option<BaseStat>,
pub decreased: Option<BaseStat>,
}
impl FromJson for Nature {
fn from_json(json: &json::JsonValue) -> Option<Self> {
Some(Self {
id: json["id"].as_u8()?,
name: json["name"].as_str()?.to_string(),
alias: json["alias"].as_str()?.to_string(),
increased: json["increased"]
.as_str()
.map_or(None, |value| BASESTAT_MAP.get(value).copied()),
decreased: json["decreased"]
.as_str()
.map_or(None, |value| BASESTAT_MAP.get(value).copied()),
})
}
}
impl GetKey for Nature {
fn alias(&self) -> String {
self.alias.clone()
}
}
#[derive(Default, Debug, Clone, Reflect, FromReflect)]
pub struct Characteristic {
stat: BaseStat,
name: String,
possible_values: Vec<u8>,
}
impl FromJson for Characteristic {
fn from_json(json: &json::JsonValue) -> Option<Self> {
Some(Self {
stat: json["stat"].as_str().map_or(BaseStat::default(), |value| {
BASESTAT_MAP.get(value).cloned().unwrap_or_default()
}),
name: json["name"].as_str()?.to_string(),
possible_values: json["possible_values"]
.members()
.map_while(|value| value.as_u8())
.collect(),
})
}
}
impl GetKey for Characteristic {
fn alias(&self) -> String {
self.name.to_lowercase()
}
}

163
src/ui.rs Normal file
View File

@ -0,0 +1,163 @@
use bevy::prelude::*;
use bevy_egui::{egui, EguiContexts, EguiPlugin};
use lazy_static::lazy_static;
use crate::{database::Database, inspector::InspectorPokemon, pokemon::*};
lazy_static! {
static ref BASE_STAT_COLOR_RANGES: Vec<(u8, egui::Color32)> = vec![
(0, egui::Color32::from_rgb(255, 0, 0)),
(30, egui::Color32::from_rgb(255, 128, 0)),
(60, egui::Color32::from_rgb(255, 255, 0)),
(90, egui::Color32::from_rgb(128, 255, 0)),
(120, egui::Color32::from_rgb(0, 255, 0)),
(150, egui::Color32::from_rgb(0, 255, 128)),
(180, egui::Color32::from_rgb(0, 255, 255)),
];
}
pub struct UiPlugin;
impl Plugin for UiPlugin {
fn build(&self, app: &mut App) {
app.add_plugin(EguiPlugin)
.insert_resource(UiAssets::default())
.insert_resource(UiState::default())
.add_startup_system(load_assets)
.add_system(handle_state)
.add_system(ui_system);
}
}
#[derive(Resource, Default, Reflect)]
#[reflect(Resource)]
pub struct UiAssets {
bar_handle: Handle<Image>,
}
#[derive(Resource, Default, Reflect)]
#[reflect(Resource)]
pub struct UiState {
name: String,
nature: String,
}
fn load_assets(mut ui_assets: ResMut<UiAssets>, assets: Res<AssetServer>) {
ui_assets.bar_handle = assets.load("ui/bar.png");
}
fn handle_state(
ui_state: Res<UiState>,
pokemon: Res<Database<Pokemon>>,
nature: Res<Database<Nature>>,
mut inspector: ResMut<InspectorPokemon>,
) {
if ui_state.is_changed() {
if let Some(pokemon) = pokemon.map.get(ui_state.name.to_lowercase().as_str()) {
inspector.pokemon = Some(pokemon.clone());
}
if let Some(nature) = nature.map.get(ui_state.nature.to_lowercase().as_str()) {
inspector.nature = Some(nature.clone());
}
}
}
fn ui_system(
mut contexts: EguiContexts,
inspector: Res<InspectorPokemon>,
ui_assets: Res<UiAssets>,
mut ui_state: ResMut<UiState>,
mut rendered_texture_id: Local<egui::TextureId>,
mut is_initialized: Local<bool>,
) {
if !*is_initialized {
*is_initialized = true;
*rendered_texture_id = contexts.add_image(ui_assets.bar_handle.clone_weak());
}
egui::CentralPanel::default().show(contexts.ctx_mut(), |ui| {
ui.horizontal(|ui| {
ui.horizontal(|ui| {
ui.set_width(60.0);
ui.label("pokemon:");
});
ui.horizontal(|ui| {
ui.set_width(150.0);
ui.text_edit_singleline(&mut ui_state.name);
});
});
ui.horizontal(|ui| {
ui.horizontal(|ui| {
ui.set_width(60.0);
ui.label("nature:");
});
ui.horizontal(|ui| {
ui.set_width(150.0);
ui.text_edit_singleline(&mut ui_state.nature);
});
});
if let Some(pokemon) = inspector.pokemon.as_ref() {
ui.heading(egui::RichText::new(pokemon.name.clone()));
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())
));
for stat in vec![
BaseStat::Hp,
BaseStat::Attack,
BaseStat::Defense,
BaseStat::SpecialAttack,
BaseStat::SpecialDefense,
BaseStat::Speed,
] {
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.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);
});
});
}
}
});
}