Honor RulesConfig for prize cards vs points in frontend game board
The game board now conditionally renders prize card zones based on the RulesConfig sent from the backend: - Add rules_config field to VisibleGameState in backend (visibility.py) - Add rules_config to frontend game types and game store - Update layout.ts to accept LayoutOptions with usePrizeCards and prizeCount - Update StateRenderer to conditionally create PrizeZone objects - Update Board to handle empty prize position arrays gracefully - Add game store computed properties: rulesConfig, usePrizeCards, prizeCount - Add tests for conditional prize zone rendering When use_prize_cards is false (Mantimon TCG points system), the prize zones are not rendered, saving screen space. When true (classic Pokemon TCG mode), the correct number of prize slots is rendered based on the rules config's prize count. https://claude.ai/code/session_01AAxKmpq2AGde327eX1nzUC
This commit is contained in:
parent
1a21d3d2d4
commit
7885b272a4
@ -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,119 @@ 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)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@ -53,12 +53,26 @@ 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
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Orientation Detection
|
||||
// =============================================================================
|
||||
@ -103,19 +117,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 +152,16 @@ 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 scale = getScaleFactor(width, height)
|
||||
|
||||
// Scale card dimensions
|
||||
@ -210,15 +240,18 @@ 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,
|
||||
@ -269,15 +302,18 @@ 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,
|
||||
@ -302,9 +338,16 @@ 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 scale = getScaleFactor(width, height) * 0.85 // Slightly smaller for portrait
|
||||
|
||||
// Scale card dimensions - use smaller cards in portrait
|
||||
@ -384,14 +427,17 @@ 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,
|
||||
@ -442,14 +488,17 @@ 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,
|
||||
@ -464,7 +513,7 @@ function calculatePortraitLayout(width: number, height: number): BoardLayout {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 +522,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 +532,17 @@ function calculatePrizePositions(
|
||||
cardH: number,
|
||||
colSpacing: number,
|
||||
rowSpacing: number,
|
||||
flipped: boolean
|
||||
flipped: boolean,
|
||||
count: number = PRIZE_SIZE
|
||||
): ZonePosition[] {
|
||||
const prizes: ZonePosition[] = []
|
||||
|
||||
for (let row = 0; row < 3; row++) {
|
||||
for (let col = 0; col < 2; col++) {
|
||||
const index = row * 2 + col
|
||||
const cols = 2
|
||||
const rows = Math.ceil(count / cols)
|
||||
|
||||
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,
|
||||
@ -498,7 +552,7 @@ function calculatePrizePositions(
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return prizes
|
||||
}
|
||||
|
||||
@ -514,7 +568,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 +577,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
|
||||
|
||||
for (let i = 0; i < PRIZE_SIZE; i++) {
|
||||
// Center the stack based on count
|
||||
const startY = centerY - ((count - 1) / 2 * Math.abs(rowSpacing))
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
prizes.push({
|
||||
x,
|
||||
y: startY + i * Math.abs(rowSpacing),
|
||||
@ -536,7 +593,7 @@ function calculatePortraitPrizePositions(
|
||||
rotation: flipped ? Math.PI : 0,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
return prizes
|
||||
}
|
||||
|
||||
|
||||
@ -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,7 +51,7 @@ interface PlayerZones {
|
||||
hand: HandZone
|
||||
deck: PileZone
|
||||
discard: PileZone
|
||||
prizes: PrizeZone
|
||||
prizes: PrizeZone | null // null when using points system instead of prize cards
|
||||
energyZone: PileZone
|
||||
}
|
||||
|
||||
@ -128,8 +128,14 @@ 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,
|
||||
}
|
||||
|
||||
// Calculate layout
|
||||
this.layout = calculateLayout(width, height)
|
||||
this.layout = calculateLayout(width, height, layoutOptions)
|
||||
|
||||
// Create zones if needed
|
||||
if (!this.zones) {
|
||||
@ -258,13 +264,16 @@ export class StateRenderer {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we should create prize zones
|
||||
const usePrizeCards = state.rules_config?.prizes.use_prize_cards ?? false
|
||||
|
||||
// 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),
|
||||
opp: this.createPlayerZones(oppState.player_id, true, usePrizeCards),
|
||||
}
|
||||
}
|
||||
|
||||
@ -273,9 +282,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)
|
||||
* @returns PlayerZones object
|
||||
*/
|
||||
private createPlayerZones(playerId: string, isOpponent: boolean): PlayerZones {
|
||||
private createPlayerZones(
|
||||
playerId: string,
|
||||
isOpponent: boolean,
|
||||
usePrizeCards: boolean
|
||||
): PlayerZones {
|
||||
// Create zones at origin - positions updated in updateZonePositions()
|
||||
const zones: PlayerZones = {
|
||||
active: new ActiveZone(this.scene, 0, 0, playerId),
|
||||
@ -283,7 +297,7 @@ 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),
|
||||
prizes: usePrizeCards ? new PrizeZone(this.scene, 0, 0, playerId) : null,
|
||||
energyZone: new PileZone(this.scene, 0, 0, playerId, 'deck'), // Energy deck uses pile zone
|
||||
}
|
||||
|
||||
@ -294,7 +308,9 @@ export class StateRenderer {
|
||||
this.container.add(zones.hand)
|
||||
this.container.add(zones.deck)
|
||||
this.container.add(zones.discard)
|
||||
this.container.add(zones.prizes)
|
||||
if (zones.prizes) {
|
||||
this.container.add(zones.prizes)
|
||||
}
|
||||
this.container.add(zones.energyZone)
|
||||
}
|
||||
|
||||
@ -312,7 +328,7 @@ export class StateRenderer {
|
||||
zones.hand.destroy()
|
||||
zones.deck.destroy()
|
||||
zones.discard.destroy()
|
||||
zones.prizes.destroy()
|
||||
zones.prizes?.destroy()
|
||||
zones.energyZone.destroy()
|
||||
}
|
||||
|
||||
@ -344,8 +360,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))
|
||||
@ -375,8 +391,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,10 +465,10 @@ 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(
|
||||
|
||||
@ -14,6 +14,7 @@ import type {
|
||||
CardDefinition,
|
||||
TurnPhase,
|
||||
Action,
|
||||
RulesConfig,
|
||||
} from '@/types'
|
||||
import { getMyPlayerState, getOpponentState, getCardDefinition, ConnectionStatus } from '@/types'
|
||||
|
||||
@ -216,6 +217,25 @@ 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
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Computed - Card Lookup
|
||||
// ---------------------------------------------------------------------------
|
||||
@ -337,6 +357,11 @@ export const useGameStore = defineStore('game', () => {
|
||||
hasForcedAction,
|
||||
isConnected,
|
||||
|
||||
// Rules config
|
||||
rulesConfig,
|
||||
usePrizeCards,
|
||||
prizeCount,
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
||||
Loading…
Reference in New Issue
Block a user