Make Pokemon.form an actual relation
[zzz-pokedex.git] / pokedex / util / media.py
1
2 """Media accessors
3
4 Most media accessor __init__s take an ORM object from the pokedex package.
5 Their various methods take a number of arguments specifying exactly which
6 file you want (such as the female sprite, backsprite, etc.).
7 ValueError is raised when the specified file cannot be found.
8
9 The accessors use fallbacks: for example Bulbasaur's males and females look the
10 same, so if you request Bulbasaur's female sprite, it will give you the common
11 image. Or for a Pokemon without individual form sprites, you will get the
12 common base sprite. Or for versions witout shiny Pokemon, you will always
13 get the non-shiny version (that's how shiny Pokemon looked there!).
14 However arguments such as `animated` don't use fallbacks.
15 You can set `strict` to True to disable these fallbacks and cause ValueError
16 to be raised when the exact specific file you asked for is not found. This is
17 useful for listing non-duplicate sprites, for example.
18
19 Use keyword arguments when calling the media-getting methods, unless noted
20 otherwise.
21
22 The returned "file" objects have useful attributes like relative_path,
23 path, and open().
24
25 All images are in the PNG format, except animations (GIF). All sounds are OGGs.
26 """
27
28 import os
29 import pkg_resources
30
31 class MediaFile(object):
32 """Represents a file: picture, sound, etc.
33
34 Attributes:
35 relative_path: Filesystem path relative to the media directory
36 path: Absolute path to the file
37
38 exists: True if the file exists
39
40 open(): Open the file
41 """
42 def __init__(self, *path_elements):
43 self.path_elements = path_elements
44 self._dexpath = '/'.join(('data', 'media') + path_elements)
45
46 @property
47 def relative_path(self):
48 return os.path.join(*self.path_elements)
49
50 @property
51 def path(self):
52 return pkg_resources.resource_filename('pokedex', self._dexpath)
53
54 def open(self):
55 """Open this file for reading, in the appropriate mode (i.e. binary)
56 """
57 return open(self.path, 'rb')
58
59 @property
60 def exists(self):
61 return pkg_resources.resource_exists('pokedex', self._dexpath)
62
63 def __eq__(self, other):
64 return self.path == other.path
65
66 def __ne__(self, other):
67 return self.path != other.path
68
69 def __str__(self):
70 return '<Pokedex file %s>' % self.relative_path
71
72 class BaseMedia(object):
73 def from_path_elements(self, path_elements, basename, extension,
74 surely_exists=False):
75 filename = basename + extension
76 path_elements = [self.toplevel_dir] + path_elements + [filename]
77 mfile = MediaFile(*path_elements)
78 if surely_exists or mfile.exists:
79 return mfile
80 else:
81 raise ValueError('File %s not found' % mfile.relative_path)
82
83 class _BasePokemonMedia(BaseMedia):
84 toplevel_dir = 'pokemon'
85 has_gender_differences = False
86 form = None
87 introduced_in = 0
88
89 # Info about of what's inside the pokemon main sprite directories, so we
90 # don't have to check directory existence all the time.
91 _pokemon_sprite_info = {
92 'red-blue': (1, set('back gray'.split())),
93 'red-green': (1, set('back gray'.split())),
94 'yellow': (1, set('back gray gbc'.split())),
95 'gold': (2, set('back shiny'.split())),
96 'silver': (2, set('back shiny'.split())),
97 'crystal': (2, set('animated back shiny'.split())),
98 'ruby-sapphire': (3, set('back shiny'.split())),
99 'emerald': (3, set('animated back shiny frame2'.split())),
100 'firered-leafgreen': (3, set('back shiny'.split())),
101 'diamond-pearl': (4, set('back shiny female frame2'.split())),
102 'platinum': (4, set('back shiny female frame2'.split())),
103 'heartgold-soulsilver': (4, set('back shiny female frame2'.split())),
104 'black-white': (5, set('back shiny female'.split())),
105 }
106
107 def __init__(self, pokemon_id, form_postfix=None):
108 BaseMedia.__init__(self)
109 self.pokemon_id = str(pokemon_id)
110 self.form_postfix = form_postfix
111
112 def _get_file(self, path_elements, extension, strict, surely_exists=False):
113 basename = str(self.pokemon_id)
114 if self.form_postfix:
115 fullname = basename + self.form_postfix
116 try:
117 return self.from_path_elements(
118 path_elements, fullname, extension,
119 surely_exists=surely_exists)
120 except ValueError:
121 if strict:
122 raise
123 return self.from_path_elements(path_elements, basename, extension,
124 surely_exists=surely_exists)
125
126 def sprite(self,
127 version='black-white',
128
129 # The media directories are in this order:
130 animated=False,
131 back=False,
132 color=None,
133 shiny=False,
134 female=False,
135 frame=None,
136
137 strict=False,
138 ):
139 """Get a main sprite sprite for a pokemon.
140
141 Everything except version should be given as a keyword argument.
142
143 Either specify version as an ORM object, or give the version path as
144 a string (which is the only way to get 'red-green'). Leave the default
145 for the latest version.
146
147 animated: get a GIF animation (currently Crystal & Emerald only)
148 back: get a backsprite instead of a front one
149 color: can be 'color' (RGBY only) or 'gbc' (Yellow only)
150 shiny: get a shiny sprite. In old versions, gives a normal sprite unless
151 `strict` is set
152 female: get a female sprite instead of male. For pokemon with no sexual
153 dimorphism, gets the common sprite unless `strict` is set.
154 frame: set to 2 to get the second frame of the animation
155 (Emerald, DPP, and HG/SS only)
156
157 If the sprite is not found, raise a ValueError.
158 """
159 if isinstance(version, basestring):
160 version_dir = version
161 try:
162 generation, info = self._pokemon_sprite_info[version_dir]
163 except KeyError:
164 raise ValueError('Version directory %s not found', version_dir)
165 else:
166 version_dir = version.identifier
167 try:
168 generation, info = self._pokemon_sprite_info[version_dir]
169 except KeyError:
170 version_group = version.version_group
171 version_dir = '-'.join(
172 v.identifier for v in version_group.versions)
173 generation, info = self._pokemon_sprite_info[version_dir]
174 if generation < self.introduced_in:
175 raise ValueError("Pokemon %s didn't exist in %s" % (
176 self.pokemon_id, version_dir))
177 path_elements = ['main-sprites', version_dir]
178 if animated:
179 if 'animated' not in info:
180 raise ValueError("No animated sprites for %s" % version_dir)
181 path_elements.append('animated')
182 extension = '.gif'
183 else:
184 extension = '.png'
185 if back:
186 if version_dir == 'emerald':
187 # Emerald backsprites are the same as ruby/sapphire
188 if strict:
189 raise ValueError("Emerald uses R/S backsprites")
190 if animated:
191 raise ValueError("No animated backsprites for Emerald")
192 path_elements[1] = version_dir = 'ruby-sapphire'
193 if version_dir == 'crystal' and animated:
194 raise ValueError("No animated backsprites for Crystal")
195 path_elements.append('back')
196 if color == 'gray':
197 if 'gray' not in info:
198 raise ValueError("No grayscale sprites for %s" % version_dir)
199 path_elements.append('gray')
200 elif color == 'gbc':
201 if 'gbc' not in info:
202 raise ValueError("No GBC sprites for %s" % version_dir)
203 path_elements.append('gbc')
204 elif color:
205 raise ValueError("Unknown color scheme: %s" % color)
206 if shiny:
207 if 'shiny' in info:
208 path_elements.append('shiny')
209 elif strict:
210 raise ValueError("No shiny sprites for %s" % version_dir)
211 if female:
212 female_sprite = self.has_gender_differences
213 # Chimecho's female back frame 2 sprite has one hand in
214 # a slightly different pose, in Platinum and HGSS
215 # (we have duplicate sprites frame 1, for convenience)
216 if self.pokemon_id == '358' and back and version_dir in (
217 'platinum', 'heartgold-soulsilver'):
218 female_sprite = True
219 female_sprite = female_sprite and 'female' in info
220 if female_sprite:
221 path_elements.append('female')
222 elif strict:
223 raise ValueError(
224 'Pokemon %s has no gender differences' % self.pokemon_id)
225 if not frame or frame == 1:
226 pass
227 elif frame == 2:
228 if 'frame2' in info:
229 path_elements.append('frame%s' % frame)
230 else:
231 raise ValueError("No frame 2 for %s" % version_dir)
232 else:
233 raise ValueError("Bad frame %s" % frame)
234 return self._get_file(path_elements, extension, strict=strict,
235 # Avoid a stat in the common case
236 surely_exists=(self.form and version_dir == 'black-white'
237 and not back and not female
238 and not self.form_postfix))
239
240 def _maybe_female(self, path_elements, female, strict):
241 if female:
242 if self.has_gender_differences:
243 elements = path_elements + ['female']
244 try:
245 return self._get_file(elements, '.png', strict=strict)
246 except ValueError:
247 if strict:
248 raise
249 elif strict:
250 raise ValueError(
251 'Pokemon %s has no gender differences' % self.pokemon_id)
252 return self._get_file(path_elements, '.png', strict=strict)
253
254 def icon(self, female=False, strict=False):
255 """Get the Pokemon's menu icon"""
256 return self._maybe_female(['icons'], female, strict)
257
258 def sugimori(self, female=False, strict=False):
259 """Get the Pokemon's official art, drawn by Ken Sugimori"""
260 return self._maybe_female(['sugimori'], female, strict)
261
262 def overworld(self,
263 direction='down',
264 shiny=False,
265 female=False,
266 frame=1,
267 strict=False,
268 ):
269 """Get an overworld sprite
270
271 direction: 'up', 'down', 'left', or 'right'
272 shiny: true for a shiny sprite
273 female: true for female sprite (or the common one for both M & F)
274 frame: 2 for the second animation frame
275
276 strict: disable fallback for `female`
277 """
278 path_elements = ['overworld']
279 if shiny:
280 path_elements.append('shiny')
281 if female:
282 if self.has_gender_differences:
283 path_elements.append('female')
284 elif strict:
285 raise ValueError('No female overworld sprite')
286 else:
287 female = False
288 path_elements.append(direction)
289 if frame and frame > 1:
290 path_elements.append('frame%s' % frame)
291 try:
292 return self._get_file(path_elements, '.png', strict=strict)
293 except ValueError:
294 if female and not strict:
295 path_elements.remove('female')
296 return self._get_file(path_elements, '.png', strict=strict)
297 else:
298 raise
299
300 def footprint(self, strict=False):
301 """Get the Pokemon's footprint"""
302 return self._get_file(['footprints'], '.png', strict=strict)
303
304 def trozei(self, strict=False):
305 """Get the Pokemon's animated Trozei sprite"""
306 return self._get_file(['trozei'], '.gif', strict=strict)
307
308 def cry(self, strict=False):
309 """Get the Pokemon's cry"""
310 return self._get_file(['cries'], '.ogg', strict=strict)
311
312 def cropped_sprite(self, strict=False):
313 """Get the Pokemon's cropped sprite"""
314 return self._get_file(['cropped'], '.png', strict=strict)
315
316 class PokemonFormMedia(_BasePokemonMedia):
317 """Media related to a Pokemon form
318 """
319 def __init__(self, pokemon_form):
320 pokemon_id = pokemon_form.form_base_pokemon_id
321 if pokemon_form.identifier:
322 form_postfix = '-' + pokemon_form.identifier
323 else:
324 form_postfix = None
325 _BasePokemonMedia.__init__(self, pokemon_id, form_postfix)
326 self.form = pokemon_form
327 pokemon = pokemon_form.form_base_pokemon
328 self.has_gender_differences = pokemon.has_gender_differences
329 self.introduced_in = pokemon.generation_id
330
331 class PokemonMedia(_BasePokemonMedia):
332 """Media related to a Pokemon
333 """
334 def __init__(self, pokemon):
335 _BasePokemonMedia.__init__(self, pokemon.id)
336 self.form = pokemon.default_form
337 self.has_gender_differences = (pokemon.has_gender_differences)
338 self.introduced_in = pokemon.generation_id
339
340 class UnknownPokemonMedia(_BasePokemonMedia):
341 """Media related to the unknown Pokemon ("?")
342
343 Note that not a lot of files are available for it.
344 """
345 def __init__(self):
346 _BasePokemonMedia.__init__(self, '0')
347
348 class EggMedia(_BasePokemonMedia):
349 """Media related to a pokemon egg
350
351 Note that not a lot of files are available for these.
352
353 Give a Manaphy as `pokemon` to get the Manaphy egg.
354 """
355 def __init__(self, pokemon=None):
356 if pokemon and pokemon.identifier == 'manaphy':
357 postfix = '-manaphy'
358 else:
359 postfix = None
360 _BasePokemonMedia.__init__(self, 'egg', postfix)
361
362 class SubstituteMedia(_BasePokemonMedia):
363 """Media related to the Substitute sprite
364
365 Note that not a lot of files are available for Substitute.
366 """
367 def __init__(self):
368 _BasePokemonMedia.__init__(self, 'substitute')
369
370 class _BaseItemMedia(BaseMedia):
371 toplevel_dir = 'items'
372 def underground(self, rotation=0):
373 """Get the item's sprite as it appears in the Sinnoh underground
374
375 Rotation can be 0, 90, 180, or 270.
376 """
377 if rotation:
378 basename = self.identifier + '-%s' % rotation
379 else:
380 basename = self.identifier
381 return self.from_path_elements(['underground'], basename, '.png')
382
383 class ItemMedia(_BaseItemMedia):
384 """Media related to an item
385 """
386 def __init__(self, item):
387 self.item = item
388 self.identifier = item.identifier
389
390 def sprite(self, version=None):
391 """Get the item's sprite
392
393 If version is not given, use the latest version.
394 """
395 identifier = self.identifier
396 # Handle machines
397 # We check the identifier, so that we don't query the machine
398 # information for any item.
399 if identifier.startswith(('tm', 'hm')):
400 try:
401 int(identifier[2:])
402 except ValueError:
403 # Not really a TM/HM
404 pass
405 else:
406 machines = self.item.machines
407 if version:
408 try:
409 machine = [
410 m for m in machines
411 if m.version_group == version.version_group
412 ][0]
413 except IndexError:
414 raise ValueError("%s doesn't exist in %s" % (
415 identifier, version.identifier))
416 else:
417 # They're ordered, so get the last one
418 machine = machines[-1]
419 type_identifier = machine.move.type.identifier
420 identifier = identifier[:2] + '-' + type_identifier
421 elif identifier.startswith('data-card-'):
422 try:
423 int(identifier[10:])
424 except ValueError:
425 # Not a real data card???
426 pass
427 else:
428 identifier = 'data-card'
429 if version is not None:
430 generation_id = version.generation.id
431 if generation_id <= 3 and identifier == 'dowsing-mchn':
432 identifier = 'itemfinder'
433 try:
434 gen = 'gen%s' % generation_id
435 return self.from_path_elements([gen], identifier, '.png')
436 except ValueError:
437 pass
438 return self.from_path_elements([], identifier, '.png',
439 surely_exists=True)
440
441 def underground(self, rotation=0):
442 """Get the item's sprite as it appears in the Sinnoh underground
443
444 Rotation can be 0, 90, 180, or 270.
445 """
446 if not self.item.appears_underground:
447 raise ValueError("%s doesn't appear underground" % self.identifier)
448 return super(ItemMedia, self).underground(rotation=rotation)
449
450 def berry_image(self):
451 """Get a berry's big sprite
452 """
453 if not self.item.berry:
454 raise ValueError("%s is not a berry" % self.identifier)
455 return self.from_path_elements(['berries'], self.identifier, '.png')
456
457 class UndergroundRockMedia(_BaseItemMedia):
458 """Media related to a rock in the Sinnoh underground
459
460 rock_type can be one of: i, ii, o, o-big, s, t, z
461 """
462 def __init__(self, rock_type):
463 self.identifier = 'rock-%s' % rock_type
464
465 class UndergroundSphereMedia(_BaseItemMedia):
466 """Media related to a sphere in the Sinnoh underground
467
468 color can be one of: red, blue, green, pale, prism
469 """
470 def __init__(self, color, big=False):
471 self.identifier = '%s-sphere' % color
472 if big:
473 self.identifier += '-big'
474
475 class _SimpleIconMedia(BaseMedia):
476 def __init__(self, thing):
477 self.identifier = thing.identifier
478
479 def icon(self):
480 return self.from_path_elements([], self.identifier, '.png')
481
482 class DamageClassMedia(_SimpleIconMedia):
483 toplevel_dir = 'damage-classes'
484
485 class HabitatMedia(_SimpleIconMedia):
486 toplevel_dir = 'habitats'
487
488 class ShapeMedia(_SimpleIconMedia):
489 toplevel_dir = 'shapes'
490
491 class ItemPocketMedia(_SimpleIconMedia):
492 toplevel_dir = 'item-pockets'
493 def icon(self, selected=False):
494 if selected:
495 return self.from_path_elements(
496 ['selected'], self.identifier, '.png')
497 else:
498 return self.from_path_elements([], self.identifier, '.png')
499
500 class _LanguageIconMedia(_SimpleIconMedia):
501 def icon(self, lang='en'):
502 return self.from_path_elements([lang], self.identifier, '.png')
503
504 class ContestTypeMedia(_LanguageIconMedia):
505 toplevel_dir = 'contest-types'
506
507 class TypeMedia(_LanguageIconMedia):
508 toplevel_dir = 'types'
509
510 ''' XXX: No accessors for:
511 chrome
512 fonts
513 ribbons
514 '''