Implement an ability changelog; add a few changes. #78
[zzz-pokedex.git] / pokedex / db / tables.py
index 3b3c995..958cfb7 100644 (file)
@@ -53,6 +53,16 @@ class Ability(TableBase):
     short_effect = Column(markdown.MarkdownColumn(255), nullable=False,
         info=dict(description="Short summary of this ability's effect", format='markdown'))
 
+class AbilityChangelog(TableBase):
+    """History of changes to abilities across main game versions."""
+    __tablename__ = 'ability_changelog'
+    ability_id = Column(Integer, ForeignKey('abilities.id'), primary_key=True, nullable=False,
+        info=dict(description="The ID of the ability that changed"))
+    changed_in_version_group_id = Column(Integer, ForeignKey('version_groups.id'), primary_key=True, nullable=False,
+        info=dict(description="The ID of the version group in which the ability changed"))
+    effect = Column(markdown.MarkdownColumn(255), nullable=False,
+        info=dict(description="A description of the old behavior", format='markdown'))
+
 class AbilityFlavorText(TableBase):
     u"""In-game flavor text of an ability
     """
@@ -672,6 +682,20 @@ class Move(TableBase):
     super_contest_effect_id = Column(Integer, ForeignKey('super_contest_effects.id'), nullable=True,
         info=dict(description="ID of the move's Super Contest effect"))
 
+class MoveChangelog(TableBase):
+    """History of changes to moves across main game versions."""
+    __tablename__ = 'move_changelog'
+    move_id = Column(Integer, ForeignKey('moves.id'), primary_key=True, nullable=False,
+        info=dict(description="ID of the move that changed"))
+    changed_in_version_group_id = Column(Integer, ForeignKey('version_groups.id'), primary_key=True, nullable=False,
+        info=dict(description="ID of the version group in which the move changed"))
+    power = Column(SmallInteger, nullable=True,
+        info=dict(description="Prior base power of the move, or NULL if unchanged"))
+    pp = Column(SmallInteger, nullable=True,
+        info=dict(description="Prior base PP of the move, or NULL if unchanged"))
+    accuracy = Column(SmallInteger, nullable=True,
+        info=dict(description="Prior accuracy of the move, or NULL if unchanged"))
+
 class Nature(TableBase):
     u"""A nature a pokémon can have, such as Calm or Brave
     """
@@ -757,36 +781,15 @@ class Pokedex(TableBase):
     description = Column(Unicode(512),
         info=dict(description=u"A longer description of the pokédex", format='plaintext'))
 
-class PokedexVersionGroup(TableBase):
-    u"""Maps a pokédex to the version group that uses it
-    """
-    __tablename__ = 'pokedex_version_groups'
-    pokedex_id = Column(Integer, ForeignKey('pokedexes.id'), primary_key=True, nullable=False, autoincrement=False,
-        info=dict(description=u"ID of the pokédex"))
-    version_group_id = Column(Integer, ForeignKey('version_groups.id'), primary_key=True, nullable=False, autoincrement=False,
-        info=dict(description=u"ID of the version group"))
-
 class Pokemon(TableBase):
-    u"""A species of pokémon. The core to this whole mess.
-
-    Note that I use both 'forme' and 'form' in both code and the database.  I
-    only use 'forme' when specifically referring to Pokémon that have multiple
-    distinct species as forms—i.e., different stats or movesets.  'Form' is a
-    more general term referring to any variation within a species, including
-    purely cosmetic forms like Unown.
+    u"""A species of Pokémon.  The core to this whole mess.
     """
-    # XXX: Refine the form-specific docs
-    # XXX: Update form/forme discussion when #179 is dealt with.
     __tablename__ = 'pokemon'
     __singlename__ = 'pokemon'
     id = Column(Integer, primary_key=True, nullable=False,
         info=dict(description=u"A numeric ID"))
     name = Column(Unicode(20), nullable=False,
-        info=dict(description=u"The English name of the pokémon", official=True, format='plaintext'))
-    forme_name = Column(Unicode(16),
-        info=dict(description=u"The name of this form, if the species has forms", format='plaintext'))
-    forme_base_pokemon_id = Column(Integer, ForeignKey('pokemon.id'),
-        info=dict(description=u"ID for the base form, if this species has one"))  # XXX: ?
+        info=dict(description=u"The English name of the Pokémon", official=True, format='plaintext'))
     generation_id = Column(Integer, ForeignKey('generations.id'),
         info=dict(description=u"ID of the generation this species first appeared in"))
     evolution_chain_id = Column(Integer, ForeignKey('evolution_chains.id'),
@@ -824,23 +827,32 @@ class Pokemon(TableBase):
     ### Stuff to handle alternate Pokémon forms
 
     @property
-    def national_id(self):
-        u"""Returns the National Pokédex number for this Pokémon.  Use this
-        instead of the id directly; alternate formes may make the id incorrect.
+    def is_base_form(self):
+        u"""Returns True iff the Pokémon is the base form for its species,
+        e.g. Land Shaymin.
         """
 
-        if self.forme_base_pokemon_id:
-            return self.forme_base_pokemon_id
-        return self.id
+        return self.unique_form is None or self.unique_form.is_default
 
     @property
-    def full_name(self):
-        u"""Returns the name of this Pokémon, including its Forme, if any.
+    def form_name(self):
+        u"""Returns the Pokémon's form name if it represents a particular form
+        and that form has a name, or None otherwise.
         """
 
-        if self.forme_name:
-            return "%s %s" % (self.forme_name.title(), self.name)
-        return self.name
+        # If self.unique_form is None, the short-circuit "and" will go ahead
+        # and return that.  Otherwise, it'll return the form's name, which may
+        # also be None.
+        return self.unique_form and self.unique_form.name
+
+    @property
+    def full_name(self):
+        u"""Returns the Pokémon's name, including its form if applicable."""
+
+        if self.form_name:
+            return '{0} {1}'.format(self.form_name, self.name)
+        else:
+            return self.name
 
     @property
     def normal_form(self):
@@ -848,9 +860,8 @@ class Pokemon(TableBase):
         regular Deoxys when called on any Deoxys form.
         """
 
-        if self.forme_base_pokemon:
-            return self.forme_base_pokemon
-
+        if self.unique_form:
+            return self.unique_form.form_base_pokemon
         return self
 
     ### Not forms!
@@ -978,29 +989,76 @@ class PokemonFlavorText(TableBase):
     flavor_text = Column(Unicode(255), nullable=False,
         info=dict(description=u"ID of the version that has this flavor text", official=True, format='gametext'))
 
+class PokemonForm(TableBase):
+    u"""An individual form of a Pokémon.
+
+    Pokémon that do not have separate forms are still given a single row to
+    represent their single form.
+    """
+    __tablename__ = 'pokemon_forms'
+    __singlename__ = 'pokemon_form'
+    id = Column(Integer, primary_key=True, nullable=False,
+        info=dict(description=u'A unique ID for this form.'))
+    name = Column(Unicode(16), nullable=True,
+        info=dict(description=u"This form's name, e.g. \"Plant\" for Plant Cloak Burmy.", official=True, format='plaintext'))
+    form_base_pokemon_id = Column(Integer, ForeignKey('pokemon.id'), nullable=False, autoincrement=False,
+        info=dict(description=u'The ID of the base Pokémon for this form.'))
+    unique_pokemon_id = Column(Integer, ForeignKey('pokemon.id'), autoincrement=False,
+        info=dict(description=u'The ID of a Pokémon that represents specifically this form, for Pokémon with functionally-different forms like Wormadam.'))
+    introduced_in_version_group_id = Column(Integer, ForeignKey('version_groups.id'), autoincrement=False,
+        info=dict(description=u'The ID of the version group in which this form first appeared.'))
+    is_default = Column(Boolean, nullable=False,
+        info=dict(description=u'Set for exactly one form used as the default for each species.'))
+    order = Column(Integer, nullable=False, autoincrement=False,
+        info=dict(description=u'The order in which forms should be sorted.  Multiple forms may have equal order, in which case they should fall back on sorting by name.'))
+
+    @property
+    def full_name(self):
+        u"""Returns the full name of this form, e.g. "Plant Cloak"."""
+
+        if not self.name:
+            return None
+        elif self.form_group and self.form_group.term:
+            return '{0} {1}'.format(self.name, self.form_group.term)
+        else:
+            return self.name
+
+    @property
+    def pokemon_name(self):
+        u"""Returns the name of this Pokémon with this form, e.g. "Plant
+        Burmy".
+        """
+
+        if self.name:
+            return '{0} {1}'.format(self.name, self.form_base_pokemon.name)
+        else:
+            return self.form_base_pokemon.name
+
 class PokemonFormGroup(TableBase):
-    # XXX: Give the docstring here & check column descriptions
+    u"""Information about a Pokémon's forms as a group."""
     __tablename__ = 'pokemon_form_groups'
     pokemon_id = Column(Integer, ForeignKey('pokemon.id'), primary_key=True, nullable=False, autoincrement=False,
-        info=dict(description=u"ID of the base form pokémon"))
+        info=dict(description=u"ID of the base form Pokémon"))
+    term = Column(Unicode(16), nullable=True,
+        info=dict(description=u"The term for this Pokémon's forms, e.g. \"Cloak\" for Burmy or \"Forme\" for Deoxys.", official=True, format='plaintext'))
     is_battle_only = Column(Boolean, nullable=False,
         info=dict(description=u"Set iff the forms only change in battle"))
     description = Column(markdown.MarkdownColumn(1024), nullable=False,
         info=dict(description=u"English description of how the forms work", format='markdown'))
 
-class PokemonFormSprite(TableBase):
-    # XXX: Give the docstring here & check column descriptions
-    __tablename__ = 'pokemon_form_sprites'
-    id = Column(Integer, primary_key=True, nullable=False,
-        info=dict(description=u"A numeric ID"))
-    pokemon_id = Column(Integer, ForeignKey('pokemon.id'), primary_key=True, nullable=False, autoincrement=False,
-        info=dict(description=u"ID of the pokémon"))
-    introduced_in_version_group_id = Column(Integer, ForeignKey('version_groups.id'), primary_key=True, nullable=False, autoincrement=False,
-        info=dict(description=u"ID of version group the form was introduced in"))
-    name = Column(Unicode(16), nullable=True,
-        info=dict(description=u"English name of the form", format='plaintext'))
-    is_default = Column(Boolean, nullable=True,
-        info=dict(description=u'Set iff the form is the base, normal, usual, or otherwise default form'))
+class PokemonFormPokeathlonStat(TableBase):
+    u"""A Pokémon form's performance in one Pokéathlon stat."""
+    __tablename__ = 'pokemon_form_pokeathlon_stats'
+    pokemon_form_id = Column(Integer, ForeignKey('pokemon_forms.id'), primary_key=True, nullable=False, autoincrement=False,
+        info=dict(description=u'The ID of the Pokémon form.'))
+    pokeathlon_stat_id = Column(Integer, ForeignKey('pokeathlon_stats.id'), primary_key=True, nullable=False, autoincrement=False,
+        info=dict(description=u'The ID of the Pokéathlon stat.'))
+    minimum_stat = Column(Integer, nullable=False, autoincrement=False,
+        info=dict(description=u'The minimum value for this stat for this Pokémon form.'))
+    base_stat = Column(Integer, nullable=False, autoincrement=False,
+        info=dict(description=u'The default value for this stat for this Pokémon form.'))
+    maximum_stat = Column(Integer, nullable=False, autoincrement=False,
+        info=dict(description=u'The maximum value for this stat for this Pokémon form.'))
 
 class PokemonHabitat(TableBase):
     u"""The habitat of a pokémon, as given in the FireRed/LeafGreen version pokédex
@@ -1200,6 +1258,8 @@ class VersionGroup(TableBase):
         info=dict(description=u"A numeric ID"))
     generation_id = Column(Integer, ForeignKey('generations.id'), nullable=False,
         info=dict(description=u"ID of the generation the games of this group belong to"))
+    pokedex_id = Column(Integer, ForeignKey('pokedexes.id'), nullable=False,
+        info=dict(description=u"ID of the regional Pokédex used in this version group."))
 
 class VersionGroupRegion(TableBase):
     u"""Maps a region to a game version group that features it
@@ -1211,18 +1271,22 @@ class VersionGroupRegion(TableBase):
         info=dict(description=u"ID of the region"))
 
 class Version(TableBase):
-    u"""A version of a mainline pokémon game
+    u"""An individual main-series Pokémon game
     """
     __tablename__ = 'versions'
     id = Column(Integer, primary_key=True, nullable=False,
-        info=dict(description=u"A numeric ID"))
+        info=dict(description=u"A unique ID for this version"))
     version_group_id = Column(Integer, ForeignKey('version_groups.id'), nullable=False,
-        info=dict(description=u"ID of the version group this game belongs to"))
+        info=dict(description=u"The ID of the version group this game belongs to"))
     name = Column(Unicode(32), nullable=False,
         info=dict(description=u'The English name of the game, without the "Pokémon" prefix', official=True, format='plaintext'))
 
 
 ### Relations down here, to avoid ordering problems
+Ability.changelog = relation(AbilityChangelog,
+    order_by=AbilityChangelog.changed_in_version_group_id.desc(),
+    backref='ability',
+)
 Ability.flavor_text = relation(AbilityFlavorText, order_by=AbilityFlavorText.version_group_id, backref='ability')
 Ability.foreign_names = relation(AbilityName, backref='ability')
 Ability.generation = relation(Generation, backref='abilities')
@@ -1230,6 +1294,8 @@ Ability.pokemon = relation(Pokemon,
     secondary=PokemonAbility.__table__,
 )
 
+AbilityChangelog.changed_in = relation(VersionGroup, backref='ability_changelog')
+
 AbilityFlavorText.version_group = relation(VersionGroup)
 
 AbilityName.language = relation(Language)
@@ -1308,6 +1374,10 @@ LocationInternalID.generation = relation(Generation)
 Machine.item = relation(Item)
 Machine.version_group = relation(VersionGroup)
 
+Move.changelog = relation(MoveChangelog,
+    order_by=MoveChangelog.changed_in_version_group_id.asc(),
+    backref='move',
+)
 Move.contest_effect = relation(ContestEffect, backref='moves')
 Move.contest_combo_next = association_proxy('contest_combo_first', 'second')
 Move.contest_combo_prev = association_proxy('contest_combo_second', 'first')
@@ -1329,6 +1399,8 @@ Move.type = relation(Type, backref='moves')
 Move.effect = markdown.MoveEffectProperty('effect')
 Move.short_effect = markdown.MoveEffectProperty('short_effect')
 
+MoveChangelog.changed_in = relation(VersionGroup, backref='move_changelog')
+
 MoveEffect.category_map = relation(MoveEffectCategoryMap)
 MoveEffect.categories = association_proxy('category_map', 'category')
 MoveEffectCategoryMap.category = relation(MoveEffectCategory)
@@ -1360,7 +1432,7 @@ NatureName.language = relation(Language)
 NaturePokeathlonStat.pokeathlon_stat = relation(PokeathlonStat, backref='nature_effects')
 
 Pokedex.region = relation(Region, backref='pokedexes')
-Pokedex.version_groups = relation(VersionGroup, secondary=PokedexVersionGroup.__table__, backref='pokedexes')
+Pokedex.version_groups = relation(VersionGroup, order_by=VersionGroup.id, back_populates='pokedex')
 
 Pokemon.all_abilities = relation(Ability,
     secondary=PokemonAbility.__table__,
@@ -1382,18 +1454,9 @@ Pokemon.dream_ability = relation(Ability,
     ),
     uselist=False,
 )
-Pokemon.formes = relation(Pokemon, primaryjoin=Pokemon.id==Pokemon.forme_base_pokemon_id,
-                                               backref=backref('forme_base_pokemon',
-                                                               remote_side=[Pokemon.id]))
 Pokemon.pokemon_color = relation(PokemonColor, backref='pokemon')
 Pokemon.color = association_proxy('pokemon_color', 'name')
 Pokemon.dex_numbers = relation(PokemonDexNumber, order_by=PokemonDexNumber.pokedex_id.asc(), backref='pokemon')
-Pokemon.default_form_sprite = relation(PokemonFormSprite,
-                                       primaryjoin=and_(
-                                            Pokemon.id==PokemonFormSprite.pokemon_id,
-                                            PokemonFormSprite.is_default==True,
-                                       ),
-                                       uselist=False)
 Pokemon.egg_groups = relation(EggGroup, secondary=PokemonEggGroup.__table__,
                                         order_by=PokemonEggGroup.egg_group_id,
                                         backref='pokemon')
@@ -1406,13 +1469,19 @@ Pokemon.child_pokemon = relation(Pokemon,
 )
 Pokemon.flavor_text = relation(PokemonFlavorText, order_by=PokemonFlavorText.version_id.asc(), backref='pokemon')
 Pokemon.foreign_names = relation(PokemonName, backref='pokemon')
+Pokemon.forms = relation(PokemonForm, primaryjoin=Pokemon.id==PokemonForm.form_base_pokemon_id,
+                         order_by=(PokemonForm.order.asc(), PokemonForm.name.asc()))
+Pokemon.default_form = relation(PokemonForm,
+    primaryjoin=and_(Pokemon.id==PokemonForm.form_base_pokemon_id, PokemonForm.is_default==True),
+    uselist=False,
+)
 Pokemon.pokemon_habitat = relation(PokemonHabitat, backref='pokemon')
 Pokemon.habitat = association_proxy('pokemon_habitat', 'name')
 Pokemon.items = relation(PokemonItem, backref='pokemon')
 Pokemon.generation = relation(Generation, backref='pokemon')
 Pokemon.shape = relation(PokemonShape, backref='pokemon')
 Pokemon.stats = relation(PokemonStat, backref='pokemon', order_by=PokemonStat.stat_id.asc())
-Pokemon.types = relation(Type, secondary=PokemonType.__table__, order_by=PokemonType.slot.asc())
+Pokemon.types = relation(Type, secondary=PokemonType.__table__, order_by=PokemonType.slot.asc(), backref='pokemon')
 
 PokemonDexNumber.pokedex = relation(Pokedex)
 
@@ -1450,13 +1519,22 @@ PokemonEvolution.party_pokemon = relation(Pokemon,
 
 PokemonFlavorText.version = relation(Version)
 
-PokemonItem.item = relation(Item, backref='pokemon')
-PokemonItem.version = relation(Version)
+PokemonForm.form_base_pokemon = relation(Pokemon, primaryjoin=PokemonForm.form_base_pokemon_id==Pokemon.id)
+PokemonForm.unique_pokemon = relation(Pokemon, backref=backref('unique_form', uselist=False),
+                                      primaryjoin=PokemonForm.unique_pokemon_id==Pokemon.id)
+PokemonForm.version_group = relation(VersionGroup)
+PokemonForm.form_group = association_proxy('form_base_pokemon', 'form_group')
+PokemonForm.pokeathlon_stats = relation(PokemonFormPokeathlonStat,
+                                        order_by=PokemonFormPokeathlonStat.pokeathlon_stat_id,
+                                        backref='pokemon_form')
 
 PokemonFormGroup.pokemon = relation(Pokemon, backref=backref('form_group',
                                                              uselist=False))
-PokemonFormSprite.pokemon = relation(Pokemon, backref='form_sprites')
-PokemonFormSprite.introduced_in = relation(VersionGroup)
+
+PokemonFormPokeathlonStat.pokeathlon_stat = relation(PokeathlonStat)
+
+PokemonItem.item = relation(Item, backref='pokemon')
+PokemonItem.version = relation(Version)
 
 PokemonMove.pokemon = relation(Pokemon, backref='pokemon_moves')
 PokemonMove.version_group = relation(VersionGroup)
@@ -1506,3 +1584,4 @@ Version.generation = association_proxy('version_group', 'generation')
 VersionGroup.generation = relation(Generation, backref='version_groups')
 VersionGroup.version_group_regions = relation(VersionGroupRegion, backref='version_group')
 VersionGroup.regions = association_proxy('version_group_regions', 'region')
+VersionGroup.pokedex = relation(Pokedex, back_populates='version_groups')