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:
parent
c430a43e19
commit
42e0116aec
@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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<boolean>(() =>
|
||||
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,
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Loading…
Reference in New Issue
Block a user