# 7. Card Model Changes and Variant System [< Back to Index](README.md) | [Next: Display and Visual Identity >](08-display.md) --- ## 7.1 How the Game Engine Resolves Ratings and Images for a Card When the bot builds a game lineup or displays a card, it checks `card.variant`: **For ratings (game engine):** 1. Read `card.variant` from the card instance 2. Call `GET battingcardratings/player/{player_id}?variant={card.variant}` — the API filters by `(player_id, variant)` and returns the matching `battingcard` row with its nested vL/vR ratings 3. Each variant has its own distinct `battingcard` row (with `battingcard.variant` matching `card.variant`). The ratings rows themselves don't carry a variant — they belong to a specific `battingcard`, and the variant distinguishes which `battingcard` to use 4. `variant = 0` is the base card (unchanged behavior); evolved/cosmetic variants use higher values > **Current implementation:** `card.variant` does not exist, yet, but `battingcard.variant` column does already exist > (default 0). The API endpoint and game engine query path (`gameplay_queries.py → > get_batter_scouting_or_none`) already pass variant through. The local Postgres cache > (`batterscouting`) keys on `battingcard_id`, which is inherently variant-specific since each > variant produces a separate `battingcard` row. Same pattern for `pitchingcard`/`pitcherscouting`. **For images (card display):** 1. Read `card.variant` from the card instance 2. If `variant` is not 0: fetch `battingcard.image_url` for that variant — the pre-rendered image (PNG or APNG) with cosmetics and evolution visuals baked in 3. If `variant` is 0: use `player.image` / `player.image2` (base card, unchanged behavior) > **Migration required:** `battingcard.image_url` and `pitchingcard.image_url` columns do not > exist yet — they must be added (nullable varchar). Current image resolution uses `player.image` > and `player.image2` fields exclusively (checked via `Player.batter_card_url` / > `Player.pitcher_card_url` properties in `gameplay_models.py`, and `helpers.player_bcard` / > `helpers.player_pcard` in the legacy cog system). Bot display logic must be updated to check > `battingcard.image_url` first when `card.variant != 0`, falling back to `player.image`. The variant field on the card instance acts as a simple pointer. The bot does not need to know about evolution tiers, cosmetics, or boost profiles — it just reads the variant and gets the right ratings and image. All complexity is in the variant creation/update path, not the read path. **All card instances of the same `player_id` on the same team share the same variant.** The variant is stored authoritatively on `evolution_card_state` and propagated to `card.variant` on all matching instances when it changes. Duplicate cards are not differentiated. ## 7.2 Evolved Card Naming When `evolution_card_state.fully_evolved = true`, the card display name is modified: - Base name: `"Mike Trout"` - Evolved name: `"[TeamName]'s Evolved Mike Trout"` The `player.p_name` field is NOT modified (it is a shared blueprint). The evolved name is computed dynamically from the card state record at display time. This preserves data integrity and allows future re-display if the naming convention changes. ## 7.3 Card Uniqueness **Within a team:** All copies of the same `player_id` are identical — same evolution state, same variant, same boosts, same cosmetics. Duplicates are not differentiated in any way. **Across teams:** Two teams that evolve the same `player_id` with the same boost profile and same cosmetics will share the same variant hash — and therefore the same ratings rows and S3 image. This is by design: shared variants avoid redundant storage and rendering. Uniqueness diverges when teams purchase different cosmetics — each distinct combination produces a different variant hash, creating a genuinely different card with its own rendered image. Premium cosmetic customization is the path to a truly unique card. Rating boosts are determined by auto-detected card profile and are not customizable.