claude-home/paper-dynasty/card-evolution-prd/07-variant-system.md
Cal Corum aafe527d51
All checks were successful
Reindex Knowledge Base / reindex (push) Successful in 5s
docs: add Major Domo and Paper Dynasty release notes and card evolution PRD
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 22:29:18 -05:00

70 lines
4.0 KiB
Markdown

# 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.