diff --git a/.github/workflows/nix-environment.yml b/.github/workflows/nix-environment.yml index d6bd5fc48..9373fb68b 100644 --- a/.github/workflows/nix-environment.yml +++ b/.github/workflows/nix-environment.yml @@ -14,7 +14,7 @@ jobs: uses: actions/checkout@v6 with: submodules: recursive - + - name: Install Nix uses: cachix/install-nix-action@v31 with: @@ -22,7 +22,7 @@ jobs: - name: Evaluate default.nix run: nix-instantiate default.nix - + - name: Test Nix shellHook runs automatically run: | nix-shell --run " @@ -30,7 +30,7 @@ jobs: uv --version && test -d .venv && echo 'Virtual environment exists' || (echo 'Virtual environment not found' && exit 1) " - + - name: Start pokeapi in Nix environment run: | nix-shell --run " @@ -38,11 +38,11 @@ jobs: make setup && make build-db " - + - name: Test pokeapi serves correctly run: | nix-shell --run " nohup make serve & sleep 3 && curl -Ss http://localhost:8000/api/v2/pokemon/1/ | grep -q 'bulbasaur' - " \ No newline at end of file + " diff --git a/Resources/scripts/data/check_encounters.py b/Resources/scripts/data/check_encounters.py index 2c707f31b..510ca052b 100755 --- a/Resources/scripts/data/check_encounters.py +++ b/Resources/scripts/data/check_encounters.py @@ -89,12 +89,15 @@ def main(): parser.error(f"output directory already exists: {args.outdir}") if args.first_encounter is None: print("WARNING: argument -e/--first-encounter not provided. a LOT of files will be written!") + if not os.path.isdir(CSVDIR): + parser.error(f"invalid directory {CSVDIR}") # Read encounter CSV files encounters = read_csv("encounters", after=args.first_encounter) encounter_condition_value_map = read_csv( "encounter_condition_value_map", after=args.first_encounter, non_unique=True ) + encounter_pokemon_details = read_csv("encounter_pokemon_details", after=args.first_encounter, non_unique=True) # Gather CSV information for other sources sources = { @@ -140,6 +143,19 @@ def expand_source(info: dict, maxdepth: int = 1): else: encounters[enc_id]["encounter_conditions"] = [sources["encounter_condition_value"][cond_id]] + # Link encounter pokemon details to relevant encounters + for entry in encounter_pokemon_details.values(): + enc_id = entry["encounter_id"] + if enc_id in encounters: + if "pokemon_details" in encounters[enc_id]: + encounters[enc_id]["pokemon_details"].append( + {key: val for key, val in entry.items() if key != "encounter_id"} + ) + else: + encounters[enc_id]["pokemon_details"] = { + key: val for key, val in entry.items() if key != "encounter_id" + } + # Group encounters by location area location_area_encounters = {} for enc in encounters.values(): @@ -230,6 +246,13 @@ def restructure_encounters(encounters: list, write_if_empty: bool = True) -> dic output["encounters"][-1]["encounter_conditions"] = [ cond["identifier"] for cond in enc["encounter_conditions"] ] + + # Add encounter pokemon details, if they exist + if "pokemon_details" in enc: + output["encounters"][-1]["pokemon_details"] = [ + {key: val for key, val in enc["pokemon_details"].items() if val} + ] + if args.sort_by_natdex: output["encounters"] = list( sorted( diff --git a/data/v2/build.py b/data/v2/build.py index fdbb33b60..535fe51fc 100644 --- a/data/v2/build.py +++ b/data/v2/build.py @@ -2084,6 +2084,21 @@ def csv_record_to_objects(info): csv_record_to_objects, ) + def csv_record_to_objects(info): + yield EncounterPokemonDetail( + encounter_id=int(info[0]), + min_perfect_ivs=int(info[1]) if info[1] != "" else None, + always_shiny=bool(int(info[2])), + never_shiny=bool(int(info[3])), + is_alpha=bool(int(info[4])), + ) + + build_generic( + (EncounterPokemonDetail,), + "encounter_pokemon_details.csv", + csv_record_to_objects, + ) + ############## # PAL PARK # diff --git a/data/v2/csv/berries.csv b/data/v2/csv/berries.csv index f7ac3b7ec..541d45864 100644 --- a/data/v2/csv/berries.csv +++ b/data/v2/csv/berries.csv @@ -66,4 +66,4 @@ id,item_id,firmness_id,natural_gift_power,natural_gift_type_id,size,max_harvest, 65,724,,100,18,,8,72,, 66,725,,100,17,,8,72,, 67,2278,,17,,,,,, -68,2279,,,,,,,, \ No newline at end of file +68,2279,,,,,,,, diff --git a/data/v2/csv/encounter_pokemon_details.csv b/data/v2/csv/encounter_pokemon_details.csv new file mode 100644 index 000000000..f83f748d6 --- /dev/null +++ b/data/v2/csv/encounter_pokemon_details.csv @@ -0,0 +1,146 @@ +encounter_id,min_perfect_ivs,always_shiny,never_shiny,is_alpha +62288,,1,0,0 +62289,,1,0,0 +62290,,1,0,0 +62291,,1,0,0 +62292,,1,0,0 +63735,,0,1,0 +63736,,0,1,0 +64082,,0,1,0 +64086,,0,1,0 +64462,,0,1,0 +64463,,0,1,0 +64464,,0,1,0 +64465,,0,1,0 +64466,,0,1,0 +64467,,0,1,0 +64706,,0,1,0 +64707,,0,1,0 +64746,,0,1,0 +64763,,0,1,0 +64832,,0,1,0 +64944,,0,1,0 +64948,,0,1,0 +65073,,0,1,0 +65081,,0,1,0 +65960,,0,1,0 +65964,,0,1,0 +65979,,0,1,0 +65980,,0,1,0 +65981,,0,1,0 +65982,,0,1,0 +65983,,0,1,0 +65984,,0,1,0 +65985,,0,1,0 +65986,,0,1,0 +65987,,0,1,0 +65988,,0,1,0 +65989,,0,1,0 +65990,,0,1,0 +65991,,0,1,0 +65992,,0,1,0 +65993,,0,1,0 +65994,,0,1,0 +66210,,0,1,0 +66216,,0,1,0 +66558,,0,1,0 +66577,,0,1,0 +66585,,0,1,0 +66627,,0,1,0 +66628,,0,1,0 +66655,,0,1,0 +66656,,0,1,0 +66657,,0,1,0 +66658,,0,1,0 +66731,,0,1,0 +66732,,0,1,0 +66739,,0,1,0 +66742,,0,1,0 +66767,,0,1,0 +66768,,0,1,0 +66769,,0,1,0 +66770,,0,1,0 +66852,,0,1,0 +66853,,0,1,0 +66854,,0,1,0 +66855,,0,1,0 +66856,,0,1,0 +66857,,0,1,0 +66858,,0,1,0 +66859,,0,1,0 +66860,,0,1,0 +66861,,0,1,0 +66994,,0,1,0 +66995,,0,1,0 +70090,,0,1,0 +70091,,0,1,0 +75309,,0,1,0 +75310,,0,1,0 +75886,,0,1,0 +75887,,0,1,0 +75888,,0,1,0 +75889,,0,1,0 +75890,,0,1,0 +75891,,0,1,0 +75892,,0,1,0 +75893,,0,1,0 +75894,,0,1,0 +75895,,0,1,0 +75912,,0,1,0 +75913,,0,1,0 +75914,,0,1,0 +75915,,0,1,0 +75916,,0,1,0 +75917,,0,1,0 +75918,,0,1,0 +75919,,0,1,0 +75920,,0,1,0 +75921,,0,1,0 +75922,,0,1,0 +75923,,0,1,0 +75924,,0,1,0 +75925,,0,1,0 +75926,,0,1,0 +75927,,0,1,0 +75928,,0,1,0 +75929,,0,1,0 +75930,,0,1,0 +75931,,0,1,0 +75932,,0,1,0 +75933,,0,1,0 +75934,,0,1,0 +75935,,0,1,0 +75936,,0,1,0 +75937,,0,1,0 +75938,,0,1,0 +75939,,0,1,0 +75940,,0,1,0 +75941,,0,1,0 +75942,,0,1,0 +75943,,0,1,0 +75896,,0,1,0 +75897,,0,1,0 +75898,,0,1,0 +75899,,0,1,0 +75900,,0,1,0 +75901,,0,1,0 +75910,,0,1,0 +75911,,0,1,0 +75944,,0,1,0 +75945,,0,1,0 +75946,,0,1,0 +75947,,0,1,0 +97808,,0,1,0 +97809,,0,1,0 +97810,,0,1,0 +97811,,0,1,0 +97812,,0,1,0 +97813,,0,1,0 +99316,,0,1,0 +99317,,0,1,0 +99348,,0,1,0 +99349,,0,1,0 +99350,,0,1,0 +99351,,0,1,0 +99352,,0,1,0 +99353,,0,1,0 diff --git a/data/v2/csv/encounters.csv b/data/v2/csv/encounters.csv index f5ba813ed..de0d37c7d 100644 --- a/data/v2/csv/encounters.csv +++ b/data/v2/csv/encounters.csv @@ -61160,8 +61160,6 @@ id,version_id,location_area_id,encounter_slot_id,pokemon_id,min_level,max_level 66737,23,718,573,143,15,15 66738,24,718,573,143,15,15 66739,23,1176,573,716,50,50 -66740,24,1176,573,716,50,50 -66741,23,1176,573,717,50,50 66742,24,1176,573,717,50,50 66743,25,388,574,100,25,25 66744,26,388,574,100,25,25 @@ -61195,49 +61193,15 @@ id,version_id,location_area_id,encounter_slot_id,pokemon_id,min_level,max_level 66772,26,1180,574,442,50,50 66773,25,1189,574,488,50,50 66774,26,1189,574,488,50,50 -66775,27,1126,869,785,60,60 -66776,28,1126,869,785,60,60 -66777,27,1128,869,786,60,60 -66778,28,1128,869,786,60,60 -66779,27,1125,869,787,60,60 -66780,28,1125,869,787,60,60 -66781,27,1127,869,788,60,60 -66782,28,1127,869,788,60,60 -66783,27,1070,869,791,55,55 -66784,28,1069,869,792,55,55 -66785,27,1078,869,793,55,55 -66786,28,1078,869,793,55,55 -66787,27,1156,869,793,55,55 -66788,28,1156,869,793,55,55 -66789,27,1102,869,794,65,65 -66790,27,1102,869,794,65,65 66791,28,1193,869,795,60,60 66792,28,1193,869,795,60,60 66793,28,1193,869,795,60,60 66794,28,1193,869,795,60,60 -66795,27,1163,869,796,65,65 -66796,28,1163,869,796,65,65 -66797,27,1104,869,796,65,65 -66798,28,1104,869,796,65,65 66799,28,1081,869,797,65,65 -66800,28,1101,869,797,65,65 66801,27,1164,869,798,60,60 -66802,27,1101,869,798,60,60 -66803,27,1123,869,799,70,70 -66804,28,1123,869,799,70,70 66805,29,1194,1133,92,19,19 66806,30,1194,1133,92,19,19 66807,30,1060,1133,769,30,30 -66808,29,1126,1133,785,60,60 -66809,30,1126,1133,785,60,60 -66810,29,1128,1133,786,60,60 -66811,30,1128,1133,786,60,60 -66812,29,1125,1133,787,60,60 -66813,30,1125,1133,787,60,60 -66814,29,1127,1133,788,60,60 -66815,30,1127,1133,788,60,60 -66816,29,1098,1133,791,60,60 -66817,30,1098,1133,792,60,60 66818,29,1141,1133,793,60,60 66819,30,1141,1133,793,60,60 66820,29,1144,1133,794,60,60 diff --git a/data/v2/csv/item_game_indices.csv b/data/v2/csv/item_game_indices.csv index a9106b3c8..592668c6f 100644 --- a/data/v2/csv/item_game_indices.csv +++ b/data/v2/csv/item_game_indices.csv @@ -7294,4 +7294,4 @@ item_id,generation_id,game_index 2230,8,1691 2230,9,1691 2231,8,1692 -2231,9,1692 \ No newline at end of file +2231,9,1692 diff --git a/data/v2/csv/locations.csv b/data/v2/csv/locations.csv index 969769056..943955cd3 100644 --- a/data/v2/csv/locations.csv +++ b/data/v2/csv/locations.csv @@ -1102,4 +1102,4 @@ id,region_id,identifier 1118,8,roaming-isle-of-armor 1119,8,roaming-crown-tundra 1120,8,galar-wild-area-max-dens -1121,6,kalos-berry-fields \ No newline at end of file +1121,6,kalos-berry-fields diff --git a/default.nix b/default.nix index c192f35d2..491a3ebbb 100644 --- a/default.nix +++ b/default.nix @@ -18,4 +18,4 @@ mkShell { unset SOURCE_DATE_EPOCH uv sync --locked --all-extras --dev ''; -} \ No newline at end of file +} diff --git a/openapi.yml b/openapi.yml index 8f9187619..877de78e9 100644 --- a/openapi.yml +++ b/openapi.yml @@ -3317,16 +3317,10 @@ components: required: - firmness - flavors - - growth_time - id - item - - max_harvest - name - - natural_gift_power - natural_gift_type - - size - - smoothness - - soil_dryness BerryFirmnessDetail: type: object properties: diff --git a/pokemon_v2/migrations/0030_alter_berry_growth_time_alter_berry_max_harvest_and_more.py b/pokemon_v2/migrations/0030_alter_berry_growth_time_alter_berry_max_harvest_and_more.py index ebd3564ca..7f081e6f9 100644 --- a/pokemon_v2/migrations/0030_alter_berry_growth_time_alter_berry_max_harvest_and_more.py +++ b/pokemon_v2/migrations/0030_alter_berry_growth_time_alter_berry_max_harvest_and_more.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ("pokemon_v2", "0029_pokemonformtrigger_pokemonformcondition"), ] diff --git a/pokemon_v2/migrations/0031_encounterpokemondetail.py b/pokemon_v2/migrations/0031_encounterpokemondetail.py new file mode 100644 index 000000000..cafeff173 --- /dev/null +++ b/pokemon_v2/migrations/0031_encounterpokemondetail.py @@ -0,0 +1,40 @@ +# Generated by Django 5.2.10 on 2026-07-24 16:35 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("pokemon_v2", "0030_alter_berry_growth_time_alter_berry_max_harvest_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="EncounterPokemonDetail", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("min_perfect_ivs", models.IntegerField(blank=True, null=True)), + ("always_shiny", models.BooleanField(default=False)), + ("never_shiny", models.BooleanField(default=False)), + ("is_alpha", models.BooleanField(default=False)), + ( + "encounter", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="pokemon_v2.encounter", + ), + ), + ], + ), + ] diff --git a/pokemon_v2/models.py b/pokemon_v2/models.py index a1a4bc5e6..085fcb63d 100644 --- a/pokemon_v2/models.py +++ b/pokemon_v2/models.py @@ -960,21 +960,21 @@ class Berry(HasName, HasItem): on_delete=models.CASCADE, ) - natural_gift_power = models.IntegerField() + natural_gift_power = models.IntegerField(blank=True, null=True) natural_gift_type = models.ForeignKey( Type, blank=True, null=True, related_name="%(class)s", on_delete=models.CASCADE ) - size = models.IntegerField() + size = models.IntegerField(blank=True, null=True) - max_harvest = models.IntegerField() + max_harvest = models.IntegerField(blank=True, null=True) - growth_time = models.IntegerField() + growth_time = models.IntegerField(blank=True, null=True) - soil_dryness = models.IntegerField() + soil_dryness = models.IntegerField(blank=True, null=True) - smoothness = models.IntegerField() + smoothness = models.IntegerField(blank=True, null=True) # Berry Flavors are a bit of a hack because their relationship @@ -1168,6 +1168,18 @@ class EncounterConditionValueMap(models.Model): ) +class EncounterPokemonDetail(models.Model): + encounter = models.ForeignKey(Encounter, blank=True, null=True, on_delete=models.CASCADE) + + min_perfect_ivs = models.IntegerField(blank=True, null=True) + + always_shiny = models.BooleanField(default=False) + + never_shiny = models.BooleanField(default=False) + + is_alpha = models.BooleanField(default=False) + + ################# # MOVE MODELS # ################# diff --git a/pokemon_v2/serializers.py b/pokemon_v2/serializers.py index c05afe4e5..f1c1a945e 100644 --- a/pokemon_v2/serializers.py +++ b/pokemon_v2/serializers.py @@ -873,11 +873,23 @@ class Meta: fields = ("id", "slot", "chance", "encounter_method", "version_group") +class EncounterPokemonDetailSerializer(serializers.ModelSerializer): + class Meta: + model = EncounterPokemonDetail + fields = ( + "min_perfect_ivs", + "always_shiny", + "never_shiny", + "is_alpha", + ) + + class EncounterDetailSerializer(serializers.ModelSerializer): version = VersionSummarySerializer() location_area = LocationAreaSummarySerializer() pokemon = PokemonSummarySerializer() condition_values = serializers.SerializerMethodField("get_encounter_conditions") + pokemon_details = serializers.SerializerMethodField("get_encounter_pokemon_details") class Meta: model = Encounter @@ -889,6 +901,7 @@ class Meta: "pokemon", "location_area", "condition_values", + "pokemon_details", ) def get_encounter_conditions(self, obj): @@ -901,6 +914,14 @@ def get_encounter_conditions(self, obj): return values + def get_encounter_pokemon_details(self, obj): + encounter_pokemon_details = EncounterPokemonDetail.objects.filter(encounter=obj) + data = EncounterPokemonDetailSerializer(encounter_pokemon_details, many=True, context=self.context).data + + pokemon_details = data[0] if len(data) else None + + return pokemon_details + class LocationAreaEncounterRateSerializer(serializers.ModelSerializer): encounter_method = EncounterMethodSummarySerializer() diff --git a/pokemon_v2/test_models.py b/pokemon_v2/test_models.py index fca14fb2f..9c78683b6 100644 --- a/pokemon_v2/test_models.py +++ b/pokemon_v2/test_models.py @@ -15,6 +15,23 @@ def fields_are_valid(self): self.assertEqual(smell.generation_id, 3) +class EncounterPokemonDetailTestCase(TestCase): + def test_unique_encounter_pokemon_details(self): + csv_dir = os.path.join(settings.BASE_DIR, "data", "v2", "csv") + with open(os.path.join(csv_dir, "encounter_pokemon_details.csv")) as infile: + reader = csv.DictReader(infile) + encounter_ids = [] + duplicate_ids = [] + for row in reader: + if row["encounter_id"] in encounter_ids: + duplicate_ids.append(row["encounter_id"]) + else: + encounter_ids.append(row["encounter_id"]) + + if duplicate_ids: + self.fail(f"Duplicate encounter ID(s) found in encounter_pokemon_details.csv: {duplicate_ids}") + + class CSVResourceNameValidationTestCase(TestCase): """ Test that all resource identifiers in CSV files follow ASCII slug format.