Compare commits

...

10 Commits

Author SHA1 Message Date
hheik 09f28d28f5 Fixed sprite caching 2023-09-28 01:07:31 +03:00
hheik a8fd4e8ba1 added ability info 2023-04-01 19:28:19 +03:00
hheik b071b55a58 added ev display 2023-04-01 17:54:54 +03:00
hheik 2917ba457e fixed nature parsing 2023-04-01 17:21:18 +03:00
hheik 642cd9a76a added pokemon data decryption and parsing 2023-04-01 16:07:09 +03:00
hheik d26da8776b pokemon data validation 2023-04-01 13:50:05 +03:00
hheik 0395cbc942 added file-watcher for memory dumps 2023-04-01 11:23:25 +03:00
hheik 2b0ba06bf1 cleaned instance rendering 2023-03-31 21:27:05 +03:00
hheik baf64181ec feat: ivpeek ui 2023-03-31 20:18:54 +03:00
hheik ffbcea507b fix: reading type data from wrong source 2023-03-31 00:04:32 +03:00
12 changed files with 1865 additions and 76 deletions

12
Cargo.lock generated
View File

@ -2557,6 +2557,16 @@ dependencies = [
"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]]
name = "ntapi"
version = "0.4.0"
@ -3293,6 +3303,8 @@ dependencies = [
"bevy_egui",
"json",
"lazy_static",
"notify",
"notify-debouncer-mini",
"reqwest",
]

View File

@ -10,6 +10,8 @@ bevy = "0.10.0"
bevy_egui = "0.20.1"
json = "0.12.4"
lazy_static = "1.4.0"
notify = "5.1.0"
notify-debouncer-mini = "0.2.1"
reqwest = { version = "0.11.14", features = ["blocking", "json"] }
# Enable a small amount of optimization in debug mode

1066
data/abilities.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,13 @@
use std::collections::HashMap;
use std::{
fs,
path::{Path, PathBuf},
};
use bevy::prelude::*;
use json::JsonValue;
use crate::{
json::{read_characteristics, read_natures, read_pokedex},
pokemon::*,
};
use crate::{json::*, pokemon::*};
pub struct DatabasePlugin;
@ -15,6 +16,7 @@ impl Plugin for DatabasePlugin {
app.insert_resource(Database::<Pokemon>::default())
.insert_resource(Database::<Nature>::default())
.insert_resource(Database::<Characteristic>::default())
.insert_resource(Database::<Ability>::default())
.add_startup_system(database_setup);
}
}
@ -42,10 +44,26 @@ where
fn database_setup(
mut pokemon_database: ResMut<Database<Pokemon>>,
mut ability_database: ResMut<Database<Ability>>,
mut nature_database: ResMut<Database<Nature>>,
mut characteristic_database: ResMut<Database<Characteristic>>,
) {
ability_database.populate_from_json(&read_abilities());
nature_database.populate_from_json(&read_natures());
pokemon_database.populate_from_json(&read_pokedex());
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))
}

View File

@ -1,8 +1,3 @@
use std::{
fs,
path::{Path, PathBuf},
};
use bevy::prelude::*;
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))
}

View File

@ -1,6 +1,8 @@
use bevy::{prelude::*, window::WindowResolution};
mod battle;
mod inspector;
mod memory_reader;
mod ui;
pub fn run() {
@ -19,6 +21,8 @@ pub fn run() {
.set(ImagePlugin::default_nearest()),
)
.add_plugin(crate::database::DatabasePlugin)
.add_plugin(inspector::InspectorPlugin)
.add_plugin(ui::UiPlugin)
.add_plugin(memory_reader::MemoryReaderPlugin)
.run()
}

257
src/ivpeek/battle.rs Normal file
View File

@ -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
}

View File

@ -1,34 +1,130 @@
use std::collections::HashMap;
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;
impl Plugin for InspectorPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(Inspector::default());
app.insert_resource(Inspector::default())
.add_system(state_changed);
}
}
#[derive(Resource, Default)]
pub struct Inspector {
pub allies: Vec<PokemonInstance>,
pub enemies: Vec<PokemonInstance>,
pub battle: Battle<PokemonInstance>,
}
impl Inspector {
pub fn clear(&mut self) {
self.allies.clear();
self.enemies.clear();
}
#[derive(Default, Clone)]
pub struct Battle<T> {
pub player_party: Vec<T>,
pub enemy_party: Vec<T>,
}
#[derive(Default)]
#[derive(Default, Clone)]
pub struct PokemonInstance {
pub pokemon: Pokemon,
pub ability: Option<Ability>,
pub nature: Option<Nature>,
pub ivs: 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)));
}
}
}
}
}

112
src/ivpeek/memory_reader.rs Normal file
View File

@ -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:?}"),
}
}
});
}

View File

@ -3,14 +3,16 @@ use bevy_egui::{egui, EguiContexts, EguiPlugin};
use lazy_static::lazy_static;
use std::collections::HashMap;
use crate::{database::Database, pokemon::*};
use crate::pokemon::*;
use super::inspector::{Inspector, PokemonInstance};
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 EV_WIDTH_MULT: f32 = 1.0;
const EV_WIDTH_MULT: f32 = 0.25;
lazy_static! {
static ref BASE_STAT_COLOR_RANGES: Vec<(u8, egui::Color32)> = vec![
@ -32,12 +34,13 @@ lazy_static! {
static ref COLUMNS: Vec<f32> = vec![
25.0, // Base stat
255.0 * BASE_STAT_WIDTH_MULT, // Base stat bar
60.0, // Stat name
40.0, // IV value
32.0 * IV_WIDTH_MULT, // IV bar
40.0, // EV value
25.0, // EV value
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;
@ -45,59 +48,258 @@ pub struct UiPlugin;
impl Plugin for UiPlugin {
fn build(&self, app: &mut App) {
app.add_plugin(EguiPlugin)
.insert_resource(Inspector::default())
.insert_resource(UiAssets::default())
.add_startup_system(load_assets)
.add_system(ui_system)
.add_startup_system(test_init);
.add_system(ui_system);
}
}
fn test_init(mut inspector: ResMut<Inspector>, pokemon_database: Res<Database<Pokemon>>) {
inspector.enemies.push(PokemonInstance {
pokemon: pokemon_database.map.get("baltoy").unwrap().clone(),
..default()
});
}
#[derive(Default)]
pub struct EguiAsset(pub Handle<Image>, pub egui::TextureId);
#[derive(Resource, Default)]
pub struct UiAssets {
pub bar_handle: Handle<Image>,
pub sprite_map: HashMap<String, (Handle<Image>, egui::TextureId)>,
pub bar_handle: EguiAsset,
pub sprite_map: HashMap<String, EguiAsset>,
}
fn load_assets(mut ui_assets: ResMut<UiAssets>, assets: Res<AssetServer>) {
ui_assets.bar_handle = assets.load("ui/bar.png");
}
fn ui_system(
fn load_assets(
mut contexts: EguiContexts,
inspector: Res<Inspector>,
ui_assets: Res<UiAssets>,
mut rendered_texture_id: Local<egui::TextureId>,
mut is_initialized: Local<bool>,
mut ui_assets: ResMut<UiAssets>,
assets: Res<AssetServer>,
) {
if !*is_initialized {
*is_initialized = true;
*rendered_texture_id = contexts.add_image(ui_assets.bar_handle.clone_weak());
ui_assets.bar_handle = load_egui_asset(&mut contexts, &assets);
}
fn ui_system(mut contexts: EguiContexts, inspector: Res<Inspector>, ui_assets: Res<UiAssets>) {
egui::CentralPanel::default().show(contexts.ctx_mut(), |ui| {
egui::ScrollArea::vertical().show(ui, |ui| {
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.);
}
egui::CentralPanel::default().show(contexts.ctx_mut(), |ui| {
let sprite_size = egui::Vec2::new(128., 128.);
for instance in inspector.enemies.iter().chain(inspector.allies.iter()) {
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 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
ui.horizontal(|ui| {
ui.horizontal(|ui| {
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_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 {
for (treshold, color) in IV_COLOR_RANGES.iter().rev() {
if iv >= *treshold {

View File

@ -11,7 +11,7 @@ use crate::pokemon::*;
lazy_static! {
static ref DATA_DIR: PathBuf = "./data".into();
pub static ref TYPE_MAP: HashMap<u8, Type> = {
let types = read_pokedex();
let types = read_types();
let mut map = HashMap::new();
for type_data in types.members() {
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()
}
pub fn read_abilities() -> JsonValue {
parse_json_file(DATA_DIR.join("abilities.json"))
}
pub fn read_natures() -> JsonValue {
parse_json_file(DATA_DIR.join("natures.json"))
}

View File

@ -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)]
pub struct Nature {
pub id: u8,