diff --git a/openapi.yml b/openapi.yml index 8adb5c044..905eb35d1 100644 --- a/openapi.yml +++ b/openapi.yml @@ -3810,10 +3810,39 @@ components: allOf: - $ref: '#/components/schemas/EvolutionChainLink' readOnly: true + transformations: + type: array + items: + $ref: '#/components/schemas/EvolutionChainTransformation' + readOnly: true required: - baby_trigger_item - chain - id + - transformations + EvolutionChainFormTransformation: + type: object + properties: + form: + $ref: '#/components/schemas/PokemonFormSummary' + trigger: + type: string + readOnly: true + item: + $ref: '#/components/schemas/ItemSummary' + ability: + $ref: '#/components/schemas/AbilitySummary' + move: + $ref: '#/components/schemas/MoveSummary' + base_form: + $ref: '#/components/schemas/PokemonFormSummary' + required: + - ability + - base_form + - form + - item + - move + - trigger EvolutionChainLink: type: object properties: @@ -3844,6 +3873,18 @@ components: readOnly: true required: - url + EvolutionChainTransformation: + type: object + properties: + species: + $ref: '#/components/schemas/PokemonSpeciesSummary' + forms: + type: array + items: + $ref: '#/components/schemas/EvolutionChainFormTransformation' + required: + - forms + - species EvolutionTriggerDetail: type: object properties: diff --git a/pokemon_v2/serializers.py b/pokemon_v2/serializers.py index 780a9fb86..eca43c3f7 100644 --- a/pokemon_v2/serializers.py +++ b/pokemon_v2/serializers.py @@ -76,8 +76,10 @@ class PokemonWithRelations(Protocol): "EncounterPokemonDetailSerializer", "EncounterSlotSerializer", "EvolutionChainDetailSerializer", + "EvolutionChainFormTransformationSerializer", "EvolutionChainLinkSerializer", "EvolutionChainSummarySerializer", + "EvolutionChainTransformationSerializer", "EvolutionTriggerDetailSerializer", "EvolutionTriggerNameSerializer", "EvolutionTriggerSummarySerializer", @@ -3372,6 +3374,24 @@ class Meta: ) +class EvolutionChainFormTransformationSerializer(serializers.ModelSerializer[PokemonFormCondition]): + form = PokemonFormSummarySerializer(source="pokemon_form") + trigger = serializers.CharField(source="form_trigger.name", read_only=True) + item = ItemSummarySerializer() + ability = AbilitySummarySerializer() + move = MoveSummarySerializer() + base_form = PokemonFormSummarySerializer() + + class Meta: + model = PokemonFormCondition + fields = ("form", "trigger", "item", "ability", "move", "base_form") + + +class EvolutionChainTransformationSerializer(serializers.Serializer[Any]): + species = PokemonSpeciesSummarySerializer() + forms = EvolutionChainFormTransformationSerializer(many=True) + + class EvolutionChainLinkSerializer(serializers.Serializer[Any]): is_baby = serializers.BooleanField() species = PokemonSpeciesSummarySerializer() @@ -3382,6 +3402,7 @@ class EvolutionChainLinkSerializer(serializers.Serializer[Any]): class EvolutionChainDetailSerializer(serializers.ModelSerializer[EvolutionChain]): baby_trigger_item = ItemSummarySerializer() chain = serializers.SerializerMethodField("build_chain") + transformations = serializers.SerializerMethodField("build_transformations") POKEMON_EVOLUTION_FK_FIELDS: ClassVar[list[str]] = [ field.name @@ -3391,7 +3412,7 @@ class EvolutionChainDetailSerializer(serializers.ModelSerializer[EvolutionChain] class Meta: model = EvolutionChain - fields = ("id", "baby_trigger_item", "chain") + fields = ("id", "baby_trigger_item", "chain", "transformations") @extend_schema_field(EvolutionChainLinkSerializer) def build_chain(self, obj: EvolutionChain) -> dict[str, Any]: @@ -3464,6 +3485,41 @@ def build_chain_link_entry( "evolves_to": [self.build_chain_link_entry(c, summary_data) for c in chain_link["children"]], } + # collects the reversible form changes of every species in the chain (mega evolution, + # primal reversion, gigantamax, in-battle transformations), grouped by species + @extend_schema_field(EvolutionChainTransformationSerializer(many=True)) + def build_transformations(self, obj: EvolutionChain) -> list[dict[str, Any]]: + conditions = ( + PokemonFormCondition.objects.filter(pokemon_form__pokemon__pokemon_species__evolution_chain=obj) + .select_related( + "form_trigger", + "item", + "ability", + "move", + "base_form", + "pokemon_form__pokemon__pokemon_species", + ) + .order_by( + "pokemon_form__pokemon__pokemon_species__order", + "pokemon_form__pokemon__pokemon_species__pk", + "pokemon_form__order", + "pk", + ) + ) + + return [ + { + "species": PokemonSpeciesSummarySerializer(species, context=self.context).data, + "forms": EvolutionChainFormTransformationSerializer( + list(species_conditions), many=True, context=self.context + ).data, + } + for species, species_conditions in itertools.groupby( + conditions, + key=lambda condition: condition.pokemon_form.pokemon.pokemon_species, + ) + ] + ############################ # POKEATHLON SERIALIZERS # diff --git a/pokemon_v2/tests.py b/pokemon_v2/tests.py index 49cc7545b..8ab716108 100644 --- a/pokemon_v2/tests.py +++ b/pokemon_v2/tests.py @@ -4837,6 +4837,7 @@ def test_evolution_chain_api(self): # base params self.assertEqual(response.data["id"], evolution_chain.pk) + self.assertEqual(response.data["transformations"], []) # baby trigger params self.assertEqual(response.data["baby_trigger_item"]["name"], baby_trigger_item.name) self.assertEqual( @@ -4921,6 +4922,73 @@ def test_evolution_chain_api(self): "{}{}/type/{}/".format(TEST_HOST, API_V2, stage_two_second_party_type.pk), ) + # verifies that the reversible form changes of the chain's species are exposed + def test_evolution_chain_api_transformations(self): + evolution_chain = self.setup_evolution_chain_data() + + basic = self.setup_pokemon_species_data(name="bsc for evo chn trnsfrm", evolution_chain=evolution_chain) + evolved = self.setup_pokemon_species_data( + name="evlvd for evo chn trnsfrm", + evolves_from_species=basic, + evolution_chain=evolution_chain, + order=2, + ) + self.setup_pokemon_evolution_data(evolved_species=evolved, min_level=16) + + evolved_pokemon = self.setup_pokemon_data(pokemon_species=evolved, name="pkmn for evo chn trnsfrm") + default_form = self.setup_pokemon_form_data( + pokemon=evolved_pokemon, name="dflt frm for evo chn trnsfrm", order=1, form_order=1 + ) + mega_form = self.setup_pokemon_form_data( + pokemon=evolved_pokemon, + name="mg frm for evo chn trnsfrm", + order=2, + form_order=2, + is_mega=True, + ) + mega_stone = self.setup_item_data(name="itm for evo chn trnsfrm") + form_trigger = self.setup_pokemon_form_trigger_data(name="frm trgr for evo chn trnsfrm") + self.setup_pokemon_form_condition_data( + pokemon_form=mega_form, + form_trigger=form_trigger, + item=mega_stone, + base_form=default_form, + ) + + response = self.client.get("{}/evolution-chain/{}/".format(API_V2, evolution_chain.pk)) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # the chain itself is untouched by the transformations + self.assertEqual(response.data["chain"]["species"]["name"], basic.name) + self.assertEqual(response.data["chain"]["evolves_to"][0]["species"]["name"], evolved.name) + self.assertEqual(response.data["chain"]["evolves_to"][0]["evolves_to"], []) + + transformations = response.data["transformations"] + self.assertEqual(len(transformations), 1) + self.assertEqual(transformations[0]["species"]["name"], evolved.name) + self.assertEqual( + transformations[0]["species"]["url"], + "{}{}/pokemon-species/{}/".format(TEST_HOST, API_V2, evolved.pk), + ) + + forms = transformations[0]["forms"] + self.assertEqual(len(forms), 1) + self.assertEqual(forms[0]["form"]["name"], mega_form.name) + self.assertEqual( + forms[0]["form"]["url"], + "{}{}/pokemon-form/{}/".format(TEST_HOST, API_V2, mega_form.pk), + ) + self.assertEqual(forms[0]["trigger"], form_trigger.name) + self.assertEqual(forms[0]["item"]["name"], mega_stone.name) + self.assertEqual( + forms[0]["item"]["url"], + "{}{}/item/{}/".format(TEST_HOST, API_V2, mega_stone.pk), + ) + self.assertEqual(forms[0]["base_form"]["name"], default_form.name) + self.assertIsNone(forms[0]["ability"]) + self.assertIsNone(forms[0]["move"]) + # verifies that the wurmple evolution chain is serialized correctly def test_evolution_chain_api_wurmple_bugfix(self): # set up wurmple-like evolution chain