Dynamic Card Faces and Post‐Battle Card Rewards - UQcsse3200/2026-studio-3 GitHub Wiki

This page describes reusable card faces, rarity frames, their screen integration and the post-battle Card reward flow. It supplements the Card System documentation, which covers configuration, card identity, upgrades, deck lifecycle, combat and save/load. Those foundational systems are not repeated here. Source code and configuration remain authoritative.

Contents

Architecture and integration contracts

The presentation boundary consumes a ResolvedCard, not raw labels or gameplay state. An existing resolver produces the snapshot; CardWidget renders it; the surrounding screen decides whether the face can be clicked, dragged or acquired. The renderer never spends energy, changes ownership, marks discovery or calculates an upgrade.

The following UML class diagram shows selected contracts, not every field or inheritance relationship in the project. Composition denotes actors created and owned by the widget; arrows denote references or dependencies, not ownership of shared textures.

classDiagram
    class CardWidget {
        +CardWidget(ResolvedCard card, CardWidgetAssets assets)
        +setCard(ResolvedCard card) void
        +getCard() ResolvedCard
    }
    class CardWidgetAssets {
        +fromManagedResources(Skin skin, ResourceService resources) CardWidgetAssets
        +collectTexturePaths(Collection configs) String[]
        +hasAuthoredFrame(Rarity rarity) boolean
    }
    class FramedCardFace {
        +layout() void
    }
    class CardFrameLayout {
        <<record>>
        String texturePath
        boolean artworkAboveFrame
    }
    class CardTextFitter {
        ~fit(Label label, float maximumScale) void
    }
    class ResolvedCard {
        <<record>>
        String instanceId
        String cardId
    }
    CardWidget --> ResolvedCard : binds snapshot
    CardWidget --> CardWidgetAssets : shared assets
    CardWidget *-- FramedCardFace : authored rendering
    CardWidgetAssets --> CardFrameLayout : rarity registry
    FramedCardFace --> CardFrameLayout : placeholder geometry
    FramedCardFace ..> CardTextFitter : bounded typography
Loading

FramedCardFace, CardFrameLayout and CardTextFitter are package-private. Public screens depend on CardWidget and CardWidgetAssets; adding a view should not require access to frame internals. The generic fallback uses the same resolved snapshot.

Public API and caller obligations

Entry point Caller obligation and result
CardWidget(resolved, assets) Supply non-null resolved values and externally owned assets. Creates a touch-disabled face with logical 225 × 456 preferred bounds.
setCard(resolved) Re-resolve outside the widget when gameplay values change. Rebinds fields and selects the resolved rarity's layout; does not subscribe to gameplay events.
getCard() Inspect the bound snapshot without scraping labels. It is not an authoritative lookup of the current deck.
CardWidgetAssets.fromManagedResources(skin, resources) Preload first. Uses managed drawables and copied label styles; widgets do not dispose the shared skin or textures.
CardWidgetAssets.fromSkin(skin, provider) Provide externally owned drawables, including frame paths. Useful for tests or a different resource owner; returning null represents an unavailable drawable.
Clickable.setVisualContent(factory) Return a fresh actor on each invocation. Existing interaction actors remain responsible for event payloads and disabled/selection tint.
RewardDisplay(record, rewards, run, cards, discovery) Supply non-null reward, catalogue and discovery services; a usable run is required for claiming. Attach/register through the existing entity lifecycle, rather than calling private selection methods.

Card faces and frame assets

Card faces combine a shared rarity frame, artwork and dynamic text. They display resolved cost, name, description and upgrade state, rather than text baked into the artwork. Type and target occupy separate placeholders. Rarity is communicated by the frame and is also written explicitly in Library details.

Frame gallery

The following are the actual frame assets, not screenshots of rendered cards. Artwork and dynamic labels are inserted at runtime into their placeholders.

Common Uncommon Rare
common_card_frame uncommon_card_frame rare_card_frame

Rendered Library examples

These Library card-face captures show the frames populated with artwork and dynamic cost, name, type, description and target. The three rarities use the same rendering pipeline, with their own frame assets and placeholder layouts.

Warding Sweep — Common Purify — Uncommon Doom Sigil — Rare
common_example uncommon_example rare_example
Placeholder Runtime content
Top-left orb Energy cost, centred independently of the name.
Header nameplate Resolved name, including an upgraded name when configured.
Artwork window Card illustration, aspect ratio preserved with Scaling.fit.
Middle ribbon Card type: Attack, Skill, Power, Status or Curse.
Parchment panel Wrapped resolved description.
Bottom plate Target: Self, One Enemy or All Enemies.
Upgrade marker A separate + for an upgraded copy.

Rendering implementation

Class Responsibility
CardWidget Public Scene2D face bound to one ResolvedCard; selects authored or generic rendering.
CardWidgetAssets Supplies managed artwork/frame drawables and maps rarity to layout.
CardFrameLayout Defines each frame's placeholder bounds, maximum text scales and artwork layer order.
FramedCardFace Positions artwork and labels inside the selected frame.
CardTextFitter Fits each label to its bounds without permanently changing the shared font.

The last three classes are package-private implementation details. Screens should construct CardWidget, not their own frame-specific renderer.

FramedCardFace centres a uniformly scaled face using min(allocatedWidth / 225, allocatedHeight / 456). Long descriptions cannot enlarge the frame. Reward choices allocate the same 225 × 456 face bounds, regardless of rarity or description length.

Descriptions use one wrapped label with one fitted font scale. The final line therefore has the same glyph size as the preceding lines. Cost, name, type and target are separately centred and fitted. Shrinking prevents overflow, but readability still needs checking at actual hand/tile sizes.

CardTextFitter first measures at the layout's maximum scale, then performs 12 binary-search steps if the whole wrapped block does not fit both width and height. It restores shared BitmapFont scale in finally and applies the result to the label. This avoids changing the appearance of other cards that use the same skin font. Fitting has no minimum readable scale guarantee: unusually verbose content still requires editorial or layout review.

Current artwork windows are opaque black. Rendering artwork behind them would hide it. artworkAboveFrame controls the draw order:

  • Opaque window: frame → inset backdrop → artwork → labels.
  • Transparent window: backdrop → artwork → frame → labels.

The inset artwork bounds protect borders and ornaments. Missing artwork hides only the illustration; missing frame assets select a generic rarity-coloured face in CardWidget. Some screen integrations retain a legacy fallback.

Screen integration

View Presentation changes
CardLibraryDisplay.java Fixed-size discovered-card faces; Common → Uncommon → Rare, then name; no displayed artwork paths. Locked entries remain hidden.
BattleScreen.java Resolved faces embedded in existing click/drag actors, including independent drag previews.
RewardDisplay.java Equal-size base-preview faces inside selectable reward buttons.
ShopDisplay.java Shared base-preview faces when the required managed assets are available.
CardUpgradeDisplay.java Owned-card faces with upgraded preview text; eligible options refresh after confirmation.
DeckEditorComponent.java Exact-copy faces while retaining selection, scrolling, badges and existing preview behaviour.

The Library uses discovery-controlled entry data before creating a face. Sorting and rendering do not unlock cards. UncommonCardLibraryWidget is a compatibility wrapper; standard CardWidget supports all three frames.

CardWidget is presentation-only and touch-disabled. Outer buttons or Clickable actors retain click, drag, targeting, selection tint and hover/tooltip responsibilities. Clickable.setVisualContent(...) takes a supplier so a drag preview gets a fresh actor: Scene2D actors cannot belong to two parents. Owned interactions retain their instanceId; Library, Shop and reward faces use read-only previews.

Replace or add a frame

  1. Add the PNG under images/cards/, without baked-in gameplay text.
  2. Update the appropriate CardFrameLayout bounds for artwork, cost, name, type, description, target and upgrade marker.
  3. Register its path/layout in CardWidgetAssets; no new rarity-specific widget is needed.
  4. Set artworkAboveFrame according to the PNG's alpha, not its apparent colour.
  5. Extend layout tests and inspect the face in the Library, battle hand and rewards.

Bounds use logical bottom-left coordinates. For a placeholder measured from the top-left of the current 2× PNG, convert with x = pixelX / 2 and y = (912 - pixelTop - pixelHeight) / 2. Changing an existing frame requires geometry review as well as replacing its image.

Post-battle Card rewards

After victory, the player chooses one reward type: Gold, Item or Card. Gold and Item complete directly. Choosing Card commits that route and displays a fixed offer of up to three distinct eligible cards. Selecting one adds exactly one fresh base-level copy to the persistent deck. Opening the card selection itself adds nothing.

Defeat does not generate victory rewards. The optional owned-card upgrade interface is separate from reward acquisition, not a fourth reward type; its confirmation is independent of the Gold/Item/Card claim.

Rendered post-battle card selection

post_battle_reward_cards

This capture shows the Card route after it has been opened: three distinct candidates with matching face dimensions, despite different description lengths. Inner Focus, Purify and Poison Dagger are all Uncommon in this example, so they share the blue frame. Their type ribbons and target footers still differ. Selecting one candidate grants only that card, not all three.

APIs and eligibility

API Contract
RewardType.CARD Extends the existing Gold/Item reward types.
RewardOption.cards(selection) Immutable Card-only reward payload; rejects null.
CardRewardSelection(List<String>) Immutable ordered list of 1–3 distinct, nonblank card definition IDs.
CardRewardSelectionGenerator.generate() Samples without replacement; injectable randomness; returns empty when the pool is empty.
CardRewardSelectionGenerator.generate(count) Requests 1–3 candidates; rejects counts outside that range and returns fewer when the pool is smaller.
RewardService.generateRewardOptions(multiplier) Generates Gold, Item and, when possible, Card.
claimRunReward(run, option, selectedCardId) Validates the selected offer and eligibility, then adds a new base copy to the persistent deck.

Implementations: source/core/src/main/com/csse3200/game/rewards

Generation dependencies can be injected for a controlled test or another reward consumer:

RewardService rewards = new RewardService(
    new RewardGenerator(new Random(1)),
    cardService,
    acquisitionPool,
    new Random(2));
List<RewardOption> fixedOptions = rewards.generateRewardOptions();

The two random sources control existing Gold/Item generation and Card sampling separately. Seeded reproducibility is a test aid, not the production reward policy. Retain the returned options for the whole choice instead of generating them again on each click or redraw.

The pool is shared eligibility policy, not a shared draw pile. Shop, applicable Events and victory rewards use it independently. Acquisition does not remove a definition. The current policy includes all 26 production definitions; sampling is uniform, without rarity weighting, discovery requirements or exclusion of already-owned cards. A one- or two-card pool produces a smaller offer; an empty pool omits Card but retains Gold/Item. Policy changes require loading a new pool/service, not merely editing JSON during play.

Reward lifecycle

sequenceDiagram
    participant UI as RewardDisplay
    participant Rewards as RewardService
    participant Discovery as CardDiscoveryService
    participant Deck as Persistent PlayerDeck
    participant Save as Map autosave
    UI->>Rewards: Generate options once
    Rewards-->>UI: Gold, Item and fixed Card offer
    Note over UI,Discovery: Hidden offers do not mark cards seen
    UI->>UI: Choose Card and remove other reward choices
    UI->>UI: Build base-preview faces
    UI->>Discovery: Record displayed candidates as seen
    UI->>Rewards: Claim fixed offer with selected cardId
    Rewards->>Rewards: Validate membership and eligibility
    alt Selected card is valid
        Rewards->>Deck: Add one fresh base instance
        Rewards-->>UI: Claim succeeded
        UI->>UI: Emit rewardClaimed and return event
        Note over UI,Save: Existing screen flow returns to Map
        Save->>Deck: Flush pending autosave including acquired copy
    else Validation rejects the claim
        Rewards-->>UI: Throw validation exception
        UI->>UI: Keep fixed choices and allow retry
    end
Loading

All displayed card candidates become discovered, including those not selected. Generating hidden candidates does not reveal them; choosing Gold/Item does not reveal them.

RewardDisplay generates once and guards in-progress, repeated, successful and disposed callbacks. Completion is emitted only after a successful claim. Exceptions keep the screen open for retry. The pending Map autosave is flushed after returning, so the acquired copy is included and remains available in the next battle.

Boundary: the service validates/applies claims but has no claimed-offer ledger. Calling it twice directly can grant twice. Another UI must provide equivalent claim guards. Pending card selection is not persisted across quitting mid-reward. The existing entity-only claimReward(entity, option) path rejects Card rewards because they require persistent RunState.

For another reward consumer, generate once, keep the fixed offer, discover only displayed candidates, and guard completion around:

rewardService.claimRunReward(runState, selectedOption, chosenCardId);

This creates a new owned copy; it must not insert the synthetic preview instance.

Failure and completion contract

Condition Behaviour Integration requirement
Empty eligible pool No Card option; Gold/Item remain available. Do not construct an empty CardRewardSelection as a placeholder.
Missing run The display logs the condition and does not claim or complete. Supply the active persistent run, not a temporary battle deck.
Offered definition missing when opening Card Opening is rejected before removing the top-level choices. Use a catalogue consistent with the generated offer.
Selected ID outside the fixed offer or no longer eligible Service rejects before adding a card; the display re-enables selection. Do not emit completion on failure or silently choose a different card.
Rapid repeated or re-entrant clicks claimInProgress blocks another claim; successful completion sets claimed. New consumers need equivalent guards, not merely a disabled-looking button.
Callback after disposal disposed prevents claiming and dynamic actors are detached. Cancel/detach consumer-owned listeners and do not reuse the old display.

Successful completion emits rewardClaimed and the existing return event only after the service call succeeds. These are notifications, not requests to grant the reward again. The retry path is not a general transaction rollback: consumers must not assume that an arbitrary exception after an external mutation can safely be retried without inspecting the mutation boundary.

Design decisions and validation-driven revisions

Decision Reason and trade-off
One renderer with rarity-specific layouts Replaces separate frame-specific rendering while allowing different placeholder geometry. Each PNG change still needs bounds review.
Fixed card bounds and whole-label fitting Prevents long descriptions from enlarging cards and keeps all description lines equal in size. Small containers can still reduce readability.
Separate presentation and interaction Reuses the same face in read-only and interactive views without duplicating input behaviour. Containers retain input/lifecycle management.
Layout-controlled artwork layering Fixes artwork hidden by opaque frame windows. Incorrect inset bounds can cover ornaments.
Fixed immutable card offers Prevents rerolling on UI refresh and validates the eventual choice against the original offer. It is not durable claim idempotency.
Shared acquisition eligibility Keeps reward routes consistent without consuming a finite inventory. Weighted rarity generation would require additional policy.

Why the implementation changed

Observed problem or earlier approach Revision and verification Critical trade-off
A separate Uncommon Library renderer could not serve later Common/Rare frames consistently. Generalised authored layout selection behind one face; retained the old wrapper for compatibility. Cross-screen tests exercise every rarity. Geometry remains asset-specific; one renderer does not mean every PNG has identical placeholders.
Long descriptions made reward faces different heights and enlarged Library faces. Bounded face geometry, fixed reward allocations and text cells with suppressed minimum sizes. Mixed-rarity/long-description tests assert equal bounds. Shrinking solves overflow, not human readability; manual inspection remains necessary.
Opaque black frame windows covered illustrations drawn behind them. Made layering a layout property and restricted overlaid artwork to safe inset bounds. Real-pixel tests inspect those bounds. Transparent future frames need the opposite order; blindly copying a layout can hide art again.
Metadata changes risked putting target in the type ribbon, and separate line handling gave inconsistent description text. Kept type and target as separate labels and description as one paragraph. Slot and glyph tests verify the resulting contract. Longer target translations or names may still require revised bounds.
Reopening upgrade choices re-offered an already-upgraded owned copy. Rebuild eligibility from the live persistent deck after commit; verify repeated confirmation/reopening. A fixed snapshot cannot observe external mutations; live consumers must use the live-deck selection path.
Discovery and visual changes touched the same Library view. Preserve discovery-controlled entry gating, then render only revealed data. Locked/discovered and listener-cleanup tests cover the integration. Existing global progress can obscure locked-state testing; isolated fixtures are required.

These revisions respond to concrete layout and lifecycle failures. Reusing interaction containers instead of adding another hover/drag system keeps visual changes from redefining input behaviour. Future revisions should preserve these contracts and add tests for the observed failure, rather than only adding another screenshot.

Troubleshooting and extension boundaries

Symptom Check first Correct boundary
Black artwork window PNG alpha, artworkAboveFrame, loaded artwork path and safe inset. Frame/assets/layout, not card effects.
Generic face instead of authored rarity frame Frame registered, preloaded, then assets constructed. Resource owner and rarity registry.
One card grows beyond its neighbours Parent cell minimum sizes and allocated bounds. Container layout; do not change gameplay text to mask a sizing bug.
Another card's font changes unexpectedly Shared font state restored after measuring; avoid mutating the skin font globally. Label fitting/style isolation.
Wrong upgraded rules or wrong duplicate is played Resolve the owned copy and retain its exact ID in the interaction payload. Resolver and caller; never search for the first matching definition ID.
Every Library card is visible in development Previously persisted discovery/debug unlock state. Isolate discovery for testing; frame rendering must not reset progress.
Acquired card missing after reload Claim persistent deck before return; inspect pending Map autosave and saved instance data. Reward settlement/save integration, not the preview widget.

Test Plan

Scope and test environment

Verify dynamic card faces, all three rarity frames, existing screen integrations and the exclusive post-battle Card reward flow. The main risks are text-driven size changes, hidden artwork, incorrect resolved values, broken interaction payloads, duplicate reward claims and loss of an acquired copy after save/load. Foundational card-system tests remain regression dependencies rather than a second specification of the whole card system.

Use Java 21 and the Gradle wrapper from source/. Automated tests use JUnit 5 with the project's headless/mocked rendering support; real-asset tests also inspect managed textures. Use seeded randomness for repeatable reward offers and temporary save directories for persistence tests. Discovery checks need isolated progress: previously discovered cards remain seen across restarts and recompilation. Do not clear normal player progress to test locked entries.

For manual checks, record the commit, window resolution and UI scale. Include Common, Uncommon and Rare cards, short and long names/descriptions, all configured types/targets, and a base/upgraded pair such as Strike and Strike+. Inspect both the normal window and a smaller supported window; Library-sized readability alone does not establish hand-sized readability.

Automated test cases

Area and expected assertions Existing test suites
Bind and rebind resolved cost, name, type, target, description, artwork and upgrade marker. A rarity change selects the new frame without leaving stale actors. CardWidgetTest, CommonCardWidgetTest, UncommonCardLibraryWidgetTest.
Maintain the logical 225 × 456 footprint and frame aspect ratio at different allocations. Long text stays within its placeholders; the last description line has the same glyph scale as preceding lines. Rebinding shorter text restores its appropriate scale. CommonCardWidgetTest, FramedCardFaceTest.
Load the correct managed frame for every rarity, preserve artwork aspect ratio and nearest-neighbour filtering, and draw artwork above opaque windows without covering ornaments. Missing textures retain safe presentation fallbacks. CommonCardWidgetTest, RepresentativeStrikeCardWidgetIntegrationTest.
Keep input outside the face. Hand and drag-preview actors are independent; playing an upgraded copy retains its exact instanceId. Shop, upgrade and deck-editor adoption preserve their surrounding interactions. CardWidgetClickableIntegrationTest, CommonCardViewIntegrationTest, RepresentativeStrikeCardWidgetIntegrationTest.
Keep undiscovered information hidden; update a face after discovery; sort Common → Uncommon → Rare, then name; omit artwork paths and clean up discovery listeners on disposal. CardLibraryDisplayTest, CardLibraryScreenTest.
Reject invalid eligibility/payloads, preserve immutable selections, and generate distinct eligible candidates reproducibly. Pools of one or two produce smaller offers; an empty pool leaves Gold/Item available. CardAcquisitionPoolTest, CardRewardSelectionTest, CardRewardSelectionGeneratorTest, RewardOptionTest.
Generate once, reveal/discover only displayed candidates, allocate equal face bounds across rarities, and commit only one reward route. Repeated input, repeated creation and stale disposed callbacks cannot grant another reward. Failed claims do not complete the screen and can be retried. RewardDisplayTest, RewardServiceTest.
Add one fresh level-0 instance, including when that definition is already owned. Preserve its ID through JSON restore and into the next battle deck. Flush pending autosave after the claim so the saved deck includes it. RewardServiceTest, PostBattleCardRewardPersistenceTest, AutosaveCoordinatorTest.
Refresh upgrade eligibility after confirmation so an already-upgraded copy is not offered again. CardUpgradeDisplayTest.

Test sources are under core/src/test. These suites are executable evidence of specific contracts, not a claim that every manual scenario below has passed.

Manual gameplay and visual acceptance

Use controlled fixtures or a development save to obtain the required cards and offers. Tests that force a particular offer must not change production eligibility or randomness.

Procedure Expected result
Inspect discovered cards of every rarity in the Library, including long text. Compare with the frame gallery above. Correct frame/artwork; centred cost/name/type/target; readable rules with uniform line size; no overlapping ornaments, labels or displayed asset paths.
Start with isolated discovery state, inspect a locked entry, then display that card in an acquisition view and return to the Library. Locked entry exposes no real name/artwork/rules/lore. Once discovered, the correct face appears in rarity/name order.
Inspect all three rarities in a real battle hand. Drag a single-target card to valid and invalid targets; exercise Self and All Enemies cards through their existing interaction paths. Artwork is visible at hand size; preview does not remove/re-parent the original face; valid plays use the selected copy and invalid input preserves existing rejection behaviour.
Inspect Strike and Strike+ together, then play the upgraded copy. Shared artwork but distinct resolved name/rules and upgrade marker; the upgraded effect executes and only that instance moves out of the hand.
Inspect Shop offers, upgrade choices and the deck editor; scroll, select/rearrange and reopen each view. Confirm an upgrade and reopen its choices. Faces retain stable bounds; prices, badges, tints and controls still work. Upgraded copies are removed from eligible upgrade choices.
Win separate battles and choose Gold, Item and Card in turn. Also finish a defeat. Each victory completes only its chosen reward route. Choosing Card removes Gold/Item choices; defeat does not generate victory rewards.
Open the Card route with a controlled mixed-rarity offer containing long descriptions. Inspect before choosing. Up to three distinct eligible candidates with equal frame sizes; all visible candidates become seen; opening adds no owned card and does not reroll the offer.
Select one candidate and attempt rapid repeated selection. Compare the persistent deck before/after, including a reward matching an already-owned definition. Exactly one new base copy with a fresh instanceId; unchosen cards are not acquired and existing owned copies are unchanged.
Claim a card, return to the Map, reload the resulting save and enter another battle. Autosave includes the reward; restored ID, definition and upgrade level match the acquired copy; the next battle deck contains it.
Repeat Library, hand and reward inspection after resizing to a smaller supported window. Faces keep their proportions and controls remain usable; text does not overflow. Unreadably small fitted text is a visual failure even if layout tests pass.

Missing-resource and claim-failure conditions are exercised with controlled automated fixtures; do not delete production assets or damage real saves to simulate them. Quitting midway through an unclaimed reward is outside the current persistence guarantee, not a successful recovery case.

JaCoCo Test Report

Class Lines covered/total Branches covered/total
CardWidget 160/162 36/40
FramedCardFace 96/99 18/22
CardTextFitter 25/26 15/18
RewardDisplay 134/146 54/82
RewardService 47/56 21/36
CardRewardSelectionGenerator 13/15 6/6
CardAcquisitionPool 24/24 14/16
⚠️ **GitHub.com Fallback** ⚠️