437 lines
12 KiB
Perl
437 lines
12 KiB
Perl
package GEmeTool::Save::Pokemon;
|
|
|
|
use v5.16.3;
|
|
|
|
use strict;
|
|
use warnings;
|
|
|
|
use Data::Dumper;
|
|
|
|
use Moo;
|
|
use Rsaves;
|
|
use Rsaves::Constants::Emerald::Species;
|
|
use Rsaves::Constants::Emerald::SpeciesData;
|
|
use Path::Tiny;
|
|
use GEmeTool::DB;
|
|
use GEmeTool::Config;
|
|
use Digest::SHA qw/sha256_hex/;
|
|
use Digest::SHA qw/sha256_hex/;
|
|
use UUID::URandom qw/create_uuid_string/;
|
|
|
|
has _pokemon => ( is => 'rw', required => 1 );
|
|
|
|
sub species {
|
|
my $self = shift;
|
|
my $arg = shift;
|
|
my $pokemon = $self->_pokemon;
|
|
my $substruct_0 = $pokemon->{substructures}[0];
|
|
if ( defined $arg ) {
|
|
$substruct_0->{species} = $arg;
|
|
}
|
|
return $substruct_0->{species};
|
|
}
|
|
|
|
sub load_from_file {
|
|
my $class = shift;
|
|
my $file = shift;
|
|
$file = path($file);
|
|
die 'No such plain file' if !-f $file;
|
|
return $class->new( _pokemon => Rsaves::read_pk3_file($file) );
|
|
}
|
|
|
|
sub export_file {
|
|
my $self = shift;
|
|
my $file = shift;
|
|
Rsaves::write_pk3_file( $self->_pokemon, $file );
|
|
}
|
|
|
|
sub export_raw {
|
|
my $self = shift;
|
|
return Rsaves::get_pk3_raw($self->_pokemon);
|
|
}
|
|
|
|
sub empty {
|
|
my $class = shift;
|
|
return $class->new( _pokemon => Rsaves::read_pk3_raw( "\0" x 0x80 ) );
|
|
}
|
|
|
|
sub copy {
|
|
my $self = shift;
|
|
my $object_to_copy_from = shift;
|
|
%{ $self->_pokemon } = %{ $object_to_copy_from->_pokemon };
|
|
return $self;
|
|
}
|
|
|
|
sub ivs {
|
|
my $self = shift;
|
|
my $arg = shift;
|
|
print Data::Dumper::Dumper $self->_pokemon;
|
|
my $current_value =
|
|
$self->_pokemon->{substructures}[3]{ivs_egg_status_and_ability};
|
|
my @stats = (
|
|
'HP', 'Attack',
|
|
'Defense', 'Speed',
|
|
'Special Attack', 'Special Defense'
|
|
);
|
|
if ( defined $arg ) {
|
|
if ( defined $arg->{HP} ) {
|
|
$current_value &= ~0x1f;
|
|
$current_value |= ( $arg->{HP} & 0x1f );
|
|
}
|
|
if ( defined $arg->{Attack} ) {
|
|
$current_value &= ~( 0x1f << 5 );
|
|
$current_value |= ( ( $arg->{Attack} & 0x1f ) << 5 );
|
|
}
|
|
if ( defined $arg->{Defense} ) {
|
|
$current_value &= ~( 0x1f << 10 );
|
|
$current_value |= ( ( $arg->{Defense} & 0x1f ) << 10 );
|
|
}
|
|
if ( defined $arg->{Speed} ) {
|
|
$current_value &= ~( 0x1f << 15 );
|
|
$current_value |= ( ( $arg->{Speed} & 0x1f ) << 15 );
|
|
}
|
|
if ( defined $arg->{SpecialAttack} ) {
|
|
$current_value &= ~( 0x1f << 20 );
|
|
$current_value |= ( ( $arg->{SpecialAttack} & 0x1f ) << 20 );
|
|
}
|
|
if ( defined $arg->{SpecialDefense} ) {
|
|
$current_value &= ~( 0x1f << 25 );
|
|
$current_value |= ( ( $arg->{SpecialDefense} & 0x1f ) << 25 );
|
|
}
|
|
$self->_pokemon->{substructures}[3]{ivs_egg_status_and_ability} =
|
|
$current_value;
|
|
}
|
|
my $ivs_egg_status_and_ability = $current_value;
|
|
return {
|
|
HP => $ivs_egg_status_and_ability & 0x1F,
|
|
Attack => ( $ivs_egg_status_and_ability >> 5 ) & 0x1F,
|
|
Defense => ( $ivs_egg_status_and_ability >> 10 ) & 0x1F,
|
|
Speed => ( $ivs_egg_status_and_ability >> 15 ) & 0x1F,
|
|
SpecialDefense => ( $ivs_egg_status_and_ability >> 25 ) & 0x1F,
|
|
SpecialAttack => ( $ivs_egg_status_and_ability >> 20 ) & 0x1F,
|
|
};
|
|
}
|
|
|
|
sub evs {
|
|
my $self = shift;
|
|
my $arg = shift;
|
|
my $pokemon = $self->_pokemon;
|
|
my $substructure = $pokemon->{substructures}[2];
|
|
if ( defined $arg ) {
|
|
if ( defined $arg->{HP} ) {
|
|
$substructure->{hp_ev} = $arg->{HP};
|
|
}
|
|
if ( defined $arg->{Attack} ) {
|
|
$substructure->{attack_ev} = $arg->{Attack};
|
|
}
|
|
if ( defined $arg->{Defense} ) {
|
|
$substructure->{defense_ev} = $arg->{Defense};
|
|
}
|
|
if ( defined $arg->{Speed} ) {
|
|
$substructure->{speed_ev} = $arg->{Speed};
|
|
}
|
|
if ( defined $arg->{SpecialAttack} ) {
|
|
$substructure->{special_attack_ev} = $arg->{SpecialAttack};
|
|
}
|
|
if ( defined $arg->{SpecialDefense} ) {
|
|
$substructure->{special_defense_ev} = $arg->{SpecialDefense};
|
|
}
|
|
}
|
|
return {
|
|
HP => $pokemon->{substructures}[2]{hp_ev},
|
|
Attack => $pokemon->{substructures}[2]{attack_ev},
|
|
Defense => $pokemon->{substructures}[2]{defense_ev},
|
|
Speed => $pokemon->{substructures}[2]{speed_ev},
|
|
SpecialAttack => $pokemon->{substructures}[2]{special_attack_ev},
|
|
SpecialDefense => $pokemon->{substructures}[2]{special_defense_ev},
|
|
};
|
|
}
|
|
|
|
sub level {
|
|
my $self = shift;
|
|
my $arg = shift;
|
|
my $pokemon = $self->_pokemon;
|
|
return 0 if $self->species == 0;
|
|
my $growth_func = sub {
|
|
my $n = shift;
|
|
if ( $n == 1 ) {
|
|
return 1;
|
|
}
|
|
return $self->growth_function->($n);
|
|
};
|
|
if ( defined $arg ) {
|
|
$pokemon->{substructures}[0]{experience} = $growth_func->($arg);
|
|
}
|
|
my $experience = $pokemon->{substructures}[0]{experience};
|
|
my $level = 1;
|
|
while ( $level <= 100 && int( $growth_func->($level) ) <= $experience ) {
|
|
$level++;
|
|
}
|
|
$level -= 1;
|
|
|
|
return $level;
|
|
}
|
|
|
|
sub growth_function {
|
|
my $self = shift;
|
|
my $growth = $self->growth;
|
|
if ( $growth eq 'GROWTH_FAST' ) {
|
|
return \&_exp_fast;
|
|
}
|
|
if ( $growth eq 'GROWTH_MEDIUM_FAST' ) {
|
|
return \&_exp_medium_fast;
|
|
}
|
|
if ( $growth eq 'GROWTH_MEDIUM_SLOW' ) {
|
|
return \&_exp_medium_slow;
|
|
}
|
|
if ( $growth eq 'GROWTH_SLOW' ) {
|
|
return \&_exp_slow;
|
|
}
|
|
if ( $growth eq 'GROWTH_ERRATIC' ) {
|
|
return \&_exp_erratic;
|
|
}
|
|
if ( $growth eq 'GROWTH_FLUCTUATING' ) {
|
|
return \&_exp_fluctuating;
|
|
}
|
|
}
|
|
|
|
sub gender_ratio {
|
|
my $self = shift;
|
|
my $pokemon_name = $self->pokemon_name;
|
|
my %pokemon_data = %Rsaves::Constants::Emerald::SpeciesData::SPECIES_DATA;
|
|
my $data = $pokemon_data{$pokemon_name};
|
|
my $gender_ratio = $data->{gender_ratio};
|
|
return $gender_ratio;
|
|
}
|
|
|
|
sub gender {
|
|
|
|
# 0 male
|
|
# 1 female
|
|
# 2 genderless
|
|
my $self = shift;
|
|
my $pokemon = $self->_pokemon;
|
|
my $personality = shift // $pokemon->{personality};
|
|
my $gender_ratio = $self->gender_ratio;
|
|
if ( $gender_ratio == 0 ) {
|
|
return 0;
|
|
}
|
|
if ( $gender_ratio == 254 ) {
|
|
return 1;
|
|
}
|
|
if ( $gender_ratio == 255 ) {
|
|
return 2;
|
|
}
|
|
if ( $gender_ratio <= ( $personality & 0xff ) ) {
|
|
return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
sub personality {
|
|
my $self = shift;
|
|
my $arg = shift;
|
|
my $pokemon = $self->_pokemon;
|
|
if ( defined $arg ) {
|
|
$self->_pokemon->{personality} = $arg;
|
|
}
|
|
return $pokemon->{personality};
|
|
}
|
|
|
|
sub growth {
|
|
my $self = shift;
|
|
my $pokemon_name = $self->pokemon_name;
|
|
my %pokemon_data = %Rsaves::Constants::Emerald::SpeciesData::SPECIES_DATA;
|
|
my $data = $pokemon_data{$pokemon_name};
|
|
return $data->{growth_rate};
|
|
}
|
|
|
|
sub _square {
|
|
return $_[0]**2;
|
|
}
|
|
|
|
sub _cube {
|
|
return $_[0]**3;
|
|
}
|
|
|
|
sub _exp_slow {
|
|
my $n = shift;
|
|
return ( 5 * _cube($n) ) / 4;
|
|
}
|
|
|
|
sub _exp_fast {
|
|
my $n = shift;
|
|
return ( 4 * _cube($n) ) / 5;
|
|
}
|
|
|
|
sub _exp_medium_fast {
|
|
return _cube( $_[0] );
|
|
}
|
|
|
|
sub _exp_medium_slow {
|
|
my $n = shift;
|
|
|
|
#define EXP_MEDIUM_SLOW(n)((6 * CUBE(n)) / 5 - (15 * SQUARE(n)) + (100 * n) - 140) // (6 * (n)^3) / 5 - (15 * (n)^2) + (100 * n) - 140
|
|
my $return =
|
|
( ( 6 * _cube($n) ) / 5 - ( 15 * _square($n) ) + ( 100 * $n ) - 140 );
|
|
return $return;
|
|
}
|
|
|
|
sub _exp_erratic {
|
|
my $n = shift;
|
|
if ( $n <= 50 ) {
|
|
return ( ( 100 - $n ) * _cube($n) / 50 );
|
|
}
|
|
if ( $n <= 68 ) {
|
|
return ( ( 150 - $n ) * _cube($n) / 100 );
|
|
}
|
|
if ( $n <= 98 ) {
|
|
return ( ( 1911 - 10 * $n ) / 3 * _cube($n) / 500 );
|
|
}
|
|
return ( ( 160 - $n ) * _cube($n) / 100 );
|
|
}
|
|
|
|
sub _exp_fluctuating {
|
|
my $n = shift;
|
|
if ( $n <= 15 ) {
|
|
return ( ( ( $n + 1 ) / 3 + 24 ) * _cube($n) / 50 );
|
|
}
|
|
if ( $n <= 36 ) {
|
|
return ( ( $n + 14 ) * _cube($n) / 50 );
|
|
}
|
|
return ( ( ( $n / 2 ) + 32 ) * _cube($n) / 50 );
|
|
}
|
|
|
|
sub pokemon_name {
|
|
my $self = shift;
|
|
return $Rsaves::Constants::Emerald::Species::SPECIES[ $self->species ];
|
|
}
|
|
|
|
sub nickname {
|
|
my $self = shift;
|
|
my $arg = shift;
|
|
if ( defined $arg ) {
|
|
die "Invalid nickname" if length $arg != 10;
|
|
$self->_pokemon->{nickname} = $arg;
|
|
}
|
|
return $self->_pokemon->{nickname};
|
|
}
|
|
|
|
sub get_icon {
|
|
my $self = shift;
|
|
my $pokemon_name =
|
|
$Rsaves::Constants::Emerald::Species::SPECIES[ $self->species ];
|
|
if ( lc($pokemon_name) eq 'unown' ) {
|
|
return "pokeemerald/graphics/pokemon/@{[lc($pokemon_name)]}/z/icon.png";
|
|
}
|
|
return "pokeemerald/graphics/pokemon/@{[lc($pokemon_name)]}/icon.png";
|
|
}
|
|
|
|
sub generate_personality {
|
|
my $self = shift;
|
|
my $target_shiny = shift;
|
|
my $target_gender = shift;
|
|
my $target_nature = shift;
|
|
my $otid = $self->otid;
|
|
my $should_search_gender = 0;
|
|
my $personality;
|
|
if ( defined $target_gender
|
|
&& !( grep { $self->gender_ratio eq $_ } ( 0, 254, 255 ) ) )
|
|
{
|
|
$should_search_gender = 1;
|
|
}
|
|
if ( defined $target_gender && $target_gender != 0 && $target_gender != 1 )
|
|
{
|
|
die "Incorrect gender $target_gender.";
|
|
}
|
|
if ( defined $target_nature
|
|
&& ( $target_nature < 0 || $target_nature > 24 ) )
|
|
{
|
|
die "Incorrect nature $target_nature.";
|
|
}
|
|
for ( my $i = 0 ; $i < 0xffffffff ; $i++ ) {
|
|
if ( defined $target_nature && $i % 25 != $target_nature ) {
|
|
next;
|
|
}
|
|
if ( defined $target_shiny
|
|
&& !( !!$target_shiny == !!Rsaves::is_shiny( $otid, $i ) ) )
|
|
{
|
|
next;
|
|
}
|
|
if ( $should_search_gender && $self->gender($i) != $target_gender ) {
|
|
next;
|
|
}
|
|
$personality = $i;
|
|
last;
|
|
}
|
|
if ( !defined $personality ) {
|
|
warn "Could not find personality combination, this is a bug.";
|
|
}
|
|
return $personality;
|
|
}
|
|
|
|
sub otid {
|
|
my $self = shift;
|
|
my $arg = shift;
|
|
my $pokemon = $self->_pokemon;
|
|
if ( defined $arg ) {
|
|
$pokemon->{otid} = $arg;
|
|
}
|
|
return $pokemon->{otid};
|
|
}
|
|
|
|
sub get_front {
|
|
my $self = shift;
|
|
my $pokemon_name =
|
|
$Rsaves::Constants::Emerald::Species::SPECIES[ $self->species ];
|
|
if ( Rsaves::pokemon_is_shiny( $self->_pokemon ) ) {
|
|
return "resources/shiny/@{[lc($pokemon_name)]}.png";
|
|
}
|
|
if ( lc($pokemon_name) eq 'castform' ) {
|
|
return
|
|
"pokeemerald/graphics/pokemon/@{[lc($pokemon_name)]}/normal/front.png";
|
|
}
|
|
if ( lc($pokemon_name) eq 'unown' ) {
|
|
return
|
|
"pokeemerald/graphics/pokemon/@{[lc($pokemon_name)]}/z/front.png";
|
|
}
|
|
return "pokeemerald/graphics/pokemon/@{[lc($pokemon_name)]}/front.png";
|
|
}
|
|
|
|
sub backup {
|
|
my $self = shift;
|
|
my $db = GEmeTool::DB->connect;
|
|
my $data_dir = GEmeTool::Config->new->data_dir;
|
|
my $backup_dir = $data_dir->child('backups/pk3');
|
|
$backup_dir->mkpath;
|
|
my $nickname = Rsaves::translate_3rd_encoding( $self->nickname );
|
|
$nickname =~ s/\.\.//g;
|
|
$nickname =~ s/\///g;
|
|
my $contents = $self->export_raw;
|
|
my $digest = sha256_hex($contents);
|
|
my $file = $backup_dir->child(
|
|
$nickname . '-' . $self->pokemon_name . '-' . $digest . '.pk3' )
|
|
->absolute;
|
|
$file->spew_raw($contents);
|
|
my $uuid = create_uuid_string();
|
|
my $query_check_exists = <<'EOF';
|
|
SELECT file FROM backups_pk3
|
|
WHERE file = ?
|
|
EOF
|
|
|
|
if ( $db->selectrow_hashref( $query_check_exists, undef, $file ) ) {
|
|
$db->do( <<'EOF', undef, $file );
|
|
UPDATE backups_pk3
|
|
SET date=datetime('now')
|
|
WHERE file = ?;
|
|
EOF
|
|
return;
|
|
}
|
|
my $query = <<'EOF';
|
|
INSERT INTO backups_pk3 (uuid, date, nickname, pokemon_name, file)
|
|
VALUES (?, datetime('now'), ?, ?, ?);
|
|
EOF
|
|
$db->do( $query, {}, $uuid, $self->nickname, $self->pokemon_name, $file );
|
|
}
|
|
1;
|