Add media accessors
authorPetr Viktorin <encukou@gmail.com>
Thu, 14 Apr 2011 10:52:18 +0000 (13:52 +0300)
committerPetr Viktorin <encukou@gmail.com>
Thu, 14 Apr 2011 10:52:18 +0000 (13:52 +0300)
pokedex/util/__init__.py [new file with mode: 0644]
pokedex/util/media.py [new file with mode: 0644]

diff --git a/pokedex/util/__init__.py b/pokedex/util/__init__.py
new file mode 100644 (file)
index 0000000..5667575
--- /dev/null
@@ -0,0 +1 @@
+# This directory is a Python package.
\ No newline at end of file
diff --git a/pokedex/util/media.py b/pokedex/util/media.py
new file mode 100644 (file)
index 0000000..9faf19f
--- /dev/null
@@ -0,0 +1,514 @@
+
+"""Media accessors
+
+Most media accessor __init__s take an ORM object from the pokedex package.
+Their various methods take a number of arguments specifying exactly which
+file you want (such as the female sprite, backsprite, etc.).
+ValueError is raised when the specified file cannot be found.
+
+The accessors use fallbacks: for example Bulbasaur's males and females look the
+same, so if you request Bulbasaur's female sprite, it will give you the common
+image. Or for a Pokemon without individual form sprites, you will get the
+common base sprite. Or for versions witout shiny Pokemon, you will always
+get the non-shiny version (that's how shiny Pokemon looked there!).
+However arguments such as `animated` don't use fallbacks.
+You can set `strict` to True to disable these fallbacks and cause ValueError
+to be raised when the exact specific file you asked for is not found. This is
+useful for listing non-duplicate sprites, for example.
+
+Use keyword arguments when calling the media-getting methods, unless noted
+otherwise.
+
+The returned "file" objects have useful attributes like relative_path,
+path, and open().
+
+All images are in the PNG format, except animations (GIF). All sounds are OGGs.
+"""
+
+import os
+import pkg_resources
+
+class MediaFile(object):
+    """Represents a file: picture, sound, etc.
+
+    Attributes:
+    relative_path: Filesystem path relative to the media directory
+    path: Absolute path to the file
+
+    exists: True if the file exists
+
+    open(): Open the file
+    """
+    def __init__(self, *path_elements):
+        self.path_elements = path_elements
+        self._dexpath = '/'.join(('data', 'media') + path_elements)
+
+    @property
+    def relative_path(self):
+        return os.path.join(*self.path_elements)
+
+    @property
+    def path(self):
+        return pkg_resources.resource_filename('pokedex', self._dexpath)
+
+    def open(self):
+        """Open this file for reading, in the appropriate mode (i.e. binary)
+        """
+        return open(self.path, 'rb')
+
+    @property
+    def exists(self):
+        return pkg_resources.resource_exists('pokedex', self._dexpath)
+
+    def __eq__(self, other):
+        return self.path == other.path
+
+    def __ne__(self, other):
+        return self.path != other.path
+
+    def __str__(self):
+        return '<Pokedex file %s>' % self.relative_path
+
+class BaseMedia(object):
+    def from_path_elements(self, path_elements, basename, extension,
+            surely_exists=False):
+        filename = basename + extension
+        path_elements = [self.toplevel_dir] + path_elements + [filename]
+        mfile = MediaFile(*path_elements)
+        if surely_exists or mfile.exists:
+            return mfile
+        else:
+            raise ValueError('File %s not found' % mfile.relative_path)
+
+class _BasePokemonMedia(BaseMedia):
+    toplevel_dir = 'pokemon'
+    has_gender_differences = False
+    form = None
+    introduced_in = 0
+
+    # Info about of what's inside the pokemon main sprite directories, so we
+    # don't have to check directory existence all the time.
+    _pokemon_sprite_info = {
+            'red-blue': (1, set('back gray'.split())),
+            'red-green': (1, set('back gray'.split())),
+            'yellow': (1, set('back gray gbc'.split())),
+            'gold': (2, set('back shiny'.split())),
+            'silver': (2, set('back shiny'.split())),
+            'crystal': (2, set('animated back shiny'.split())),
+            'ruby-sapphire': (3, set('back shiny'.split())),
+            'emerald': (3, set('animated back shiny frame2'.split())),
+            'firered-leafgreen': (3, set('back shiny'.split())),
+            'diamond-pearl': (4, set('back shiny female frame2'.split())),
+            'platinum': (4, set('back shiny female frame2'.split())),
+            'heartgold-soulsilver': (4, set('back shiny female frame2'.split())),
+            'black-white': (5, set('back shiny female'.split())),
+        }
+
+    def __init__(self, pokemon_id, form_postfix=None):
+        BaseMedia.__init__(self)
+        self.pokemon_id = str(pokemon_id)
+        self.form_postfix = form_postfix
+
+    def _get_file(self, path_elements, extension, strict, surely_exists=False):
+        basename = str(self.pokemon_id)
+        if self.form_postfix:
+            fullname = basename + self.form_postfix
+            try:
+                return self.from_path_elements(
+                        path_elements, fullname, extension,
+                        surely_exists=surely_exists)
+            except ValueError:
+                if strict:
+                    raise
+        return self.from_path_elements(path_elements, basename, extension,
+                surely_exists=surely_exists)
+
+    def sprite(self,
+            version='black-white',
+
+            # The media directories are in this order:
+            animated=False,
+            back=False,
+            color=None,
+            shiny=False,
+            female=False,
+            frame=None,
+
+            strict=False,
+        ):
+        """Get a main sprite sprite for a pokemon.
+
+        Everything except version should be given as a keyword argument.
+
+        Either specify version as an ORM object, or give the version path as
+        a string (which is the only way to get 'red-green'). Leave the default
+        for the latest version.
+
+        animated: get a GIF animation (currently Crystal & Emerald only)
+        back: get a backsprite instead of a front one
+        color: can be 'color' (RGBY only) or 'gbc' (Yellow only)
+        shiny: get a shiny sprite. In old versions, gives a normal sprite unless
+            `strict` is set
+        female: get a female sprite instead of male. For pokemon with no sexual
+            dimorphism, gets the common sprite unless `strict` is set.
+        frame: set to 2 to get the second frame of the animation
+            (Emerald, DPP, and HG/SS only)
+
+        If the sprite is not found, raise a ValueError.
+        """
+        if isinstance(version, basestring):
+            version_dir = version
+            try:
+                generation, info = self._pokemon_sprite_info[version_dir]
+            except KeyError:
+                raise ValueError('Version directory %s not found', version_dir)
+        else:
+            version_dir = version.identifier
+            try:
+                generation, info = self._pokemon_sprite_info[version_dir]
+            except KeyError:
+                version_group = version.version_group
+                version_dir = '-'.join(
+                        v.identifier for v in version_group.versions)
+                generation, info = self._pokemon_sprite_info[version_dir]
+        if generation < self.introduced_in:
+            raise ValueError("Pokemon %s didn't exist in %s" % (
+                    self.pokemon_id, version_dir))
+        path_elements = ['main-sprites', version_dir]
+        if animated:
+            if 'animated' not in info:
+                raise ValueError("No animated sprites for %s" % version_dir)
+            path_elements.append('animated')
+            extension = '.gif'
+        else:
+            extension = '.png'
+        if back:
+            if version_dir == 'emerald':
+                # Emerald backsprites are the same as ruby/sapphire
+                if strict:
+                    raise ValueError("Emerald uses R/S backsprites")
+                if animated:
+                    raise ValueError("No animated backsprites for Emerald")
+                path_elements[1] = version_dir = 'ruby-sapphire'
+            if version_dir == 'crystal' and animated:
+                raise ValueError("No animated backsprites for Crystal")
+            path_elements.append('back')
+        if color == 'gray':
+            if 'gray' not in info:
+                raise ValueError("No grayscale sprites for %s" % version_dir)
+            path_elements.append('gray')
+        elif color == 'gbc':
+            if 'gbc' not in info:
+                raise ValueError("No GBC sprites for %s" % version_dir)
+            path_elements.append('gbc')
+        elif color:
+            raise ValueError("Unknown color scheme: %s" % color)
+        if shiny:
+            if 'shiny' in info:
+                path_elements.append('shiny')
+            elif strict:
+                raise ValueError("No shiny sprites for %s" % version_dir)
+        if female:
+            female_sprite = self.has_gender_differences
+            # Chimecho's female back frame 2 sprite has one hand in
+            # a slightly different pose, in Platinum and HGSS
+            # (we have duplicate sprites frame 1, for convenience)
+            if self.pokemon_id == '358' and back and version_dir in (
+                    'platinum', 'heartgold-soulsilver'):
+                female_sprite = True
+            female_sprite = female_sprite and 'female' in info
+            if female_sprite:
+                path_elements.append('female')
+            elif strict:
+                raise ValueError(
+                    'Pokemon %s has no gender differences' % self.pokemon_id)
+        if not frame or frame == 1:
+            pass
+        elif frame == 2:
+            if 'frame2' in info:
+                path_elements.append('frame%s' % frame)
+            else:
+                raise ValueError("No frame 2 for %s" % version_dir)
+        else:
+            raise ValueError("Bad frame %s" % frame)
+        return self._get_file(path_elements, extension, strict=strict,
+                # Avoid a stat in the common case
+                surely_exists=(self.form and version_dir == 'black-white'
+                    and not back and not female
+                    and not self.form_postfix))
+
+    def _maybe_female(self, path_elements, female, strict):
+        if female:
+            if self.has_gender_differences:
+                elements = path_elements + ['female']
+                try:
+                    return self._get_file(elements, '.png', strict=strict)
+                except ValueError:
+                    if strict:
+                        raise
+            elif strict:
+                raise ValueError(
+                    'Pokemon %s has no gender differences' % self.pokemon_id)
+        return self._get_file(path_elements, '.png', strict=strict)
+
+    def icon(self, female=False, strict=False):
+        """Get the Pokemon's menu icon"""
+        return self._maybe_female(['icons'], female, strict)
+
+    def sugimori(self, female=False, strict=False):
+        """Get the Pokemon's official art, drawn by Ken Sugimori"""
+        return self._maybe_female(['sugimori'], female, strict)
+
+    def overworld(self,
+            direction='down',
+            shiny=False,
+            female=False,
+            frame=1,
+            strict=False,
+        ):
+        """Get an overworld sprite
+
+        direction: 'up', 'down', 'left', or 'right'
+        shiny: true for a shiny sprite
+        female: true for female sprite (or the common one for both M & F)
+        frame: 2 for the second animation frame
+
+        strict: disable fallback for `female`
+        """
+        path_elements = ['overworld']
+        if shiny:
+            path_elements.append('shiny')
+        if female:
+            if self.has_gender_differences:
+                path_elements.append('female')
+            elif strict:
+                raise ValueError('No female overworld sprite')
+            else:
+                female = False
+        path_elements.append(direction)
+        if frame and frame > 1:
+            path_elements.append('frame%s' % frame)
+        try:
+            return self._get_file(path_elements, '.png', strict=strict)
+        except ValueError:
+            if female and not strict:
+                path_elements.remove('female')
+                return self._get_file(path_elements, '.png', strict=strict)
+            else:
+                raise
+
+    def footprint(self, strict=False):
+        """Get the Pokemon's footprint"""
+        return self._get_file(['footprints'], '.png', strict=strict)
+
+    def trozei(self, strict=False):
+        """Get the Pokemon's animated Trozei sprite"""
+        return self._get_file(['trozei'], '.gif', strict=strict)
+
+    def cry(self, strict=False):
+        """Get the Pokemon's cry"""
+        return self._get_file(['cries'], '.ogg', strict=strict)
+
+    def cropped_sprite(self, strict=False):
+        """Get the Pokemon's cropped sprite"""
+        return self._get_file(['cropped'], '.png', strict=strict)
+
+class PokemonFormMedia(_BasePokemonMedia):
+    """Media related to a Pokemon form
+    """
+    def __init__(self, pokemon_form):
+        pokemon_id = pokemon_form.form_base_pokemon_id
+        if pokemon_form.identifier:
+            form_postfix = '-' + pokemon_form.identifier
+        else:
+            form_postfix = None
+        _BasePokemonMedia.__init__(self, pokemon_id, form_postfix)
+        self.form = pokemon_form
+        pokemon = pokemon_form.form_base_pokemon
+        self.has_gender_differences = pokemon.has_gender_differences
+        self.introduced_in = pokemon.generation_id
+
+class PokemonMedia(_BasePokemonMedia):
+    """Media related to a Pokemon
+    """
+    def __init__(self, pokemon):
+        _BasePokemonMedia.__init__(self, pokemon.id)
+        self.form = pokemon.default_form
+        self.has_gender_differences = (pokemon.has_gender_differences)
+        self.introduced_in = pokemon.generation_id
+
+class UnknownPokemonMedia(_BasePokemonMedia):
+    """Media related to the unknown Pokemon ("?")
+
+    Note that not a lot of files are available for it.
+    """
+    def __init__(self):
+        _BasePokemonMedia.__init__(self, '0')
+
+class EggMedia(_BasePokemonMedia):
+    """Media related to a pokemon egg
+
+    Note that not a lot of files are available for these.
+
+    Give a Manaphy as `pokemon` to get the Manaphy egg.
+    """
+    def __init__(self, pokemon=None):
+        if pokemon and pokemon.identifier == 'manaphy':
+            postfix = '-manaphy'
+        else:
+            postfix = None
+        _BasePokemonMedia.__init__(self, 'egg', postfix)
+
+class SubstituteMedia(_BasePokemonMedia):
+    """Media related to the Substitute sprite
+
+    Note that not a lot of files are available for Substitute.
+    """
+    def __init__(self):
+        _BasePokemonMedia.__init__(self, 'substitute')
+
+class _BaseItemMedia(BaseMedia):
+    toplevel_dir = 'items'
+    def underground(self, rotation=0):
+        """Get the item's sprite as it appears in the Sinnoh underground
+
+        Rotation can be 0, 90, 180, or 270.
+        """
+        if rotation:
+            basename = self.identifier + '-%s' % rotation
+        else:
+            basename = self.identifier
+        return self.from_path_elements(['underground'], basename, '.png')
+
+class ItemMedia(_BaseItemMedia):
+    """Media related to an item
+    """
+    def __init__(self, item):
+        self.item = item
+        self.identifier = item.identifier
+
+    def sprite(self, version=None):
+        """Get the item's sprite
+
+        If version is not given, use the latest version.
+        """
+        identifier = self.identifier
+        # Handle machines
+        # We check the identifier, so that we don't query the machine
+        # information for any item.
+        if identifier.startswith(('tm', 'hm')):
+            try:
+                int(identifier[2:])
+            except ValueError:
+                # Not really a TM/HM
+                pass
+            else:
+                machines = self.item.machines
+                if version:
+                    try:
+                        machine = [
+                                m for m in machines
+                                if m.version_group == version.version_group
+                            ][0]
+                    except IndexError:
+                        raise ValueError("%s doesn't exist in %s" % (
+                                identifier, version.identifier))
+                else:
+                    # They're ordered, so get the last one
+                    machine = machines[-1]
+                type_identifier = machine.move.type.identifier
+                identifier = identifier[:2] + '-' + type_identifier
+        elif identifier.startswith('data-card-'):
+            try:
+                int(identifier[10:])
+            except ValueError:
+                # Not a real data card???
+                pass
+            else:
+                identifier = 'data-card'
+        if version is not None:
+            generation_id = version.generation.id
+            if generation_id <= 3 and identifier == 'dowsing-mchn':
+                identifier = 'itemfinder'
+            try:
+                gen = 'gen%s' % generation_id
+                return self.from_path_elements([gen], identifier, '.png')
+            except ValueError:
+                pass
+        return self.from_path_elements([], identifier, '.png',
+                surely_exists=True)
+
+    def underground(self, rotation=0):
+        """Get the item's sprite as it appears in the Sinnoh underground
+
+        Rotation can be 0, 90, 180, or 270.
+        """
+        if not self.item.appears_underground:
+            raise ValueError("%s doesn't appear underground" % self.identifier)
+        return super(ItemMedia, self).underground(rotation=rotation)
+
+    def berry_image(self):
+        """Get a berry's big sprite
+        """
+        if not self.item.berry:
+            raise ValueError("%s is not a berry" % self.identifier)
+        return self.from_path_elements(['berries'], self.identifier, '.png')
+
+class UndergroundRockMedia(_BaseItemMedia):
+    """Media related to a rock in the Sinnoh underground
+
+    rock_type can be one of: i, ii, o, o-big, s, t, z
+    """
+    def __init__(self, rock_type):
+        self.identifier = 'rock-%s' % rock_type
+
+class UndergroundSphereMedia(_BaseItemMedia):
+    """Media related to a sphere in the Sinnoh underground
+
+    color can be one of: red, blue, green, pale, prism
+    """
+    def __init__(self, color, big=False):
+        self.identifier = '%s-sphere' % color
+        if big:
+            self.identifier += '-big'
+
+class _SimpleIconMedia(BaseMedia):
+    def __init__(self, thing):
+        self.identifier = thing.identifier
+
+    def icon(self):
+        return self.from_path_elements([], self.identifier, '.png')
+
+class DamageClassMedia(_SimpleIconMedia):
+    toplevel_dir = 'damage-classes'
+
+class HabitatMedia(_SimpleIconMedia):
+    toplevel_dir = 'habitats'
+
+class ShapeMedia(_SimpleIconMedia):
+    toplevel_dir = 'shapes'
+
+class ItemPocketMedia(_SimpleIconMedia):
+    toplevel_dir = 'item-pockets'
+    def icon(self, selected=False):
+        if selected:
+            return self.from_path_elements(
+                    ['selected'], self.identifier, '.png')
+        else:
+            return self.from_path_elements([], self.identifier, '.png')
+
+class _LanguageIconMedia(_SimpleIconMedia):
+    def icon(self, lang='en'):
+        return self.from_path_elements([lang], self.identifier, '.png')
+
+class ContestTypeMedia(_LanguageIconMedia):
+    toplevel_dir = 'contest-types'
+
+class TypeMedia(_LanguageIconMedia):
+    toplevel_dir = 'types'
+
+''' XXX: No accessors for:
+chrome
+fonts
+ribbons
+'''