index-cards.php — Cards (Step 4)
The card authoring surface, scoped to one deck at a time. Everything upstream (briefing, pieces, decks) converges here — this is where the AI actually writes cards, the designer reviews them, and images get generated. The last step before "print-ready" (which doesn't exist yet).
?deck_id=...
Looks like card generation is currently broken — see Bugs
What this page is, conceptually
Same trap as Step 3. This page is about authoring cards, not playing with them. Everything on screen — the generate button, the distribution progress bars, the style editor — is for the designer making the game, not the eventual player of the game. The cards being authored here will, much later, be printed and played; this page is strictly about their creation.
Given a deck from Step 3 (say "Machine Cult" in a Star-Realms-like game), this page is the workshop where its cards come to life. The workflow is:
- The system looks at the deck's composition quota (from Step 3: 50% Unit, 30% Event, 20% Item) and decides "the next card should be a Unit".
- It assembles a context prompt from everything upstream: briefing (genre, tone, audience), resources, enabled card types, active behaviours, effects vocabulary, card families.
- GPT generates a single card (name, description, type, attributes, cost, effects, img_prompt) as JSON.
- The card is saved to
game_cardsand rendered as a block on the page. - A separate call (on demand) generates the card's image
via DALL·E, using the game's style defaults + the
img_prompt.
The page also hosts a live distribution progress tracker (how many Units authored vs 30 target, etc.) and a style editor for the deck's visual identity.
What is a card, conceptually?
Three layers live inside a game_cards row:
-
Card data (
card_json): name, description, type, subtype, cost, effects, attributes, behaviours,meta_tags. The mechanical identity of the card. -
AI metadata (
card_ia_json): fields the AI produced that aren't part of the card itself — currently justimg_prompt, but conceptually could hold the prompt history, generation parameters, etc. -
Image references (
card_image_main,card_image_full,selected_image_idpointing intogame_card_images): the visual. Versioned. Multiple renditions can exist per card; one is "selected".
The user's journey on this page
-
Arrival. User clicks "Cards" on a deck row from
Step 3 (or "Cards" from My Games). URL is
index-cards.php?deck_id=XXX. The page derives thegameUniqueIdfrom the deck row if not passed. - Default Game Styles panel. Collapsed by default. Five textareas: background, color palette, drawing style, lighting, perspective. Two buttons: Update Game Defaults or Override Defaults for this Deck. These feed into image generation later.
- Card Distribution Progress. A horizontal row of progress cards, one per distribution group (family, type, per-root subtype). Each shows "live 2 / expected 5" with a progress bar. There's an inline Edit Distribution collapse that exposes the slider partial — same sliders as on Step 3.
- Game Cards list. Previously-authored cards render as blocks (image + details). At the bottom, a "Create a New Card" placeholder with a Generate New Card via AI button.
-
Generate a card. Click the button; a POST to
ajax-cards-autogenerate.php. The server picks the next type/family (respecting the distribution), builds a prompt, asks GPT for a card, saves it, renders the block, returns JSON. Front-end prepends the new block above the placeholder and nudges the progress bars up. -
Generate an image. Per card block, a
Generate Image button calls
ajax-generate-card-image.php. Returns a DALL·E image via the game's style + the card'simg_prompt. -
Favourite an image. Each image version has a star
toggle (
ajax-toggle-favorite-image.php) that marks it as the canonical version. Prevents further regeneration on that card. -
Insta-feedback. Quick-answer buttons per card
("Good?", "On-theme?") plus a free-text field.
Currently a pure UI feature — the feedback is
console.logged, not sent to the server.
What's really captured
Card authoring data
game_cards.card_json,
card_ia_json,
card_image_main,
selected_image_id
Each generated card becomes one row. The card_json holds
the authored card as JSON; later steps (print-ready export, web
playtester) will read it. Note the name/description are also
extracted into top-level columns (card_name,
card_desc) for easier SQL filtering.
Visual style at deck scope
game_default_styles.default.style (game-wide),
game_default_styles.cardtype-{type} (per-type override)
The style editor writes to game_default_styles game-meta.
Five axes: background / color-palette / drawing-style /
lighting-and-atmosphere / perspective-and-composition. On image
generation, per-card-type overrides merge on top of game defaults.
Per-deck overrides are wired as a UI concept but not fully
implemented on the save path (save-style-settings.php
referenced as the form target; existence not verified in this pass).
Per-deck composition overrides
game_decks.deck_meta.frequency_distribution
The distribution editor saves per-deck overrides to
save-deck-distribution.php (same "existence not
verified" caveat). The page merges game-wide and per-deck
distributions for display and for the AI's "pick next type" logic.
Progress signal (read-only, derived)
Live counts are computed from the existing cards' meta_tags
and rendered as "live 3 / expected 5" labels + progress bars. Not
stored anywhere — regenerated on every page load from
$cards. Correct approach.
What happens on each action
Generate a card
- JS POSTs to
ajax-cards-autogenerate.php?gameUniqueId=X&deckUniqueId=Y&singleCard=true. If acardUniqueIdis passed, the endpoint UPDATEs that card instead of inserting. - Server loads deck meta, resources, card type schema, behaviours, mechanics, effects.
-
pickNextTypeAndFamily()ininc-functions-distribution.phpcompares the deck's distribution to existing cards and picks the next target type (root or subtype) and optionally a family. -
generateExampleCardFromSchema()builds a JSON template matching the target type's fields + forced/blocked behaviours. - A context prompt is assembled from briefing, resources, card types, and enabled effects.
- The prompt plus JSON template is sent to GPT
(
sendOpenAIResponseRequest). - Response parsed, card inserted/updated,
renderCardBlockproduces the HTML, JSON returned to the client. - JS replaces (update) or prepends (insert) the block.
Generate an image
- JS GETs
ajax-generate-card-image.php?gameUniqueId&deckUniqueId&cardUniqueId. - Server loads the card's
img_promptand the game'sgame_default_styles. - If no style default exists, it auto-generates one
(
generateNewDefaultStyles). - Per-card-type style overrides merge on top of defaults.
- A DALL·E prompt is composed: style JSON +
img_prompt. - Image is generated, stored in
game_card_images, card row updated with references. - Server returns updated HTML block; JS replaces the existing one.
Mark image as favourite
POST to ajax-toggle-favorite-image.php. Updates a
markFavorite flag. When marked, the
Generate Image button is disabled for that card so the
favourite isn't overwritten.
Save style / distribution shipped
Originally the two HTML forms POSTed to
save-style-settings.php and
save-deck-distribution.php, neither of which
existed on disk. The buttons were 404s.
Now folded into ajax-steps.php as two new cases:
-
method=save_deck_styles— handles both Update Game Defaults (writes togame_default_styles.default.style) and Override Defaults for this Deck (writes togame_decks.deck_meta.style), based on which button was clicked (action=update_game_defaultvsaction=override_deck). -
method=save_deck_distribution— writes the slider payload togame_decks.deck_meta.frequency_distribution. Each group is normalised to sum to 100 (meta keys starting with_are preserved).
Both are HTML form submits (not AJAX), so they redirect back to
index-cards.php?deck_id=...&saved=style|distribution
on success.
Also fixed two adjacent read-side bugs in index-cards.php:
$defaultStyles was unwrapped one level short
($raw['default'] instead of
$raw['default']['style']), so the textareas always
rendered blank even when data was saved; and $deckStyles
was hardcoded to [] with a "TODO: get deck meta
styles" — now reads from the loaded $deckMeta['style'].
Insta-feedback
Quick-answer taps build up a feedbackData[cardId] object
in JS. On submit, the data is console.logged. No AJAX,
no persistence. Commented "Optional: send via AJAX" in the
source. Skeleton only.
How cards feed downstream
This is the last wizard step. Cards are the final authored artefact. Two downstream surfaces should read them but largely don't exist yet:
-
A print-ready export — the reason the product
exists. No code path today.
excel.phpandSimpleXLSXGen.phpare spreadsheet exports, not print-ready PDFs with bleed, crop marks, or back-of-card layouts. Flagged as a global TODO. - A simulation / playtesting surface — would read cards + flow (once Flow exists) and let the designer run through turns. Not present.
In the current shipped pipeline, cards end their journey in
game_cards. The path from authored card to
physical printed card is the missing last mile.
What works well, conceptually
- Deck-scoped authoring. Working on one deck at a time keeps the cognitive surface manageable. A user with 5 decks has 5 authoring sessions, not one 300-card list.
- Distribution progress tracker. A live view of "how many of what I've built vs what I'm targeting" is the right feedback signal for a quota-driven authoring tool.
-
"Pick next type" is automatic. The user doesn't
have to manually decide "I'll author a Striker now";
pickNextTypeAndFamilyhandles the distribution accounting. Good default. - Cards and images are decoupled. You can regenerate an image without touching the card data, and vice versa. Preserves the card's identity while iterating visually.
-
Image versions are persisted.
game_card_imageskeeps history; the "selected" one is a pointer. Favourites protect against accidental regeneration. - Context-rich AI prompts. The generation call assembles briefing + resources + card types + behaviours + effects — far richer than a naive "generate a card" prompt. Result: cards that feel coherent with the game.
-
Example-based generation.
generateExampleCardFromSchemashows GPT an exact JSON shape to return. Much better than asking for free-form text and parsing. Reduces malformed-response rate. - Three-layer style model. Game default → per-card-type override → (future) per-deck override. Good layering; matches how designers actually think about visual consistency.
-
URL accepts either
gameUniqueIdordeck_idand derives the other. Allows users to bookmark a deck and come back to authoring directly.
What's rough, conceptually
No preview-before-save on AI generation
Click Generate; the card is in the DB before the user sees it. No "here's what I came up with — accept / regenerate / edit?" moment. Bad cards pollute the deck and the distribution tracker, forcing the user to delete them afterwards (and the delete UI doesn't exist, see below).
No manual card editor
Cards can only come from the AI. If the generated card is almost right — wrong numbers, awkward phrasing, slight name tweak — the designer's only recourse is to regenerate the whole card (which loses the things they liked). A simple edit form on each card block would be high-leverage.
One card at a time
For a 60-card deck, the designer clicks Generate 60 times.
Each click is a full AJAX round-trip. No batch mode ("generate 10
cards", "fill this deck to target"). The server side already loops
over $generated['cards'], so the plumbing is there
— just the client and the prompt ask for one card.
No delete / archive on individual cards
Same pattern as decks on Step 3 — the DB has an
is_archived column, the list query filters on it, but
there's no UI button. Combined with "AI writes directly to DB",
bad generations accumulate with no way to remove them.
Insta-feedback is write-only (to console)
The feature looks shipped — quick-answer buttons, a
free-text field, a "thank you" message. But the submit handler just
console.logs the result. Nothing reaches the server,
nothing feeds back into future generation prompts. Either wire it up
or remove it — the current state is user-confusing.
"Pick next type" is opaque
The designer has no UI to say "I actually want to generate an Event next". The server decides silently based on distribution. Mostly that's right; occasionally the designer wants to deliberately over- or under-sample a type for a session.
Style editor fields are unlabelled by purpose
Five textareas named Background, Color Palette, Drawing Style, Lighting & Atmosphere, Perspective & Composition. No explanation of what to write, what the AI does with it, or examples of good content. The user is expected to guess.
Progress bars show absolute counts, not ratio of target
“3 / 5” tells the user how far they are per type, but not how close the deck is to completion overall. A header number ("18 of 60 cards authored (30%)") would be a more actionable headline.
Style defaults vs per-type overrides vs per-deck overrides
Good model, rough implementation. The "per-deck override" form
target (save-style-settings.php) is referenced but its
existence isn't verified. Per-card-type overrides live in
game_default_styles.cardtype-* but the editor only
exposes game-wide defaults. Designer can't override just the "Event"
look on this screen.
Page-render performance
buildNormalizedFrequencyDistribution is called twice per
page load (lines 67 and 218 of index-cards.php) with
the same arguments. Each call walks the whole schema. Not dramatic
but wasteful, and a sign of the page having grown by copy-paste.
The print-first goal never shows up on this page
This is the closest DeckCraft gets to a "finished card" today — and yet nothing on the page references card size, print-ready state, copy count per card, or export. A designer reaching this step would rightly ask "where do I get my cards printed?". There's no answer in the current UI. That's the largest conceptual gap on the whole wizard.
Suggestions
1. Unblock the card-generation pipeline
Do this before anything else. An unconditional
die() at
ajax-cards-autogenerate.php:362 kills the handler
before the DB insert and the JSON response. See Bugs.
Until it's removed, nothing else on this page works end-to-end.
2. Preview-before-save on generation
Modal or inline panel: "I drafted this card. Accept, tweak, regenerate, discard?". Only accepted cards hit the DB. Keeps the distribution tracker honest and saves the missing delete feature from needing to exist on day one.
3. Inline manual edit on each card
Editable fields on the card block: name, description, numeric
attributes, effect parameters. Saves via a simple
ajax-steps.php?method=update-card branch. Dramatically
improves the authoring workflow.
4. Batch generation mode
"Fill deck to target" button. Server loops over
pickNextTypeAndFamily until the deck hits the soft cap.
Accept-all / accept-selected UX on the returned batch.
5. Delete / archive on individual cards
Same fix as the decks table — UI button sets
is_archived = 1. The schema already supports it.
6. Wire (or remove) Insta-feedback
If it stays: POST to a new
ajax-steps.php?method=feedback-card; store as a
game_cards column or a separate table; feed into
future generation prompts ("Your last 10 cards were rated as
too powerful — try generating a weaker one"). If not:
rip out the component; the current state is misleading.
7. Let the user override "pick next type" per generation
A small dropdown next to Generate: "Auto / Unit / Event / Item". Default auto. Preserves distribution accounting without forcing it.
8. Add physical-print context to this page
Even a read-only strip is enough: "Printing 60 poker-sized cards, English, single copy each". Plus a future Generate print-ready PDF button. Connects the authoring work to the product's stated end goal.
9. Expose per-card-type style overrides on this page
Right now the model supports them but the UI on this page only edits game-wide defaults. A small "Customise for <type>" expansion would match the underlying data model.
10. Overall progress headline
A big "18 / 60 cards authored (30%)" at the top of the distribution panel. The per-type progress bars stay; they're useful. The headline gives the designer an at-a-glance "where am I?" signal.
Bugs & smells
ajax-cards-autogenerate.php:362 has an unconditional
die():
if ($returned_and_parsed) {
storeDeckResponseHistory(...);
$generated = findJsonInTextAndConvertToArray(...);
...
}
die(); // ← line 362, kills execution before the DB insert / JSON response
if ($isDebug) { ... }
if (empty($generated['cards'])) { ... }
/* INSERT / UPDATE / renderCardBlock / echo json_encode(...) */
Effect: every call hits the GPT API, parses the response, stores the
deck history — and then exits. No card is inserted, no HTML
is returned, no JSON response is produced. The endpoint already
sent header('Content-Type: text/html') earlier, which
the client's dataType: "json" AJAX call can't parse,
so the UI goes to the error handler. Fixing this is the
first step for any Step 4 work.
Confirmed that save-style-settings.php and
save-deck-distribution.php did not exist on disk —
the buttons were 404s. Folded into ajax-steps.php as
method=save_deck_styles and
method=save_deck_distribution. Both forms now post to
ajax-steps.php with a hidden method input
and redirect back on success. Also fixed the adjacent display bug
where $defaultStyles was unwrapped one level short.
$contextPrompt .= ... before declaration
ajax-cards-autogenerate.php:46 uses .= on
$contextPrompt before the variable exists. Generates a
PHP warning on every request; later, line 67 replaces the
variable with =, so the earlier append is wasted.
Cosmetic but smells like a refactor half-done.
buildNormalizedFrequencyDistribution call
index-cards.php:67 and :218. Same
arguments. The result of the first call is discarded.
$deckMeta['soft_cap'] key mismatch
index-cards.php:179 reads
$deckMeta['soft_cap'], but the field is stored as
soft_card_cap everywhere else (see
ajax-cards-autogenerate.php:44). The default of 30
gets used on every page load regardless of the real deck size.
FILTER_SANITIZE_STRINGLines 36 and 37. Same PHP 8.1+ deprecation as other pages.
Line 4. Consistent with every other entry point.
ajax-cards-autogenerate.php:333: the condition
if ($last_response = ... && 0) has a
&& 0 at the end, deliberately disabling the
"reuse previous GPT response context" branch. TODO comment above
it says "remove 0 as we are debugging". Either commit to
it (re-enable) or remove the dead branch.
TODOs
Checkbox state persists locally per page.
Critical (fix first)
-
Remove the blocking
die()inajax-cards-autogenerate.phpLine 362. No cards are being persisted end-to-end until this is lifted. -
Wire
save-style-settings.php/save-deck-distribution.phpfixedNeither file existed. Folded intoajax-steps.phpassave_deck_styles+save_deck_distribution. Forms redirect back on success. Also fixed the adjacent display bug where saved values never re-rendered because$defaultStyleswas unwrapped one level short.
Bugs & cleanup
-
Fix
$contextPrompt .=before declarationInitialize to''at the top, or remove the early.=call. -
Remove duplicate
buildNormalizedFrequencyDistributioncallindex-cards.php:67OR:218— both compute the same thing. -
Fix
$deckMeta['soft_cap']key nameShould besoft_card_cap. Currently always falls back to the 30 default. -
Re-enable or delete the reused-context branch
ajax-cards-autogenerate.php:333. The&& 0sentinel is a debug stub; pick one. -
Replace
FILTER_SANITIZE_STRINGLines 36–37. Same fix pattern as other pages.
UX & workflow
-
Preview-before-save on AI generationShow the draft card in a confirm panel; only persist on accept.
-
Inline manual edit on each cardName, description, numeric attributes. Saves via an
update-cardmethod. -
Batch "fill deck to target" modeLoops
pickNextTypeAndFamilyuntil soft cap reached. Huge speedup for authoring. -
Delete / archive action on cardsSame pattern as decks. DB already supports
is_archived. -
Overall progress headline"18 / 60 cards authored (30%)" at the top of the distribution panel.
-
Let user override next-type per generationSmall dropdown next to Generate. Default "Auto".
-
Document the style-editor fieldsShort help text + examples per axis. Today the user guesses.
-
Wire or remove Insta-feedbackCurrently only
console.logs. Decide; the middle state is misleading.
Aligned with the print-first goal
-
Print-ready context stripSurface the (future) card size, copy count, language on this page. Keeps the authoring work grounded in its physical end state.
-
Card back design surfaceEvery printed card has a back. Nothing in the current model captures it. Could live at deck or game level.
-
Copies-per-cardA card might appear N times in a printed deck (e.g., 4x Copper in Dominion). No field for this today. Belongs on the card row.
-
Per-card-type style-override surfaceThe data model supports it; the UI on this page doesn't. Match what's already there.
Open questions for the product
- Should AI generation always preview before saving, or only above a complexity threshold? Always-preview slows quick iteration; never-preview gives you the current mess.
- Is a manual card editor a first-class feature or only a fallback for when AI fails? (Affects how much UI real estate to budget.)
- Batch generation: one API call per card, or one big call asking for N cards? The latter is cheaper but makes distribution accounting trickier.
-
How is copies-per-card supposed to be captured?
On the card row as a
copiescolumn? Derived from something else? - Is the Insta-feedback feature intended to close the loop into future prompts, or just a sentiment log?
- When the print pipeline lands, does each deck become one or more physical card stacks? (This is the design-deck ↔ gameplay-zone question from Step 3, inherited.)