import { boolean, index, integer, text, unique } from 'drizzle-orm/pg-core'; import { gamesSchema, refSchema } from './schemas'; import { availabilityValues } from '../../../availability'; import { user } from './auth'; import { id, timestamps } from './helpers'; export const availability = refSchema.enum('availability', availabilityValues); // Where a catalog row came from. Seeding replaces `pokeapi` and `derived` rows // but never touches `manual` ones, which are admin corrections. export const catalogSources = ['pokeapi', 'derived', 'manual'] as const; const source = () => text('source', { enum: catalogSources }).notNull().default('pokeapi'); export const pokemon = refSchema.table('pokemon', { id: id(), number: integer('number').notNull().unique(), generation: integer('generation').notNull().default(1), name: text('name').notNull(), type1: text('type1').notNull(), type2: text('type2'), hp: integer('hp').notNull().default(0), attack: integer('attack').notNull().default(0), defense: integer('defense').notNull().default(0), specialAttack: integer('special_attack').notNull().default(0), specialDefense: integer('special_defense').notNull().default(0), speed: integer('speed').notNull().default(0), // national numbers, so the chain survives reseeding without id lookups evolvesFrom: integer('evolves_from'), evolutionChain: integer('evolution_chain'), isBaby: boolean('is_baby').notNull().default(false), isLegendary: boolean('is_legendary').notNull().default(false), isMythical: boolean('is_mythical').notNull().default(false), ...timestamps }); export const games = refSchema.table('pokemon_games', { id: id(), sourceId: integer('source_id').unique(), slug: text('slug').unique(), name: text('name').notNull(), generation: integer('generation').notNull().default(1), sortOrder: integer('sort_order').notNull().default(0), breeding: boolean('breeding').notNull().default(true), ...timestamps }); export const pokemonGameEntries = refSchema.table( 'pokemon_game_entries', { id: id(), pokemonId: integer('pokemon_id') .notNull() .references(() => pokemon.id, { onDelete: 'cascade' }), gameId: integer('game_id') .notNull() .references(() => games.id, { onDelete: 'cascade' }), availability: availability('availability').notNull().default('catchable'), // how to obtain it, e.g. "Evolve Wartortle: Level 36+" or "Route 22, Viridian Forest" note: text('note'), source: source(), ...timestamps }, (table) => [unique().on(table.pokemonId, table.gameId), index().on(table.gameId)] ); export const pokemonLocations = refSchema.table('pokemon_locations', { id: id(), sourceId: integer('source_id').unique(), slug: text('slug').notNull(), name: text('name').notNull(), region: text('region'), description: text('description'), // R2 object key of an admin-uploaded picture imageKey: text('image_key'), imageCredit: text('image_credit'), source: source(), ...timestamps }); export const pokemonEncounters = refSchema.table( 'pokemon_encounters', { id: id(), pokemonId: integer('pokemon_id') .notNull() .references(() => pokemon.id, { onDelete: 'cascade' }), gameId: integer('game_id') .notNull() .references(() => games.id, { onDelete: 'cascade' }), locationId: integer('location_id') .notNull() .references(() => pokemonLocations.id, { onDelete: 'cascade' }), area: text('area'), method: text('method').notNull(), minLevel: integer('min_level'), maxLevel: integer('max_level'), // summed encounter-slot percentage; null for gifts and folded raids rarity: integer('rarity'), form: text('form'), conditions: text('conditions').array().notNull().default([]), note: text('note'), source: source(), ...timestamps }, (table) => [index().on(table.pokemonId, table.gameId), index().on(table.locationId)] ); // The method that applies in each game, e.g. Magneton → Magnezone needs Mt. // Coronet in Platinum but a Thunder Stone in Sword. export const pokemonEvolutions = refSchema.table( 'pokemon_evolutions', { id: id(), fromPokemonId: integer('from_pokemon_id') .notNull() .references(() => pokemon.id, { onDelete: 'cascade' }), toPokemonId: integer('to_pokemon_id') .notNull() .references(() => pokemon.id, { onDelete: 'cascade' }), gameId: integer('game_id') .notNull() .references(() => games.id, { onDelete: 'cascade' }), method: text('method').notNull(), source: source(), ...timestamps }, (table) => [ unique().on(table.fromPokemonId, table.toPokemonId, table.gameId, table.method), index().on(table.gameId) ] ); export const userEntries = gamesSchema.table( 'catch_em_all_entries', { id: id(), pokemonGameEntryId: integer('pokemon_game_entry_id') .notNull() .references(() => pokemonGameEntries.id, { onDelete: 'cascade' }), userId: text('user_id') .notNull() .references(() => user.id, { onDelete: 'cascade' }), ...timestamps }, (table) => [unique().on(table.pokemonGameEntryId, table.userId), index().on(table.userId)] ); // which games a user has chosen to track ("rules") export const userGames = gamesSchema.table( 'catch_em_all_games', { id: id(), userId: text('user_id') .notNull() .references(() => user.id, { onDelete: 'cascade' }), gameId: integer('game_id') .notNull() .references(() => games.id, { onDelete: 'cascade' }), ...timestamps }, (table) => [unique().on(table.userId, table.gameId), index().on(table.userId)] ); // A row distinguishes an explicit empty game selection from a new account. export const catchEmAllSettings = gamesSchema.table('catch_em_all_settings', { id: id(), userId: text('user_id') .notNull() .unique() .references(() => user.id, { onDelete: 'cascade' }), ...timestamps }); export type Pokemon = typeof pokemon.$inferSelect; export type Game = typeof games.$inferSelect; export type PokemonGameEntry = typeof pokemonGameEntries.$inferSelect; export type UserEntry = typeof userEntries.$inferSelect; export type UserGame = typeof userGames.$inferSelect; export type Availability = (typeof availability.enumValues)[number]; export type PokemonLocation = typeof pokemonLocations.$inferSelect; export type PokemonEncounter = typeof pokemonEncounters.$inferSelect; export type PokemonEvolution = typeof pokemonEvolutions.$inferSelect;