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.
- Architecture and integration contracts
- Card faces and frame assets
- Post-battle Card rewards
- Design decisions and validation-driven revisions
- Troubleshooting and extension boundaries
- Test Plan
- Known limitations
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
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.
| 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 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.
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 |
|---|---|---|
![]() |
![]() |
![]() |
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 |
|---|---|---|
![]() |
![]() |
![]() |
| 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. |
| 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.
| 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.
- Add the PNG under
images/cards/, without baked-in gameplay text. - Update the appropriate
CardFrameLayoutbounds for artwork, cost, name, type, description, target and upgrade marker. - Register its path/layout in
CardWidgetAssets; no new rarity-specific widget is needed. - Set
artworkAboveFrameaccording to the PNG's alpha, not its apparent colour. - 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.
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.
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.
| 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.
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
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.
| 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.
| 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. |
| 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.
| 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. |
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.
| 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.
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.
| 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 |





