Skip to content

Default alt arts: one setting for the defaults, each deck keeps its own prints (Re:Builder) - #153

Merged
julesduroux merged 39 commits into
preprodfrom
feat/alt-art-defaults
Oct 22, 2026
Merged

julesduroux merged 39 commits into
preprodfrom
feat/alt-art-defaults

Conversation

@Yutsa

@Yutsa Yutsa commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

Why

Players choose their alt arts in the ownership service (AlteredOwnership), in one of two modes:

  • « Global »: the service rewrites every deck with the player's preferences, on the site and on Board Game Arena (AltArtService.ApplyToDeckAsync). A deck cannot keep prints of its own.
  • « Par deck »: each deck plays its own prints. Before this PR, the settings page only offered tokens in this mode, and a card added to a deck took the plain print.

Players want to set their alt arts once, as defaults, and still change them deck by deck. Players in « Global » must keep what they see on BGA until they choose otherwise.

How

The two modes

The two modes stay. The preferences keep their storage in the service (slotReferences), and the mode only changes from the settings page, after a confirmation.

« Par deck » « Global »
Settings page « Arts alternatifs par défaut » Every card and token. The 1st, 2nd and 3rd card of each card get their illustration: the defaults Same page, same defaults
Card added in Re:Builder Takes the default of its copy Takes the defaults, like the whole deck
Per-deck override The brush, card by card, for this deck only None: no brush, no « Arts par défaut »
What BGA plays The deck's prints (the base art for copies not owned) The defaults, written by the service, unchanged

In « Global », Re:Builder writes the defaults in the deck when it opens and after each change, so the site's list matches what BGA plays. Preprod already did this; the PR now also falls back to the base art past the owned copies, as the service does.

Switching modes

  • To « Global »: the mode is set after a confirmation, as before.
  • Leaving « Global » (api/alt-art-leave-global.php, includes/func.alt-arts.php): the page lists the player's decks and writes in each one what BGA showed of it (altArtApplyToDeck, while still in « Global »), four at a time with a progress bar. Then it sets the mode.
    • This is best effort: a deck that cannot be written is skipped, and so is a deck list that cannot be read. The mode changes all the same.
    • A written deck drops its brush choices.
    • The endpoint refuses to write a deck outside « Global » (409).

Re:Builder

The logic is in core/alt-art-defaults.ts (pure, unit tested) and features/editor/editor-alt-arts.ts.

  • Adding a card from the search: it takes the default alt art of its copy, within the copies owned. The search shows each card once, its plain print standing for all its prints.
  • The brush (features/editor/alt-art-picker/) chooses the illustration of the 1st, 2nd and 3rd card of a card, for this deck only.
    • Three keys above each illustration. A pressed key means that card takes the illustration. A key that the owned copies rule out is disabled.
    • « Ce que le deck utilise » shows the deck's copies.
    • The decks API stores neither the order of the copies nor anything past them, so the site keeps the brush's choices: table rebuilder_deck_alt_arts (sql/migration_009, endpoint papi/rebuilder/deck-alt-arts).
      • One row per player, deck and card family.
      • No row when the three cards take the plain print.
      • The rows go when the deck is deleted, through the services relay (api/v1/services/proxy.php) or the site's deck pages (cacDeckApiDelete).
  • « Arts par défaut » applies the defaults to the whole deck after a confirmation, and drops the deck's brush choices.
  • The defaults also apply to an Equinox import, a copy of someone else's deck, and a hero picked without « Alt arts ». A copy of one's own deck keeps its prints.
  • « Regrouper les illustrations » (deck board, on by default, remembered in localStorage): a card's prints share one pile, copy 1 in front.
  • « Illustrations des jetons » shows each token large, with its illustrations.
  • Deck board: a card brought to 0 leaves the board.
  • Removed: the old « Global » code paths (card zoom widget and tilt, applyAltArtSlots) and alt-art-slots.
  • Merged with preprod's loading states: skeletons hold the place of « Illustrations des jetons » and « Arts par défaut » while the deck's owner and the mode are unknown.

Settings page « Arts alternatifs par défaut »

Files: plugins/ownership/pages/alt-arts.php, js/alt-arts.js, js/alt-art-widget.js.

  • One row per card: its name, type, faction and rarity on top.
    • On the left, its illustrations under their keys, on one line that scrolls sideways.
    • On the right, what decks take (its cards fanned out).
  • A token has one « Choisir / Choisie » key per illustration.
  • A card of the fan or an illustration opens large in core-altered-cards' card lightbox.
  • The card modals that embed the same widget get the keys too.

Keys, a design system component

ac-keys / ac-key (design-system/css/components/key.css, docs/components/key.md, /pages/design-system): raised when off, pressed and filled when on. Used by the brush and by the settings page.

Not changed

The legacy deck builder (no defaults when a card is added in « par deck »), altered.json, the decks API.

Release

One new table (migration_009, CREATE TABLE IF NOT EXISTS), applied by the deploy's « Bootstrap & migrate DB » step. No data to migrate, and no change for players in « Global ».

Local stack

docker-compose.ownership.yml runs the real AlteredOwnership (pinned commit) with Postgres and Redis. docker/stack/ownership-real/ seeds owned alt arts, defaults, alice in « Global » and two demo decks. CI keeps the mock, which now numbers slots from 1 and lists every card type in the alt-art search, as the service does.

Files to review first

  • includes/func.alt-arts.php, plugins/ownership/api/alt-art-leave-global.php, plugins/ownership/pages/alt-arts.php
  • plugins/rebuilder/papi/deck-alt-arts.php
  • plugins/rebuilder/app/src/app/core/alt-art-defaults.ts, features/editor/editor-alt-arts.ts, features/editor/alt-art-picker/
  • design-system/css/components/key.css, plugins/ownership/js/alt-art-widget.js

Outcomes

The settings page screenshots come from the local stack with the real ownership service: bob is in « par deck », alice in « Global ». The Re:Builder screenshots come from the e2e scenarios (ownership mock).

Settings page, « par deck »: the mode and what it does, above the search.

mode-1-per-deck

The defaults: one row per card. Keys above each illustration on the left, what a card added to a deck takes on the right.

mode-5-defaults-list

Keys and owned copies: Fée Clochette's MUSUBI print is owned once, so its 2nd and 3rd card keys are disabled. Scarabot, a token, has one key per illustration.

40-default-alt-arts-keys

A card large: a card of the fan or an illustration opens in the cards pages' lightbox.

41-default-alt-arts-zoom

To « Global »: the illustrations chosen deck by deck will be replaced.

mode-2-to-global

« Global »: the defaults apply to every deck, on the site and on BGA.

mode-3-global

Back to « par deck »: the defaults are written in every deck first (best effort).

mode-4-to-per-deck

Adding a card in Re:Builder: Élémentaire de Kélon, once in the search, takes the default alt art of each copy.

31-alt-arts-search

The brush: Élémentaire de Kélon, 2 copies. The 1st card's key is pressed on the alt art, the 2nd and 3rd on the plain print, and the deck uses the 1st and 2nd cards.

30-alt-art-brush

Keys of the brush: the alt art is on the 1st and 3rd cards. Its 2 owned copies are used, so its 2nd card key is disabled.

30-alt-art-brush-keys

« Arts par défaut »: applied to the whole deck after a confirmation.

32-default-alt-arts-confirm

« Illustrations des jetons »: one illustration per token, for every deck.

16-token-arts

Tests

  • npm run lint (ESLint, i18n all translated), npm test (57 files, 367 tests, including alt-art-defaults.spec.ts and editor-alt-arts.spec.ts), npm run build
  • php tests/run.php: 116 passed
  • Playwright, the whole suite on the local stack (ownership mock), after merging preprod: 157 passed, 36 skipped. 1 failed outside this change: community decks « Légaux uniquement » on mobile, whose first public page depends on the decks earlier local runs left. Alt-art scenarios:
    • « Global »: the deck opened in Re:Builder takes the defaults, without brush or « Arts par défaut »;
    • leaving « Global »: every deck gets what apply-to-deck showed, then the mode is « par deck »;
    • leaving « Global » when every deck write fails: the mode is « par deck » all the same;
    • settings page (plugins/ownership/e2e/alt-arts.spec.ts): a key gives an illustration to a card and saves it, the fan follows, the owned copies disable keys, a token's key; a refused choice (409) puts the keys and the fan back and shows why;
    • the brush: the 1st and 3rd cards, the 2nd disabled once the owned copies are used, the choices kept after a reload, an added copy taking the 3rd card's illustration, the defaults untouched, the rows gone once the deck is deleted;
    • « Mon deck » on a phone: nothing moves while the owner and the mode arrive.
  • Settings page tried on the local stack with the real service (docker-compose.ownership.yml, README § Full stack)

🤖 Generated with Claude Code

… in Re:Builder

The ownership service's « Global » mode rewrote every deck with the global preferences, on the site and on
Board Game Arena (apply-to-deck), so a deck could not keep prints of its own. The settings page now sets
default alt arts only, and a deck stores the prints it uses:

- Settings (plugin ownership): one mode, « Arts alternatifs par défaut », for cards and tokens.
- A player still in « Global » is switched to « par deck » on their next visit (includes/func.alt-arts.php);
  their decks wait for their default alt arts until Re:Builder opens them (table alt_art_pending_decks).
- Re:Builder: a card added from the search takes the default alt art of its copy; « Arts alternatifs » in
  the search lists the alt arts the player owns after their card, added as shown; the brush of a card
  changes its prints for the deck; « Arts par défaut » applies the defaults to the deck; the token window
  shows each token large with its illustrations. Import, a copy of someone else's deck and a hero picked
  without « Alt arts » take the defaults.
- Local stack: docker-compose.ownership.yml runs the real AlteredOwnership service, with owned alt arts,
  default alt arts and a deck to switch from « Global ».

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Yutsa added a commit that referenced this pull request Oct 8, 2026
@Yutsa
Yutsa marked this pull request as draft October 8, 2026 13:10
Yutsa and others added 2 commits October 8, 2026 16:12
The search no longer lists the owned alt arts after their card: a card's plain print stands for all its
prints, and a copy added from the search takes its default alt art. The brush changes the prints in the deck.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…h or drag; a card at 0 leaves the deck board

A copy left free shows its default alt art, faded, and takes it; a touched
illustration goes on the first free copy, a dragged one on the copy it is
dropped on; a touched copy goes back to its default (slotDefaults,
slotChoices). The deck board no longer keeps a card brought to 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Yutsa added a commit that referenced this pull request Oct 8, 2026
Yutsa and others added 2 commits October 8, 2026 16:48
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ne pile of the deck board, copy 1 in front

On by default, remembered in the browser. The brush's copy 1 is the deck's
own choice: the prints the defaults do not explain come first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Yutsa added a commit that referenced this pull request Oct 8, 2026
… is freed for the next illustration

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Yutsa added a commit that referenced this pull request Oct 8, 2026
Yutsa and others added 13 commits October 8, 2026 17:16
…o that saving moves nothing

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e ribbons, and what the deck uses

The brush lists the card's illustrations; touching one opens a menu to make it the 1st, 2nd or 3rd choice. The
choices are the player's default alt arts (ownership service) and the deck's copies take them at once. A choice the
deck does not use (beyond its copies) has a dashed ribbon; the « Ce que le deck utilise » panel fans out the copies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… sentence, without the list of copies

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…te), one look for every ribbon

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rrow to « Ce que le deck utilise » on the same level

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…chevron that fits into « Ce que le deck utilise »

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…; the plain print is not an explicit choice

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… in {rebuilder_deck_alt_arts}

The brush no longer saves the player's default alt arts (ownership service): its 1st, 2nd and 3rd card are kept for
the deck by papi/rebuilder/deck-alt-arts (core migration 010, the plugin has no install SQL), as the decks API stores
only the deck's references. The deck's copies take them; an added copy takes the next card's. « Arts par défaut » in
the brush and « Appliquer les arts par défaut » drop the deck's choices.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ee cards take the plain print

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e does not move them

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t, 2nd in the middle, 3rd on the right

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…k, the menu on ac-menu

- core-altered-cards: cacDeckApiDelete sends the DELETE and forgets the deck (three pages had their own copy)
- altArtForgetDeck takes the player: the DELETEs use the tables' primary keys; pending decks in one INSERT
- deck-alt-arts: plugin API URL and the CSRF interceptor; exactly three cards
- EditorAltArts: ownDeckId, keepCards, familyCopies; the picker gets the copies from it
- the brush's menu uses ac-menu; dead CSS and the unused altArtVersion removed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…too; shared illustration labels

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Yutsa added a commit that referenced this pull request Oct 10, 2026
…very deck

The site no longer switches players out of « Global » on their next visit. A player picks
the mode on the default alt arts page:
- « Global »: the service rewrites every deck with the defaults on BGA, as before;
  Re:Builder writes them in the deck when it opens and after each change, without the brush.
- « Par deck »: defaults on add, the brush per deck (unchanged).

Leaving « Global » writes in each deck what BGA showed of it (apply-to-deck, while still in
« Global »), then sets the mode; a failure keeps « Global ». Switching to « Global » drops the
brush choices. The pending-decks table and endpoint are gone; migration_010 becomes 009.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Yutsa added a commit that referenced this pull request Oct 13, 2026
Yutsa and others added 3 commits October 13, 2026 14:49
- Every deck of the player is written when leaving « Global » (the decks are read 1000 a
  page), and the four parallel writes no longer wait on the session lock.
- One apply-to-deck helper (altArtApplyToDeck) for leaving « Global » and the site's deck
  builder (cacApplyAltArtPreferencesToCards wraps it).
- A deck written on leaving « Global » drops its brush choices there, instead of every
  deck's on entering « Global ».
- The mode endpoint relies on the router (methods, sign-in, CSRF); the page sends the
  token once and maps each failed step to its message.
- Re:Builder asks the mode only when signed in, and the families of the deck stay equal
  while search results add others.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ns and a menu

Each illustration has a 1st, 2nd and 3rd card key: pressed down and filled when that
card takes the illustration, raised and faded otherwise. Pressing a raised key gives
the illustration that card; a key the copies owned rule out is disabled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…bove each illustration

The keys of the Re:Builder brush become a design system component (ac-keys / ac-key),
used by the brush and by the « Arts alternatifs par défaut » page. Each card of that page
is a row: its name, type, faction and rarity, what decks take (its cards fanned out, as in
the brush), then its illustrations under their keys. The marker flags are gone; the card
modals that embed the same widget get the keys too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Yutsa and others added 2 commits October 13, 2026 17:31
- Re:Builder brush: a pressed key is never disabled (it stayed faded when the copies
  owned no longer covered the deck's choice).
- Token key named by its text (« Choisir » / « Choisie »).
- The fan of the default alt arts page is an image named after what each card takes.
- One CDN URL helper (the widget's), fan images made once.
- e2e: a refused choice puts the keys and the fan back and shows why.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Yutsa added a commit that referenced this pull request Oct 13, 2026
… on a phone

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Yutsa added a commit that referenced this pull request Oct 13, 2026
Yutsa and others added 2 commits October 13, 2026 17:44
Without a type filter the mock only searched tokens, so the default alt arts page of the
local stack showed no character, spell or permanent. It now reads the Common and Rare card
groups (Uniques have no alt art) 200 at a time and counts skip / take in families, as
AlteredOwnership's /api/alt-arts/search does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fan of what decks take is larger, a token's or a hero's single card much larger, and
the illustrations under the keys are 200 px wide. A card of the fan or an illustration
opens in core-altered-cards' card lightbox, as on the cards pages (tilt, « Accéder au
détail »), without its alt-art widget since the keys of the page already choose.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Yutsa added a commit that referenced this pull request Oct 13, 2026
… and faded

A key that was off and a disabled key looked alike (60 % and 30 % opacity). A key that can
be pressed now has its text in --ac-color-text-2 and a darker bottom edge at full opacity;
a disabled key loses its edge and fill, in --ac-color-text-disabled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Yutsa added a commit that referenced this pull request Oct 13, 2026
…e right

As in Re:Builder's brush (step 1 the illustrations, step 2 « Ce que le deck utilise »): the
card's name on top, its illustrations and keys on the left, the fan of what decks take on a
panel on the right (below the illustrations on a phone).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Yutsa added a commit that referenced this pull request Oct 14, 2026
…decks take, centred

As in Re:Builder's brush: the illustrations on a band ending in a chevron, which fits into
the notch of the « what decks take » panel, its fan centred vertically; on a phone the
panel goes under the band, the chevron pointing down.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Yutsa added a commit that referenced this pull request Oct 14, 2026
Yutsa and others added 6 commits October 14, 2026 09:00
…s sideways

A family with many prints (heroes have up to 5) made rows over 1000 px tall, the fan of
what decks take far from the first keys. The illustrations now stay on one line that
scrolls sideways, snapping to each print, with an arrow on each side while there is more
that way; on a phone two prints and the edge of the next are in view.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While a card's line of illustrations has more on one side, a mask fades the cut
illustration out on that side (10 % of the line, 96 px at most).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GET /api/decks sends every deck whatever the page: an account of 1000 decks or more got the same list
again and again, until the decks API ran out of memory and the switch to « par deck » failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The service fills each line from the family's 1st card: a deck with the alt art on one line and the plain
print on another (1st card alt art, 2nd and 3rd plain) got the alt art twice when leaving « Global »,
though only one copy is owned. The lines of a family are now sent as one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he chevron

The e2e reads the player's mode instead of expecting « par deck ».

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Re:Builder conflicts: the PR's alt-art editor and deck board kept, with
preprod's loading states (DeckStore.steppersShown, skeletons while the
deck's owner and the alt-art mode are not known, now for « Appliquer les
arts par défaut » too). The e2e createServerDeck helper takes the copies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
github-actions Bot added a commit that referenced this pull request Oct 22, 2026
@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

⚠️ Loading: CLS over budget

Not a failure: the CLS of a screen varies from one CI run to the next. Each screenshot is the settled page, the boxes that moved drawn on it (dashed: before, solid: after). Frame by frame: npm run loading:timeline -- <screen> in tests/e2e. Run: https://github.com/Altered-Community/alteredcore-website/actions/runs/39771314329

desktop · editor, Main, second visit at 1280 px: skeleton until the first screen, styled, no layout shift

CLS 0.2019 (budget 0.01)

Shifts
1448 ms  0.0001
  x937 y343 38×38 → x937 y308 38×38 (path < svg.ac-icon < ac-icon)
  x137 y98 75×16 → x137 y93 75×16 (a.back < div.text < div.identity)
1633 ms  0.0006
  x623 y111 217×24 → x545 y111 217×24 (img < ac-badge.ac-badge < ac-rarity-limits)
  x856 y111 87×24 → x778 y111 165×24 (ac-badge.ac-badge.ac-badge--orange < button.legality < app-deck-bar.reduced)
  x606 y97 1×52 → x528 y97 1×52 (span.sep < app-deck-bar.reduced < div.top)
1749 ms  0.2012
  x24 y306 908×466 → x0 y0 0×0
  x157 y242 147×40 → x181 y242 171×40 (button.ac-button.ac-button--secondary < div.toolbar < app-test-hand)
  x24 y796 908×4 → x0 y0 0×0

Yutsa added a commit that referenced this pull request Oct 22, 2026
…ger keeps the player in « Global »

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
github-actions Bot added a commit that referenced this pull request Oct 22, 2026
…, no layout shift at 1024 px

The switch and « Arts par défaut » come with the deck's owner: at 1024 px the
row wrapped then, and the board moved 44 px down (CLS 0.043). Their skeletons,
at their widths, make the row wrap before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
github-actions Bot added a commit that referenced this pull request Oct 22, 2026
@Yutsa
Yutsa marked this pull request as ready for review October 22, 2026 11:56
@julesduroux
julesduroux merged commit 4fa6646 into preprod Oct 22, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants