Pages  ›  Cards

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

630 lines (+ 474 ajax-cards-autogenerate + 194 ajax-generate-card-image) Scoped by ?deck_id=... Looks like card generation is currently broken — see Bugs

What this page is, conceptually

Authoring vs playing (again)

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:

  1. 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".
  2. It assembles a context prompt from everything upstream: briefing (genre, tone, audience), resources, enabled card types, active behaviours, effects vocabulary, card families.
  3. GPT generates a single card (name, description, type, attributes, cost, effects, img_prompt) as JSON.
  4. The card is saved to game_cards and rendered as a block on the page.
  5. 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 just img_prompt, but conceptually could hold the prompt history, generation parameters, etc.
  • Image references (card_image_main, card_image_full, selected_image_id pointing into game_card_images): the visual. Versioned. Multiple renditions can exist per card; one is "selected".

The user's journey on this page

  1. 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 the gameUniqueId from the deck row if not passed.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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's img_prompt.
  7. 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.
  8. 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

  1. JS POSTs to ajax-cards-autogenerate.php?gameUniqueId=X&deckUniqueId=Y&singleCard=true. If a cardUniqueId is passed, the endpoint UPDATEs that card instead of inserting.
  2. Server loads deck meta, resources, card type schema, behaviours, mechanics, effects.
  3. pickNextTypeAndFamily() in inc-functions-distribution.php compares the deck's distribution to existing cards and picks the next target type (root or subtype) and optionally a family.
  4. generateExampleCardFromSchema() builds a JSON template matching the target type's fields + forced/blocked behaviours.
  5. A context prompt is assembled from briefing, resources, card types, and enabled effects.
  6. The prompt plus JSON template is sent to GPT (sendOpenAIResponseRequest).
  7. Response parsed, card inserted/updated, renderCardBlock produces the HTML, JSON returned to the client.
  8. JS replaces (update) or prepends (insert) the block.

Generate an image

  1. JS GETs ajax-generate-card-image.php?gameUniqueId&deckUniqueId&cardUniqueId.
  2. Server loads the card's img_prompt and the game's game_default_styles.
  3. If no style default exists, it auto-generates one (generateNewDefaultStyles).
  4. Per-card-type style overrides merge on top of defaults.
  5. A DALL·E prompt is composed: style JSON + img_prompt.
  6. Image is generated, stored in game_card_images, card row updated with references.
  7. 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 to game_default_styles.default.style) and Override Defaults for this Deck (writes to game_decks.deck_meta.style), based on which button was clicked (action=update_game_default vs action=override_deck).
  • method=save_deck_distribution — writes the slider payload to game_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.php and SimpleXLSXGen.php are 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"; pickNextTypeAndFamily handles 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_images keeps 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. generateExampleCardFromSchema shows 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 gameUniqueId or deck_id and 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

🚨 Card generation appears broken end-to-end

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.

Form targets now exist fixed

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.

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

Deprecated FILTER_SANITIZE_STRING

Lines 36 and 37. Same PHP 8.1+ deprecation as other pages.

Hardcoded session user 123

Line 4. Consistent with every other entry point.

Reused-context code path is force-disabled

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() in ajax-cards-autogenerate.php
    Line 362. No cards are being persisted end-to-end until this is lifted.
  • Wire save-style-settings.php / save-deck-distribution.php fixed
    Neither file existed. Folded into ajax-steps.php as save_deck_styles + save_deck_distribution. Forms redirect back on success. Also fixed the adjacent display bug where saved values never re-rendered because $defaultStyles was unwrapped one level short.

Bugs & cleanup

  • Fix $contextPrompt .= before declaration
    Initialize to '' at the top, or remove the early .= call.
  • Remove duplicate buildNormalizedFrequencyDistribution call
    index-cards.php:67 OR :218 — both compute the same thing.
  • Fix $deckMeta['soft_cap'] key name
    Should be soft_card_cap. Currently always falls back to the 30 default.
  • Re-enable or delete the reused-context branch
    ajax-cards-autogenerate.php:333. The && 0 sentinel is a debug stub; pick one.
  • Replace FILTER_SANITIZE_STRING
    Lines 36–37. Same fix pattern as other pages.

UX & workflow

  • Preview-before-save on AI generation
    Show the draft card in a confirm panel; only persist on accept.
  • Inline manual edit on each card
    Name, description, numeric attributes. Saves via an update-card method.
  • Batch "fill deck to target" mode
    Loops pickNextTypeAndFamily until soft cap reached. Huge speedup for authoring.
  • Delete / archive action on cards
    Same 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 generation
    Small dropdown next to Generate. Default "Auto".
  • Document the style-editor fields
    Short help text + examples per axis. Today the user guesses.
  • Wire or remove Insta-feedback
    Currently only console.logs. Decide; the middle state is misleading.

Aligned with the print-first goal

  • Print-ready context strip
    Surface the (future) card size, copy count, language on this page. Keeps the authoring work grounded in its physical end state.
  • Card back design surface
    Every printed card has a back. Nothing in the current model captures it. Could live at deck or game level.
  • Copies-per-card
    A 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 surface
    The 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 copies column? 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.)