root/veekun/trunk/lib/Vee/Controller/Dex/Utils.pm @ 406

Revision 406, 16.7 KB (checked in by eevee, 22 months ago)

Database refactoring. Renamed columns and tables to be more consistent and more readable. (#58)

Line 
1package Vee::Controller::Dex::Utils;
2
3use strict;
4use warnings;
5use base 'Catalyst::Controller';
6
7use List::MoreUtils qw/first_index last_index/;
8use Vee::Dex;
9use Vee::Form;
10
11__PACKAGE__->config->{namespace} = 'dex';
12
13=head1 NAME
14
15Vee::Controller::Dex::Utils - Pokedex utility Controller
16
17=head1 SYNOPSIS
18
19See L<Vee>
20
21=head1 DESCRIPTION
22
23Catalyst Controller for Pokedex-related utilities, calculators, etc.
24
25=head1 METHODS
26
27=cut
28
29=head2 compare
30
31=cut
32
33my $MAX_POKEMON = 8;
34my $FONT_MAX = 24;
35my $FONT_MIN = 8;
36my @versions = qw/rb y gs c rusa frlg dp/;
37
38sub compare : Path('pokemon/compare') : Args(0) {
39    my ($self, $c) = @_;
40    my $s = $c->stash;
41
42    $s->{page_title} = 'Pok&eacute;mon Comparifier';
43    $s->{template} = 'dex/utils/compare.tt';
44
45    $s->{versions} = \@versions;
46    $s->{MAX_POKEMON} = $MAX_POKEMON;
47    $s->{FONT_MAX} = $FONT_MAX;
48    $s->{FONT_MIN} = $FONT_MIN;
49   
50    my $params = $c->req->params;
51    my $version = $params->{version};
52    $version = $versions[-1] unless $version and Vee::Utils::in($version, @versions);
53    $s->{version} = $version;
54
55    return unless %{$c->req->params};
56
57    # get pokemons
58    my @pokemon_raw = Vee::Utils::array($params->{poke});
59    @pokemon_raw = @pokemon_raw[0 .. $MAX_POKEMON - 1] if $#pokemon_raw >= $MAX_POKEMON;
60
61    my %randoms;  # used to make sure no Pokemon is randomly picked twice
62    my (@pokemon_ids, @bad_pokemon, @pokemon_notes);
63    for my $p (0 .. $#pokemon_raw) {
64        my $poke = $pokemon_raw[$p];
65        next unless $poke;
66       
67        if ($poke eq 'random') {  # special case: random
68            $pokemon_ids[$p] = 1 + int rand $#PokemonNames;
69            while ($randoms{ $pokemon_ids[$p] }) {
70                $pokemon_ids[$p] = 1 + int rand $#PokemonNames;
71            }
72            $randoms{ $pokemon_ids[$p] } = 1;
73
74        } elsif (my $result = get_pokemon($poke)) {
75            if (ref $result eq 'SCALAR') {
76                $pokemon_notes[$p] = $$result
77            } else {
78                $pokemon_ids[$p] = $result
79            }
80
81        } elsif (4 > length $poke) {
82            $pokemon_notes[$p] = 'Too short for fuzzy search.';
83        } else {
84            my @fuzzies = get_fuzzy($poke, 'pokemon');
85            if (@fuzzies) {
86                $pokemon_ids[$p] = ( shift @fuzzies )->{id};
87                @fuzzies = @fuzzies[0 .. 3] if @fuzzies > 4;
88                $pokemon_notes[$p] = \@fuzzies;
89            } else {
90                $pokemon_notes[$p] = "No match found."; 
91            }
92        }           
93    }
94    $s->{pokemon_notes} = \@pokemon_notes;
95   
96    # grab the Pokemon's rows and stick them into an array in the correct order
97    my %pokemon_order;
98    for my $i (0 .. $#pokemon_ids) {
99        next unless $pokemon_ids[$i];
100        push @{ $pokemon_order{ $pokemon_ids[$i] } }, $i;
101    }
102    my @pokemon;
103
104    my (@rows, @pokemon_moves);
105    # only do this if there are actually correct Pokemon to look up!
106    if (@pokemon_ids) {
107        my $pokemon_rs = $c->model('DBIC::Pokemon')->search({
108            'me.id' => \@pokemon_ids
109        }, {
110            # TODO: this sucks; patch dbic
111            prefetch => { pokemon_abilities => 'ability' },
112        });
113        while (my $poke = $pokemon_rs->next) {
114            $pokemon[$_] = $poke for @{ $pokemon_order{$poke->id} };
115        }
116       
117        # get moves
118        my $move_rs = $c->model('DBIC::PokemonMoves')->search({
119            pokemon_id => [ keys %pokemon_order ],
120            method     => [qw/level machine egg tutor/],
121            -nest      => \ "FIND_IN_SET('$version', versions)",
122        });
123        while (my $row = $move_rs->next) {
124            push @{ $pokemon_moves[ $_ ]{ $row->method } }, $row for @{ $pokemon_order{$row->pokemon_id} }
125        }
126    }
127
128    $s->{pokemon}       = \@pokemon;
129    $s->{pokemon_raw}   = \@pokemon_raw;
130    $s->{pokemon_moves} = \@pokemon_moves;
131
132    $s->{template} = 'dex/utils/compare-results.tt';
133}
134
135=head2 backtrace
136
137Calculates parents for an egg move.
138
139=cut
140
141sub backtrace : Chained('pokemon_chain') : Args(1) {
142    my ($self, $c) = @_;
143    my $s = $c->stash;
144   
145    my $gen = 'dp'; # TODO: :(
146
147    my $pokemon_name = $c->req->captures->[0];
148    my $move_name    = $c->req->args    ->[0];
149
150    # For some reason, Catalyst::DispatchType::Chained doesn't url-decode either
151    # args or captures, so do it here; this is safe to do since valid move and
152    # Pokemon names will never ever contain percent signs..  I hope.
153    $pokemon_name =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;
154    $move_name    =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;
155   
156    my $pokemon = $c->model('DBIC::Pokemon')->single({ name => $pokemon_name }, { prefetch => 'breeds' });
157    my $move    = $c->model('DBIC::Moves')  ->single({ name => $move_name    });
158
159    $c->vee_stop('No such Pok&eacute;mon ', $pokemon_name, '.')
160        if not $pokemon;
161    $c->vee_stop('No such move ', $move_name, '.')
162        if not $move;
163
164    $s->{page_title} = $pokemon->name . ' - ' . $move->name . ' parents';
165    $s->{crumbs}     = [
166        '<a href="' . $c->uri('Dex') . '">Pok&eacute;dex</a>',
167        '<a href="' . $c->uri('Dex', 'pokemon_list') . '">Pok&eacute;mon</a>',
168        '<a href="' . $c->uri('Dex', 'pokemon', lc $pokemon->name) . '">' . $pokemon->name . '</a>',
169        'Breeding chains',
170        '<a href="' . $c->uri('Dex', 'moves', lc $move->name) . '">' . $move->name . '</a>',
171    ];
172
173    # ensure this move is actually inheritable
174    # TODO: should I error if the move is learnable normally?
175    my $inheritable_ct = $c->model('DBIC::PokemonMoves')->count({
176        pokemon_id => $pokemon->id,
177        move_id    => $move->id,
178        method     => [qw/ egg machine /],
179        -nest      => \ "FIND_IN_SET('$gen', versions)",
180    });
181    $c->vee_stop('', $pokemon->name, " can't inherit ", $move->name, '.') unless $inheritable_ct;
182   
183    my $gender_restriction;
184    if ($pokemon->gender_rate == 255) {
185        # must also be genderless, i.e. bred with Ditto
186        $gender_restriction = 255;
187    } else {
188        # cannot be single-gender or genderless
189        $gender_restriction = { -not_in => [ 0, 254, 255 ] };
190    }
191
192    ### grab the methods by which any Pokemon learn the move
193
194    my $methods_rs = $c->model('DBIC::PokemonMoves')->search({
195        move_id => $move->id,
196        method  => [qw[ level egg machine ]],
197        -nest   => \ "FIND_IN_SET('$gen', versions)",
198        'pokemon.gender_rate' => $gender_restriction,
199    }, {
200        join => 'pokemon',
201    });
202    my %learn_methods;
203    while (my $row = $methods_rs->next) {
204        my $method = ucfirst $row->method;
205        if ($row->method eq 'level') {
206            $method .= ' ' . $row->level;
207        } elsif ($row->method eq 'machine') {
208            $method = 'TM/HM';
209        }
210        push @{ $learn_methods{ $row->pokemon_id } }, $method;
211    }
212
213    ### do a SEPARATE query to grab all the possible Pokemon at once
214    # (DBIC seems to get confused if I prefetch { pokemon => breeds } above)
215    # XXX: is this my fault or theirs?
216
217    my $learners_rs = $c->model('DBIC::Pokemon')->search({
218        id => [ keys %learn_methods ],
219    }, {
220        prefetch => 'breeds',
221        order_by => 'me.id ASC',
222    });
223
224    my (%pokemon);
225    while (my $row = $learners_rs->next) {
226        $pokemon{ $row->id } = $row;
227    }
228
229    my %breeding_tree = ( $pokemon->id => {} );
230
231    my %seen = ( $pokemon->id => 0 );   # hash of ( poke_id => 1, ... )
232    my $cur_level = 0;                  # current tree level
233    my @this_level = %breeding_tree;    # array of ( poke_id, hashref, ... ) on this level
234
235    ### build the breeding-chain tree
236    # What we need is a tree of ancestors that can learn the move and pass it
237    # down; it should look something like this:
238    # pokemon => {
239    #     a => {
240    #         b => {},
241    #         c => {},
242    #         d => {
243    #             e => {},
244    #         },
245    #     },
246    #     f => {
247    #         g => {},
248    #         d => [ref to former d],
249    #     },
250    # }
251    # The rules are basically that:
252    # 1. Every Pokemon is a key in a hash
253    # 2. Each corresponding value is a similar hash, containing a list of
254    #    Pokemon that are compatible
255    # 3. No infinite looping!
256
257    do {
258        $cur_level++;
259        my %branches;
260
261        while (my $pokemon_id = shift @this_level) {
262            my $this_poke = $pokemon{$pokemon_id};
263            my $hashref = shift @this_level;
264
265            for my $pokemon_id (keys %pokemon) {
266                next if not $this_poke->can_breed_with( $pokemon{$pokemon_id} );
267
268                if (exists $seen{ $pokemon_id }) {
269                    # don't put anything if this Pokemon has been seen higher
270                    # in the chain; needless descending is bad
271                    $hashref->{ $pokemon_id } = undef
272                        unless $seen{ $pokemon_id } < $cur_level;
273                } else {
274                    $hashref->{$pokemon_id} = ( $branches{$pokemon_id} ||= {} );
275                    $seen{$pokemon_id} = $cur_level;
276                }
277            }
278        }
279
280        @this_level = %branches;
281    } while ($cur_level < 10 and @this_level);
282
283    ### find the optimal path
284   
285    # since there should only be a small finite number of Pokemon, we can just
286    # iterate over every possibility; flatten into arrays, and take whichever
287    # is shortest
288
289    # TODO: merge this with the actual building of the tree, and toss useless
290    # branches as we go; right now this sub MODIFIES THE TREE, pruning any
291    # branches it finds that don't end with a level; this makes the dupe
292    # removal up top a little silly!
293    my $flattener; $flattener = sub {
294        my ($branch) = @_;
295        my @subchains;
296
297        for my $key (keys %$branch) {
298            if (grep /level/i, @{ $learn_methods{$key} }) {
299                # consider this Pokemon an endpoint if it learns the move
300                # naturally; TMs don't count since they are of limited quantity
301                push @subchains, [ $key ];
302                $branch->{$key} &&= {};
303                next;
304            }
305
306            # ...otherwise, only add more chains if this branch has any valid
307            # subchains of its own
308            my @child_chains = $flattener->( $branch->{$key} );
309            if (@child_chains) {
310                push @subchains, map [ $key, @$_ ], @child_chains;
311            } else {
312                delete $branch->{$key};
313            }
314        }
315
316        return @subchains;
317    };
318
319    # only take the shortest chains; it's possible there could be several!
320    my @flat_chains  = sort { $#$a <=> $#$b } $flattener->( \%breeding_tree );
321    my @optimal_path = grep { $#$_ == $#{ $flat_chains[0] } } @flat_chains;
322    @$_ = reverse @$_ for @optimal_path;
323   
324    ### cram everything important into the stash
325    $s->{breeding_tree} = \%breeding_tree;
326    $s->{pokemon}       = \%pokemon;
327    $s->{learn_methods} = \%learn_methods;
328    $s->{optimal}       = \@optimal_path;
329
330    $s->{template} = 'dex/utils/backtrace.tt';
331   
332#    $c->res->body(join "<br/>", map { qq'<a href="/dex/pokemon/$PokemonNames[$_]">$PokemonNames[$_]</a>' } @other_learners);
333   
334#    TODO:
335#        ! CHECK to make sure this is a move that the Pokemon can learn via egg *or* TM
336#        - female is obvious and is the species of the target child
337#        - for previous generations, just look for a Pokemon that only learns via breeding
338#            - I suggest removing the breed requirement and doing that with perl code so you can reuse the data
339#            - do not bother with anything elses!
340#        - also: later look for entire families that match and combine them somehow?  ugh that will be a pain in the ass
341#        - GENDERLESS!  they must be dealt with entirely separately
342#        - use gender distro, steps, and other stats to figure out optimal chain?
343#       
344#        - db: merge the egg moves that need it, not sure why that hasn't happened
345#            MYSTERY SOLVED: d/p data used babies as bases, I use the first evo as the base.  have to go through babies and fix this  :(
346#        - start on my own visual refresh?  use some stock background to try for now I dunno
347#        ! don't slurp @other_learners
348#        - make a /dex/pokemon/eevee/backtrace that lists all traceable moves?
349
350}
351
352=head2 damage
353
354Calculates damage.
355
356=cut
357
358sub damage : Path('calculators/damage') : Form('dex/damage') : Args(0) {
359    my ($self, $c) = @_;
360    my $s = $c->stash;
361   
362    if (%{$c->req->params} and $c->form->validate) {
363        my $attack  = $c->req->params->{attack};
364        my $defense = $c->req->params->{defense};
365        my $power   = $c->req->params->{power};
366        my $level   = $c->req->params->{level};
367       
368        my $damage = int(int( (2 + int($level * 0.4)) * $attack * $power / $defense ) / 50) + 2;
369        $damage = 999 if $damage > 999;
370       
371        my $random = int rand 39;
372       
373        $s->{damage}     = $damage;
374        $s->{min_damage} = int($damage * (217 + $random) / 255);
375    }
376
377    $s->{page_title} = 'Damage Calculator';
378    $s->{extra_css}  = 'dex';
379    $s->{template}   = 'dex/utils/damage.tt';
380   
381#    $c->res->body("Max damage: $damage <br/> Min damage: " . int($damage * ($random + 217) / 255));
382}
383
384=head2 iv
385
386Calculates IVs.
387
388=cut
389
390my $iv_fields = {
391    pokemon => { type => 'text', class => 'js-dexsuggest js-dexsuggest-pokemon' },
392    level   => { type => 'text' },
393    nature  => { type => 'select', options => [ ['ignore' => 'Ignore'], map [ $_ => ucfirst $_ ], sort keys %Natures ], },
394};
395
396for my $s (0 .. $#StatColumns) {
397    my $stat = $StatColumns[$s];
398    $iv_fields->{ $stat }         = { type => 'text', maxlength => 3, tabindex => $s + 1, title => "Enter your Pokemon's calculated stat.", };
399    $iv_fields->{ $stat . '_ev' } = { type => 'text', maxlength => 3, tabindex => $s + @StatColumns + 1, title => "Enter your Pokemon's effort value; this is 0 if it was just caught.", };
400}
401
402
403sub iv : Path('calculators/iv') : Args(0) {
404    my ($self, $c) = @_;
405    my $s = $c->stash;
406    my $p = $c->req->params;
407
408    my $form = $s->{form} = Vee::Form->new(
409        id => 'stat_calculator',
410        fields => $iv_fields,
411        params => $c->req->params,
412    );
413
414    $s->{page_title} = 'IV/Stat Calculator';
415    $s->{extra_css}  = 'dex';
416    $s->{template}   = 'dex/utils/iv.tt';
417
418    if ($form->submitted) {
419        # TODO: hide this nonsense in Vee::Form?
420        my @errors;
421        my $pokemon = $c->model('DBIC::Pokemon')->find( get_pokemon($p->{pokemon}) );
422
423        push @errors, "Invalid Pok&eacute;mon entered." if not $pokemon;
424        push @errors, "Invalid level entered." if $p->{level} !~ /^(?: [1-9] | \d\d | 100 )$/x;
425        if (@errors) {
426            $s->{error_msg} = \@errors;
427            return;
428        }
429
430        # ok done with that validation nonsense
431           
432        my (@ivs, @iv_ranges, @possible_stats);
433        my @nature_change = (1) x 6;
434        my $nature_updown = $Natures{ $p->{nature} };
435        if (defined $nature_updown) {
436            $nature_change[ $nature_updown->{up}   ] = 1.1;
437            $nature_change[ $nature_updown->{down} ] = 0.9;
438        }
439
440        my $all_exact = 1;
441        for my $s (0 .. $#StatColumns) {
442            my $stat = $StatColumns[$s];
443            my $effort = $p->{ $stat . '_ev' } || 0;
444            my $base = $pokemon->$stat;
445            my $func = ($stat =~ /hp/) ? \&ingame_hp : \&ingame_stats;
446
447            # possible IVs; doing this inversely is very difficult due to rounding
448            for my $iv (0 .. 31) {
449                my $calculated = $func->($base, $p->{level}, $iv, $effort);
450                $calculated = int( $calculated * $nature_change[$s] );
451                $ivs[$s][$iv] = ($calculated == $p->{$stat});
452            }
453
454            # IV range in more readable text
455            $iv_ranges[$s]{min} = first_index { $_ } @{ $ivs[$s] };
456            $iv_ranges[$s]{max} =  last_index { $_ } @{ $ivs[$s] };
457            $all_exact = 0
458                if $iv_ranges[$s]{min} != $iv_ranges[$s]{max}
459                or $iv_ranges[$s]{min} == -1;
460
461            # entire range of possible stats for this Pokemon
462            $possible_stats[$s] =
463                int( $nature_change[$s] * $func->($base, $p->{level}, 0,  $effort) ) . ' - ' .
464                int( $nature_change[$s] * $func->($base, $p->{level}, 31, $effort) );
465        }
466        $s->{ivs}            = \@ivs;
467        $s->{iv_ranges}      = \@iv_ranges;
468        $s->{possible_stats} = \@possible_stats;
469
470        # TODO: improve this a bit since even at L100 you rarely get perfect results
471        if ($all_exact) {
472            $s->{hp_power} = ingame_hp_power( map { $_->{min} } @iv_ranges );
473            $s->{hp_type}  = ingame_hp_type ( map { $_->{min} } @iv_ranges );
474        }
475
476        $s->{nature}         = $p->{nature};
477        $s->{pokemon}        = $pokemon;
478    }
479}
480
481=head1 AUTHOR
482
483Maintainer: Alex "Eevee" Munroe (C<veekun@veekun.com>)
484
485See the included F<AUTHORS> file for a full list of contributers.
486
487=head1 LICENSE
488
489See the included F<LICENSE> file.
490
491=cut
492
4931;
Note: See TracBrowser for help on using the browser.