Merge pull request #1 from calcorum/claude/honor-rules-config-3ZRDG
Honor RulesConfig for prize cards vs points in frontend game board
This commit is contained in:
commit
8a416c8ace
@ -37,6 +37,7 @@ from typing import TYPE_CHECKING
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.core.config import RulesConfig
|
||||
from app.core.enums import GameEndReason, TurnPhase
|
||||
from app.core.models.card import CardDefinition, CardInstance
|
||||
|
||||
@ -124,6 +125,7 @@ class VisibleGameState(BaseModel):
|
||||
stadium_owner_id: Player who played the current stadium (public).
|
||||
forced_action: Current forced action, if any.
|
||||
card_registry: Card definitions (needed to display cards).
|
||||
rules_config: Rules configuration for UI rendering decisions (e.g., prize cards vs points).
|
||||
"""
|
||||
|
||||
game_id: str
|
||||
@ -154,6 +156,9 @@ class VisibleGameState(BaseModel):
|
||||
# Card definitions for display
|
||||
card_registry: dict[str, CardDefinition] = Field(default_factory=dict)
|
||||
|
||||
# Rules configuration for UI rendering decisions
|
||||
rules_config: RulesConfig = Field(default_factory=RulesConfig)
|
||||
|
||||
|
||||
def _create_visible_zone(
|
||||
cards: list[CardInstance],
|
||||
@ -298,6 +303,7 @@ def get_visible_state(game: GameState, viewer_id: str) -> VisibleGameState:
|
||||
forced_action_type=forced_action_type,
|
||||
forced_action_reason=forced_action_reason,
|
||||
card_registry=game.card_registry,
|
||||
rules_config=game.rules,
|
||||
)
|
||||
|
||||
|
||||
@ -393,4 +399,5 @@ def get_spectator_state(game: GameState) -> VisibleGameState:
|
||||
forced_action_type=forced_action_type,
|
||||
forced_action_reason=forced_action_reason,
|
||||
card_registry=game.card_registry,
|
||||
rules_config=game.rules,
|
||||
)
|
||||
|
||||
@ -582,3 +582,377 @@ describe('getAllZones', () => {
|
||||
expect(allZones.find(z => z.label === 'oppBench[0]')).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
// =============================================================================
|
||||
// Conditional Prize Zone Tests (RulesConfig support)
|
||||
// =============================================================================
|
||||
|
||||
describe('calculateLayout - prize zone options', () => {
|
||||
it('returns empty prize arrays when usePrizeCards is false', () => {
|
||||
/**
|
||||
* Test that prize zones are not generated in points-based mode.
|
||||
*
|
||||
* When using the Mantimon TCG points system instead of classic prize cards,
|
||||
* the prize zones should be omitted from the layout to save screen space.
|
||||
*/
|
||||
const layout = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT, {
|
||||
usePrizeCards: false,
|
||||
})
|
||||
|
||||
expect(layout.myPrizes).toHaveLength(0)
|
||||
expect(layout.oppPrizes).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('returns correct prize count when usePrizeCards is true with custom count', () => {
|
||||
/**
|
||||
* Test that prize zones can have a custom count.
|
||||
*
|
||||
* Different rule variants may use 4, 6, or other prize counts.
|
||||
* The layout should generate exactly the requested number of slots.
|
||||
*/
|
||||
const layout = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT, {
|
||||
usePrizeCards: true,
|
||||
prizeCount: 4,
|
||||
})
|
||||
|
||||
expect(layout.myPrizes).toHaveLength(4)
|
||||
expect(layout.oppPrizes).toHaveLength(4)
|
||||
})
|
||||
|
||||
it('defaults to 6 prizes when usePrizeCards is true without count', () => {
|
||||
/**
|
||||
* Test that classic mode defaults to 6 prize cards.
|
||||
*
|
||||
* When using prize cards without specifying a count, the standard
|
||||
* Pokemon TCG count of 6 should be used.
|
||||
*/
|
||||
const layout = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT, {
|
||||
usePrizeCards: true,
|
||||
})
|
||||
|
||||
expect(layout.myPrizes).toHaveLength(PRIZE_SIZE)
|
||||
expect(layout.oppPrizes).toHaveLength(PRIZE_SIZE)
|
||||
})
|
||||
|
||||
it('returns 6 prizes when no options provided (backwards compatibility)', () => {
|
||||
/**
|
||||
* Test backwards compatibility with existing code.
|
||||
*
|
||||
* When called without options, the layout should behave as before,
|
||||
* generating 6 prize slots for each player.
|
||||
*/
|
||||
const layout = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT)
|
||||
|
||||
// Default behavior assumes classic prize card mode with 6 prizes
|
||||
expect(layout.myPrizes).toHaveLength(PRIZE_SIZE)
|
||||
expect(layout.oppPrizes).toHaveLength(PRIZE_SIZE)
|
||||
})
|
||||
|
||||
it('works correctly in portrait mode with usePrizeCards false', () => {
|
||||
/**
|
||||
* Test that portrait mode also respects prize card settings.
|
||||
*
|
||||
* The portrait layout should also omit prize zones when using
|
||||
* the points system.
|
||||
*/
|
||||
const layout = calculateLayout(PORTRAIT_WIDTH, PORTRAIT_HEIGHT, {
|
||||
usePrizeCards: false,
|
||||
})
|
||||
|
||||
expect(layout.myPrizes).toHaveLength(0)
|
||||
expect(layout.oppPrizes).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('works correctly in portrait mode with custom prize count', () => {
|
||||
/**
|
||||
* Test that portrait mode generates correct prize count.
|
||||
*
|
||||
* The portrait layout should create the correct number of
|
||||
* prize positions when using prize cards with a custom count.
|
||||
*/
|
||||
const layout = calculateLayout(PORTRAIT_WIDTH, PORTRAIT_HEIGHT, {
|
||||
usePrizeCards: true,
|
||||
prizeCount: 4,
|
||||
})
|
||||
|
||||
expect(layout.myPrizes).toHaveLength(4)
|
||||
expect(layout.oppPrizes).toHaveLength(4)
|
||||
})
|
||||
|
||||
it('prize positions are valid when generated', () => {
|
||||
/**
|
||||
* Test that generated prize positions are all valid.
|
||||
*
|
||||
* Each prize slot should have positive dimensions and valid coordinates.
|
||||
*/
|
||||
const layout = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT, {
|
||||
usePrizeCards: true,
|
||||
prizeCount: 4,
|
||||
})
|
||||
|
||||
for (const prize of layout.myPrizes) {
|
||||
expect(isValidZone(prize), 'Player prize zone should be valid').toBe(true)
|
||||
}
|
||||
for (const prize of layout.oppPrizes) {
|
||||
expect(isValidZone(prize), 'Opponent prize zone should be valid').toBe(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// =============================================================================
|
||||
// Conditional Bench Size Tests (RulesConfig support)
|
||||
// =============================================================================
|
||||
|
||||
describe('calculateLayout - bench size options', () => {
|
||||
it('returns default 5 bench slots when no options provided', () => {
|
||||
/**
|
||||
* Test backwards compatibility with existing code.
|
||||
*
|
||||
* When called without options, the layout should generate 5 bench slots
|
||||
* for each player (standard Pokemon TCG bench size).
|
||||
*/
|
||||
const layout = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT)
|
||||
|
||||
expect(layout.myBench).toHaveLength(BENCH_SIZE)
|
||||
expect(layout.oppBench).toHaveLength(BENCH_SIZE)
|
||||
})
|
||||
|
||||
it('returns correct bench count with custom benchSize option', () => {
|
||||
/**
|
||||
* Test that bench zones can have a custom count.
|
||||
*
|
||||
* Some rule variants may use fewer bench slots (e.g., 3 for a faster game).
|
||||
* The layout should generate exactly the requested number of slots.
|
||||
*/
|
||||
const layout = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT, {
|
||||
benchSize: 3,
|
||||
})
|
||||
|
||||
expect(layout.myBench).toHaveLength(3)
|
||||
expect(layout.oppBench).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('works with larger bench sizes', () => {
|
||||
/**
|
||||
* Test that larger bench sizes are supported.
|
||||
*
|
||||
* Some custom variants might use more than 5 bench slots.
|
||||
*/
|
||||
const layout = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT, {
|
||||
benchSize: 8,
|
||||
})
|
||||
|
||||
expect(layout.myBench).toHaveLength(8)
|
||||
expect(layout.oppBench).toHaveLength(8)
|
||||
})
|
||||
|
||||
it('works correctly in portrait mode with custom bench size', () => {
|
||||
/**
|
||||
* Test that portrait mode also respects bench size settings.
|
||||
*
|
||||
* The portrait layout should also generate the correct number
|
||||
* of bench positions based on the benchSize option.
|
||||
*/
|
||||
const layout = calculateLayout(PORTRAIT_WIDTH, PORTRAIT_HEIGHT, {
|
||||
benchSize: 3,
|
||||
})
|
||||
|
||||
expect(layout.myBench).toHaveLength(3)
|
||||
expect(layout.oppBench).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('bench positions are valid when using custom size', () => {
|
||||
/**
|
||||
* Test that generated bench positions are all valid.
|
||||
*
|
||||
* Each bench slot should have positive dimensions and valid coordinates.
|
||||
*/
|
||||
const layout = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT, {
|
||||
benchSize: 4,
|
||||
})
|
||||
|
||||
for (const bench of layout.myBench) {
|
||||
expect(isValidZone(bench), 'Player bench zone should be valid').toBe(true)
|
||||
}
|
||||
for (const bench of layout.oppBench) {
|
||||
expect(isValidZone(bench), 'Opponent bench zone should be valid').toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
it('bench slots are centered properly with different sizes', () => {
|
||||
/**
|
||||
* Test that bench zones remain centered regardless of count.
|
||||
*
|
||||
* Whether using 3, 5, or 8 bench slots, they should all be
|
||||
* horizontally centered on the screen.
|
||||
*/
|
||||
const layout3 = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT, { benchSize: 3 })
|
||||
const layout5 = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT, { benchSize: 5 })
|
||||
const layout7 = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT, { benchSize: 7 })
|
||||
|
||||
const centerX = LANDSCAPE_WIDTH / 2
|
||||
|
||||
// Calculate average X position of each player's bench
|
||||
const avg3 = layout3.myBench.reduce((sum, b) => sum + b.x, 0) / 3
|
||||
const avg5 = layout5.myBench.reduce((sum, b) => sum + b.x, 0) / 5
|
||||
const avg7 = layout7.myBench.reduce((sum, b) => sum + b.x, 0) / 7
|
||||
|
||||
// All should be centered
|
||||
expect(avg3).toBeCloseTo(centerX, 0)
|
||||
expect(avg5).toBeCloseTo(centerX, 0)
|
||||
expect(avg7).toBeCloseTo(centerX, 0)
|
||||
})
|
||||
|
||||
it('can combine benchSize with prize options', () => {
|
||||
/**
|
||||
* Test that bench and prize options work together.
|
||||
*
|
||||
* Custom rules might have both custom bench size and prize count,
|
||||
* and both should be applied correctly.
|
||||
*/
|
||||
const layout = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT, {
|
||||
benchSize: 3,
|
||||
usePrizeCards: true,
|
||||
prizeCount: 4,
|
||||
})
|
||||
|
||||
expect(layout.myBench).toHaveLength(3)
|
||||
expect(layout.oppBench).toHaveLength(3)
|
||||
expect(layout.myPrizes).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()
|
||||
})
|
||||
})
|
||||
|
||||
@ -53,12 +53,30 @@ export const EDGE_PADDING = 16
|
||||
/** Number of bench slots */
|
||||
export const BENCH_SIZE = 5
|
||||
|
||||
/** Number of prize slots */
|
||||
/** Default number of prize slots (for classic Pokemon TCG mode) */
|
||||
export const PRIZE_SIZE = 6
|
||||
|
||||
/** Portrait mode threshold - below this aspect ratio we use portrait layout */
|
||||
export const PORTRAIT_THRESHOLD = 0.9
|
||||
|
||||
// =============================================================================
|
||||
// Layout Options
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Options for customizing board layout based on rules config.
|
||||
*/
|
||||
export interface LayoutOptions {
|
||||
/** Whether to use prize cards (classic mode). If false, no prize zones are rendered. */
|
||||
usePrizeCards?: boolean
|
||||
/** Number of prize cards when using classic mode. Defaults to 6. */
|
||||
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
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Orientation Detection
|
||||
// =============================================================================
|
||||
@ -103,19 +121,28 @@ export function getScaleFactor(width: number, height: number): number {
|
||||
*
|
||||
* @param width - Canvas width in pixels
|
||||
* @param height - Canvas height in pixels
|
||||
* @param options - Optional layout customization based on rules config
|
||||
* @returns Complete board layout with all zone positions
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const layout = calculateLayout(1920, 1080)
|
||||
* console.log(layout.myActive) // { x: 960, y: 700, width: 120, height: 168 }
|
||||
*
|
||||
* // For points-based mode (no prize cards)
|
||||
* const layoutNoCards = calculateLayout(1920, 1080, { usePrizeCards: false })
|
||||
* console.log(layoutNoCards.myPrizes) // [] (empty array)
|
||||
* ```
|
||||
*/
|
||||
export function calculateLayout(width: number, height: number): BoardLayout {
|
||||
export function calculateLayout(
|
||||
width: number,
|
||||
height: number,
|
||||
options?: LayoutOptions
|
||||
): BoardLayout {
|
||||
if (isPortrait(width, height)) {
|
||||
return calculatePortraitLayout(width, height)
|
||||
return calculatePortraitLayout(width, height, options)
|
||||
}
|
||||
return calculateLandscapeLayout(width, height)
|
||||
return calculateLandscapeLayout(width, height, options)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -129,9 +156,18 @@ export function calculateLayout(width: number, height: number): BoardLayout {
|
||||
*
|
||||
* @param width - Canvas width
|
||||
* @param height - Canvas height
|
||||
* @param options - Optional layout customization
|
||||
* @returns Board layout for landscape orientation
|
||||
*/
|
||||
function calculateLandscapeLayout(width: number, height: number): BoardLayout {
|
||||
function calculateLandscapeLayout(
|
||||
width: number,
|
||||
height: number,
|
||||
options?: LayoutOptions
|
||||
): BoardLayout {
|
||||
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
|
||||
@ -153,7 +189,7 @@ function calculateLandscapeLayout(width: number, height: number): BoardLayout {
|
||||
// Horizontal layout - center of screen
|
||||
const centerX = width / 2
|
||||
const benchSpacing = cardW + padding
|
||||
const totalBenchWidth = (BENCH_SIZE - 1) * benchSpacing
|
||||
const totalBenchWidth = (benchSize - 1) * benchSpacing
|
||||
|
||||
// Deck/discard positions (right side)
|
||||
const deckX = width - edgePad - cardW / 2 - padding
|
||||
@ -178,7 +214,7 @@ function calculateLandscapeLayout(width: number, height: number): BoardLayout {
|
||||
rotation: 0,
|
||||
},
|
||||
|
||||
myBench: Array.from({ length: BENCH_SIZE }, (_, i) => ({
|
||||
myBench: Array.from({ length: benchSize }, (_, i) => ({
|
||||
x: centerX - totalBenchWidth / 2 + i * benchSpacing,
|
||||
y: playerY + benchOffsetY,
|
||||
width: cardW,
|
||||
@ -210,23 +246,28 @@ function calculateLandscapeLayout(width: number, height: number): BoardLayout {
|
||||
rotation: 0,
|
||||
},
|
||||
|
||||
myPrizes: calculatePrizePositions(
|
||||
prizesStartX,
|
||||
playerY - cardH / 2,
|
||||
smallCardW,
|
||||
smallCardH,
|
||||
prizesColSpacing,
|
||||
prizesRowSpacing,
|
||||
false // Not flipped for player
|
||||
),
|
||||
myPrizes: usePrizeCards
|
||||
? calculatePrizePositions(
|
||||
prizesStartX,
|
||||
playerY - cardH / 2,
|
||||
smallCardW,
|
||||
smallCardH,
|
||||
prizesColSpacing,
|
||||
prizesRowSpacing,
|
||||
false, // Not flipped for player
|
||||
prizeCount
|
||||
)
|
||||
: [],
|
||||
|
||||
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: {
|
||||
@ -237,7 +278,7 @@ function calculateLandscapeLayout(width: number, height: number): BoardLayout {
|
||||
rotation: Math.PI, // Upside down
|
||||
},
|
||||
|
||||
oppBench: Array.from({ length: BENCH_SIZE }, (_, i) => ({
|
||||
oppBench: Array.from({ length: benchSize }, (_, i) => ({
|
||||
x: centerX + totalBenchWidth / 2 - i * benchSpacing, // Reversed order
|
||||
y: oppY - benchOffsetY, // Above active for opponent
|
||||
width: cardW,
|
||||
@ -269,23 +310,28 @@ function calculateLandscapeLayout(width: number, height: number): BoardLayout {
|
||||
rotation: Math.PI,
|
||||
},
|
||||
|
||||
oppPrizes: calculatePrizePositions(
|
||||
width - prizesStartX - prizesColSpacing, // Right side for opponent
|
||||
oppY + cardH / 2,
|
||||
smallCardW,
|
||||
smallCardH,
|
||||
-prizesColSpacing, // Reversed column spacing
|
||||
-prizesRowSpacing, // Reversed row spacing
|
||||
true // Flipped for opponent
|
||||
),
|
||||
oppPrizes: usePrizeCards
|
||||
? calculatePrizePositions(
|
||||
width - prizesStartX - prizesColSpacing, // Right side for opponent
|
||||
oppY + cardH / 2,
|
||||
smallCardW,
|
||||
smallCardH,
|
||||
-prizesColSpacing, // Reversed column spacing
|
||||
-prizesRowSpacing, // Reversed row spacing
|
||||
true, // Flipped for opponent
|
||||
prizeCount
|
||||
)
|
||||
: [],
|
||||
|
||||
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
|
||||
@ -302,9 +348,18 @@ function calculateLandscapeLayout(width: number, height: number): BoardLayout {
|
||||
*
|
||||
* @param width - Canvas width
|
||||
* @param height - Canvas height
|
||||
* @param options - Optional layout customization
|
||||
* @returns Board layout for portrait orientation
|
||||
*/
|
||||
function calculatePortraitLayout(width: number, height: number): BoardLayout {
|
||||
function calculatePortraitLayout(
|
||||
width: number,
|
||||
height: number,
|
||||
options?: LayoutOptions
|
||||
): BoardLayout {
|
||||
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
|
||||
@ -325,8 +380,8 @@ function calculatePortraitLayout(width: number, height: number): BoardLayout {
|
||||
|
||||
// Horizontal layout - tighter spacing for portrait
|
||||
const centerX = width / 2
|
||||
const benchSpacing = Math.min(cardW + padding, (width - edgePad * 2) / BENCH_SIZE)
|
||||
const totalBenchWidth = (BENCH_SIZE - 1) * benchSpacing
|
||||
const benchSpacing = Math.min(cardW + padding, (width - edgePad * 2) / benchSize)
|
||||
const totalBenchWidth = (benchSize - 1) * benchSpacing
|
||||
|
||||
// Deck/discard positions (right side, more compact)
|
||||
// Use reduced size (0.8) for these zones
|
||||
@ -352,7 +407,7 @@ function calculatePortraitLayout(width: number, height: number): BoardLayout {
|
||||
rotation: 0,
|
||||
},
|
||||
|
||||
myBench: Array.from({ length: BENCH_SIZE }, (_, i) => ({
|
||||
myBench: Array.from({ length: benchSize }, (_, i) => ({
|
||||
x: centerX - totalBenchWidth / 2 + i * benchSpacing,
|
||||
y: playerY + benchOffsetY,
|
||||
width: cardW * 0.9, // Slightly smaller bench cards
|
||||
@ -384,22 +439,27 @@ function calculatePortraitLayout(width: number, height: number): BoardLayout {
|
||||
rotation: 0,
|
||||
},
|
||||
|
||||
myPrizes: calculatePortraitPrizePositions(
|
||||
prizesX,
|
||||
playerY,
|
||||
smallCardW,
|
||||
smallCardH,
|
||||
prizesRowSpacing,
|
||||
false
|
||||
),
|
||||
myPrizes: usePrizeCards
|
||||
? calculatePortraitPrizePositions(
|
||||
prizesX,
|
||||
playerY,
|
||||
smallCardW,
|
||||
smallCardH,
|
||||
prizesRowSpacing,
|
||||
false,
|
||||
prizeCount
|
||||
)
|
||||
: [],
|
||||
|
||||
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: {
|
||||
@ -410,7 +470,7 @@ function calculatePortraitLayout(width: number, height: number): BoardLayout {
|
||||
rotation: Math.PI,
|
||||
},
|
||||
|
||||
oppBench: Array.from({ length: BENCH_SIZE }, (_, i) => ({
|
||||
oppBench: Array.from({ length: benchSize }, (_, i) => ({
|
||||
x: centerX + totalBenchWidth / 2 - i * benchSpacing,
|
||||
y: oppY - benchOffsetY,
|
||||
width: cardW * 0.9,
|
||||
@ -442,29 +502,34 @@ function calculatePortraitLayout(width: number, height: number): BoardLayout {
|
||||
rotation: Math.PI,
|
||||
},
|
||||
|
||||
oppPrizes: calculatePortraitPrizePositions(
|
||||
width - prizesX,
|
||||
oppY,
|
||||
smallCardW,
|
||||
smallCardH,
|
||||
-prizesRowSpacing,
|
||||
true
|
||||
),
|
||||
oppPrizes: usePrizeCards
|
||||
? calculatePortraitPrizePositions(
|
||||
width - prizesX,
|
||||
oppY,
|
||||
smallCardW,
|
||||
smallCardH,
|
||||
-prizesRowSpacing,
|
||||
true,
|
||||
prizeCount
|
||||
)
|
||||
: [],
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate prize card positions in a 2x3 grid.
|
||||
* Calculate prize card positions in a 2xN grid.
|
||||
*
|
||||
* @param startX - X position of first column
|
||||
* @param startY - Y position of first row center
|
||||
@ -473,7 +538,8 @@ function calculatePortraitLayout(width: number, height: number): BoardLayout {
|
||||
* @param colSpacing - Horizontal spacing between columns
|
||||
* @param rowSpacing - Vertical spacing between rows
|
||||
* @param flipped - Whether cards are upside down (opponent)
|
||||
* @returns Array of 6 prize zone positions
|
||||
* @param count - Number of prize cards (defaults to PRIZE_SIZE)
|
||||
* @returns Array of prize zone positions
|
||||
*/
|
||||
function calculatePrizePositions(
|
||||
startX: number,
|
||||
@ -482,13 +548,17 @@ function calculatePrizePositions(
|
||||
cardH: number,
|
||||
colSpacing: number,
|
||||
rowSpacing: number,
|
||||
flipped: boolean
|
||||
flipped: boolean,
|
||||
count: number = PRIZE_SIZE
|
||||
): ZonePosition[] {
|
||||
const prizes: ZonePosition[] = []
|
||||
const cols = 2
|
||||
const rows = Math.ceil(count / cols)
|
||||
|
||||
for (let row = 0; row < 3; row++) {
|
||||
for (let col = 0; col < 2; col++) {
|
||||
const index = row * 2 + col
|
||||
for (let row = 0; row < rows; row++) {
|
||||
for (let col = 0; col < cols; col++) {
|
||||
const index = row * cols + col
|
||||
if (index >= count) break
|
||||
prizes.push({
|
||||
x: startX + col * colSpacing,
|
||||
y: startY + row * rowSpacing,
|
||||
@ -514,7 +584,8 @@ function calculatePrizePositions(
|
||||
* @param cardH - Card height
|
||||
* @param rowSpacing - Vertical spacing (can be negative for overlap)
|
||||
* @param flipped - Whether cards are upside down
|
||||
* @returns Array of 6 prize zone positions
|
||||
* @param count - Number of prize cards (defaults to PRIZE_SIZE)
|
||||
* @returns Array of prize zone positions
|
||||
*/
|
||||
function calculatePortraitPrizePositions(
|
||||
x: number,
|
||||
@ -522,12 +593,14 @@ function calculatePortraitPrizePositions(
|
||||
cardW: number,
|
||||
cardH: number,
|
||||
rowSpacing: number,
|
||||
flipped: boolean
|
||||
flipped: boolean,
|
||||
count: number = PRIZE_SIZE
|
||||
): ZonePosition[] {
|
||||
const prizes: ZonePosition[] = []
|
||||
const startY = centerY - (2.5 * Math.abs(rowSpacing)) // Center the stack
|
||||
// Center the stack based on count
|
||||
const startY = centerY - ((count - 1) / 2 * Math.abs(rowSpacing))
|
||||
|
||||
for (let i = 0; i < PRIZE_SIZE; i++) {
|
||||
for (let i = 0; i < count; i++) {
|
||||
prizes.push({
|
||||
x,
|
||||
y: startY + i * Math.abs(rowSpacing),
|
||||
@ -640,7 +713,9 @@ 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 })
|
||||
@ -649,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
|
||||
}
|
||||
|
||||
@ -29,7 +29,7 @@ import type {
|
||||
} from '@/types/game'
|
||||
import type { BoardLayout } from '@/types/phaser'
|
||||
import { getMyPlayerState, getOpponentState } from '@/types/game'
|
||||
import { calculateLayout } from '../layout'
|
||||
import { calculateLayout, type LayoutOptions } from '../layout'
|
||||
import { Card } from '../objects/Card'
|
||||
import { ActiveZone } from '../objects/ActiveZone'
|
||||
import { BenchZone } from '../objects/BenchZone'
|
||||
@ -51,8 +51,8 @@ interface PlayerZones {
|
||||
hand: HandZone
|
||||
deck: PileZone
|
||||
discard: PileZone
|
||||
prizes: PrizeZone
|
||||
energyZone: PileZone
|
||||
prizes: PrizeZone | null // null when using points system instead of prize cards
|
||||
energyZone: PileZone | null // null when energy deck is disabled (classic mode)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -128,8 +128,16 @@ export class StateRenderer {
|
||||
// Get canvas dimensions
|
||||
const { width, height } = this.scene.cameras.main
|
||||
|
||||
// Extract layout options from rules config
|
||||
const layoutOptions: LayoutOptions = {
|
||||
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
|
||||
this.layout = calculateLayout(width, height)
|
||||
this.layout = calculateLayout(width, height, layoutOptions)
|
||||
|
||||
// Create zones if needed
|
||||
if (!this.zones) {
|
||||
@ -258,13 +266,17 @@ export class StateRenderer {
|
||||
return
|
||||
}
|
||||
|
||||
// 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),
|
||||
opp: this.createPlayerZones(oppState.player_id, true),
|
||||
my: this.createPlayerZones(myState.player_id, false, usePrizeCards, energyDeckEnabled),
|
||||
opp: this.createPlayerZones(oppState.player_id, true, usePrizeCards, energyDeckEnabled),
|
||||
}
|
||||
}
|
||||
|
||||
@ -273,9 +285,16 @@ 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): PlayerZones {
|
||||
private createPlayerZones(
|
||||
playerId: string,
|
||||
isOpponent: boolean,
|
||||
usePrizeCards: boolean,
|
||||
energyDeckEnabled: boolean
|
||||
): PlayerZones {
|
||||
// Create zones at origin - positions updated in updateZonePositions()
|
||||
const zones: PlayerZones = {
|
||||
active: new ActiveZone(this.scene, 0, 0, playerId),
|
||||
@ -283,8 +302,8 @@ export class StateRenderer {
|
||||
hand: new HandZone(this.scene, 0, 0, playerId),
|
||||
deck: new PileZone(this.scene, 0, 0, playerId, 'deck'),
|
||||
discard: new PileZone(this.scene, 0, 0, playerId, 'discard'),
|
||||
prizes: new PrizeZone(this.scene, 0, 0, playerId),
|
||||
energyZone: new PileZone(this.scene, 0, 0, playerId, 'deck'), // Energy deck uses pile zone
|
||||
prizes: usePrizeCards ? new PrizeZone(this.scene, 0, 0, playerId) : null,
|
||||
energyZone: energyDeckEnabled ? new PileZone(this.scene, 0, 0, playerId, 'deck') : null,
|
||||
}
|
||||
|
||||
// Add all zones to container
|
||||
@ -294,8 +313,12 @@ export class StateRenderer {
|
||||
this.container.add(zones.hand)
|
||||
this.container.add(zones.deck)
|
||||
this.container.add(zones.discard)
|
||||
this.container.add(zones.prizes)
|
||||
this.container.add(zones.energyZone)
|
||||
if (zones.prizes) {
|
||||
this.container.add(zones.prizes)
|
||||
}
|
||||
if (zones.energyZone) {
|
||||
this.container.add(zones.energyZone)
|
||||
}
|
||||
}
|
||||
|
||||
return zones
|
||||
@ -312,8 +335,8 @@ export class StateRenderer {
|
||||
zones.hand.destroy()
|
||||
zones.deck.destroy()
|
||||
zones.discard.destroy()
|
||||
zones.prizes.destroy()
|
||||
zones.energyZone.destroy()
|
||||
zones.prizes?.destroy()
|
||||
zones.energyZone?.destroy()
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
@ -331,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
|
||||
@ -344,8 +369,8 @@ export class StateRenderer {
|
||||
this.zones.my.bench.setZoneDimensions(totalWidth, benchLayout[0].height)
|
||||
}
|
||||
|
||||
// Prizes - use the first prize position for the zone
|
||||
if (this.layout.myPrizes.length > 0) {
|
||||
// Prizes - use the first prize position for the zone (only if using prize cards)
|
||||
if (this.zones.my.prizes && this.layout.myPrizes.length > 0) {
|
||||
const prizePositions = this.layout.myPrizes
|
||||
const minX = Math.min(...prizePositions.map(p => p.x - p.width / 2))
|
||||
const maxX = Math.max(...prizePositions.map(p => p.x + p.width / 2))
|
||||
@ -362,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
|
||||
@ -375,8 +402,8 @@ export class StateRenderer {
|
||||
this.zones.opp.bench.setZoneDimensions(totalWidth, oppBenchLayout[0].height)
|
||||
}
|
||||
|
||||
// Opponent prizes
|
||||
if (this.layout.oppPrizes.length > 0) {
|
||||
// Opponent prizes (only if using prize cards)
|
||||
if (this.zones.opp.prizes && this.layout.oppPrizes.length > 0) {
|
||||
const prizePositions = this.layout.oppPrizes
|
||||
const minX = Math.min(...prizePositions.map(p => p.x - p.width / 2))
|
||||
const maxX = Math.max(...prizePositions.map(p => p.x + p.width / 2))
|
||||
@ -449,20 +476,22 @@ export class StateRenderer {
|
||||
true // always visible
|
||||
)
|
||||
|
||||
// Update prizes
|
||||
// Update prizes (only if using prize cards)
|
||||
// Viewer sees their own prize count (cards still hidden)
|
||||
// Opponent just sees count
|
||||
zones.prizes.setRemainingCount(playerState.prizes_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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -14,6 +14,7 @@ import type {
|
||||
CardDefinition,
|
||||
TurnPhase,
|
||||
Action,
|
||||
RulesConfig,
|
||||
} from '@/types'
|
||||
import { getMyPlayerState, getOpponentState, getCardDefinition, ConnectionStatus } from '@/types'
|
||||
|
||||
@ -216,6 +217,35 @@ export const useGameStore = defineStore('game', () => {
|
||||
forcedAction.value?.player === gameState.value?.viewer_id
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Computed - Rules Config
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Current game rules configuration */
|
||||
const rulesConfig = computed<RulesConfig | null>(() =>
|
||||
gameState.value?.rules_config ?? null
|
||||
)
|
||||
|
||||
/** Whether this game uses classic prize cards (vs points system) */
|
||||
const usePrizeCards = computed<boolean>(() =>
|
||||
rulesConfig.value?.prizes.use_prize_cards ?? false
|
||||
)
|
||||
|
||||
/** Number of prizes/points needed to win */
|
||||
const prizeCount = computed<number>(() =>
|
||||
rulesConfig.value?.prizes.count ?? 4
|
||||
)
|
||||
|
||||
/** Maximum number of Pokemon on the bench */
|
||||
const benchSize = computed<number>(() =>
|
||||
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
|
||||
// ---------------------------------------------------------------------------
|
||||
@ -337,6 +367,13 @@ export const useGameStore = defineStore('game', () => {
|
||||
hasForcedAction,
|
||||
isConnected,
|
||||
|
||||
// Rules config
|
||||
rulesConfig,
|
||||
usePrizeCards,
|
||||
prizeCount,
|
||||
benchSize,
|
||||
energyDeckEnabled,
|
||||
|
||||
// Card lookup
|
||||
lookupCard,
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
* and sends this structure via WebSocket.
|
||||
*/
|
||||
|
||||
import type { ModifierMode } from './rules'
|
||||
import type { ModifierMode, RulesConfig } from './rules'
|
||||
|
||||
// =============================================================================
|
||||
// Enums and Constants
|
||||
@ -407,6 +407,9 @@ export interface VisibleGameState {
|
||||
|
||||
/** Card definitions for display (definition_id -> CardDefinition) */
|
||||
card_registry: Record<string, CardDefinition>
|
||||
|
||||
/** Rules configuration for UI rendering decisions (e.g., prize cards vs points) */
|
||||
rules_config?: RulesConfig
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@ -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