Add encounter condition support with rate display
Add a `condition` column to RouteEncounter for time-of-day, weather, and season variants. Seed loader expands `conditions` dict into per-condition rows. EncounterModal shows condition filter tabs with per-condition encounter rates, and displays rates for all standard encounter methods (walk, surf, fishing, etc.). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
"""add condition to route encounters
|
||||
|
||||
Revision ID: h9c0d1e2f3a4
|
||||
Revises: g8b9c0d1e2f3
|
||||
Create Date: 2026-02-14 12:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "h9c0d1e2f3a4"
|
||||
down_revision: str | Sequence[str] | None = "g8b9c0d1e2f3"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
"route_encounters",
|
||||
sa.Column("condition", sa.String(30), nullable=False, server_default=""),
|
||||
)
|
||||
|
||||
op.drop_constraint(
|
||||
"uq_route_pokemon_method_game", "route_encounters", type_="unique"
|
||||
)
|
||||
|
||||
op.create_unique_constraint(
|
||||
"uq_route_pokemon_method_game_condition",
|
||||
"route_encounters",
|
||||
["route_id", "pokemon_id", "encounter_method", "game_id", "condition"],
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_constraint(
|
||||
"uq_route_pokemon_method_game_condition",
|
||||
"route_encounters",
|
||||
type_="unique",
|
||||
)
|
||||
|
||||
op.create_unique_constraint(
|
||||
"uq_route_pokemon_method_game",
|
||||
"route_encounters",
|
||||
["route_id", "pokemon_id", "encounter_method", "game_id"],
|
||||
)
|
||||
|
||||
op.drop_column("route_encounters", "condition")
|
||||
@@ -69,8 +69,9 @@ async def export_game_routes(
|
||||
game_encounters = [
|
||||
enc for enc in route.route_encounters if enc.game_id == game_id
|
||||
]
|
||||
return [
|
||||
{
|
||||
result = []
|
||||
for enc in sorted(game_encounters, key=lambda e: -e.encounter_rate):
|
||||
entry: dict = {
|
||||
"pokeapi_id": enc.pokemon.pokeapi_id,
|
||||
"pokemon_name": enc.pokemon.name,
|
||||
"method": enc.encounter_method,
|
||||
@@ -78,8 +79,10 @@ async def export_game_routes(
|
||||
"min_level": enc.min_level,
|
||||
"max_level": enc.max_level,
|
||||
}
|
||||
for enc in sorted(game_encounters, key=lambda e: -e.encounter_rate)
|
||||
]
|
||||
if enc.condition:
|
||||
entry["condition"] = enc.condition
|
||||
result.append(entry)
|
||||
return result
|
||||
|
||||
def format_route(route: Route) -> dict:
|
||||
data: dict = {
|
||||
|
||||
@@ -213,6 +213,7 @@ async def get_pokemon_encounter_locations(
|
||||
route_name=enc.route.name,
|
||||
encounter_method=enc.encounter_method,
|
||||
encounter_rate=enc.encounter_rate,
|
||||
condition=enc.condition,
|
||||
min_level=enc.min_level,
|
||||
max_level=enc.max_level,
|
||||
)
|
||||
|
||||
@@ -12,7 +12,8 @@ class RouteEncounter(Base):
|
||||
"pokemon_id",
|
||||
"encounter_method",
|
||||
"game_id",
|
||||
name="uq_route_pokemon_method_game",
|
||||
"condition",
|
||||
name="uq_route_pokemon_method_game_condition",
|
||||
),
|
||||
)
|
||||
|
||||
@@ -22,6 +23,7 @@ class RouteEncounter(Base):
|
||||
game_id: Mapped[int] = mapped_column(ForeignKey("games.id"), index=True)
|
||||
encounter_method: Mapped[str] = mapped_column(String(30))
|
||||
encounter_rate: Mapped[int] = mapped_column(SmallInteger)
|
||||
condition: Mapped[str] = mapped_column(String(30), default="", server_default="")
|
||||
min_level: Mapped[int] = mapped_column(SmallInteger)
|
||||
max_level: Mapped[int] = mapped_column(SmallInteger)
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ class RouteEncounterResponse(CamelModel):
|
||||
game_id: int
|
||||
encounter_method: str
|
||||
encounter_rate: int
|
||||
condition: str = ""
|
||||
min_level: int
|
||||
max_level: int
|
||||
|
||||
@@ -55,6 +56,7 @@ class PokemonEncounterLocationItem(CamelModel):
|
||||
route_name: str
|
||||
encounter_method: str
|
||||
encounter_rate: int
|
||||
condition: str = ""
|
||||
min_level: int
|
||||
max_level: int
|
||||
|
||||
@@ -89,6 +91,7 @@ class RouteEncounterCreate(CamelModel):
|
||||
game_id: int
|
||||
encounter_method: str
|
||||
encounter_rate: int
|
||||
condition: str = ""
|
||||
min_level: int
|
||||
max_level: int
|
||||
|
||||
@@ -96,6 +99,7 @@ class RouteEncounterCreate(CamelModel):
|
||||
class RouteEncounterUpdate(CamelModel):
|
||||
encounter_method: str | None = None
|
||||
encounter_rate: int | None = None
|
||||
condition: str | None = None
|
||||
min_level: int | None = None
|
||||
max_level: int | None = None
|
||||
|
||||
@@ -178,6 +182,7 @@ class BulkRouteEncounterItem(BaseModel):
|
||||
pokeapi_id: int
|
||||
method: str
|
||||
encounter_rate: int
|
||||
condition: str = ""
|
||||
min_level: int
|
||||
max_level: int
|
||||
|
||||
|
||||
@@ -169,11 +169,19 @@
|
||||
"min_level": 2,
|
||||
"max_level": 5
|
||||
},
|
||||
{
|
||||
"pokeapi_id": 16,
|
||||
"pokemon_name": "Pidgey",
|
||||
"method": "walk",
|
||||
"encounter_rate": 55,
|
||||
"min_level": 2,
|
||||
"max_level": 4
|
||||
},
|
||||
{
|
||||
"pokeapi_id": 163,
|
||||
"pokemon_name": "Hoothoot",
|
||||
"method": "walk",
|
||||
"encounter_rate": 85,
|
||||
"encounter_rate": 50,
|
||||
"min_level": 2,
|
||||
"max_level": 4
|
||||
},
|
||||
@@ -181,10 +189,18 @@
|
||||
"pokeapi_id": 16,
|
||||
"pokemon_name": "Pidgey",
|
||||
"method": "walk",
|
||||
"encounter_rate": 55,
|
||||
"encounter_rate": 50,
|
||||
"min_level": 2,
|
||||
"max_level": 4
|
||||
},
|
||||
{
|
||||
"pokeapi_id": 161,
|
||||
"pokemon_name": "Sentret",
|
||||
"method": "walk",
|
||||
"encounter_rate": 50,
|
||||
"min_level": 2,
|
||||
"max_level": 3
|
||||
},
|
||||
{
|
||||
"pokeapi_id": 102,
|
||||
"pokemon_name": "Exeggcute",
|
||||
@@ -209,6 +225,14 @@
|
||||
"min_level": 2,
|
||||
"max_level": 3
|
||||
},
|
||||
{
|
||||
"pokeapi_id": 19,
|
||||
"pokemon_name": "Rattata",
|
||||
"method": "walk",
|
||||
"encounter_rate": 40,
|
||||
"min_level": 2,
|
||||
"max_level": 4
|
||||
},
|
||||
{
|
||||
"pokeapi_id": 204,
|
||||
"pokemon_name": "Pineco",
|
||||
@@ -241,11 +265,51 @@
|
||||
"min_level": 3,
|
||||
"max_level": 3
|
||||
},
|
||||
{
|
||||
"pokeapi_id": 163,
|
||||
"pokemon_name": "Hoothoot",
|
||||
"method": "walk",
|
||||
"encounter_rate": 10,
|
||||
"min_level": 2,
|
||||
"max_level": 4
|
||||
},
|
||||
{
|
||||
"pokeapi_id": 163,
|
||||
"pokemon_name": "Hoothoot",
|
||||
"method": "walk",
|
||||
"encounter_rate": 0,
|
||||
"min_level": 2,
|
||||
"max_level": 4
|
||||
},
|
||||
{
|
||||
"pokeapi_id": 16,
|
||||
"pokemon_name": "Pidgey",
|
||||
"method": "walk",
|
||||
"encounter_rate": 0,
|
||||
"min_level": 2,
|
||||
"max_level": 4
|
||||
},
|
||||
{
|
||||
"pokeapi_id": 161,
|
||||
"pokemon_name": "Sentret",
|
||||
"method": "walk",
|
||||
"encounter_rate": 0,
|
||||
"min_level": 2,
|
||||
"max_level": 3
|
||||
},
|
||||
{
|
||||
"pokeapi_id": 19,
|
||||
"pokemon_name": "Rattata",
|
||||
"method": "walk",
|
||||
"encounter_rate": 20,
|
||||
"encounter_rate": 0,
|
||||
"min_level": 2,
|
||||
"max_level": 4
|
||||
},
|
||||
{
|
||||
"pokeapi_id": 19,
|
||||
"pokemon_name": "Rattata",
|
||||
"method": "walk",
|
||||
"encounter_rate": 0,
|
||||
"min_level": 2,
|
||||
"max_level": 4
|
||||
}
|
||||
|
||||
@@ -192,6 +192,41 @@ async def upsert_routes(
|
||||
return {row.name: row.id for row in result}
|
||||
|
||||
|
||||
async def _upsert_single_encounter(
|
||||
session: AsyncSession,
|
||||
route_id: int,
|
||||
pokemon_id: int,
|
||||
game_id: int,
|
||||
method: str,
|
||||
encounter_rate: int,
|
||||
min_level: int,
|
||||
max_level: int,
|
||||
condition: str = "",
|
||||
) -> None:
|
||||
stmt = (
|
||||
insert(RouteEncounter)
|
||||
.values(
|
||||
route_id=route_id,
|
||||
pokemon_id=pokemon_id,
|
||||
game_id=game_id,
|
||||
encounter_method=method,
|
||||
encounter_rate=encounter_rate,
|
||||
condition=condition,
|
||||
min_level=min_level,
|
||||
max_level=max_level,
|
||||
)
|
||||
.on_conflict_do_update(
|
||||
constraint="uq_route_pokemon_method_game_condition",
|
||||
set_={
|
||||
"encounter_rate": encounter_rate,
|
||||
"min_level": min_level,
|
||||
"max_level": max_level,
|
||||
},
|
||||
)
|
||||
)
|
||||
await session.execute(stmt)
|
||||
|
||||
|
||||
async def upsert_route_encounters(
|
||||
session: AsyncSession,
|
||||
route_id: int,
|
||||
@@ -207,28 +242,33 @@ async def upsert_route_encounters(
|
||||
print(f" Warning: no pokemon_id for pokeapi_id {enc['pokeapi_id']}")
|
||||
continue
|
||||
|
||||
stmt = (
|
||||
insert(RouteEncounter)
|
||||
.values(
|
||||
route_id=route_id,
|
||||
pokemon_id=pokemon_id,
|
||||
game_id=game_id,
|
||||
encounter_method=enc["method"],
|
||||
encounter_rate=enc["encounter_rate"],
|
||||
min_level=enc["min_level"],
|
||||
max_level=enc["max_level"],
|
||||
conditions = enc.get("conditions")
|
||||
if conditions:
|
||||
for condition_name, rate in conditions.items():
|
||||
await _upsert_single_encounter(
|
||||
session,
|
||||
route_id,
|
||||
pokemon_id,
|
||||
game_id,
|
||||
enc["method"],
|
||||
rate,
|
||||
enc["min_level"],
|
||||
enc["max_level"],
|
||||
condition=condition_name,
|
||||
)
|
||||
count += 1
|
||||
else:
|
||||
await _upsert_single_encounter(
|
||||
session,
|
||||
route_id,
|
||||
pokemon_id,
|
||||
game_id,
|
||||
enc["method"],
|
||||
enc["encounter_rate"],
|
||||
enc["min_level"],
|
||||
enc["max_level"],
|
||||
)
|
||||
.on_conflict_do_update(
|
||||
constraint="uq_route_pokemon_method_game",
|
||||
set_={
|
||||
"encounter_rate": enc["encounter_rate"],
|
||||
"min_level": enc["min_level"],
|
||||
"max_level": enc["max_level"],
|
||||
},
|
||||
)
|
||||
)
|
||||
await session.execute(stmt)
|
||||
count += 1
|
||||
count += 1
|
||||
|
||||
return count
|
||||
|
||||
|
||||
Reference in New Issue
Block a user