Skip to content

Commit c01ac01

Browse files
committed
Merge wl-CG-0MLU86JTG1OXPMDW: Extract card texture helpers and shared constants to src/ui/
2 parents 247937b + f5df7d0 commit c01ac01

File tree

6 files changed

+271
-114
lines changed

6 files changed

+271
-114
lines changed

example-games/beleaguered-castle/scenes/BeleagueredCastleScene.ts

Lines changed: 10 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
*/
1515

1616
import Phaser from 'phaser';
17-
import type { Rank, Suit } from '../../../src/card-system/Card';
17+
import type { Suit } from '../../../src/card-system/Card';
1818
import type { BeleagueredCastleState, BCMove } from '../BeleagueredCastleState';
1919
import { FOUNDATION_COUNT, TABLEAU_COUNT, FOUNDATION_SUITS } from '../BeleagueredCastleState';
2020
import {
@@ -34,19 +34,18 @@ import type { Command } from '../../../src/core-engine/UndoRedoManager';
3434
import { UndoRedoManager, CompoundCommand } from '../../../src/core-engine/UndoRedoManager';
3535
import { BCTranscriptRecorder } from '../GameTranscript';
3636
import type { BCGameTranscript } from '../GameTranscript';
37-
import { HelpPanel, HelpButton } from '../../../src/ui';
37+
import {
38+
HelpPanel, HelpButton,
39+
CARD_W, CARD_H, GAME_W, GAME_H, FONT_FAMILY,
40+
cardTextureKey, preloadCardAssets,
41+
} from '../../../src/ui';
3842
import type { HelpSection } from '../../../src/ui';
3943
import helpContent from '../help-content.json';
4044

4145
// ── Constants ───────────────────────────────────────────────
4246

43-
const CARD_W = 48;
44-
const CARD_H = 65;
4547
const CARD_GAP = 6;
4648

47-
const GAME_W = 800;
48-
const GAME_H = 600;
49-
5049
const ANIM_DURATION = 300; // ms per card deal animation
5150
const DEAL_STAGGER = 40; // ms between successive card deal tweens
5251
const SNAP_BACK_DURATION = 200; // ms to snap card back on invalid drop
@@ -65,8 +64,6 @@ const TABLEAU_TOP_Y = 135;
6564
/** Z-depth for a card being dragged. */
6665
const DRAG_DEPTH = 1000;
6766

68-
const FONT_FAMILY = 'Arial, sans-serif';
69-
7067
// ── Suit symbol mapping for foundation labels ───────────────
7168

7269
const SUIT_SYMBOL: Record<Suit, string> = {
@@ -235,28 +232,7 @@ export class BeleagueredCastleScene extends Phaser.Scene {
235232
// ── Preload ─────────────────────────────────────────────
236233

237234
preload(): void {
238-
// Load card back
239-
this.load.svg('card_back', 'assets/cards/card_back.svg', {
240-
width: CARD_W,
241-
height: CARD_H,
242-
});
243-
244-
// Load all 52 card faces
245-
const suits: Suit[] = ['clubs', 'diamonds', 'hearts', 'spades'];
246-
const ranks: Rank[] = [
247-
'A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K',
248-
];
249-
250-
for (const suit of suits) {
251-
for (const rank of ranks) {
252-
const key = this.cardTextureKey(rank, suit);
253-
const fileName = this.cardFileName(rank, suit);
254-
this.load.svg(key, `assets/cards/${fileName}`, {
255-
width: CARD_W,
256-
height: CARD_H,
257-
});
258-
}
259-
}
235+
preloadCardAssets(this);
260236
}
261237

262238
// ── Create ──────────────────────────────────────────────
@@ -1282,7 +1258,7 @@ export class BeleagueredCastleScene extends Phaser.Scene {
12821258
const topCard = foundation.peek();
12831259

12841260
if (topCard) {
1285-
const texture = this.cardTextureKey(topCard.rank, topCard.suit);
1261+
const texture = cardTextureKey(topCard.rank, topCard.suit);
12861262
this.foundationSprites[i].setTexture(texture).setVisible(true);
12871263
} else {
12881264
this.foundationSprites[i].setVisible(false);
@@ -1342,7 +1318,7 @@ export class BeleagueredCastleScene extends Phaser.Scene {
13421318
const card = cards[row];
13431319
const targetX = this.tableauColumnX(col);
13441320
const targetY = this.tableauCardY(row);
1345-
const texture = this.cardTextureKey(card.rank, card.suit);
1321+
const texture = cardTextureKey(card.rank, card.suit);
13461322

13471323
// Create the sprite at the deal origin (center), invisible initially
13481324
const sprite = this.add
@@ -1465,7 +1441,7 @@ export class BeleagueredCastleScene extends Phaser.Scene {
14651441
const card = cards[row];
14661442
const x = this.tableauColumnX(col);
14671443
const y = this.tableauCardY(row);
1468-
const texture = this.cardTextureKey(card.rank, card.suit);
1444+
const texture = cardTextureKey(card.rank, card.suit);
14691445

14701446
const sprite = this.add
14711447
.image(x, y, texture)
@@ -1524,27 +1500,6 @@ export class BeleagueredCastleScene extends Phaser.Scene {
15241500
}
15251501
}
15261502

1527-
// ── Texture helpers ─────────────────────────────────────
1528-
1529-
private cardTextureKey(rank: Rank, suit: Suit): string {
1530-
const rankName = this.rankFileName(rank);
1531-
return `${rankName}_of_${suit}`;
1532-
}
1533-
1534-
private cardFileName(rank: Rank, suit: Suit): string {
1535-
return `${this.rankFileName(rank)}_of_${suit}.svg`;
1536-
}
1537-
1538-
private rankFileName(rank: Rank): string {
1539-
switch (rank) {
1540-
case 'A': return 'ace';
1541-
case 'J': return 'jack';
1542-
case 'Q': return 'queen';
1543-
case 'K': return 'king';
1544-
default: return rank; // 2-10
1545-
}
1546-
}
1547-
15481503
// ── Accessors (for tests and future features) ──────────
15491504

15501505
/** Get the current game state. */

example-games/golf/scenes/GolfScene.ts

Lines changed: 11 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -23,21 +23,20 @@ import type { GameTranscript, BoardSnapshot, CardSnapshot } from '../GameTranscr
2323
import { TranscriptStore } from '../../../src/core-engine/TranscriptStore';
2424
import { GameEventEmitter } from '../../../src/core-engine/GameEventEmitter';
2525
import { PhaserEventBridge } from '../../../src/core-engine/PhaserEventBridge';
26-
import { HelpPanel, HelpButton } from '../../../src/ui';
26+
import {
27+
HelpPanel, HelpButton,
28+
CARD_W, CARD_H, GAME_W, GAME_H, FONT_FAMILY,
29+
cardTextureKey, getCardTexture, preloadCardAssets,
30+
} from '../../../src/ui';
2731
import type { HelpSection } from '../../../src/ui';
2832
import helpContent from '../help-content.json';
2933

3034
// ── Constants ───────────────────────────────────────────────
3135

32-
const CARD_W = 48;
33-
const CARD_H = 65;
3436
const CARD_GAP = 5;
3537
const GRID_COLS = 3;
3638
const GRID_ROWS = 3;
3739

38-
const GAME_W = 800;
39-
const GAME_H = 600;
40-
4140
const AI_DELAY = 600; // ms before AI chooses
4241
const AI_SHOW_DRAW_DELAY = 1000; // ms to show drawn card before moving
4342
const ANIM_DURATION = 300; // ms for animations
@@ -50,8 +49,6 @@ const PILE_Y = 295; // center Y of stock/discard
5049
const STOCK_X = GAME_W / 2 - 50;
5150
const DISCARD_X = GAME_W / 2 + 50;
5251

53-
const FONT_FAMILY = 'Arial, sans-serif';
54-
5552
// ── Turn state machine ──────────────────────────────────────
5653

5754
type TurnPhase =
@@ -112,26 +109,7 @@ export class GolfScene extends Phaser.Scene {
112109
// ── Preload ─────────────────────────────────────────────
113110

114111
preload(): void {
115-
// Load card back
116-
this.load.svg('card_back', 'assets/cards/card_back.svg', {
117-
width: CARD_W,
118-
height: CARD_H,
119-
});
120-
121-
// Load all 52 card faces
122-
const suits: Suit[] = ['clubs', 'diamonds', 'hearts', 'spades'];
123-
const ranks: Rank[] = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'];
124-
125-
for (const suit of suits) {
126-
for (const rank of ranks) {
127-
const key = this.cardTextureKey(rank, suit);
128-
const fileName = this.cardFileName(rank, suit);
129-
this.load.svg(key, `assets/cards/${fileName}`, {
130-
width: CARD_W,
131-
height: CARD_H,
132-
});
133-
}
134-
}
112+
preloadCardAssets(this);
135113
}
136114

137115
// ── Create ──────────────────────────────────────────────
@@ -424,32 +402,6 @@ export class GolfScene extends Phaser.Scene {
424402
};
425403
}
426404

427-
// ── Texture helpers ─────────────────────────────────────
428-
429-
private cardTextureKey(rank: Rank, suit: Suit): string {
430-
const rankName = this.rankFileName(rank);
431-
return `${rankName}_of_${suit}`;
432-
}
433-
434-
private cardFileName(rank: Rank, suit: Suit): string {
435-
return `${this.rankFileName(rank)}_of_${suit}.svg`;
436-
}
437-
438-
private rankFileName(rank: Rank): string {
439-
switch (rank) {
440-
case 'A': return 'ace';
441-
case 'J': return 'jack';
442-
case 'Q': return 'queen';
443-
case 'K': return 'king';
444-
default: return rank; // 2-10
445-
}
446-
}
447-
448-
private getCardTexture(card: Card): string {
449-
if (!card.faceUp) return 'card_back';
450-
return this.cardTextureKey(card.rank, card.suit);
451-
}
452-
453405
// ── Refresh display ─────────────────────────────────────
454406

455407
private refreshAll(): void {
@@ -466,7 +418,7 @@ export class GolfScene extends Phaser.Scene {
466418
const sprites = player === 'human' ? this.humanCardSprites : this.aiCardSprites;
467419

468420
for (let i = 0; i < 9; i++) {
469-
sprites[i].setTexture(this.getCardTexture(grid[i]));
421+
sprites[i].setTexture(getCardTexture(grid[i]));
470422
}
471423
}
472424

@@ -483,7 +435,7 @@ export class GolfScene extends Phaser.Scene {
483435
const top = this.session.shared.discardPile.peek();
484436
if (top) {
485437
this.discardSprite.setVisible(true);
486-
this.discardSprite.setTexture(this.getCardTexture(top));
438+
this.discardSprite.setTexture(getCardTexture(top));
487439
} else {
488440
this.discardSprite.setVisible(false);
489441
}
@@ -755,7 +707,7 @@ export class GolfScene extends Phaser.Scene {
755707
ease: 'Power2',
756708
onComplete: () => {
757709
// Reveal the card's actual face at the midpoint of the flip
758-
sprite.setTexture(this.getCardTexture(grid[idx]));
710+
sprite.setTexture(getCardTexture(grid[idx]));
759711
// Second half: scaleX → 1 while completing movement to discard
760712
this.tweens.add({
761713
targets: sprite,
@@ -803,7 +755,7 @@ export class GolfScene extends Phaser.Scene {
803755
duration: SWAP_ANIM_DURATION / 4,
804756
ease: 'Power2',
805757
onComplete: () => {
806-
sprite.setTexture(this.getCardTexture(grid[idx]));
758+
sprite.setTexture(getCardTexture(grid[idx]));
807759
this.tweens.add({
808760
targets: sprite,
809761
scaleX: 1,
@@ -838,7 +790,7 @@ export class GolfScene extends Phaser.Scene {
838790
// Show drawn card to the right of the discard pile
839791
const x = DISCARD_X + CARD_W + 20;
840792
const y = PILE_Y;
841-
const texture = this.cardTextureKey(card.rank, card.suit);
793+
const texture = cardTextureKey(card.rank, card.suit);
842794

843795
this.drawnCardSprite = this.add.image(x, y, texture);
844796
this.drawnCardSprite.setAlpha(0);

src/ui/CardTextureHelpers.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/**
2+
* Card Texture Helpers
3+
*
4+
* Shared functions for mapping Card rank/suit values to Phaser texture keys
5+
* and SVG file names. These are used by every game scene that renders
6+
* standard playing cards from the `public/assets/cards/` sprite set.
7+
*
8+
* Also provides a convenience function to preload all 52 card face SVGs
9+
* plus the card back into a Phaser scene.
10+
*/
11+
12+
import type { Card, Rank, Suit } from '@card-system/Card';
13+
import { RANKS, SUITS } from '@card-system/Card';
14+
import { CARD_W, CARD_H } from './constants';
15+
16+
/**
17+
* Map a rank abbreviation to the full name used in SVG file names.
18+
*
19+
* - Face cards and ace: `'A'` -> `'ace'`, `'J'` -> `'jack'`, etc.
20+
* - Number cards: returned as-is (`'2'` -> `'2'`, `'10'` -> `'10'`).
21+
*/
22+
export function rankFileName(rank: Rank): string {
23+
switch (rank) {
24+
case 'A': return 'ace';
25+
case 'J': return 'jack';
26+
case 'Q': return 'queen';
27+
case 'K': return 'king';
28+
default: return rank; // 2-10
29+
}
30+
}
31+
32+
/**
33+
* Build the Phaser texture key for a given rank and suit.
34+
*
35+
* Example: `cardTextureKey('A', 'spades')` -> `'ace_of_spades'`
36+
*/
37+
export function cardTextureKey(rank: Rank, suit: Suit): string {
38+
return `${rankFileName(rank)}_of_${suit}`;
39+
}
40+
41+
/**
42+
* Build the SVG file name (without directory) for a given rank and suit.
43+
*
44+
* Example: `cardFileName('A', 'spades')` -> `'ace_of_spades.svg'`
45+
*/
46+
export function cardFileName(rank: Rank, suit: Suit): string {
47+
return `${rankFileName(rank)}_of_${suit}.svg`;
48+
}
49+
50+
/**
51+
* Return the correct Phaser texture key for a card, taking face-up state
52+
* into account. Face-down cards return `'card_back'`.
53+
*/
54+
export function getCardTexture(card: Card): string {
55+
if (!card.faceUp) return 'card_back';
56+
return cardTextureKey(card.rank, card.suit);
57+
}
58+
59+
/**
60+
* Preload all 52 card face SVGs and the card back SVG into a Phaser scene.
61+
*
62+
* Call this from your scene's `preload()` method instead of manually
63+
* iterating over ranks and suits.
64+
*
65+
* @param scene The Phaser scene whose loader should be used.
66+
* @param width Card sprite width in pixels (defaults to `CARD_W`).
67+
* @param height Card sprite height in pixels (defaults to `CARD_H`).
68+
*/
69+
export function preloadCardAssets(
70+
scene: Phaser.Scene,
71+
width: number = CARD_W,
72+
height: number = CARD_H,
73+
): void {
74+
// Card back
75+
scene.load.svg('card_back', 'assets/cards/card_back.svg', {
76+
width,
77+
height,
78+
});
79+
80+
// All 52 card faces
81+
for (const suit of SUITS) {
82+
for (const rank of RANKS) {
83+
const key = cardTextureKey(rank, suit);
84+
const file = cardFileName(rank, suit);
85+
scene.load.svg(key, `assets/cards/${file}`, { width, height });
86+
}
87+
}
88+
}

src/ui/constants.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* Shared UI constants for the Tableau Card Engine.
3+
*
4+
* These values define the default rendering dimensions and font settings
5+
* used across all example-game scenes. Individual games may still define
6+
* game-specific constants (e.g. grid gaps, animation timings) locally.
7+
*/
8+
9+
/** Default card sprite width (pixels). */
10+
export const CARD_W = 48;
11+
12+
/** Default card sprite height (pixels). */
13+
export const CARD_H = 65;
14+
15+
/** Default game viewport width (pixels). */
16+
export const GAME_W = 800;
17+
18+
/** Default game viewport height (pixels). */
19+
export const GAME_H = 600;
20+
21+
/** Default font family used for in-game text. */
22+
export const FONT_FAMILY = 'Arial, sans-serif';

src/ui/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,15 @@ export type { HelpButtonConfig } from './HelpButton';
1414

1515
export { GameSelectorScene, REGISTRY_KEY_GAMES } from './GameSelectorScene';
1616
export type { GameEntry } from './GameSelectorScene';
17+
18+
// Shared constants
19+
export { CARD_W, CARD_H, GAME_W, GAME_H, FONT_FAMILY } from './constants';
20+
21+
// Card texture helpers
22+
export {
23+
rankFileName,
24+
cardTextureKey,
25+
cardFileName,
26+
getCardTexture,
27+
preloadCardAssets,
28+
} from './CardTextureHelpers';

0 commit comments

Comments
 (0)