Add conditional energy deck zone based on RulesConfig

Extends RulesConfig support in the frontend game board to conditionally
render energy deck zones based on deck.energy_deck_enabled setting.
When disabled (classic mode), energy zones are null and omitted from
the layout, saving screen space.

Changes:
- Add energyDeckEnabled option to LayoutOptions interface
- Update landscape/portrait layouts to conditionally generate energy zones
- Make myEnergyZone/oppEnergyZone nullable in BoardLayout type
- Update StateRenderer to conditionally create and update energy zones
- Add energyDeckEnabled computed property to game store
- Add 7 tests for conditional energy deck rendering

https://claude.ai/code/session_01AAxKmpq2AGde327eX1nzUC
This commit is contained in:
Claude 2026-02-02 09:41:25 +00:00
parent c430a43e19
commit 42e0116aec
No known key found for this signature in database
5 changed files with 230 additions and 62 deletions

View File

@ -822,3 +822,137 @@ describe('calculateLayout - bench size options', () => {
expect(layout.oppPrizes).toHaveLength(4) expect(layout.oppPrizes).toHaveLength(4)
}) })
}) })
// =============================================================================
// Conditional Energy Deck Zone Tests (RulesConfig support)
// =============================================================================
describe('calculateLayout - energy deck options', () => {
it('returns energy zones when energyDeckEnabled is true (default)', () => {
/**
* Test that energy deck zones are generated by default.
*
* The Pokemon Pocket-style energy deck zone should be included
* when the energy deck feature is enabled (the default behavior).
*/
const layout = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT)
expect(layout.myEnergyZone).not.toBeNull()
expect(layout.oppEnergyZone).not.toBeNull()
})
it('returns null energy zones when energyDeckEnabled is false', () => {
/**
* Test that energy zones are omitted in classic mode.
*
* When using classic Pokemon TCG rules without an energy deck,
* the energy zones should be null to save screen space.
*/
const layout = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT, {
energyDeckEnabled: false,
})
expect(layout.myEnergyZone).toBeNull()
expect(layout.oppEnergyZone).toBeNull()
})
it('energy zones have valid positions when enabled', () => {
/**
* Test that generated energy zones have valid positions.
*
* Each energy zone should have positive dimensions and valid coordinates.
*/
const layout = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT, {
energyDeckEnabled: true,
})
expect(layout.myEnergyZone).not.toBeNull()
expect(layout.oppEnergyZone).not.toBeNull()
if (layout.myEnergyZone) {
expect(isValidZone(layout.myEnergyZone), 'Player energy zone should be valid').toBe(true)
}
if (layout.oppEnergyZone) {
expect(isValidZone(layout.oppEnergyZone), 'Opponent energy zone should be valid').toBe(true)
}
})
it('works correctly in portrait mode with energyDeckEnabled false', () => {
/**
* Test that portrait mode also respects energy deck settings.
*
* The portrait layout should also omit energy zones when using
* classic mode without an energy deck.
*/
const layout = calculateLayout(PORTRAIT_WIDTH, PORTRAIT_HEIGHT, {
energyDeckEnabled: false,
})
expect(layout.myEnergyZone).toBeNull()
expect(layout.oppEnergyZone).toBeNull()
})
it('works correctly in portrait mode with energyDeckEnabled true', () => {
/**
* Test that portrait mode generates energy zones when enabled.
*
* The portrait layout should include energy zones when the
* energy deck feature is enabled.
*/
const layout = calculateLayout(PORTRAIT_WIDTH, PORTRAIT_HEIGHT, {
energyDeckEnabled: true,
})
expect(layout.myEnergyZone).not.toBeNull()
expect(layout.oppEnergyZone).not.toBeNull()
})
it('can combine energyDeckEnabled with other options', () => {
/**
* Test that energy deck option works with other layout options.
*
* Custom rules might combine energy deck settings with bench size
* and prize options, and all should be applied correctly.
*/
const layout = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT, {
energyDeckEnabled: false,
benchSize: 3,
usePrizeCards: true,
prizeCount: 4,
})
expect(layout.myEnergyZone).toBeNull()
expect(layout.oppEnergyZone).toBeNull()
expect(layout.myBench).toHaveLength(3)
expect(layout.oppBench).toHaveLength(3)
expect(layout.myPrizes).toHaveLength(4)
expect(layout.oppPrizes).toHaveLength(4)
})
it('getAllZones excludes null energy zones', () => {
/**
* Test that getAllZones handles null energy zones correctly.
*
* When energy deck is disabled, getAllZones should not include
* energy zone entries in its output.
*/
const layoutWithEnergy = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT, {
energyDeckEnabled: true,
})
const layoutWithoutEnergy = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT, {
energyDeckEnabled: false,
})
const zonesWithEnergy = getAllZones(layoutWithEnergy)
const zonesWithoutEnergy = getAllZones(layoutWithoutEnergy)
// Should have 2 fewer zones when energy deck is disabled
expect(zonesWithoutEnergy.length).toBe(zonesWithEnergy.length - 2)
// Verify energy zone labels exist/don't exist
expect(zonesWithEnergy.find(z => z.label === 'myEnergyZone')).toBeDefined()
expect(zonesWithEnergy.find(z => z.label === 'oppEnergyZone')).toBeDefined()
expect(zonesWithoutEnergy.find(z => z.label === 'myEnergyZone')).toBeUndefined()
expect(zonesWithoutEnergy.find(z => z.label === 'oppEnergyZone')).toBeUndefined()
})
})

View File

@ -73,6 +73,8 @@ export interface LayoutOptions {
prizeCount?: number prizeCount?: number
/** Number of bench slots. Defaults to 5. */ /** Number of bench slots. Defaults to 5. */
benchSize?: number benchSize?: number
/** Whether to show energy deck zone (Pokemon Pocket style). Defaults to true. */
energyDeckEnabled?: boolean
} }
// ============================================================================= // =============================================================================
@ -165,6 +167,7 @@ function calculateLandscapeLayout(
const usePrizeCards = options?.usePrizeCards ?? true const usePrizeCards = options?.usePrizeCards ?? true
const prizeCount = options?.prizeCount ?? PRIZE_SIZE const prizeCount = options?.prizeCount ?? PRIZE_SIZE
const benchSize = options?.benchSize ?? BENCH_SIZE const benchSize = options?.benchSize ?? BENCH_SIZE
const energyDeckEnabled = options?.energyDeckEnabled ?? true
const scale = getScaleFactor(width, height) const scale = getScaleFactor(width, height)
// Scale card dimensions // Scale card dimensions
@ -256,13 +259,15 @@ function calculateLandscapeLayout(
) )
: [], : [],
myEnergyZone: { myEnergyZone: energyDeckEnabled
? {
x: centerX + energyOffsetX, x: centerX + energyOffsetX,
y: playerY, y: playerY,
width: cardW, width: cardW,
height: cardH, height: cardH,
rotation: 0, rotation: 0,
}, }
: null,
// Opponent zones (top of screen, mirrored) // Opponent zones (top of screen, mirrored)
oppActive: { oppActive: {
@ -318,13 +323,15 @@ function calculateLandscapeLayout(
) )
: [], : [],
oppEnergyZone: { oppEnergyZone: energyDeckEnabled
? {
x: centerX - energyOffsetX, x: centerX - energyOffsetX,
y: oppY, y: oppY,
width: cardW, width: cardW,
height: cardH, height: cardH,
rotation: Math.PI, rotation: Math.PI,
}, }
: null,
} }
return layout return layout
@ -352,6 +359,7 @@ function calculatePortraitLayout(
const usePrizeCards = options?.usePrizeCards ?? true const usePrizeCards = options?.usePrizeCards ?? true
const prizeCount = options?.prizeCount ?? PRIZE_SIZE const prizeCount = options?.prizeCount ?? PRIZE_SIZE
const benchSize = options?.benchSize ?? BENCH_SIZE const benchSize = options?.benchSize ?? BENCH_SIZE
const energyDeckEnabled = options?.energyDeckEnabled ?? true
const scale = getScaleFactor(width, height) * 0.85 // Slightly smaller for portrait const scale = getScaleFactor(width, height) * 0.85 // Slightly smaller for portrait
// Scale card dimensions - use smaller cards in portrait // Scale card dimensions - use smaller cards in portrait
@ -443,13 +451,15 @@ function calculatePortraitLayout(
) )
: [], : [],
myEnergyZone: { myEnergyZone: energyDeckEnabled
? {
x: centerX + energyOffsetX, x: centerX + energyOffsetX,
y: playerY, y: playerY,
width: cardW * 0.8, width: cardW * 0.8,
height: cardH * 0.8, height: cardH * 0.8,
rotation: 0, rotation: 0,
}, }
: null,
// Opponent zones (top, mirrored) // Opponent zones (top, mirrored)
oppActive: { oppActive: {
@ -504,13 +514,15 @@ function calculatePortraitLayout(
) )
: [], : [],
oppEnergyZone: { oppEnergyZone: energyDeckEnabled
? {
x: centerX - energyOffsetX, x: centerX - energyOffsetX,
y: oppY, y: oppY,
width: cardW * 0.8, width: cardW * 0.8,
height: cardH * 0.8, height: cardH * 0.8,
rotation: Math.PI, rotation: Math.PI,
}, }
: null,
} }
return layout return layout
@ -701,7 +713,9 @@ export function getAllZones(
zones.push({ label: 'myDeck', zone: layout.myDeck }) zones.push({ label: 'myDeck', zone: layout.myDeck })
zones.push({ label: 'myDiscard', zone: layout.myDiscard }) zones.push({ label: 'myDiscard', zone: layout.myDiscard })
layout.myPrizes.forEach((z, i) => zones.push({ label: `myPrizes[${i}]`, zone: z })) layout.myPrizes.forEach((z, i) => zones.push({ label: `myPrizes[${i}]`, zone: z }))
if (layout.myEnergyZone) {
zones.push({ label: 'myEnergyZone', zone: layout.myEnergyZone }) zones.push({ label: 'myEnergyZone', zone: layout.myEnergyZone })
}
// Opponent zones // Opponent zones
zones.push({ label: 'oppActive', zone: layout.oppActive }) zones.push({ label: 'oppActive', zone: layout.oppActive })
@ -710,7 +724,9 @@ export function getAllZones(
zones.push({ label: 'oppDeck', zone: layout.oppDeck }) zones.push({ label: 'oppDeck', zone: layout.oppDeck })
zones.push({ label: 'oppDiscard', zone: layout.oppDiscard }) zones.push({ label: 'oppDiscard', zone: layout.oppDiscard })
layout.oppPrizes.forEach((z, i) => zones.push({ label: `oppPrizes[${i}]`, zone: z })) layout.oppPrizes.forEach((z, i) => zones.push({ label: `oppPrizes[${i}]`, zone: z }))
if (layout.oppEnergyZone) {
zones.push({ label: 'oppEnergyZone', zone: layout.oppEnergyZone }) zones.push({ label: 'oppEnergyZone', zone: layout.oppEnergyZone })
}
return zones return zones
} }

View File

@ -52,7 +52,7 @@ interface PlayerZones {
deck: PileZone deck: PileZone
discard: PileZone discard: PileZone
prizes: PrizeZone | null // null when using points system instead of prize cards prizes: PrizeZone | null // null when using points system instead of prize cards
energyZone: PileZone energyZone: PileZone | null // null when energy deck is disabled (classic mode)
} }
/** /**
@ -133,6 +133,7 @@ export class StateRenderer {
usePrizeCards: state.rules_config?.prizes.use_prize_cards ?? false, usePrizeCards: state.rules_config?.prizes.use_prize_cards ?? false,
prizeCount: state.rules_config?.prizes.count ?? 4, prizeCount: state.rules_config?.prizes.count ?? 4,
benchSize: state.rules_config?.bench.max_size ?? 5, benchSize: state.rules_config?.bench.max_size ?? 5,
energyDeckEnabled: state.rules_config?.deck.energy_deck_enabled ?? true,
} }
// Calculate layout // Calculate layout
@ -265,16 +266,17 @@ export class StateRenderer {
return return
} }
// Check if we should create prize zones // Check if we should create optional zones
const usePrizeCards = state.rules_config?.prizes.use_prize_cards ?? false const usePrizeCards = state.rules_config?.prizes.use_prize_cards ?? false
const energyDeckEnabled = state.rules_config?.deck.energy_deck_enabled ?? true
// Create main container // Create main container
this.container = this.scene.add.container(0, 0) this.container = this.scene.add.container(0, 0)
// Create player zones // Create player zones
this.zones = { this.zones = {
my: this.createPlayerZones(myState.player_id, false, usePrizeCards), my: this.createPlayerZones(myState.player_id, false, usePrizeCards, energyDeckEnabled),
opp: this.createPlayerZones(oppState.player_id, true, usePrizeCards), opp: this.createPlayerZones(oppState.player_id, true, usePrizeCards, energyDeckEnabled),
} }
} }
@ -284,12 +286,14 @@ export class StateRenderer {
* @param playerId - The player's ID * @param playerId - The player's ID
* @param isOpponent - Whether this is the opponent (affects layout orientation) * @param isOpponent - Whether this is the opponent (affects layout orientation)
* @param usePrizeCards - Whether to create prize zones (false for points system) * @param usePrizeCards - Whether to create prize zones (false for points system)
* @param energyDeckEnabled - Whether to create energy deck zone (false for classic mode)
* @returns PlayerZones object * @returns PlayerZones object
*/ */
private createPlayerZones( private createPlayerZones(
playerId: string, playerId: string,
isOpponent: boolean, isOpponent: boolean,
usePrizeCards: boolean usePrizeCards: boolean,
energyDeckEnabled: boolean
): PlayerZones { ): PlayerZones {
// Create zones at origin - positions updated in updateZonePositions() // Create zones at origin - positions updated in updateZonePositions()
const zones: PlayerZones = { const zones: PlayerZones = {
@ -299,7 +303,7 @@ export class StateRenderer {
deck: new PileZone(this.scene, 0, 0, playerId, 'deck'), deck: new PileZone(this.scene, 0, 0, playerId, 'deck'),
discard: new PileZone(this.scene, 0, 0, playerId, 'discard'), discard: new PileZone(this.scene, 0, 0, playerId, 'discard'),
prizes: usePrizeCards ? new PrizeZone(this.scene, 0, 0, playerId) : null, prizes: usePrizeCards ? new PrizeZone(this.scene, 0, 0, playerId) : null,
energyZone: new PileZone(this.scene, 0, 0, playerId, 'deck'), // Energy deck uses pile zone energyZone: energyDeckEnabled ? new PileZone(this.scene, 0, 0, playerId, 'deck') : null,
} }
// Add all zones to container // Add all zones to container
@ -312,8 +316,10 @@ export class StateRenderer {
if (zones.prizes) { if (zones.prizes) {
this.container.add(zones.prizes) this.container.add(zones.prizes)
} }
if (zones.energyZone) {
this.container.add(zones.energyZone) this.container.add(zones.energyZone)
} }
}
return zones return zones
} }
@ -330,7 +336,7 @@ export class StateRenderer {
zones.deck.destroy() zones.deck.destroy()
zones.discard.destroy() zones.discard.destroy()
zones.prizes?.destroy() zones.prizes?.destroy()
zones.energyZone.destroy() zones.energyZone?.destroy()
} }
// =========================================================================== // ===========================================================================
@ -348,7 +354,9 @@ export class StateRenderer {
this.zones.my.hand.applyLayout(this.layout.myHand) this.zones.my.hand.applyLayout(this.layout.myHand)
this.zones.my.deck.applyLayout(this.layout.myDeck) this.zones.my.deck.applyLayout(this.layout.myDeck)
this.zones.my.discard.applyLayout(this.layout.myDiscard) this.zones.my.discard.applyLayout(this.layout.myDiscard)
if (this.zones.my.energyZone && this.layout.myEnergyZone) {
this.zones.my.energyZone.applyLayout(this.layout.myEnergyZone) this.zones.my.energyZone.applyLayout(this.layout.myEnergyZone)
}
// Bench zone - use the center position and full width // Bench zone - use the center position and full width
const benchLayout = this.layout.myBench const benchLayout = this.layout.myBench
@ -379,7 +387,9 @@ export class StateRenderer {
this.zones.opp.hand.applyLayout(this.layout.oppHand) this.zones.opp.hand.applyLayout(this.layout.oppHand)
this.zones.opp.deck.applyLayout(this.layout.oppDeck) this.zones.opp.deck.applyLayout(this.layout.oppDeck)
this.zones.opp.discard.applyLayout(this.layout.oppDiscard) this.zones.opp.discard.applyLayout(this.layout.oppDiscard)
if (this.zones.opp.energyZone && this.layout.oppEnergyZone) {
this.zones.opp.energyZone.applyLayout(this.layout.oppEnergyZone) this.zones.opp.energyZone.applyLayout(this.layout.oppEnergyZone)
}
// Opponent bench // Opponent bench
const oppBenchLayout = this.layout.oppBench const oppBenchLayout = this.layout.oppBench
@ -471,7 +481,8 @@ export class StateRenderer {
// Opponent just sees count // Opponent just sees count
zones.prizes?.setRemainingCount(playerState.prizes_count) zones.prizes?.setRemainingCount(playerState.prizes_count)
// Update energy zone // Update energy zone (only if energy deck is enabled)
if (zones.energyZone) {
this.updateZone( this.updateZone(
zones.energyZone, zones.energyZone,
playerState.energy_zone.cards, playerState.energy_zone.cards,
@ -481,6 +492,7 @@ export class StateRenderer {
true // always visible true // always visible
) )
} }
}
/** /**
* Update a single zone with cards. * Update a single zone with cards.

View File

@ -241,6 +241,11 @@ export const useGameStore = defineStore('game', () => {
rulesConfig.value?.bench.max_size ?? 5 rulesConfig.value?.bench.max_size ?? 5
) )
/** Whether this game uses an energy deck zone (Pokemon Pocket style) */
const energyDeckEnabled = computed<boolean>(() =>
rulesConfig.value?.deck.energy_deck_enabled ?? true
)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Computed - Card Lookup // Computed - Card Lookup
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -367,6 +372,7 @@ export const useGameStore = defineStore('game', () => {
usePrizeCards, usePrizeCards,
prizeCount, prizeCount,
benchSize, benchSize,
energyDeckEnabled,
// Card lookup // Card lookup
lookupCard, lookupCard,

View File

@ -174,21 +174,21 @@ export interface ZonePosition {
export interface BoardLayout { export interface BoardLayout {
// My zones (bottom of screen) // My zones (bottom of screen)
myActive: ZonePosition myActive: ZonePosition
myBench: ZonePosition[] // 5 slots myBench: ZonePosition[] // configurable slots (default 5)
myHand: ZonePosition myHand: ZonePosition
myDeck: ZonePosition myDeck: ZonePosition
myDiscard: ZonePosition myDiscard: ZonePosition
myPrizes: ZonePosition[] // 6 slots myPrizes: ZonePosition[] // configurable slots (default 6), empty when using points
myEnergyZone: ZonePosition myEnergyZone: ZonePosition | null // null when energy deck is disabled
// Opponent zones (top of screen, mirrored) // Opponent zones (top of screen, mirrored)
oppActive: ZonePosition oppActive: ZonePosition
oppBench: ZonePosition[] // 5 slots oppBench: ZonePosition[] // configurable slots (default 5)
oppHand: ZonePosition oppHand: ZonePosition
oppDeck: ZonePosition oppDeck: ZonePosition
oppDiscard: ZonePosition oppDiscard: ZonePosition
oppPrizes: ZonePosition[] // 6 slots oppPrizes: ZonePosition[] // configurable slots (default 6), empty when using points
oppEnergyZone: ZonePosition oppEnergyZone: ZonePosition | null // null when energy deck is disabled
} }
/** /**