From 42e0116aecf74556c36d55a7bcf207eb498a6bf9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Feb 2026 09:41:25 +0000 Subject: [PATCH] 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 --- frontend/src/game/layout.spec.ts | 134 ++++++++++++++++++++++++ frontend/src/game/layout.ts | 90 +++++++++------- frontend/src/game/sync/StateRenderer.ts | 50 +++++---- frontend/src/stores/game.ts | 6 ++ frontend/src/types/phaser.ts | 12 +-- 5 files changed, 230 insertions(+), 62 deletions(-) diff --git a/frontend/src/game/layout.spec.ts b/frontend/src/game/layout.spec.ts index 1338c34..5af82a1 100644 --- a/frontend/src/game/layout.spec.ts +++ b/frontend/src/game/layout.spec.ts @@ -822,3 +822,137 @@ describe('calculateLayout - bench size options', () => { 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() + }) +}) diff --git a/frontend/src/game/layout.ts b/frontend/src/game/layout.ts index 82905c3..49bc869 100644 --- a/frontend/src/game/layout.ts +++ b/frontend/src/game/layout.ts @@ -73,6 +73,8 @@ export interface LayoutOptions { prizeCount?: number /** Number of bench slots. Defaults to 5. */ 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 prizeCount = options?.prizeCount ?? PRIZE_SIZE const benchSize = options?.benchSize ?? BENCH_SIZE + const energyDeckEnabled = options?.energyDeckEnabled ?? true const scale = getScaleFactor(width, height) // Scale card dimensions @@ -256,14 +259,16 @@ function calculateLandscapeLayout( ) : [], - myEnergyZone: { - x: centerX + energyOffsetX, - y: playerY, - width: cardW, - height: cardH, - rotation: 0, - }, - + myEnergyZone: energyDeckEnabled + ? { + x: centerX + energyOffsetX, + y: playerY, + width: cardW, + height: cardH, + rotation: 0, + } + : null, + // Opponent zones (top of screen, mirrored) oppActive: { x: centerX, @@ -318,15 +323,17 @@ function calculateLandscapeLayout( ) : [], - oppEnergyZone: { - x: centerX - energyOffsetX, - y: oppY, - width: cardW, - height: cardH, - rotation: Math.PI, - }, + oppEnergyZone: energyDeckEnabled + ? { + x: centerX - energyOffsetX, + y: oppY, + width: cardW, + height: cardH, + rotation: Math.PI, + } + : null, } - + return layout } @@ -352,6 +359,7 @@ function calculatePortraitLayout( const usePrizeCards = options?.usePrizeCards ?? true const prizeCount = options?.prizeCount ?? PRIZE_SIZE const benchSize = options?.benchSize ?? BENCH_SIZE + const energyDeckEnabled = options?.energyDeckEnabled ?? true const scale = getScaleFactor(width, height) * 0.85 // Slightly smaller for portrait // Scale card dimensions - use smaller cards in portrait @@ -443,14 +451,16 @@ function calculatePortraitLayout( ) : [], - myEnergyZone: { - x: centerX + energyOffsetX, - y: playerY, - width: cardW * 0.8, - height: cardH * 0.8, - rotation: 0, - }, - + myEnergyZone: energyDeckEnabled + ? { + x: centerX + energyOffsetX, + y: playerY, + width: cardW * 0.8, + height: cardH * 0.8, + rotation: 0, + } + : null, + // Opponent zones (top, mirrored) oppActive: { x: centerX, @@ -504,15 +514,17 @@ function calculatePortraitLayout( ) : [], - oppEnergyZone: { - x: centerX - energyOffsetX, - y: oppY, - width: cardW * 0.8, - height: cardH * 0.8, - rotation: Math.PI, - }, + oppEnergyZone: energyDeckEnabled + ? { + x: centerX - energyOffsetX, + y: oppY, + width: cardW * 0.8, + height: cardH * 0.8, + rotation: Math.PI, + } + : null, } - + return layout } @@ -693,7 +705,7 @@ export function getAllZones( layout: BoardLayout ): Array<{ label: string; zone: ZonePosition }> { const zones: Array<{ label: string; zone: ZonePosition }> = [] - + // My zones zones.push({ label: 'myActive', zone: layout.myActive }) layout.myBench.forEach((z, i) => zones.push({ label: `myBench[${i}]`, zone: z })) @@ -701,8 +713,10 @@ export function getAllZones( zones.push({ label: 'myDeck', zone: layout.myDeck }) zones.push({ label: 'myDiscard', zone: layout.myDiscard }) layout.myPrizes.forEach((z, i) => zones.push({ label: `myPrizes[${i}]`, zone: z })) - zones.push({ label: 'myEnergyZone', zone: layout.myEnergyZone }) - + if (layout.myEnergyZone) { + zones.push({ label: 'myEnergyZone', zone: layout.myEnergyZone }) + } + // Opponent zones zones.push({ label: 'oppActive', zone: layout.oppActive }) layout.oppBench.forEach((z, i) => zones.push({ label: `oppBench[${i}]`, zone: z })) @@ -710,7 +724,9 @@ export function getAllZones( zones.push({ label: 'oppDeck', zone: layout.oppDeck }) zones.push({ label: 'oppDiscard', zone: layout.oppDiscard }) layout.oppPrizes.forEach((z, i) => zones.push({ label: `oppPrizes[${i}]`, zone: z })) - zones.push({ label: 'oppEnergyZone', zone: layout.oppEnergyZone }) - + if (layout.oppEnergyZone) { + zones.push({ label: 'oppEnergyZone', zone: layout.oppEnergyZone }) + } + return zones } diff --git a/frontend/src/game/sync/StateRenderer.ts b/frontend/src/game/sync/StateRenderer.ts index 61d1221..edc4d88 100644 --- a/frontend/src/game/sync/StateRenderer.ts +++ b/frontend/src/game/sync/StateRenderer.ts @@ -52,7 +52,7 @@ interface PlayerZones { deck: PileZone discard: PileZone 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, prizeCount: state.rules_config?.prizes.count ?? 4, benchSize: state.rules_config?.bench.max_size ?? 5, + energyDeckEnabled: state.rules_config?.deck.energy_deck_enabled ?? true, } // Calculate layout @@ -265,16 +266,17 @@ export class StateRenderer { 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 energyDeckEnabled = state.rules_config?.deck.energy_deck_enabled ?? true // Create main container this.container = this.scene.add.container(0, 0) // Create player zones this.zones = { - my: this.createPlayerZones(myState.player_id, false, usePrizeCards), - opp: this.createPlayerZones(oppState.player_id, true, usePrizeCards), + my: this.createPlayerZones(myState.player_id, false, usePrizeCards, energyDeckEnabled), + opp: this.createPlayerZones(oppState.player_id, true, usePrizeCards, energyDeckEnabled), } } @@ -284,12 +286,14 @@ export class StateRenderer { * @param playerId - The player's ID * @param isOpponent - Whether this is the opponent (affects layout orientation) * @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 */ private createPlayerZones( playerId: string, isOpponent: boolean, - usePrizeCards: boolean + usePrizeCards: boolean, + energyDeckEnabled: boolean ): PlayerZones { // Create zones at origin - positions updated in updateZonePositions() const zones: PlayerZones = { @@ -299,7 +303,7 @@ export class StateRenderer { deck: new PileZone(this.scene, 0, 0, playerId, 'deck'), discard: new PileZone(this.scene, 0, 0, playerId, 'discard'), 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 @@ -312,7 +316,9 @@ export class StateRenderer { if (zones.prizes) { this.container.add(zones.prizes) } - this.container.add(zones.energyZone) + if (zones.energyZone) { + this.container.add(zones.energyZone) + } } return zones @@ -330,7 +336,7 @@ export class StateRenderer { zones.deck.destroy() zones.discard.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.deck.applyLayout(this.layout.myDeck) this.zones.my.discard.applyLayout(this.layout.myDiscard) - this.zones.my.energyZone.applyLayout(this.layout.myEnergyZone) + if (this.zones.my.energyZone && this.layout.myEnergyZone) { + this.zones.my.energyZone.applyLayout(this.layout.myEnergyZone) + } // Bench zone - use the center position and full width const benchLayout = this.layout.myBench @@ -379,7 +387,9 @@ export class StateRenderer { this.zones.opp.hand.applyLayout(this.layout.oppHand) this.zones.opp.deck.applyLayout(this.layout.oppDeck) this.zones.opp.discard.applyLayout(this.layout.oppDiscard) - this.zones.opp.energyZone.applyLayout(this.layout.oppEnergyZone) + if (this.zones.opp.energyZone && this.layout.oppEnergyZone) { + this.zones.opp.energyZone.applyLayout(this.layout.oppEnergyZone) + } // Opponent bench const oppBenchLayout = this.layout.oppBench @@ -471,15 +481,17 @@ export class StateRenderer { // Opponent just sees count zones.prizes?.setRemainingCount(playerState.prizes_count) - // Update energy zone - this.updateZone( - zones.energyZone, - playerState.energy_zone.cards, - registry, - playerId, - 'energy_zone', - true // always visible - ) + // Update energy zone (only if energy deck is enabled) + if (zones.energyZone) { + this.updateZone( + zones.energyZone, + playerState.energy_zone.cards, + registry, + playerId, + 'energy_zone', + true // always visible + ) + } } /** diff --git a/frontend/src/stores/game.ts b/frontend/src/stores/game.ts index 6c4afdc..e7e96d0 100644 --- a/frontend/src/stores/game.ts +++ b/frontend/src/stores/game.ts @@ -241,6 +241,11 @@ export const useGameStore = defineStore('game', () => { rulesConfig.value?.bench.max_size ?? 5 ) + /** Whether this game uses an energy deck zone (Pokemon Pocket style) */ + const energyDeckEnabled = computed(() => + rulesConfig.value?.deck.energy_deck_enabled ?? true + ) + // --------------------------------------------------------------------------- // Computed - Card Lookup // --------------------------------------------------------------------------- @@ -367,6 +372,7 @@ export const useGameStore = defineStore('game', () => { usePrizeCards, prizeCount, benchSize, + energyDeckEnabled, // Card lookup lookupCard, diff --git a/frontend/src/types/phaser.ts b/frontend/src/types/phaser.ts index 722eb6a..5778052 100644 --- a/frontend/src/types/phaser.ts +++ b/frontend/src/types/phaser.ts @@ -174,21 +174,21 @@ export interface ZonePosition { export interface BoardLayout { // My zones (bottom of screen) myActive: ZonePosition - myBench: ZonePosition[] // 5 slots + myBench: ZonePosition[] // configurable slots (default 5) myHand: ZonePosition myDeck: ZonePosition myDiscard: ZonePosition - myPrizes: ZonePosition[] // 6 slots - myEnergyZone: ZonePosition + myPrizes: ZonePosition[] // configurable slots (default 6), empty when using points + myEnergyZone: ZonePosition | null // null when energy deck is disabled // Opponent zones (top of screen, mirrored) oppActive: ZonePosition - oppBench: ZonePosition[] // 5 slots + oppBench: ZonePosition[] // configurable slots (default 5) oppHand: ZonePosition oppDeck: ZonePosition oppDiscard: ZonePosition - oppPrizes: ZonePosition[] // 6 slots - oppEnergyZone: ZonePosition + oppPrizes: ZonePosition[] // configurable slots (default 6), empty when using points + oppEnergyZone: ZonePosition | null // null when energy deck is disabled } /**