Compare commits
10 Commits
635e67d6c7
...
09f28d28f5
| Author | SHA1 | Date |
|---|---|---|
|
|
09f28d28f5 | |
|
|
a8fd4e8ba1 | |
|
|
b071b55a58 | |
|
|
2917ba457e | |
|
|
642cd9a76a | |
|
|
d26da8776b | |
|
|
0395cbc942 | |
|
|
2b0ba06bf1 | |
|
|
baf64181ec | |
|
|
ffbcea507b |
|
|
@ -2557,6 +2557,16 @@ dependencies = [
|
||||||
"windows-sys 0.42.0",
|
"windows-sys 0.42.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "notify-debouncer-mini"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e23e9fa24f094b143c1eb61f90ac6457de87be6987bc70746e0179f7dbc9007b"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-channel",
|
||||||
|
"notify",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ntapi"
|
name = "ntapi"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
|
|
@ -3293,6 +3303,8 @@ dependencies = [
|
||||||
"bevy_egui",
|
"bevy_egui",
|
||||||
"json",
|
"json",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
|
"notify",
|
||||||
|
"notify-debouncer-mini",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ bevy = "0.10.0"
|
||||||
bevy_egui = "0.20.1"
|
bevy_egui = "0.20.1"
|
||||||
json = "0.12.4"
|
json = "0.12.4"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
|
notify = "5.1.0"
|
||||||
|
notify-debouncer-mini = "0.2.1"
|
||||||
reqwest = { version = "0.11.14", features = ["blocking", "json"] }
|
reqwest = { version = "0.11.14", features = ["blocking", "json"] }
|
||||||
|
|
||||||
# Enable a small amount of optimization in debug mode
|
# Enable a small amount of optimization in debug mode
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,12 +1,13 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::{
|
||||||
|
fs,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use json::JsonValue;
|
use json::JsonValue;
|
||||||
|
|
||||||
use crate::{
|
use crate::{json::*, pokemon::*};
|
||||||
json::{read_characteristics, read_natures, read_pokedex},
|
|
||||||
pokemon::*,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct DatabasePlugin;
|
pub struct DatabasePlugin;
|
||||||
|
|
||||||
|
|
@ -15,6 +16,7 @@ impl Plugin for DatabasePlugin {
|
||||||
app.insert_resource(Database::<Pokemon>::default())
|
app.insert_resource(Database::<Pokemon>::default())
|
||||||
.insert_resource(Database::<Nature>::default())
|
.insert_resource(Database::<Nature>::default())
|
||||||
.insert_resource(Database::<Characteristic>::default())
|
.insert_resource(Database::<Characteristic>::default())
|
||||||
|
.insert_resource(Database::<Ability>::default())
|
||||||
.add_startup_system(database_setup);
|
.add_startup_system(database_setup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -42,10 +44,26 @@ where
|
||||||
|
|
||||||
fn database_setup(
|
fn database_setup(
|
||||||
mut pokemon_database: ResMut<Database<Pokemon>>,
|
mut pokemon_database: ResMut<Database<Pokemon>>,
|
||||||
|
mut ability_database: ResMut<Database<Ability>>,
|
||||||
mut nature_database: ResMut<Database<Nature>>,
|
mut nature_database: ResMut<Database<Nature>>,
|
||||||
mut characteristic_database: ResMut<Database<Characteristic>>,
|
mut characteristic_database: ResMut<Database<Characteristic>>,
|
||||||
) {
|
) {
|
||||||
|
ability_database.populate_from_json(&read_abilities());
|
||||||
nature_database.populate_from_json(&read_natures());
|
nature_database.populate_from_json(&read_natures());
|
||||||
pokemon_database.populate_from_json(&read_pokedex());
|
pokemon_database.populate_from_json(&read_pokedex());
|
||||||
characteristic_database.populate_from_json(&read_characteristics());
|
characteristic_database.populate_from_json(&read_characteristics());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn load_url_asset(
|
||||||
|
url: String,
|
||||||
|
path: PathBuf,
|
||||||
|
assets: &Res<AssetServer>,
|
||||||
|
) -> Result<Handle<Image>, Box<dyn std::error::Error>> {
|
||||||
|
let system_path = Path::new("assets").join(&path);
|
||||||
|
if Path::exists(&system_path) {
|
||||||
|
return Ok(assets.load(path));
|
||||||
|
}
|
||||||
|
let data = reqwest::blocking::get(&url)?.bytes()?;
|
||||||
|
fs::write(&system_path, data).unwrap();
|
||||||
|
Ok(assets.load(path))
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,3 @@
|
||||||
use std::{
|
|
||||||
fs,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
};
|
|
||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy_egui::EguiContexts;
|
use bevy_egui::EguiContexts;
|
||||||
|
|
||||||
|
|
@ -112,17 +107,3 @@ fn request_handler(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_url_asset(
|
|
||||||
url: String,
|
|
||||||
path: PathBuf,
|
|
||||||
assets: &Res<AssetServer>,
|
|
||||||
) -> Result<Handle<Image>, Box<dyn std::error::Error>> {
|
|
||||||
let system_path = Path::new("assets").join(&path);
|
|
||||||
if Path::exists(&path) {
|
|
||||||
return Ok(assets.load(path));
|
|
||||||
}
|
|
||||||
let data = reqwest::blocking::get(&url)?.bytes()?;
|
|
||||||
fs::write(&system_path, data).unwrap();
|
|
||||||
Ok(assets.load(path))
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
use bevy::{prelude::*, window::WindowResolution};
|
use bevy::{prelude::*, window::WindowResolution};
|
||||||
|
|
||||||
|
mod battle;
|
||||||
mod inspector;
|
mod inspector;
|
||||||
|
mod memory_reader;
|
||||||
mod ui;
|
mod ui;
|
||||||
|
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
|
|
@ -19,6 +21,8 @@ pub fn run() {
|
||||||
.set(ImagePlugin::default_nearest()),
|
.set(ImagePlugin::default_nearest()),
|
||||||
)
|
)
|
||||||
.add_plugin(crate::database::DatabasePlugin)
|
.add_plugin(crate::database::DatabasePlugin)
|
||||||
|
.add_plugin(inspector::InspectorPlugin)
|
||||||
.add_plugin(ui::UiPlugin)
|
.add_plugin(ui::UiPlugin)
|
||||||
|
.add_plugin(memory_reader::MemoryReaderPlugin)
|
||||||
.run()
|
.run()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,257 @@
|
||||||
|
use super::inspector::{Battle, ParsedPokemonInstance};
|
||||||
|
use crate::pokemon::*;
|
||||||
|
|
||||||
|
const PLAYER_PARTY_OFFSET: usize = 0x21E42C;
|
||||||
|
const ENEMY_PARTY_OFFSET: usize = 0x258874;
|
||||||
|
const POKEMON_SIZE: usize = 0xDC;
|
||||||
|
const PARTY_MAX_SIZE: usize = 6;
|
||||||
|
const BLOCK_SIZE: usize = 0x20;
|
||||||
|
|
||||||
|
/// All the block offsets for table A (growth) based on pokemon's shift value
|
||||||
|
const OFFSET_TABLE_A: [usize; 24] = [
|
||||||
|
0, 0, 0, 0, 0, 0, 1, 1, 2, 3, 2, 3, 1, 1, 2, 3, 2, 3, 1, 1, 2, 3, 2, 3,
|
||||||
|
];
|
||||||
|
/// All the block offsets for table B (attack) based on pokemon's shift value
|
||||||
|
const OFFSET_TABLE_B: [usize; 24] = [
|
||||||
|
1, 1, 2, 3, 2, 3, 0, 0, 0, 0, 0, 0, 2, 3, 1, 1, 3, 2, 2, 3, 1, 1, 3, 2,
|
||||||
|
];
|
||||||
|
/// All the block offsets for table C (effort) based on pokemon's shift value
|
||||||
|
const OFFSET_TABLE_C: [usize; 24] = [
|
||||||
|
2, 3, 1, 1, 3, 2, 2, 3, 1, 1, 3, 2, 0, 0, 0, 0, 0, 0, 3, 2, 3, 2, 1, 1,
|
||||||
|
];
|
||||||
|
/// All the block offsets for table D (misc) based on pokemon's shift value
|
||||||
|
const OFFSET_TABLE_D: [usize; 24] = [
|
||||||
|
3, 2, 3, 2, 1, 1, 3, 2, 3, 2, 1, 1, 3, 2, 3, 2, 1, 1, 0, 0, 0, 0, 0, 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
struct Header {
|
||||||
|
pid: u32,
|
||||||
|
checksum: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_battle_party(data: &Vec<u8>) -> Result<Battle<ParsedPokemonInstance>, String> {
|
||||||
|
let mut battle = Battle::default();
|
||||||
|
for i in 0..PARTY_MAX_SIZE {
|
||||||
|
if let Some(instance) = try_parse(data, PLAYER_PARTY_OFFSET + i * POKEMON_SIZE) {
|
||||||
|
battle.player_party.push(instance);
|
||||||
|
}
|
||||||
|
if let Some(instance) = try_parse(data, ENEMY_PARTY_OFFSET + i * POKEMON_SIZE) {
|
||||||
|
battle.enemy_party.push(instance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(battle)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_parse(data: &[u8], offset: usize) -> Option<ParsedPokemonInstance> {
|
||||||
|
let encrypted = &data[offset..offset + POKEMON_SIZE];
|
||||||
|
if !contains_pokemon(encrypted) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let decrypted = decrypt_pokemon(encrypted).ok()?;
|
||||||
|
println!("{}", debug_pokemon_data(&decrypted));
|
||||||
|
let pokemon = parse_pokemon(&decrypted).ok()?;
|
||||||
|
Some(pokemon)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decrypts and unshuffles pokemon data
|
||||||
|
fn decrypt_pokemon(encrypted: &[u8]) -> Result<Vec<u8>, String> {
|
||||||
|
validate_size(encrypted)?;
|
||||||
|
|
||||||
|
// Decrypt data
|
||||||
|
const PRNG_ADD: u32 = 0x6073;
|
||||||
|
const PRNG_MULT: u32 = 0x41C64E6D;
|
||||||
|
|
||||||
|
let mut decrypted = vec![0; POKEMON_SIZE];
|
||||||
|
let header = read_header(encrypted)?;
|
||||||
|
|
||||||
|
// First 8 bytes are not encrypted
|
||||||
|
for i in 0x00..0x08 {
|
||||||
|
decrypted[i] = encrypted[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut prng = header.checksum as u32;
|
||||||
|
for i in (0x08..0x88).step_by(2) {
|
||||||
|
prng = prng.wrapping_mul(PRNG_MULT).wrapping_add(PRNG_ADD);
|
||||||
|
let decrypted_bytes = (read_short(encrypted, i) ^ (prng >> 16) as u16).to_le_bytes();
|
||||||
|
decrypted[i] = decrypted_bytes[0];
|
||||||
|
decrypted[i + 1] = decrypted_bytes[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut prng = header.pid as u32;
|
||||||
|
for i in (0x88..0xDC).step_by(2) {
|
||||||
|
prng = prng.wrapping_mul(PRNG_MULT).wrapping_add(PRNG_ADD);
|
||||||
|
let decrypted_bytes = (read_short(encrypted, i) ^ (prng >> 16) as u16).to_le_bytes();
|
||||||
|
decrypted[i] = decrypted_bytes[0];
|
||||||
|
decrypted[i + 1] = decrypted_bytes[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reverse block shuffling
|
||||||
|
const SHUFFLE_AND: u32 = 0x3E000;
|
||||||
|
const SHUFFLE_RSHIFT: u32 = 0xD;
|
||||||
|
|
||||||
|
let shift = ((header.pid & SHUFFLE_AND) >> SHUFFLE_RSHIFT) % 24;
|
||||||
|
let offset_a: usize = OFFSET_TABLE_A[shift as usize] * BLOCK_SIZE + 0x08;
|
||||||
|
let offset_b: usize = OFFSET_TABLE_B[shift as usize] * BLOCK_SIZE + 0x08;
|
||||||
|
let offset_c: usize = OFFSET_TABLE_C[shift as usize] * BLOCK_SIZE + 0x08;
|
||||||
|
let offset_d: usize = OFFSET_TABLE_D[shift as usize] * BLOCK_SIZE + 0x08;
|
||||||
|
|
||||||
|
let mut unshuffled = decrypted.clone();
|
||||||
|
|
||||||
|
for i in 0..BLOCK_SIZE {
|
||||||
|
unshuffled[0x08 + 0 * BLOCK_SIZE + i] = decrypted[offset_a + i];
|
||||||
|
}
|
||||||
|
for i in 0..BLOCK_SIZE {
|
||||||
|
unshuffled[0x08 + 1 * BLOCK_SIZE + i] = decrypted[offset_b + i];
|
||||||
|
}
|
||||||
|
for i in 0..BLOCK_SIZE {
|
||||||
|
unshuffled[0x08 + 2 * BLOCK_SIZE + i] = decrypted[offset_c + i];
|
||||||
|
}
|
||||||
|
for i in 0..BLOCK_SIZE {
|
||||||
|
unshuffled[0x08 + 3 * BLOCK_SIZE + i] = decrypted[offset_d + i];
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(unshuffled)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_pokemon(data: &[u8]) -> Result<ParsedPokemonInstance, String> {
|
||||||
|
validate_size(data)?;
|
||||||
|
|
||||||
|
let mut instance = ParsedPokemonInstance::default();
|
||||||
|
|
||||||
|
let offset_a: usize = 0x08 + 0 * BLOCK_SIZE;
|
||||||
|
let offset_b: usize = 0x08 + 1 * BLOCK_SIZE;
|
||||||
|
let offset_c: usize = 0x08 + 2 * BLOCK_SIZE;
|
||||||
|
let offset_d: usize = 0x08 + 3 * BLOCK_SIZE;
|
||||||
|
|
||||||
|
instance.pokemon = read_short(data, offset_a + 0x00);
|
||||||
|
instance.ability = read_byte(data, offset_a + 0x0D);
|
||||||
|
|
||||||
|
instance
|
||||||
|
.evs
|
||||||
|
.insert(BaseStat::Hp, read_byte(data, offset_a + 0x10));
|
||||||
|
instance
|
||||||
|
.evs
|
||||||
|
.insert(BaseStat::Attack, read_byte(data, offset_a + 0x11));
|
||||||
|
instance
|
||||||
|
.evs
|
||||||
|
.insert(BaseStat::Defense, read_byte(data, offset_a + 0x12));
|
||||||
|
instance
|
||||||
|
.evs
|
||||||
|
.insert(BaseStat::Speed, read_byte(data, offset_a + 0x13));
|
||||||
|
instance
|
||||||
|
.evs
|
||||||
|
.insert(BaseStat::SpecialAttack, read_byte(data, offset_a + 0x14));
|
||||||
|
instance
|
||||||
|
.evs
|
||||||
|
.insert(BaseStat::SpecialDefense, read_byte(data, offset_a + 0x15));
|
||||||
|
|
||||||
|
let ivs = read_long(data, offset_b + 0x10);
|
||||||
|
instance.ivs.insert(BaseStat::Hp, ((ivs >> 0) & 31) as u8);
|
||||||
|
instance
|
||||||
|
.ivs
|
||||||
|
.insert(BaseStat::Attack, ((ivs >> 5) & 31) as u8);
|
||||||
|
instance
|
||||||
|
.ivs
|
||||||
|
.insert(BaseStat::Defense, ((ivs >> 10) & 31) as u8);
|
||||||
|
instance
|
||||||
|
.ivs
|
||||||
|
.insert(BaseStat::Speed, ((ivs >> 15) & 31) as u8);
|
||||||
|
instance
|
||||||
|
.ivs
|
||||||
|
.insert(BaseStat::SpecialAttack, ((ivs >> 20) & 31) as u8);
|
||||||
|
instance
|
||||||
|
.ivs
|
||||||
|
.insert(BaseStat::SpecialDefense, ((ivs >> 25) & 31) as u8);
|
||||||
|
|
||||||
|
instance.nature = read_byte(data, offset_b + 0x19);
|
||||||
|
|
||||||
|
Ok(instance)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_header(data: &[u8]) -> Result<Header, String> {
|
||||||
|
validate_size(data)?;
|
||||||
|
Ok(Header {
|
||||||
|
pid: read_long(data, 0),
|
||||||
|
checksum: read_short(data, 6),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the raw data contains a pokemon.
|
||||||
|
/// If the header section (8 bytes) are all 0x00, then the data doens't contain a pokemon.
|
||||||
|
/// This works with both encrypted and decryped forms, as the header is never encrypted.
|
||||||
|
fn contains_pokemon(data: &[u8]) -> bool {
|
||||||
|
if let Err(_) = validate_size(data) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
read_long(data, 0) != 0 || read_long(data, 4) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_size(data: &[u8]) -> Result<(), String> {
|
||||||
|
if data.len() != POKEMON_SIZE {
|
||||||
|
return Err(format!(
|
||||||
|
"Invalid data size. expected: {POKEMON_SIZE} - actual: {}",
|
||||||
|
data.len()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a 1 byte value
|
||||||
|
fn read_byte(data: &[u8], offset: usize) -> u8 {
|
||||||
|
data[offset]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a little-endian 2 byte value
|
||||||
|
fn read_short(data: &[u8], offset: usize) -> u16 {
|
||||||
|
data[offset] as u16 | ((data[offset + 1] as u16) << 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a little-endian 4 byte value
|
||||||
|
fn read_long(data: &[u8], offset: usize) -> u32 {
|
||||||
|
data[offset] as u32
|
||||||
|
| ((data[offset + 1] as u32) << 8)
|
||||||
|
| ((data[offset + 2] as u32) << 16)
|
||||||
|
| ((data[offset + 3] as u32) << 24)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn debug_pokemon_data(data: &[u8]) -> String {
|
||||||
|
let mut result = String::new();
|
||||||
|
let mut offset = 0;
|
||||||
|
for (address, value) in data.iter().enumerate() {
|
||||||
|
offset = match address {
|
||||||
|
0x00 => {
|
||||||
|
result += "---------header-------------\n";
|
||||||
|
0x00
|
||||||
|
}
|
||||||
|
0x08 => {
|
||||||
|
result += "---------block A (growth)---\n";
|
||||||
|
0x08
|
||||||
|
}
|
||||||
|
0x28 => {
|
||||||
|
result += "---------block B (effort)---\n";
|
||||||
|
0x28
|
||||||
|
}
|
||||||
|
0x48 => {
|
||||||
|
result += "---------block C (battle)---\n";
|
||||||
|
0x48
|
||||||
|
}
|
||||||
|
0x68 => {
|
||||||
|
result += "---------block D (misc)-----\n";
|
||||||
|
0x68
|
||||||
|
}
|
||||||
|
0x88 => {
|
||||||
|
result += "---------battle-------------\n";
|
||||||
|
0x88
|
||||||
|
}
|
||||||
|
_ => offset,
|
||||||
|
};
|
||||||
|
if address % 4 == 0 {
|
||||||
|
result += format!("[ {:#06X} :: {:#06X} ]", address, address - offset).as_str();
|
||||||
|
}
|
||||||
|
result += format!(" {value:#04X}").as_str();
|
||||||
|
if address % 4 == 3 {
|
||||||
|
result += "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
@ -1,34 +1,130 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
use bevy_egui::EguiContexts;
|
||||||
|
|
||||||
use crate::pokemon::*;
|
use crate::{
|
||||||
|
database::{load_url_asset, Database},
|
||||||
|
pokemon::*,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::ui::{EguiAsset, UiAssets};
|
||||||
|
|
||||||
pub struct InspectorPlugin;
|
pub struct InspectorPlugin;
|
||||||
|
|
||||||
impl Plugin for InspectorPlugin {
|
impl Plugin for InspectorPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
app.insert_resource(Inspector::default());
|
app.insert_resource(Inspector::default())
|
||||||
|
.add_system(state_changed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Resource, Default)]
|
#[derive(Resource, Default)]
|
||||||
pub struct Inspector {
|
pub struct Inspector {
|
||||||
pub allies: Vec<PokemonInstance>,
|
pub battle: Battle<PokemonInstance>,
|
||||||
pub enemies: Vec<PokemonInstance>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Inspector {
|
#[derive(Default, Clone)]
|
||||||
pub fn clear(&mut self) {
|
pub struct Battle<T> {
|
||||||
self.allies.clear();
|
pub player_party: Vec<T>,
|
||||||
self.enemies.clear();
|
pub enemy_party: Vec<T>,
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default, Clone)]
|
||||||
pub struct PokemonInstance {
|
pub struct PokemonInstance {
|
||||||
pub pokemon: Pokemon,
|
pub pokemon: Pokemon,
|
||||||
|
pub ability: Option<Ability>,
|
||||||
pub nature: Option<Nature>,
|
pub nature: Option<Nature>,
|
||||||
pub ivs: HashMap<BaseStat, u8>,
|
pub ivs: HashMap<BaseStat, u8>,
|
||||||
pub evs: HashMap<BaseStat, u8>,
|
pub evs: HashMap<BaseStat, u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PokemonInstance {
|
||||||
|
pub fn from_parsed(
|
||||||
|
parsed: &ParsedPokemonInstance,
|
||||||
|
pokedex: &Res<Database<Pokemon>>,
|
||||||
|
natures: &Res<Database<Nature>>,
|
||||||
|
abilities: &Res<Database<Ability>>,
|
||||||
|
) -> Result<Self, String> {
|
||||||
|
let mut instance = PokemonInstance::default();
|
||||||
|
|
||||||
|
// pokemon data
|
||||||
|
if let Some(pokemon) = pokedex
|
||||||
|
.map
|
||||||
|
.values()
|
||||||
|
.find(|pokemon| pokemon.national == parsed.pokemon)
|
||||||
|
{
|
||||||
|
instance.pokemon = pokemon.clone();
|
||||||
|
} else {
|
||||||
|
return Err(format!("National dex not found: {}", parsed.pokemon));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ability data
|
||||||
|
if let Some(ability) = abilities
|
||||||
|
.map
|
||||||
|
.values()
|
||||||
|
.find(|ability| ability.id == parsed.ability as u16)
|
||||||
|
{
|
||||||
|
instance.ability = Some(ability.clone());
|
||||||
|
} else {
|
||||||
|
return Err(format!("Ability id not found: {}", parsed.ability));
|
||||||
|
}
|
||||||
|
|
||||||
|
// nature data
|
||||||
|
if let Some(nature) = natures
|
||||||
|
.map
|
||||||
|
.values()
|
||||||
|
.find(|nature| nature.pid == parsed.nature)
|
||||||
|
{
|
||||||
|
instance.nature = Some(nature.clone());
|
||||||
|
} else {
|
||||||
|
return Err(format!("Nature pid not found: {}", parsed.nature));
|
||||||
|
}
|
||||||
|
|
||||||
|
instance.ivs = parsed.ivs.clone();
|
||||||
|
instance.evs = parsed.evs.clone();
|
||||||
|
|
||||||
|
Ok(instance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
pub struct ParsedPokemonInstance {
|
||||||
|
pub pokemon: u16,
|
||||||
|
pub ability: u8,
|
||||||
|
pub nature: u8,
|
||||||
|
pub ivs: HashMap<BaseStat, u8>,
|
||||||
|
pub evs: HashMap<BaseStat, u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state_changed(
|
||||||
|
inspector: Res<Inspector>,
|
||||||
|
assets: Res<AssetServer>,
|
||||||
|
mut ui_assets: ResMut<UiAssets>,
|
||||||
|
mut contexts: EguiContexts,
|
||||||
|
) {
|
||||||
|
if inspector.is_changed() {
|
||||||
|
for instance in inspector
|
||||||
|
.battle
|
||||||
|
.enemy_party
|
||||||
|
.iter()
|
||||||
|
.chain(inspector.battle.player_party.iter())
|
||||||
|
{
|
||||||
|
let pokemon = instance.pokemon.clone();
|
||||||
|
|
||||||
|
if !ui_assets.sprite_map.contains_key(&pokemon.key()) {
|
||||||
|
let handle = load_url_asset(
|
||||||
|
pokemon.sprite_url(),
|
||||||
|
format!("cache/{name}.png", name = pokemon.key()).into(),
|
||||||
|
&assets,
|
||||||
|
);
|
||||||
|
if let Ok(handle) = handle {
|
||||||
|
let weak = handle.clone_weak();
|
||||||
|
ui_assets
|
||||||
|
.sprite_map
|
||||||
|
.insert(pokemon.key(), EguiAsset(handle, contexts.add_image(weak)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use notify_debouncer_mini::new_debouncer;
|
||||||
|
use std::{
|
||||||
|
fs,
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
thread,
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{database::Database, pokemon::*};
|
||||||
|
|
||||||
|
use super::{battle::parse_battle_party, inspector::*};
|
||||||
|
|
||||||
|
pub struct MemoryReaderPlugin;
|
||||||
|
|
||||||
|
impl Plugin for MemoryReaderPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_event::<BattleEvent>()
|
||||||
|
.insert_resource(EventQueue::default())
|
||||||
|
.add_startup_system(init_memory_reader)
|
||||||
|
.add_system(flush_updates)
|
||||||
|
.add_system(update_inspector);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct BattleEvent(Battle<PokemonInstance>);
|
||||||
|
|
||||||
|
#[derive(Resource, Default)]
|
||||||
|
struct EventQueue {
|
||||||
|
updates: Arc<Mutex<Vec<Battle<ParsedPokemonInstance>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush_updates(
|
||||||
|
queue: Res<EventQueue>,
|
||||||
|
pokedex: Res<Database<Pokemon>>,
|
||||||
|
abilities: Res<Database<Ability>>,
|
||||||
|
natures: Res<Database<Nature>>,
|
||||||
|
mut events: EventWriter<BattleEvent>,
|
||||||
|
) {
|
||||||
|
match queue.updates.lock() {
|
||||||
|
Ok(mut updates) => {
|
||||||
|
for update in updates.drain(..) {
|
||||||
|
let mut battle = Battle::default();
|
||||||
|
|
||||||
|
for parsed in update.player_party.iter() {
|
||||||
|
match PokemonInstance::from_parsed(parsed, &pokedex, &natures, &abilities) {
|
||||||
|
Ok(instance) => battle.player_party.push(instance),
|
||||||
|
Err(err) => warn!("{err}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for parsed in update.enemy_party.iter() {
|
||||||
|
match PokemonInstance::from_parsed(parsed, &pokedex, &natures, &abilities) {
|
||||||
|
Ok(instance) => battle.enemy_party.push(instance),
|
||||||
|
Err(err) => warn!("{err}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
events.send(BattleEvent(battle))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!("{err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_inspector(mut inspector: ResMut<Inspector>, mut events: EventReader<BattleEvent>) {
|
||||||
|
for event in events.iter() {
|
||||||
|
inspector.battle = event.0.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_memory_reader(queue: Res<EventQueue>) {
|
||||||
|
let updates = queue.updates.clone();
|
||||||
|
|
||||||
|
thread::spawn(move || {
|
||||||
|
let watch_path = std::path::Path::new("/tmp/ivpeek/");
|
||||||
|
fs::create_dir_all(watch_path).expect("Failed to create path for {watch_path:?}");
|
||||||
|
|
||||||
|
let (tx, rx) = std::sync::mpsc::channel();
|
||||||
|
|
||||||
|
let mut debouncer = new_debouncer(Duration::from_millis(200), None, tx)
|
||||||
|
.expect("Could not create notify debouncer");
|
||||||
|
|
||||||
|
debouncer
|
||||||
|
.watcher()
|
||||||
|
.watch(watch_path, notify::RecursiveMode::NonRecursive)
|
||||||
|
.expect("Could not watch for {watch_path:?}");
|
||||||
|
|
||||||
|
for result in rx {
|
||||||
|
match result {
|
||||||
|
Ok(events) => {
|
||||||
|
for event in events.iter() {
|
||||||
|
match fs::read(&event.path) {
|
||||||
|
Ok(data) => match parse_battle_party(&data) {
|
||||||
|
Ok(battle) => match updates.lock() {
|
||||||
|
Ok(mut updates) => updates.push(battle),
|
||||||
|
Err(err) => error!("{err}"),
|
||||||
|
},
|
||||||
|
Err(err) => error!("Failed to parse party data: ${err}"),
|
||||||
|
},
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => error!("Watcher error: {err:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
281
src/ivpeek/ui.rs
281
src/ivpeek/ui.rs
|
|
@ -3,14 +3,16 @@ use bevy_egui::{egui, EguiContexts, EguiPlugin};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::{database::Database, pokemon::*};
|
use crate::pokemon::*;
|
||||||
|
|
||||||
use super::inspector::{Inspector, PokemonInstance};
|
use super::inspector::{Inspector, PokemonInstance};
|
||||||
|
|
||||||
const BAR_HEIGHT: f32 = 12.0;
|
const BAR_HEIGHT: f32 = 12.0;
|
||||||
const BASE_STAT_WIDTH_MULT: f32 = 1.0;
|
const SPRITE_SIZE: f32 = 128.0;
|
||||||
|
const SPRITE_PADDING: f32 = 16.0;
|
||||||
|
const BASE_STAT_WIDTH_MULT: f32 = 0.5;
|
||||||
const IV_WIDTH_MULT: f32 = 4.0;
|
const IV_WIDTH_MULT: f32 = 4.0;
|
||||||
const EV_WIDTH_MULT: f32 = 1.0;
|
const EV_WIDTH_MULT: f32 = 0.25;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref BASE_STAT_COLOR_RANGES: Vec<(u8, egui::Color32)> = vec![
|
static ref BASE_STAT_COLOR_RANGES: Vec<(u8, egui::Color32)> = vec![
|
||||||
|
|
@ -32,12 +34,13 @@ lazy_static! {
|
||||||
static ref COLUMNS: Vec<f32> = vec![
|
static ref COLUMNS: Vec<f32> = vec![
|
||||||
25.0, // Base stat
|
25.0, // Base stat
|
||||||
255.0 * BASE_STAT_WIDTH_MULT, // Base stat bar
|
255.0 * BASE_STAT_WIDTH_MULT, // Base stat bar
|
||||||
60.0, // Stat name
|
25.0, // EV value
|
||||||
40.0, // IV value
|
|
||||||
32.0 * IV_WIDTH_MULT, // IV bar
|
|
||||||
40.0, // EV value
|
|
||||||
255.0 * EV_WIDTH_MULT, // EV bar
|
255.0 * EV_WIDTH_MULT, // EV bar
|
||||||
|
60.0, // Stat name
|
||||||
|
25.0, // IV value
|
||||||
|
32.0 * IV_WIDTH_MULT, // IV bar
|
||||||
];
|
];
|
||||||
|
static ref TRACK_COLOR: egui::Color32 = egui::Color32::from_rgb(48, 48, 48);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct UiPlugin;
|
pub struct UiPlugin;
|
||||||
|
|
@ -45,59 +48,258 @@ pub struct UiPlugin;
|
||||||
impl Plugin for UiPlugin {
|
impl Plugin for UiPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
app.add_plugin(EguiPlugin)
|
app.add_plugin(EguiPlugin)
|
||||||
.insert_resource(Inspector::default())
|
|
||||||
.insert_resource(UiAssets::default())
|
.insert_resource(UiAssets::default())
|
||||||
.add_startup_system(load_assets)
|
.add_startup_system(load_assets)
|
||||||
.add_system(ui_system)
|
.add_system(ui_system);
|
||||||
.add_startup_system(test_init);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_init(mut inspector: ResMut<Inspector>, pokemon_database: Res<Database<Pokemon>>) {
|
#[derive(Default)]
|
||||||
inspector.enemies.push(PokemonInstance {
|
pub struct EguiAsset(pub Handle<Image>, pub egui::TextureId);
|
||||||
pokemon: pokemon_database.map.get("baltoy").unwrap().clone(),
|
|
||||||
..default()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Resource, Default)]
|
#[derive(Resource, Default)]
|
||||||
pub struct UiAssets {
|
pub struct UiAssets {
|
||||||
pub bar_handle: Handle<Image>,
|
pub bar_handle: EguiAsset,
|
||||||
pub sprite_map: HashMap<String, (Handle<Image>, egui::TextureId)>,
|
pub sprite_map: HashMap<String, EguiAsset>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_assets(mut ui_assets: ResMut<UiAssets>, assets: Res<AssetServer>) {
|
fn load_assets(
|
||||||
ui_assets.bar_handle = assets.load("ui/bar.png");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ui_system(
|
|
||||||
mut contexts: EguiContexts,
|
mut contexts: EguiContexts,
|
||||||
inspector: Res<Inspector>,
|
mut ui_assets: ResMut<UiAssets>,
|
||||||
ui_assets: Res<UiAssets>,
|
assets: Res<AssetServer>,
|
||||||
mut rendered_texture_id: Local<egui::TextureId>,
|
|
||||||
mut is_initialized: Local<bool>,
|
|
||||||
) {
|
) {
|
||||||
if !*is_initialized {
|
ui_assets.bar_handle = load_egui_asset(&mut contexts, &assets);
|
||||||
*is_initialized = true;
|
|
||||||
*rendered_texture_id = contexts.add_image(ui_assets.bar_handle.clone_weak());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn ui_system(mut contexts: EguiContexts, inspector: Res<Inspector>, ui_assets: Res<UiAssets>) {
|
||||||
egui::CentralPanel::default().show(contexts.ctx_mut(), |ui| {
|
egui::CentralPanel::default().show(contexts.ctx_mut(), |ui| {
|
||||||
let sprite_size = egui::Vec2::new(128., 128.);
|
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||||
for instance in inspector.enemies.iter().chain(inspector.allies.iter()) {
|
ui.heading("Enemy team");
|
||||||
|
for instance in inspector.battle.enemy_party.iter() {
|
||||||
|
ui.add_space(8.);
|
||||||
|
render_pokemon_instance(ui, &ui_assets, instance);
|
||||||
|
ui.add_space(8.);
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
ui.heading("Player team");
|
||||||
|
for instance in inspector.battle.player_party.iter() {
|
||||||
|
ui.add_space(8.);
|
||||||
|
render_pokemon_instance(ui, &ui_assets, instance);
|
||||||
|
ui.add_space(8.);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_egui_asset(contexts: &mut EguiContexts, assets: &Res<AssetServer>) -> EguiAsset {
|
||||||
|
let handle = assets.load("ui/bar.png");
|
||||||
|
let egui_id = contexts.add_image(handle.clone_weak());
|
||||||
|
EguiAsset(handle, egui_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_pokemon_instance(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
ui_assets: &Res<UiAssets>,
|
||||||
|
instance: &PokemonInstance,
|
||||||
|
) {
|
||||||
|
let sprite_size = egui::Vec2::new(SPRITE_SIZE, SPRITE_SIZE);
|
||||||
let pokemon = instance.pokemon.clone();
|
let pokemon = instance.pokemon.clone();
|
||||||
|
let ability = instance.ability.clone();
|
||||||
|
let nature = instance.nature.clone();
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.set_min_width(sprite_size.x + SPRITE_PADDING);
|
||||||
|
ui.set_max_width(sprite_size.x + SPRITE_PADDING);
|
||||||
|
// Name
|
||||||
|
ui.label(egui::RichText::new(&pokemon.full_name).color(egui::Color32::LIGHT_GRAY));
|
||||||
|
|
||||||
|
// Types
|
||||||
|
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.label(ability.as_ref().map_or("-", |a| a.name.as_str()));
|
||||||
|
|
||||||
// Sprite
|
// Sprite
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.set_min_size(sprite_size);
|
ui.set_min_size(sprite_size);
|
||||||
if let Some((_, rendered_texture_id)) = ui_assets.sprite_map.get(&pokemon.key())
|
ui.set_max_size(sprite_size);
|
||||||
|
if let Some(EguiAsset(_, rendered_texture_id)) =
|
||||||
|
ui_assets.sprite_map.get(&pokemon.key())
|
||||||
{
|
{
|
||||||
ui.add(egui::Image::new(*rendered_texture_id, sprite_size));
|
ui.add(egui::Image::new(*rendered_texture_id, sprite_size));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ui.add_space(SPRITE_PADDING);
|
||||||
|
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
ui.add_space(16.);
|
||||||
|
// Headings
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
for (i, width) in COLUMNS.iter().enumerate() {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.set_width(*width);
|
||||||
|
match i {
|
||||||
|
1 => {
|
||||||
|
ui.label(egui::RichText::new("Base stat"));
|
||||||
}
|
}
|
||||||
|
2 => {
|
||||||
|
ui.label(egui::RichText::new("EV"));
|
||||||
|
}
|
||||||
|
3 => {
|
||||||
|
ui.label(egui::RichText::new(format!(
|
||||||
|
"{} / 510",
|
||||||
|
instance.evs.values().map(|v| *v as i32).sum::<i32>()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
5 => {
|
||||||
|
ui.label(egui::RichText::new("IV"));
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
for stat in BaseStat::all() {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
let mut column_size = COLUMNS.iter();
|
||||||
|
|
||||||
|
// Base stat number
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
if let Some(width) = column_size.next() {
|
||||||
|
ui.set_width(*width);
|
||||||
|
}
|
||||||
|
let base_stat = &pokemon.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.base_value(stat);
|
||||||
|
let bar_length = base_value as f32 * BASE_STAT_WIDTH_MULT;
|
||||||
|
let color = base_stat_color(base_value);
|
||||||
|
|
||||||
|
let image =
|
||||||
|
egui::Image::new(ui_assets.bar_handle.1, [bar_length, BAR_HEIGHT])
|
||||||
|
.tint(color);
|
||||||
|
ui.add(image);
|
||||||
|
});
|
||||||
|
|
||||||
|
// EV stat
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
if let Some(width) = column_size.next() {
|
||||||
|
ui.set_width(*width);
|
||||||
|
}
|
||||||
|
if let Some(ev) = instance.evs.get(&stat) {
|
||||||
|
ui.label(egui::RichText::new(format!("{ev}")));
|
||||||
|
} else {
|
||||||
|
ui.label("-");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// EV stat bar
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
if let Some(width) = column_size.next() {
|
||||||
|
ui.set_width(*width);
|
||||||
|
}
|
||||||
|
if let Some(ev) = instance.evs.get(&stat) {
|
||||||
|
let bar_length = *ev as f32 * EV_WIDTH_MULT;
|
||||||
|
let track_length = 255. * EV_WIDTH_MULT - bar_length;
|
||||||
|
let color = egui::Color32::LIGHT_GRAY;
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.spacing_mut().item_spacing = egui::Vec2::ZERO;
|
||||||
|
|
||||||
|
// Bar
|
||||||
|
ui.add(
|
||||||
|
egui::Image::new(
|
||||||
|
ui_assets.bar_handle.1,
|
||||||
|
[bar_length, BAR_HEIGHT],
|
||||||
|
)
|
||||||
|
.tint(color),
|
||||||
|
);
|
||||||
|
// Track
|
||||||
|
ui.add(
|
||||||
|
egui::Image::new(
|
||||||
|
ui_assets.bar_handle.1,
|
||||||
|
[track_length, BAR_HEIGHT],
|
||||||
|
)
|
||||||
|
.tint(TRACK_COLOR.clone()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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.as_ref())),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// IV stat
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
if let Some(width) = column_size.next() {
|
||||||
|
ui.set_width(*width);
|
||||||
|
}
|
||||||
|
if let Some(iv) = instance.ivs.get(&stat) {
|
||||||
|
ui.label(egui::RichText::new(format!("{iv}")).color(iv_color(*iv)));
|
||||||
|
} else {
|
||||||
|
ui.label("-");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// IV stat bar
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
if let Some(width) = column_size.next() {
|
||||||
|
ui.set_width(*width);
|
||||||
|
}
|
||||||
|
if let Some(iv) = instance.ivs.get(&stat) {
|
||||||
|
let bar_length = *iv as f32 * IV_WIDTH_MULT;
|
||||||
|
let track_length = 31. * IV_WIDTH_MULT - bar_length;
|
||||||
|
let color = iv_color(*iv);
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.spacing_mut().item_spacing = egui::Vec2::ZERO;
|
||||||
|
|
||||||
|
// Bar
|
||||||
|
ui.add(
|
||||||
|
egui::Image::new(
|
||||||
|
ui_assets.bar_handle.1,
|
||||||
|
[bar_length, BAR_HEIGHT],
|
||||||
|
)
|
||||||
|
.tint(color),
|
||||||
|
);
|
||||||
|
// Track
|
||||||
|
ui.add(
|
||||||
|
egui::Image::new(
|
||||||
|
ui_assets.bar_handle.1,
|
||||||
|
[track_length, BAR_HEIGHT],
|
||||||
|
)
|
||||||
|
.tint(TRACK_COLOR.clone()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -115,6 +317,17 @@ fn nature_color(stat: BaseStat, nature: Option<&Nature>) -> egui::Color32 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn base_stat_color(base_stat: u8) -> egui::Color32 {
|
||||||
|
for (treshold, color) in BASE_STAT_COLOR_RANGES.iter().rev() {
|
||||||
|
if base_stat >= *treshold {
|
||||||
|
return color.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BASE_STAT_COLOR_RANGES
|
||||||
|
.first()
|
||||||
|
.map_or(egui::Color32::LIGHT_GRAY, |(_, color)| color.clone())
|
||||||
|
}
|
||||||
|
|
||||||
fn iv_color(iv: u8) -> egui::Color32 {
|
fn iv_color(iv: u8) -> egui::Color32 {
|
||||||
for (treshold, color) in IV_COLOR_RANGES.iter().rev() {
|
for (treshold, color) in IV_COLOR_RANGES.iter().rev() {
|
||||||
if iv >= *treshold {
|
if iv >= *treshold {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ use crate::pokemon::*;
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref DATA_DIR: PathBuf = "./data".into();
|
static ref DATA_DIR: PathBuf = "./data".into();
|
||||||
pub static ref TYPE_MAP: HashMap<u8, Type> = {
|
pub static ref TYPE_MAP: HashMap<u8, Type> = {
|
||||||
let types = read_pokedex();
|
let types = read_types();
|
||||||
let mut map = HashMap::new();
|
let mut map = HashMap::new();
|
||||||
for type_data in types.members() {
|
for type_data in types.members() {
|
||||||
if let Some(type_data) = Type::from_json(type_data) {
|
if let Some(type_data) = Type::from_json(type_data) {
|
||||||
|
|
@ -26,6 +26,10 @@ pub fn parse_json_file<P: AsRef<Path>>(path: P) -> JsonValue {
|
||||||
json::parse(fs::read_to_string(path).unwrap().as_ref()).unwrap()
|
json::parse(fs::read_to_string(path).unwrap().as_ref()).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn read_abilities() -> JsonValue {
|
||||||
|
parse_json_file(DATA_DIR.join("abilities.json"))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn read_natures() -> JsonValue {
|
pub fn read_natures() -> JsonValue {
|
||||||
parse_json_file(DATA_DIR.join("natures.json"))
|
parse_json_file(DATA_DIR.join("natures.json"))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -247,6 +247,30 @@ impl GetKey for Type {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone)]
|
||||||
|
pub struct Ability {
|
||||||
|
pub id: u16,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromJson for Ability {
|
||||||
|
fn from_json(json: &json::JsonValue) -> Option<Self>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
Some(Self {
|
||||||
|
id: json["id"].as_u16()?,
|
||||||
|
name: json["name"].as_str()?.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GetKey for Ability {
|
||||||
|
fn key(&self) -> String {
|
||||||
|
self.name.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Reflect, FromReflect, PartialEq)]
|
#[derive(Default, Debug, Clone, Reflect, FromReflect, PartialEq)]
|
||||||
pub struct Nature {
|
pub struct Nature {
|
||||||
pub id: u8,
|
pub id: u8,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue