refactor: reorg
This commit is contained in:
parent
bf62805c38
commit
72e159e52b
|
|
@ -3,9 +3,12 @@ import {
|
||||||
Part,
|
Part,
|
||||||
MutableSignal,
|
MutableSignal,
|
||||||
createRegion,
|
createRegion,
|
||||||
|
createPartPool,
|
||||||
createPart,
|
createPart,
|
||||||
|
moveToRegion,
|
||||||
isCellOccupied as isCellOccupiedUtil,
|
isCellOccupied as isCellOccupiedUtil,
|
||||||
getPartAtPosition,
|
getPartAtPosition,
|
||||||
|
applyAlign,
|
||||||
} from 'boardgame-core';
|
} from 'boardgame-core';
|
||||||
|
|
||||||
const BOARD_SIZE = 6;
|
const BOARD_SIZE = 6;
|
||||||
|
|
@ -15,20 +18,45 @@ const WIN_LENGTH = 3;
|
||||||
export type PlayerType = 'white' | 'black';
|
export type PlayerType = 'white' | 'black';
|
||||||
export type PieceType = 'kitten' | 'cat';
|
export type PieceType = 'kitten' | 'cat';
|
||||||
export type WinnerType = PlayerType | 'draw' | null;
|
export type WinnerType = PlayerType | 'draw' | null;
|
||||||
|
export type RegionId = 'board' | 'white-kitten' | 'white-cat' | 'black-kitten' | 'black-cat';
|
||||||
|
|
||||||
export type BoopPart = Part<{ player: PlayerType; pieceType: PieceType }>;
|
export type BoopPart = Part<{ player: PlayerType; pieceType: PieceType }>;
|
||||||
|
|
||||||
type PieceSupply = { supply: number; placed: number };
|
|
||||||
|
|
||||||
type Player = {
|
type Player = {
|
||||||
id: PlayerType;
|
id: PlayerType;
|
||||||
kitten: PieceSupply;
|
kittenPool: ReturnType<typeof createPartPool<{ player: PlayerType; pieceType: PieceType }>>;
|
||||||
cat: PieceSupply;
|
catPool: ReturnType<typeof createPartPool<{ player: PlayerType; pieceType: PieceType }>>;
|
||||||
|
graduatedCount: number; // 已毕业但未放置的大猫数量
|
||||||
};
|
};
|
||||||
|
|
||||||
type PlayerData = Record<PlayerType, Player>;
|
type PlayerData = Record<PlayerType, Player>;
|
||||||
|
|
||||||
export function createInitialState() {
|
export type BoopState = {
|
||||||
|
board: ReturnType<typeof createRegion>;
|
||||||
|
pieces: Record<string, BoopPart>;
|
||||||
|
currentPlayer: PlayerType;
|
||||||
|
winner: WinnerType;
|
||||||
|
players: PlayerData;
|
||||||
|
};
|
||||||
|
|
||||||
|
function createPlayer(id: PlayerType): Player {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
kittenPool: createPartPool(
|
||||||
|
{ regionId: `${id}-kitten`, player: id, pieceType: 'kitten' as PieceType },
|
||||||
|
MAX_PIECES_PER_PLAYER,
|
||||||
|
`${id}-kitten`
|
||||||
|
),
|
||||||
|
catPool: createPartPool(
|
||||||
|
{ regionId: `${id}-cat`, player: id, pieceType: 'cat' as PieceType },
|
||||||
|
MAX_PIECES_PER_PLAYER, // 预创建 MAX_PIECES_PER_PLAYER 只猫
|
||||||
|
`${id}-cat`
|
||||||
|
),
|
||||||
|
graduatedCount: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createInitialState(): BoopState {
|
||||||
return {
|
return {
|
||||||
board: createRegion('board', [
|
board: createRegion('board', [
|
||||||
{ name: 'x', min: 0, max: BOARD_SIZE - 1 },
|
{ name: 'x', min: 0, max: BOARD_SIZE - 1 },
|
||||||
|
|
@ -44,31 +72,14 @@ export function createInitialState() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createPlayer(id: PlayerType): Player {
|
export type BoopStateType = ReturnType<typeof createInitialState>;
|
||||||
return {
|
const registration = createGameCommandRegistry<BoopStateType>();
|
||||||
id,
|
|
||||||
kitten: { supply: MAX_PIECES_PER_PLAYER, placed: 0 },
|
|
||||||
cat: { supply: 0, placed: 0 },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export type BoopState = ReturnType<typeof createInitialState>;
|
|
||||||
const registration = createGameCommandRegistry<BoopState>();
|
|
||||||
export const registry = registration.registry;
|
export const registry = registration.registry;
|
||||||
|
|
||||||
export function getPlayer(host: MutableSignal<BoopState>, player: PlayerType): Player {
|
export function getPlayer(host: MutableSignal<BoopStateType>, player: PlayerType): Player {
|
||||||
return host.value.players[player];
|
return host.value.players[player];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function decrementSupply(player: Player, pieceType: PieceType) {
|
|
||||||
player[pieceType].supply--;
|
|
||||||
player[pieceType].placed++;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function incrementSupply(player: Player, pieceType: PieceType, count?: number) {
|
|
||||||
player[pieceType].supply += count ?? 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
registration.add('setup', async function() {
|
registration.add('setup', async function() {
|
||||||
const {context} = this;
|
const {context} = this;
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|
@ -108,9 +119,16 @@ registration.add('turn <player>', async function(cmd) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const playerData = getPlayer(this.context, player);
|
const playerData = getPlayer(this.context, player);
|
||||||
const supply = playerData[pieceType].supply;
|
if (pieceType === 'kitten') {
|
||||||
if (supply <= 0) {
|
if (playerData.kittenPool.remaining() <= 0) {
|
||||||
return `No ${pieceType}s left in ${player}'s supply.`;
|
return `No kittens left in ${player}'s supply.`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Can place cat if pool has remaining OR graduatedCount > 0
|
||||||
|
const availableCats = playerData.catPool.remaining() + playerData.graduatedCount;
|
||||||
|
if (availableCats <= 0) {
|
||||||
|
return `No cats available for ${player}. Graduate some kittens first.`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|
@ -120,7 +138,7 @@ registration.add('turn <player>', async function(cmd) {
|
||||||
const pieceType = type === 'cat' ? 'cat' : 'kitten';
|
const pieceType = type === 'cat' ? 'cat' : 'kitten';
|
||||||
|
|
||||||
placePiece(this.context, row, col, turnPlayer, pieceType);
|
placePiece(this.context, row, col, turnPlayer, pieceType);
|
||||||
await applyBoops(this.context, row, col, pieceType);
|
applyBoops(this.context, row, col, pieceType);
|
||||||
|
|
||||||
const graduatedLines = checkGraduation(this.context, turnPlayer);
|
const graduatedLines = checkGraduation(this.context, turnPlayer);
|
||||||
if (graduatedLines.length > 0) {
|
if (graduatedLines.length > 0) {
|
||||||
|
|
@ -138,18 +156,14 @@ registration.add('turn <player>', async function(cmd) {
|
||||||
'graduate <row:number> <col:number>',
|
'graduate <row:number> <col:number>',
|
||||||
(command) => {
|
(command) => {
|
||||||
const [row, col] = command.params as [number, number];
|
const [row, col] = command.params as [number, number];
|
||||||
const posKey = `${row},${col}`;
|
const part = availableKittens.find(p => p.position[0] === row && p.position[1] === col);
|
||||||
const part = availableKittens.find(p => `${p.position[0]},${p.position[1]}` === posKey);
|
|
||||||
if (!part) return `No kitten at (${row}, ${col}).`;
|
if (!part) return `No kitten at (${row}, ${col}).`;
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
this.context.value.currentPlayer
|
this.context.value.currentPlayer
|
||||||
);
|
);
|
||||||
const [row, col] = graduateCmd.params as [number, number];
|
const [row, col] = graduateCmd.params as [number, number];
|
||||||
const part = availableKittens.find(p => p.position[0] === row && p.position[1] === col)!;
|
graduatePiece(this.context, row, col, turnPlayer);
|
||||||
removePieceFromBoard(this.context, part);
|
|
||||||
const playerData = getPlayer(this.context, turnPlayer);
|
|
||||||
incrementSupply(playerData, 'cat', 1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -163,40 +177,59 @@ function isValidMove(row: number, col: number): boolean {
|
||||||
return !isNaN(row) && !isNaN(col) && row >= 0 && row < BOARD_SIZE && col >= 0 && col < BOARD_SIZE;
|
return !isNaN(row) && !isNaN(col) && row >= 0 && row < BOARD_SIZE && col >= 0 && col < BOARD_SIZE;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getBoardRegion(host: MutableSignal<BoopState>) {
|
export function getBoardRegion(host: MutableSignal<BoopStateType>) {
|
||||||
return host.value.board;
|
return host.value.board;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isCellOccupied(host: MutableSignal<BoopState>, row: number, col: number): boolean {
|
export function isCellOccupied(host: MutableSignal<BoopStateType>, row: number, col: number): boolean {
|
||||||
return isCellOccupiedUtil(host.value.pieces, 'board', [row, col]);
|
return isCellOccupiedUtil(host.value.pieces, 'board', [row, col]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPartAt(host: MutableSignal<BoopState>, row: number, col: number): BoopPart | null {
|
export function getPartAt(host: MutableSignal<BoopStateType>, row: number, col: number): BoopPart | null {
|
||||||
return getPartAtPosition(host.value.pieces, 'board', [row, col]) || null;
|
return getPartAtPosition(host.value.pieces, 'board', [row, col]) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function placePiece(host: MutableSignal<BoopState>, row: number, col: number, player: PlayerType, pieceType: PieceType) {
|
export function placePiece(host: MutableSignal<BoopStateType>, row: number, col: number, player: PlayerType, pieceType: PieceType) {
|
||||||
const board = getBoardRegion(host);
|
const board = getBoardRegion(host);
|
||||||
const playerData = getPlayer(host, player);
|
const playerData = getPlayer(host, player);
|
||||||
const count = playerData[pieceType].placed + 1;
|
|
||||||
|
|
||||||
const piece = createPart<{ player: PlayerType; pieceType: PieceType }>(
|
let piece: BoopPart;
|
||||||
{ regionId: 'board', position: [row, col], player, pieceType },
|
|
||||||
`${player}-${pieceType}-${count}`
|
if (pieceType === 'kitten') {
|
||||||
);
|
const drawn = playerData.kittenPool.draw();
|
||||||
|
if (!drawn) throw new Error(`No kitten available in ${player}'s supply`);
|
||||||
|
piece = drawn;
|
||||||
|
moveToRegion(piece, null, board, [row, col]);
|
||||||
|
} else {
|
||||||
|
// Try to use graduated count first
|
||||||
|
if (playerData.graduatedCount > 0) {
|
||||||
|
// Create a new cat piece (graduated)
|
||||||
|
const count = playerData.catPool.remaining() + playerData.graduatedCount;
|
||||||
|
piece = createPart<{ player: PlayerType; pieceType: PieceType }>(
|
||||||
|
{ regionId: 'board', position: [row, col], player, pieceType },
|
||||||
|
`${player}-cat-graduated-${count}`
|
||||||
|
);
|
||||||
|
playerData.graduatedCount--;
|
||||||
|
} else {
|
||||||
|
const drawn = playerData.catPool.draw();
|
||||||
|
if (!drawn) throw new Error(`No cat available in ${player}'s supply`);
|
||||||
|
piece = drawn;
|
||||||
|
moveToRegion(piece, null, board, [row, col]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
host.produce(s => {
|
host.produce(s => {
|
||||||
s.pieces[piece.id] = piece;
|
s.pieces[piece.id] = piece;
|
||||||
board.childIds.push(piece.id);
|
|
||||||
board.partMap[`${row},${col}`] = piece.id;
|
|
||||||
});
|
});
|
||||||
decrementSupply(playerData, pieceType);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function applyBoops(host: MutableSignal<BoopState>, placedRow: number, placedCol: number, placedType: PieceType) {
|
export function applyBoops(host: MutableSignal<BoopStateType>, placedRow: number, placedCol: number, placedType: PieceType) {
|
||||||
|
const board = getBoardRegion(host);
|
||||||
const pieces = host.value.pieces;
|
const pieces = host.value.pieces;
|
||||||
const piecesArray = Object.values(pieces);
|
const piecesArray = Object.values(pieces);
|
||||||
|
|
||||||
const piecesToBoop: { part: BoopPart; dr: number; dc: number }[] = [];
|
const piecesToBoop: { part: BoopPart; dr: number; dc: number }[] = [];
|
||||||
|
const piecesOffBoard: BoopPart[] = [];
|
||||||
|
|
||||||
for (const part of piecesArray) {
|
for (const part of piecesArray) {
|
||||||
const [r, c] = part.position;
|
const [r, c] = part.position;
|
||||||
|
|
@ -215,49 +248,66 @@ export async function applyBoops(host: MutableSignal<BoopState>, placedRow: numb
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await host.produceAsync(state => {
|
host.produce(state => {
|
||||||
const board = state.board;
|
|
||||||
const currentPieces = state.pieces;
|
|
||||||
|
|
||||||
for (const { part, dr, dc } of piecesToBoop) {
|
for (const { part, dr, dc } of piecesToBoop) {
|
||||||
const [r, c] = part.position;
|
const [r, c] = part.position;
|
||||||
const newRow = r + dr;
|
const newRow = r + dr;
|
||||||
const newCol = c + dc;
|
const newCol = c + dc;
|
||||||
|
|
||||||
if (newRow < 0 || newRow >= BOARD_SIZE || newCol < 0 || newCol >= BOARD_SIZE) {
|
if (newRow < 0 || newRow >= BOARD_SIZE || newCol < 0 || newCol >= BOARD_SIZE) {
|
||||||
const pt = part.pieceType;
|
// Mark for removal
|
||||||
const pl = part.player;
|
piecesOffBoard.push(part);
|
||||||
const playerData = state.players[pl];
|
delete state.pieces[part.id];
|
||||||
// Remove piece from board
|
state.board.childIds = state.board.childIds.filter(id => id !== part.id);
|
||||||
board.childIds = board.childIds.filter(id => id !== part.id);
|
|
||||||
delete board.partMap[part.position.join(',')];
|
|
||||||
delete currentPieces[part.id];
|
|
||||||
playerData[pt].placed--;
|
|
||||||
playerData[pt].supply++;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if target cell is occupied
|
if (isCellOccupied(host, newRow, newCol)) continue;
|
||||||
const targetPosKey = `${newRow},${newCol}`;
|
|
||||||
if (board.partMap[targetPosKey]) continue;
|
|
||||||
|
|
||||||
// Move piece to new position
|
moveToRegion(part, board, board, [newRow, newCol]);
|
||||||
delete board.partMap[part.position.join(',')];
|
|
||||||
part.position = [newRow, newCol];
|
|
||||||
board.partMap[targetPosKey] = part.id;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Return booped pieces to pools outside of produce
|
||||||
|
for (const part of piecesOffBoard) {
|
||||||
|
const playerData = getPlayer(host, part.player);
|
||||||
|
const pool = part.pieceType === 'kitten' ? playerData.kittenPool : playerData.catPool;
|
||||||
|
pool.return(part);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removePieceFromBoard(host: MutableSignal<BoopState>, part: BoopPart) {
|
export function graduatePiece(host: MutableSignal<BoopStateType>, row: number, col: number, player: PlayerType) {
|
||||||
|
const pieces = host.value.pieces;
|
||||||
|
const part = Object.values(pieces).find(p => p.player === player && p.pieceType === 'kitten' && p.position[0] === row && p.position[1] === col);
|
||||||
|
if (!part) return;
|
||||||
|
|
||||||
|
const board = getBoardRegion(host);
|
||||||
|
const playerData = getPlayer(host, player);
|
||||||
|
|
||||||
host.produce(state => {
|
host.produce(state => {
|
||||||
const board = state.board;
|
// Remove from board
|
||||||
const playerData = state.players[part.player];
|
|
||||||
board.childIds = board.childIds.filter(id => id !== part.id);
|
|
||||||
delete board.partMap[part.position.join(',')];
|
|
||||||
delete state.pieces[part.id];
|
delete state.pieces[part.id];
|
||||||
playerData[part.pieceType].placed--;
|
state.board.childIds = state.board.childIds.filter(id => id !== part.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Return kitten to supply
|
||||||
|
playerData.kittenPool.return(part);
|
||||||
|
|
||||||
|
// Increment graduated count (available cats to place)
|
||||||
|
playerData.graduatedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removePieceFromBoard(host: MutableSignal<BoopStateType>, part: BoopPart) {
|
||||||
|
const board = getBoardRegion(host);
|
||||||
|
const playerData = getPlayer(host, part.player);
|
||||||
|
const pool = part.pieceType === 'kitten' ? playerData.kittenPool : playerData.catPool;
|
||||||
|
|
||||||
|
host.produce(state => {
|
||||||
|
delete state.pieces[part.id];
|
||||||
|
state.board.childIds = state.board.childIds.filter(id => id !== part.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
pool.return(part);
|
||||||
}
|
}
|
||||||
|
|
||||||
const DIRECTIONS: [number, number][] = [
|
const DIRECTIONS: [number, number][] = [
|
||||||
|
|
@ -311,7 +361,7 @@ export function hasWinningLine(positions: number[][]): boolean {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function checkGraduation(host: MutableSignal<BoopState>, player: PlayerType): number[][][] {
|
export function checkGraduation(host: MutableSignal<BoopStateType>, player: PlayerType): number[][][] {
|
||||||
const pieces = host.value.pieces;
|
const pieces = host.value.pieces;
|
||||||
const piecesArray = Object.values(pieces);
|
const piecesArray = Object.values(pieces);
|
||||||
const posSet = new Set<string>();
|
const posSet = new Set<string>();
|
||||||
|
|
@ -331,7 +381,7 @@ export function checkGraduation(host: MutableSignal<BoopState>, player: PlayerTy
|
||||||
return winningLines;
|
return winningLines;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function processGraduation(host: MutableSignal<BoopState>, player: PlayerType, lines: number[][][]) {
|
export function processGraduation(host: MutableSignal<BoopStateType>, player: PlayerType, lines: number[][][]) {
|
||||||
const allPositions = new Set<string>();
|
const allPositions = new Set<string>();
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
for (const [r, c] of line) {
|
for (const [r, c] of line) {
|
||||||
|
|
@ -339,27 +389,22 @@ export function processGraduation(host: MutableSignal<BoopState>, player: Player
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const board = getBoardRegion(host);
|
|
||||||
const pieces = host.value.pieces;
|
const pieces = host.value.pieces;
|
||||||
const partsToRemove = Object.values(pieces).filter(
|
const partsToRemove = Object.values(pieces).filter(
|
||||||
p => p.player === player && p.pieceType === 'kitten' && allPositions.has(`${p.position[0]},${p.position[1]}`)
|
p => p.player === player && p.pieceType === 'kitten' && allPositions.has(`${p.position[0]},${p.position[1]}`)
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const part of partsToRemove) {
|
for (const part of partsToRemove) {
|
||||||
removePieceFromBoard(host, part);
|
graduatePiece(host, part.position[0], part.position[1], player);
|
||||||
}
|
}
|
||||||
|
|
||||||
const count = partsToRemove.length;
|
|
||||||
const playerData = getPlayer(host, player);
|
|
||||||
incrementSupply(playerData, 'cat', count);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function countPiecesOnBoard(host: MutableSignal<BoopState>, player: PlayerType): number {
|
export function countPiecesOnBoard(host: MutableSignal<BoopStateType>, player: PlayerType): number {
|
||||||
const pieces = host.value.pieces;
|
const pieces = host.value.pieces;
|
||||||
return Object.values(pieces).filter(p => p.player === player).length;
|
return Object.values(pieces).filter(p => p.player === player).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function checkWinner(host: MutableSignal<BoopState>): WinnerType {
|
export function checkWinner(host: MutableSignal<BoopStateType>): WinnerType {
|
||||||
const pieces = host.value.pieces;
|
const pieces = host.value.pieces;
|
||||||
const piecesArray = Object.values(pieces);
|
const piecesArray = Object.values(pieces);
|
||||||
|
|
||||||
|
|
@ -375,7 +420,7 @@ export function checkWinner(host: MutableSignal<BoopState>): WinnerType {
|
||||||
|
|
||||||
// 命令构建器
|
// 命令构建器
|
||||||
export const commands = {
|
export const commands = {
|
||||||
play: (player: PlayerType, row: number, col: number, type?: PieceType) =>
|
play: (player: PlayerType, row: number, col: number, type?: PieceType) =>
|
||||||
`play ${player} ${row} ${col}${type ? ` ${type}` : ''}`,
|
`play ${player} ${row} ${col}${type ? ` ${type}` : ''}`,
|
||||||
turn: (player: PlayerType) => `turn ${player}`,
|
turn: (player: PlayerType) => `turn ${player}`,
|
||||||
graduate: (row: number, col: number) => `graduate ${row} ${col}`,
|
graduate: (row: number, col: number) => `graduate ${row} ${col}`,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
import Phaser from 'phaser';
|
||||||
|
import type { BoopState, PlayerType } from '@/game/boop';
|
||||||
|
import type { ReadonlySignal } from '@preact/signals-core';
|
||||||
|
|
||||||
|
const BOARD_SIZE = 6;
|
||||||
|
const CELL_SIZE = 80;
|
||||||
|
const BOARD_OFFSET = { x: 80, y: 100 };
|
||||||
|
|
||||||
|
export { BOARD_SIZE, CELL_SIZE, BOARD_OFFSET };
|
||||||
|
|
||||||
|
export class BoardRenderer {
|
||||||
|
private container: Phaser.GameObjects.Container;
|
||||||
|
private gridGraphics: Phaser.GameObjects.Graphics;
|
||||||
|
private turnText: Phaser.GameObjects.Text;
|
||||||
|
private infoText: Phaser.GameObjects.Text;
|
||||||
|
|
||||||
|
constructor(private scene: Phaser.Scene) {
|
||||||
|
this.container = this.scene.add.container(0, 0);
|
||||||
|
this.gridGraphics = this.scene.add.graphics();
|
||||||
|
this.drawGrid();
|
||||||
|
|
||||||
|
this.turnText = this.scene.add.text(
|
||||||
|
BOARD_OFFSET.x + (BOARD_SIZE * CELL_SIZE) / 2,
|
||||||
|
BOARD_OFFSET.y + BOARD_SIZE * CELL_SIZE + 30,
|
||||||
|
'',
|
||||||
|
{
|
||||||
|
fontSize: '22px',
|
||||||
|
fontFamily: 'Arial',
|
||||||
|
color: '#4b5563',
|
||||||
|
}
|
||||||
|
).setOrigin(0.5);
|
||||||
|
|
||||||
|
this.infoText = this.scene.add.text(
|
||||||
|
BOARD_OFFSET.x + (BOARD_SIZE * CELL_SIZE) / 2,
|
||||||
|
BOARD_OFFSET.y + BOARD_SIZE * CELL_SIZE + 60,
|
||||||
|
'Click to place kitten. Cats win with 3 in a row!',
|
||||||
|
{
|
||||||
|
fontSize: '16px',
|
||||||
|
fontFamily: 'Arial',
|
||||||
|
color: '#6b7280',
|
||||||
|
}
|
||||||
|
).setOrigin(0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawGrid(): void {
|
||||||
|
const g = this.gridGraphics;
|
||||||
|
g.lineStyle(2, 0x6b7280);
|
||||||
|
|
||||||
|
for (let i = 1; i < BOARD_SIZE; i++) {
|
||||||
|
g.lineBetween(
|
||||||
|
BOARD_OFFSET.x + i * CELL_SIZE,
|
||||||
|
BOARD_OFFSET.y,
|
||||||
|
BOARD_OFFSET.x + i * CELL_SIZE,
|
||||||
|
BOARD_OFFSET.y + BOARD_SIZE * CELL_SIZE,
|
||||||
|
);
|
||||||
|
g.lineBetween(
|
||||||
|
BOARD_OFFSET.x,
|
||||||
|
BOARD_OFFSET.y + i * CELL_SIZE,
|
||||||
|
BOARD_OFFSET.x + BOARD_SIZE * CELL_SIZE,
|
||||||
|
BOARD_OFFSET.y + i * CELL_SIZE,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
g.strokePath();
|
||||||
|
|
||||||
|
this.scene.add.text(BOARD_OFFSET.x + (BOARD_SIZE * CELL_SIZE) / 2, BOARD_OFFSET.y - 50, 'Boop Game', {
|
||||||
|
fontSize: '32px',
|
||||||
|
fontFamily: 'Arial',
|
||||||
|
color: '#1f2937',
|
||||||
|
}).setOrigin(0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTurnText(player: PlayerType, state: BoopState): void {
|
||||||
|
const current = player === 'white' ? state.players.white : state.players.black;
|
||||||
|
const catsAvailable = current.catPool.remaining() + current.graduatedCount;
|
||||||
|
|
||||||
|
this.turnText.setText(
|
||||||
|
`${player.toUpperCase()}'s turn | Kittens: ${current.kittenPool.remaining()} | Cats: ${catsAvailable}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setupInput(
|
||||||
|
state: ReadonlySignal<BoopState>,
|
||||||
|
onCellClick: (row: number, col: number) => void
|
||||||
|
): void {
|
||||||
|
for (let row = 0; row < BOARD_SIZE; row++) {
|
||||||
|
for (let col = 0; col < BOARD_SIZE; col++) {
|
||||||
|
const x = BOARD_OFFSET.x + col * CELL_SIZE + CELL_SIZE / 2;
|
||||||
|
const y = BOARD_OFFSET.y + row * CELL_SIZE + CELL_SIZE / 2;
|
||||||
|
|
||||||
|
const zone = this.scene.add.zone(x, y, CELL_SIZE, CELL_SIZE).setInteractive();
|
||||||
|
|
||||||
|
zone.on('pointerdown', () => {
|
||||||
|
const isOccupied = !!state.value.board.partMap[`${row},${col}`];
|
||||||
|
if (!isOccupied && !state.value.winner) {
|
||||||
|
onCellClick(row, col);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
this.container.destroy();
|
||||||
|
this.gridGraphics.destroy();
|
||||||
|
this.turnText.destroy();
|
||||||
|
this.infoText.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,31 +1,18 @@
|
||||||
import Phaser from 'phaser';
|
import Phaser from 'phaser';
|
||||||
import type {BoopState, BoopPart, PlayerType, PieceType} from '@/game/boop';
|
import type { BoopState, PlayerType, PieceType } from '@/game/boop';
|
||||||
import { GameHostScene } from 'boardgame-phaser';
|
import { GameHostScene } from 'boardgame-phaser';
|
||||||
import { spawnEffect, type Spawner } from 'boardgame-phaser';
|
import { commands } from '@/game/boop';
|
||||||
import type { ReadonlySignal } from '@preact/signals-core';
|
import { BoardRenderer } from './BoardRenderer';
|
||||||
import {commands} from "@/game/boop";
|
import { createPieceSpawner } from './PieceSpawner';
|
||||||
import {MutableSignal} from "boardgame-core";
|
import { SupplyUI } from './SupplyUI';
|
||||||
|
import { PieceTypeSelector } from './PieceTypeSelector';
|
||||||
const BOARD_SIZE = 6;
|
import { WinnerOverlay } from './WinnerOverlay';
|
||||||
const CELL_SIZE = 80;
|
|
||||||
const BOARD_OFFSET = { x: 80, y: 100 };
|
|
||||||
|
|
||||||
export class GameScene extends GameHostScene<BoopState> {
|
export class GameScene extends GameHostScene<BoopState> {
|
||||||
private boardContainer!: Phaser.GameObjects.Container;
|
private boardRenderer!: BoardRenderer;
|
||||||
private gridGraphics!: Phaser.GameObjects.Graphics;
|
private supplyUI!: SupplyUI;
|
||||||
private turnText!: Phaser.GameObjects.Text;
|
private pieceTypeSelector!: PieceTypeSelector;
|
||||||
private infoText!: Phaser.GameObjects.Text;
|
private winnerOverlay!: WinnerOverlay;
|
||||||
private winnerOverlay?: Phaser.GameObjects.Container;
|
|
||||||
private whiteSupplyContainer!: Phaser.GameObjects.Container;
|
|
||||||
private blackSupplyContainer!: Phaser.GameObjects.Container;
|
|
||||||
private whiteSupplyBg!: Phaser.GameObjects.Rectangle;
|
|
||||||
private blackSupplyBg!: Phaser.GameObjects.Rectangle;
|
|
||||||
private whiteSupplyText!: Phaser.GameObjects.Text;
|
|
||||||
private blackSupplyText!: Phaser.GameObjects.Text;
|
|
||||||
private pieceTypeSelector!: Phaser.GameObjects.Container;
|
|
||||||
private selectedPieceType: PieceType = 'kitten';
|
|
||||||
private kittenButton!: Phaser.GameObjects.Container;
|
|
||||||
private catButton!: Phaser.GameObjects.Container;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super('GameScene');
|
super('GameScene');
|
||||||
|
|
@ -34,437 +21,51 @@ export class GameScene extends GameHostScene<BoopState> {
|
||||||
create(): void {
|
create(): void {
|
||||||
super.create();
|
super.create();
|
||||||
|
|
||||||
this.boardContainer = this.add.container(0, 0);
|
// 初始化 UI 组件
|
||||||
this.gridGraphics = this.add.graphics();
|
this.boardRenderer = new BoardRenderer(this);
|
||||||
this.drawGrid();
|
this.supplyUI = new SupplyUI(this);
|
||||||
this.createSupplyUI();
|
this.pieceTypeSelector = new PieceTypeSelector(this);
|
||||||
|
this.winnerOverlay = new WinnerOverlay(this, () => this.restartGame());
|
||||||
|
|
||||||
this.disposables.add(spawnEffect(new BoopPartSpawner(this)));
|
// 设置棋子生成器
|
||||||
|
this.disposables.add(createPieceSpawner(this));
|
||||||
|
|
||||||
|
// 设置输入处理
|
||||||
|
this.boardRenderer.setupInput(this.gameHost.state, (row, col) => {
|
||||||
|
this.handleCellClick(row, col);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听状态变化
|
||||||
this.watch(() => {
|
this.watch(() => {
|
||||||
const winner = this.state.winner;
|
const winner = this.state.winner;
|
||||||
if (winner) {
|
if (winner) {
|
||||||
this.showWinner(winner);
|
this.winnerOverlay.show(winner);
|
||||||
} else if (this.winnerOverlay) {
|
} else {
|
||||||
this.winnerOverlay.destroy();
|
this.winnerOverlay.hide();
|
||||||
this.winnerOverlay = undefined;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.watch(() => {
|
this.watch(() => {
|
||||||
const currentPlayer = this.state.currentPlayer;
|
const currentPlayer = this.state.currentPlayer;
|
||||||
this.updateTurnText(currentPlayer);
|
this.boardRenderer.updateTurnText(currentPlayer, this.state);
|
||||||
this.updateSupplyUI();
|
this.supplyUI.update(this.state);
|
||||||
this.updatePieceTypeSelector();
|
this.pieceTypeSelector.update(this.state);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.createPieceTypeSelector();
|
// 设置棋子类型选择器回调
|
||||||
this.setupInput();
|
// 可以在这里添加类型改变时的额外逻辑
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupInput(): void {
|
private handleCellClick(row: number, col: number): void {
|
||||||
for (let row = 0; row < BOARD_SIZE; row++) {
|
const selectedType = this.pieceTypeSelector.getSelectedType();
|
||||||
for (let col = 0; col < BOARD_SIZE; col++) {
|
const cmd = commands.play(this.state.currentPlayer, row, col, selectedType);
|
||||||
const x = BOARD_OFFSET.x + col * CELL_SIZE + CELL_SIZE / 2;
|
const error = this.gameHost.onInput(cmd);
|
||||||
const y = BOARD_OFFSET.y + row * CELL_SIZE + CELL_SIZE / 2;
|
if (error) {
|
||||||
|
console.warn('Invalid move:', error);
|
||||||
const zone = this.add.zone(x, y, CELL_SIZE, CELL_SIZE).setInteractive();
|
|
||||||
|
|
||||||
zone.on('pointerdown', () => {
|
|
||||||
if (this.state.winner) return;
|
|
||||||
if (this.isCellOccupied(row, col)) return;
|
|
||||||
|
|
||||||
const cmd = commands.play(this.state.currentPlayer, row, col, this.selectedPieceType);
|
|
||||||
const error = this.gameHost.onInput(cmd);
|
|
||||||
if (error) {
|
|
||||||
console.warn('Invalid move:', error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private drawGrid(): void {
|
private restartGame(): void {
|
||||||
const g = this.gridGraphics;
|
this.gameHost.setup('setup');
|
||||||
g.lineStyle(2, 0x6b7280);
|
|
||||||
|
|
||||||
for (let i = 1; i < BOARD_SIZE; i++) {
|
|
||||||
g.lineBetween(
|
|
||||||
BOARD_OFFSET.x + i * CELL_SIZE,
|
|
||||||
BOARD_OFFSET.y,
|
|
||||||
BOARD_OFFSET.x + i * CELL_SIZE,
|
|
||||||
BOARD_OFFSET.y + BOARD_SIZE * CELL_SIZE,
|
|
||||||
);
|
|
||||||
g.lineBetween(
|
|
||||||
BOARD_OFFSET.x,
|
|
||||||
BOARD_OFFSET.y + i * CELL_SIZE,
|
|
||||||
BOARD_OFFSET.x + BOARD_SIZE * CELL_SIZE,
|
|
||||||
BOARD_OFFSET.y + i * CELL_SIZE,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
g.strokePath();
|
|
||||||
|
|
||||||
this.add.text(BOARD_OFFSET.x + (BOARD_SIZE * CELL_SIZE) / 2, BOARD_OFFSET.y - 50, 'Boop Game', {
|
|
||||||
fontSize: '32px',
|
|
||||||
fontFamily: 'Arial',
|
|
||||||
color: '#1f2937',
|
|
||||||
}).setOrigin(0.5);
|
|
||||||
|
|
||||||
this.turnText = this.add.text(
|
|
||||||
BOARD_OFFSET.x + (BOARD_SIZE * CELL_SIZE) / 2,
|
|
||||||
BOARD_OFFSET.y + BOARD_SIZE * CELL_SIZE + 30,
|
|
||||||
'',
|
|
||||||
{
|
|
||||||
fontSize: '22px',
|
|
||||||
fontFamily: 'Arial',
|
|
||||||
color: '#4b5563',
|
|
||||||
}
|
|
||||||
).setOrigin(0.5);
|
|
||||||
|
|
||||||
this.infoText = this.add.text(
|
|
||||||
BOARD_OFFSET.x + (BOARD_SIZE * CELL_SIZE) / 2,
|
|
||||||
BOARD_OFFSET.y + BOARD_SIZE * CELL_SIZE + 60,
|
|
||||||
'Click to place kitten. Cats win with 3 in a row!',
|
|
||||||
{
|
|
||||||
fontSize: '16px',
|
|
||||||
fontFamily: 'Arial',
|
|
||||||
color: '#6b7280',
|
|
||||||
}
|
|
||||||
).setOrigin(0.5);
|
|
||||||
|
|
||||||
this.updateTurnText(this.state.currentPlayer);
|
|
||||||
}
|
|
||||||
|
|
||||||
private createSupplyUI(): void {
|
|
||||||
const boardCenterX = BOARD_OFFSET.x + (BOARD_SIZE * CELL_SIZE) / 2;
|
|
||||||
const uiY = BOARD_OFFSET.y - 20;
|
|
||||||
|
|
||||||
// 白色玩家容器
|
|
||||||
this.whiteSupplyContainer = this.add.container(boardCenterX - 150, uiY);
|
|
||||||
this.whiteSupplyBg = this.add.rectangle(0, 0, 120, 50, 0x000000);
|
|
||||||
this.whiteSupplyText = this.add.text(0, 0, '', {
|
|
||||||
fontSize: '16px',
|
|
||||||
fontFamily: 'Arial',
|
|
||||||
color: '#ffffff',
|
|
||||||
align: 'center',
|
|
||||||
}).setOrigin(0.5);
|
|
||||||
this.whiteSupplyContainer.add([this.whiteSupplyBg, this.whiteSupplyText]);
|
|
||||||
this.whiteSupplyContainer.setDepth(100);
|
|
||||||
|
|
||||||
// 黑色玩家容器
|
|
||||||
this.blackSupplyContainer = this.add.container(boardCenterX + 150, uiY);
|
|
||||||
this.blackSupplyBg = this.add.rectangle(0, 0, 120, 50, 0x333333);
|
|
||||||
this.blackSupplyText = this.add.text(0, 0, '', {
|
|
||||||
fontSize: '16px',
|
|
||||||
fontFamily: 'Arial',
|
|
||||||
color: '#ffffff',
|
|
||||||
align: 'center',
|
|
||||||
}).setOrigin(0.5);
|
|
||||||
this.blackSupplyContainer.add([this.blackSupplyBg, this.blackSupplyText]);
|
|
||||||
this.blackSupplyContainer.setDepth(100);
|
|
||||||
|
|
||||||
this.updateSupplyUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateSupplyUI(): void {
|
|
||||||
const white = this.state.players.white;
|
|
||||||
const black = this.state.players.black;
|
|
||||||
|
|
||||||
this.whiteSupplyText.setText(
|
|
||||||
`⚪ WHITE\n🐾 ${white.kitten.supply} | 🐱 ${white.cat.supply}`
|
|
||||||
);
|
|
||||||
|
|
||||||
this.blackSupplyText.setText(
|
|
||||||
`⚫ BLACK\n🐾 ${black.kitten.supply} | 🐱 ${black.cat.supply}`
|
|
||||||
);
|
|
||||||
|
|
||||||
// 高亮当前玩家(使用动画)
|
|
||||||
const isWhiteTurn = this.state.currentPlayer === 'white';
|
|
||||||
|
|
||||||
// 停止之前的动画
|
|
||||||
this.tweens.killTweensOf(this.whiteSupplyContainer);
|
|
||||||
this.tweens.killTweensOf(this.blackSupplyContainer);
|
|
||||||
|
|
||||||
if (isWhiteTurn) {
|
|
||||||
// 白色玩家弹跳 + 脉冲
|
|
||||||
this.whiteSupplyBg.setFillStyle(0xfbbf24);
|
|
||||||
this.whiteSupplyContainer.setScale(1);
|
|
||||||
this.tweens.add({
|
|
||||||
targets: this.whiteSupplyContainer,
|
|
||||||
scale: 1.15,
|
|
||||||
duration: 200,
|
|
||||||
ease: 'Back.easeOut',
|
|
||||||
yoyo: true,
|
|
||||||
hold: 400,
|
|
||||||
onComplete: () => {
|
|
||||||
this.tweens.add({
|
|
||||||
targets: this.whiteSupplyContainer,
|
|
||||||
alpha: 0.6,
|
|
||||||
duration: 600,
|
|
||||||
ease: 'Sine.easeInOut',
|
|
||||||
yoyo: true,
|
|
||||||
repeat: -1,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.blackSupplyBg.setFillStyle(0x333333);
|
|
||||||
this.blackSupplyContainer.setAlpha(1);
|
|
||||||
this.blackSupplyContainer.setScale(1);
|
|
||||||
} else {
|
|
||||||
// 黑色玩家弹跳 + 脉冲
|
|
||||||
this.blackSupplyBg.setFillStyle(0xfbbf24);
|
|
||||||
this.blackSupplyContainer.setScale(1);
|
|
||||||
this.tweens.add({
|
|
||||||
targets: this.blackSupplyContainer,
|
|
||||||
scale: 1.15,
|
|
||||||
duration: 200,
|
|
||||||
ease: 'Back.easeOut',
|
|
||||||
yoyo: true,
|
|
||||||
hold: 400,
|
|
||||||
onComplete: () => {
|
|
||||||
this.tweens.add({
|
|
||||||
targets: this.blackSupplyContainer,
|
|
||||||
alpha: 0.6,
|
|
||||||
duration: 600,
|
|
||||||
ease: 'Sine.easeInOut',
|
|
||||||
yoyo: true,
|
|
||||||
repeat: -1,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.whiteSupplyBg.setFillStyle(0x000000);
|
|
||||||
this.whiteSupplyContainer.setAlpha(1);
|
|
||||||
this.whiteSupplyContainer.setScale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private createPieceTypeSelector(): void {
|
|
||||||
const boardCenterX = BOARD_OFFSET.x + (BOARD_SIZE * CELL_SIZE) / 2;
|
|
||||||
const selectorY = BOARD_OFFSET.y + BOARD_SIZE * CELL_SIZE + 100;
|
|
||||||
|
|
||||||
this.pieceTypeSelector = this.add.container(boardCenterX, selectorY);
|
|
||||||
|
|
||||||
// 标签文字
|
|
||||||
const label = this.add.text(-160, 0, '放置:', {
|
|
||||||
fontSize: '18px',
|
|
||||||
fontFamily: 'Arial',
|
|
||||||
color: '#4b5563',
|
|
||||||
}).setOrigin(0.5, 0.5);
|
|
||||||
|
|
||||||
// 小猫按钮
|
|
||||||
this.kittenButton = this.createPieceButton('kitten', '🐾 小猫', -50);
|
|
||||||
// 大猫按钮
|
|
||||||
this.catButton = this.createPieceButton('cat', '🐱 大猫', 70);
|
|
||||||
|
|
||||||
this.pieceTypeSelector.add([label, this.kittenButton, this.catButton]);
|
|
||||||
this.updatePieceTypeSelector();
|
|
||||||
}
|
|
||||||
|
|
||||||
private createPieceButton(type: PieceType, text: string, xOffset: number): Phaser.GameObjects.Container {
|
|
||||||
const container = this.add.container(xOffset, 0);
|
|
||||||
|
|
||||||
const bg = this.add.rectangle(0, 0, 100, 40, 0xe5e7eb)
|
|
||||||
.setStrokeStyle(2, 0x9ca3af);
|
|
||||||
|
|
||||||
const textObj = this.add.text(0, 0, text, {
|
|
||||||
fontSize: '16px',
|
|
||||||
fontFamily: 'Arial',
|
|
||||||
color: '#1f2937',
|
|
||||||
}).setOrigin(0.5);
|
|
||||||
|
|
||||||
container.add([bg, textObj]);
|
|
||||||
|
|
||||||
// 使按钮可交互
|
|
||||||
bg.setInteractive({ useHandCursor: true });
|
|
||||||
bg.on('pointerdown', () => {
|
|
||||||
this.selectedPieceType = type;
|
|
||||||
this.updatePieceTypeSelector();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 存储引用以便后续更新
|
|
||||||
if (type === 'kitten') {
|
|
||||||
this.kittenButton = container;
|
|
||||||
} else {
|
|
||||||
this.catButton = container;
|
|
||||||
}
|
|
||||||
|
|
||||||
return container;
|
|
||||||
}
|
|
||||||
|
|
||||||
private updatePieceTypeSelector(): void {
|
|
||||||
if (!this.kittenButton || !this.catButton) return;
|
|
||||||
|
|
||||||
const white = this.state.players.white;
|
|
||||||
const black = this.state.players.black;
|
|
||||||
const currentPlayer = this.state.players[this.state.currentPlayer];
|
|
||||||
const isWhiteTurn = this.state.currentPlayer === 'white';
|
|
||||||
|
|
||||||
// 更新按钮状态
|
|
||||||
const kittenAvailable = currentPlayer.kitten.supply > 0;
|
|
||||||
const catAvailable = currentPlayer.cat.supply > 0;
|
|
||||||
|
|
||||||
// 更新小猫按钮
|
|
||||||
const kittenBg = this.kittenButton.list[0] as Phaser.GameObjects.Rectangle;
|
|
||||||
const kittenText = this.kittenButton.list[1] as Phaser.GameObjects.Text;
|
|
||||||
const isKittenSelected = this.selectedPieceType === 'kitten';
|
|
||||||
|
|
||||||
kittenBg.setFillStyle(isKittenSelected ? 0xfbbf24 : (kittenAvailable ? 0xe5e7eb : 0xd1d5db));
|
|
||||||
kittenText.setText(`🐾 小猫 (${currentPlayer.kitten.supply})`);
|
|
||||||
|
|
||||||
// 更新大猫按钮
|
|
||||||
const catBg = this.catButton.list[0] as Phaser.GameObjects.Rectangle;
|
|
||||||
const catText = this.catButton.list[1] as Phaser.GameObjects.Text;
|
|
||||||
const isCatSelected = this.selectedPieceType === 'cat';
|
|
||||||
|
|
||||||
catBg.setFillStyle(isCatSelected ? 0xfbbf24 : (catAvailable ? 0xe5e7eb : 0xd1d5db));
|
|
||||||
catText.setText(`🐱 大猫 (${currentPlayer.cat.supply})`);
|
|
||||||
|
|
||||||
// 如果选中的类型不可用,切换到可用的
|
|
||||||
if (!kittenAvailable && this.selectedPieceType === 'kitten' && catAvailable) {
|
|
||||||
this.selectedPieceType = 'cat';
|
|
||||||
this.updatePieceTypeSelector();
|
|
||||||
} else if (!catAvailable && this.selectedPieceType === 'cat' && kittenAvailable) {
|
|
||||||
this.selectedPieceType = 'kitten';
|
|
||||||
this.updatePieceTypeSelector();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateTurnText(player: PlayerType): void {
|
|
||||||
if (this.turnText) {
|
|
||||||
const whitePieces = this.state.players.white;
|
|
||||||
const blackPieces = this.state.players.black;
|
|
||||||
const current = player === 'white' ? whitePieces : blackPieces;
|
|
||||||
|
|
||||||
this.turnText.setText(
|
|
||||||
`${player.toUpperCase()}'s turn | Kittens: ${current.kitten.supply} | Cats: ${current.cat.supply}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private showWinner(winner: PlayerType | 'draw' | null): void {
|
|
||||||
if (this.winnerOverlay) {
|
|
||||||
this.winnerOverlay.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.winnerOverlay = this.add.container();
|
|
||||||
|
|
||||||
const text = winner === 'draw' ? "It's a draw!" : winner ? `${winner.toUpperCase()} wins!` : '';
|
|
||||||
|
|
||||||
const bg = this.add.rectangle(
|
|
||||||
BOARD_OFFSET.x + (BOARD_SIZE * CELL_SIZE) / 2,
|
|
||||||
BOARD_OFFSET.y + (BOARD_SIZE * CELL_SIZE) / 2,
|
|
||||||
BOARD_SIZE * CELL_SIZE,
|
|
||||||
BOARD_SIZE * CELL_SIZE,
|
|
||||||
0x000000,
|
|
||||||
0.6,
|
|
||||||
).setInteractive({ useHandCursor: true });
|
|
||||||
|
|
||||||
bg.on('pointerdown', () => {
|
|
||||||
this.gameHost.setup('setup');
|
|
||||||
});
|
|
||||||
|
|
||||||
this.winnerOverlay.add(bg);
|
|
||||||
|
|
||||||
const winText = this.add.text(
|
|
||||||
BOARD_OFFSET.x + (BOARD_SIZE * CELL_SIZE) / 2,
|
|
||||||
BOARD_OFFSET.y + (BOARD_SIZE * CELL_SIZE) / 2,
|
|
||||||
text,
|
|
||||||
{
|
|
||||||
fontSize: '40px',
|
|
||||||
fontFamily: 'Arial',
|
|
||||||
color: '#fbbf24',
|
|
||||||
},
|
|
||||||
).setOrigin(0.5);
|
|
||||||
|
|
||||||
this.winnerOverlay.add(winText);
|
|
||||||
|
|
||||||
this.tweens.add({
|
|
||||||
targets: winText,
|
|
||||||
scale: 1.2,
|
|
||||||
duration: 500,
|
|
||||||
yoyo: true,
|
|
||||||
repeat: 1,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private isCellOccupied(row: number, col: number): boolean {
|
|
||||||
return !!this.state.board.partMap[`${row},${col}`];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class BoopPartSpawner implements Spawner<BoopPart, Phaser.GameObjects.Container> {
|
|
||||||
constructor(public readonly scene: GameHostScene<BoopState>) {}
|
|
||||||
|
|
||||||
*getData() {
|
|
||||||
for (const part of Object.values(this.scene.state.pieces)) {
|
|
||||||
yield part;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getKey(part: BoopPart): string {
|
|
||||||
return part.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
onUpdate(part: BoopPart, obj: Phaser.GameObjects.Container): void {
|
|
||||||
const [row, col] = part.position;
|
|
||||||
const x = BOARD_OFFSET.x + col * CELL_SIZE + CELL_SIZE / 2;
|
|
||||||
const y = BOARD_OFFSET.y + row * CELL_SIZE + CELL_SIZE / 2;
|
|
||||||
|
|
||||||
// 使用 tween 动画平滑移动棋子
|
|
||||||
this.scene.tweens.add({
|
|
||||||
targets: obj,
|
|
||||||
x: x,
|
|
||||||
y: y,
|
|
||||||
duration: 200,
|
|
||||||
ease: 'Power2',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onSpawn(part: BoopPart) {
|
|
||||||
const [row, col] = part.position;
|
|
||||||
const x = BOARD_OFFSET.x + col * CELL_SIZE + CELL_SIZE / 2;
|
|
||||||
const y = BOARD_OFFSET.y + row * CELL_SIZE + CELL_SIZE / 2;
|
|
||||||
|
|
||||||
const container = this.scene.add.container(x, y);
|
|
||||||
|
|
||||||
const isCat = part.pieceType === 'cat';
|
|
||||||
const baseColor = part.player === 'white' ? 0xffffff : 0x333333;
|
|
||||||
const strokeColor = part.player === 'white' ? 0x000000 : 0xffffff;
|
|
||||||
|
|
||||||
// 绘制圆形背景
|
|
||||||
const circle = this.scene.add.circle(0, 0, CELL_SIZE * 0.4, baseColor)
|
|
||||||
.setStrokeStyle(3, strokeColor);
|
|
||||||
|
|
||||||
// 添加文字标识
|
|
||||||
const text = isCat ? '🐱' : '🐾';
|
|
||||||
const textObj = this.scene.add.text(0, 0, text, {
|
|
||||||
fontSize: `${isCat ? 40 : 32}px`,
|
|
||||||
fontFamily: 'Arial',
|
|
||||||
}).setOrigin(0.5);
|
|
||||||
|
|
||||||
container.add([circle, textObj]);
|
|
||||||
|
|
||||||
// 添加落子动画
|
|
||||||
container.setScale(0);
|
|
||||||
this.scene.addTweenInterruption(this.scene.tweens.add({
|
|
||||||
targets: container,
|
|
||||||
scale: 1,
|
|
||||||
duration: 200,
|
|
||||||
ease: 'Back.easeOut',
|
|
||||||
}));
|
|
||||||
|
|
||||||
return container;
|
|
||||||
}
|
|
||||||
|
|
||||||
onDespawn(obj: Phaser.GameObjects.Container) {
|
|
||||||
this.scene.tweens.add({
|
|
||||||
targets: obj,
|
|
||||||
alpha: 0,
|
|
||||||
scale: 0.5,
|
|
||||||
duration: 200,
|
|
||||||
ease: 'Back.easeIn',
|
|
||||||
onComplete: () => obj.destroy(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
import Phaser from 'phaser';
|
||||||
|
import type { BoopState, BoopPart } from '@/game/boop';
|
||||||
|
import { GameHostScene, spawnEffect, type Spawner } from 'boardgame-phaser';
|
||||||
|
import { BOARD_OFFSET, CELL_SIZE } from './BoardRenderer';
|
||||||
|
|
||||||
|
class BoopPartSpawner implements Spawner<BoopPart, Phaser.GameObjects.Container> {
|
||||||
|
constructor(public readonly scene: GameHostScene<BoopState>) {}
|
||||||
|
|
||||||
|
*getData() {
|
||||||
|
for (const part of Object.values(this.scene.state.pieces)) {
|
||||||
|
yield part;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getKey(part: BoopPart): string {
|
||||||
|
return part.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
onUpdate(part: BoopPart, obj: Phaser.GameObjects.Container): void {
|
||||||
|
const [row, col] = part.position;
|
||||||
|
const x = BOARD_OFFSET.x + col * CELL_SIZE + CELL_SIZE / 2;
|
||||||
|
const y = BOARD_OFFSET.y + row * CELL_SIZE + CELL_SIZE / 2;
|
||||||
|
|
||||||
|
this.scene.tweens.add({
|
||||||
|
targets: obj,
|
||||||
|
x: x,
|
||||||
|
y: y,
|
||||||
|
duration: 200,
|
||||||
|
ease: 'Power2',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onSpawn(part: BoopPart) {
|
||||||
|
const [row, col] = part.position;
|
||||||
|
const x = BOARD_OFFSET.x + col * CELL_SIZE + CELL_SIZE / 2;
|
||||||
|
const y = BOARD_OFFSET.y + row * CELL_SIZE + CELL_SIZE / 2;
|
||||||
|
|
||||||
|
const container = this.scene.add.container(x, y);
|
||||||
|
|
||||||
|
const isCat = part.pieceType === 'cat';
|
||||||
|
const baseColor = part.player === 'white' ? 0xffffff : 0x333333;
|
||||||
|
const strokeColor = part.player === 'white' ? 0x000000 : 0xffffff;
|
||||||
|
|
||||||
|
const circle = this.scene.add.circle(0, 0, CELL_SIZE * 0.4, baseColor)
|
||||||
|
.setStrokeStyle(3, strokeColor);
|
||||||
|
|
||||||
|
const text = isCat ? '🐱' : '🐾';
|
||||||
|
const textObj = this.scene.add.text(0, 0, text, {
|
||||||
|
fontSize: `${isCat ? 40 : 32}px`,
|
||||||
|
fontFamily: 'Arial',
|
||||||
|
}).setOrigin(0.5);
|
||||||
|
|
||||||
|
container.add([circle, textObj]);
|
||||||
|
|
||||||
|
container.setScale(0);
|
||||||
|
this.scene.addTweenInterruption(this.scene.tweens.add({
|
||||||
|
targets: container,
|
||||||
|
scale: 1,
|
||||||
|
duration: 200,
|
||||||
|
ease: 'Back.easeOut',
|
||||||
|
}));
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
onDespawn(obj: Phaser.GameObjects.Container) {
|
||||||
|
this.scene.tweens.add({
|
||||||
|
targets: obj,
|
||||||
|
alpha: 0,
|
||||||
|
scale: 0.5,
|
||||||
|
duration: 200,
|
||||||
|
ease: 'Back.easeIn',
|
||||||
|
onComplete: () => obj.destroy(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createPieceSpawner(scene: GameHostScene<BoopState>) {
|
||||||
|
return spawnEffect(new BoopPartSpawner(scene));
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
import Phaser from 'phaser';
|
||||||
|
import type { BoopState, PlayerType, PieceType } from '@/game/boop';
|
||||||
|
import { BOARD_OFFSET, CELL_SIZE, BOARD_SIZE } from './BoardRenderer';
|
||||||
|
|
||||||
|
export class PieceTypeSelector {
|
||||||
|
private container: Phaser.GameObjects.Container;
|
||||||
|
private kittenButton: Phaser.GameObjects.Container;
|
||||||
|
private catButton: Phaser.GameObjects.Container;
|
||||||
|
private selectedType: PieceType = 'kitten';
|
||||||
|
private typeChangeCallback?: (type: PieceType) => void;
|
||||||
|
|
||||||
|
constructor(private scene: Phaser.Scene) {
|
||||||
|
const boardCenterX = BOARD_OFFSET.x + (BOARD_SIZE * CELL_SIZE) / 2;
|
||||||
|
const selectorY = BOARD_OFFSET.y + BOARD_SIZE * CELL_SIZE + 100;
|
||||||
|
|
||||||
|
this.container = this.scene.add.container(boardCenterX, selectorY);
|
||||||
|
|
||||||
|
const label = this.scene.add.text(-160, 0, '放置:', {
|
||||||
|
fontSize: '18px',
|
||||||
|
fontFamily: 'Arial',
|
||||||
|
color: '#4b5563',
|
||||||
|
}).setOrigin(0.5, 0.5);
|
||||||
|
|
||||||
|
this.kittenButton = this.createPieceButton('kitten', '🐾 小猫', -50);
|
||||||
|
this.catButton = this.createPieceButton('cat', '🐱 大猫', 70);
|
||||||
|
|
||||||
|
this.container.add([label, this.kittenButton, this.catButton]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private createPieceButton(type: PieceType, text: string, xOffset: number): Phaser.GameObjects.Container {
|
||||||
|
const container = this.scene.add.container(xOffset, 0);
|
||||||
|
|
||||||
|
const bg = this.scene.add.rectangle(0, 0, 100, 40, 0xe5e7eb)
|
||||||
|
.setStrokeStyle(2, 0x9ca3af);
|
||||||
|
|
||||||
|
const textObj = this.scene.add.text(0, 0, text, {
|
||||||
|
fontSize: '16px',
|
||||||
|
fontFamily: 'Arial',
|
||||||
|
color: '#1f2937',
|
||||||
|
}).setOrigin(0.5);
|
||||||
|
|
||||||
|
container.add([bg, textObj]);
|
||||||
|
|
||||||
|
bg.setInteractive({ useHandCursor: true });
|
||||||
|
bg.on('pointerdown', () => {
|
||||||
|
this.selectedType = type;
|
||||||
|
this.typeChangeCallback?.(this.selectedType);
|
||||||
|
});
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
onTypeChanged(callback: (type: PieceType) => void): void {
|
||||||
|
this.typeChangeCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSelectedType(): PieceType {
|
||||||
|
return this.selectedType;
|
||||||
|
}
|
||||||
|
|
||||||
|
update(state: BoopState): void {
|
||||||
|
const currentPlayer = state.players[state.currentPlayer];
|
||||||
|
const kittenAvailable = currentPlayer.kittenPool.remaining() > 0;
|
||||||
|
const catsAvailable = currentPlayer.catPool.remaining() + currentPlayer.graduatedCount > 0;
|
||||||
|
|
||||||
|
this.updateButton(
|
||||||
|
this.kittenButton,
|
||||||
|
kittenAvailable,
|
||||||
|
this.selectedType === 'kitten',
|
||||||
|
`🐾 小猫 (${currentPlayer.kittenPool.remaining()})`
|
||||||
|
);
|
||||||
|
|
||||||
|
this.updateButton(
|
||||||
|
this.catButton,
|
||||||
|
catsAvailable,
|
||||||
|
this.selectedType === 'cat',
|
||||||
|
`🐱 大猫 (${currentPlayer.catPool.remaining() + currentPlayer.graduatedCount})`
|
||||||
|
);
|
||||||
|
|
||||||
|
// 自动切换到可用类型
|
||||||
|
if (!kittenAvailable && this.selectedType === 'kitten' && catsAvailable) {
|
||||||
|
this.selectedType = 'cat';
|
||||||
|
this.typeChangeCallback?.(this.selectedType);
|
||||||
|
} else if (!catsAvailable && this.selectedType === 'cat' && kittenAvailable) {
|
||||||
|
this.selectedType = 'kitten';
|
||||||
|
this.typeChangeCallback?.(this.selectedType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateButton(
|
||||||
|
button: Phaser.GameObjects.Container,
|
||||||
|
available: boolean,
|
||||||
|
isSelected: boolean,
|
||||||
|
text: string
|
||||||
|
): void {
|
||||||
|
const bg = button.list[0] as Phaser.GameObjects.Rectangle;
|
||||||
|
const textObj = button.list[1] as Phaser.GameObjects.Text;
|
||||||
|
|
||||||
|
bg.setFillStyle(isSelected ? 0xfbbf24 : (available ? 0xe5e7eb : 0xd1d5db));
|
||||||
|
textObj.setText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
this.container.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
import Phaser from 'phaser';
|
||||||
|
import type { BoopState, PlayerType } from '@/game/boop';
|
||||||
|
import { BOARD_OFFSET, CELL_SIZE, BOARD_SIZE } from './BoardRenderer';
|
||||||
|
|
||||||
|
export class SupplyUI {
|
||||||
|
private whiteContainer: Phaser.GameObjects.Container;
|
||||||
|
private blackContainer: Phaser.GameObjects.Container;
|
||||||
|
private whiteBg: Phaser.GameObjects.Rectangle;
|
||||||
|
private blackBg: Phaser.GameObjects.Rectangle;
|
||||||
|
private whiteText: Phaser.GameObjects.Text;
|
||||||
|
private blackText: Phaser.GameObjects.Text;
|
||||||
|
|
||||||
|
constructor(private scene: Phaser.Scene) {
|
||||||
|
const boardCenterX = BOARD_OFFSET.x + (BOARD_SIZE * CELL_SIZE) / 2;
|
||||||
|
const uiY = BOARD_OFFSET.y - 20;
|
||||||
|
|
||||||
|
// 白色玩家容器
|
||||||
|
this.whiteContainer = this.scene.add.container(boardCenterX - 150, uiY);
|
||||||
|
this.whiteBg = this.scene.add.rectangle(0, 0, 120, 50, 0x000000);
|
||||||
|
this.whiteText = this.scene.add.text(0, 0, '', {
|
||||||
|
fontSize: '16px',
|
||||||
|
fontFamily: 'Arial',
|
||||||
|
color: '#ffffff',
|
||||||
|
align: 'center',
|
||||||
|
}).setOrigin(0.5);
|
||||||
|
this.whiteContainer.add([this.whiteBg, this.whiteText]);
|
||||||
|
this.whiteContainer.setDepth(100);
|
||||||
|
|
||||||
|
// 黑色玩家容器
|
||||||
|
this.blackContainer = this.scene.add.container(boardCenterX + 150, uiY);
|
||||||
|
this.blackBg = this.scene.add.rectangle(0, 0, 120, 50, 0x333333);
|
||||||
|
this.blackText = this.scene.add.text(0, 0, '', {
|
||||||
|
fontSize: '16px',
|
||||||
|
fontFamily: 'Arial',
|
||||||
|
color: '#ffffff',
|
||||||
|
align: 'center',
|
||||||
|
}).setOrigin(0.5);
|
||||||
|
this.blackContainer.add([this.blackBg, this.blackText]);
|
||||||
|
this.blackContainer.setDepth(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(state: BoopState): void {
|
||||||
|
const white = state.players.white;
|
||||||
|
const black = state.players.black;
|
||||||
|
|
||||||
|
const whiteCatsAvailable = white.catPool.remaining() + white.graduatedCount;
|
||||||
|
const blackCatsAvailable = black.catPool.remaining() + black.graduatedCount;
|
||||||
|
|
||||||
|
this.whiteText.setText(
|
||||||
|
`⚪ WHITE\n🐾 ${white.kittenPool.remaining()} | 🐱 ${whiteCatsAvailable}`
|
||||||
|
);
|
||||||
|
|
||||||
|
this.blackText.setText(
|
||||||
|
`⚫ BLACK\n🐾 ${black.kittenPool.remaining()} | 🐱 ${blackCatsAvailable}`
|
||||||
|
);
|
||||||
|
|
||||||
|
this.updateHighlight(state.currentPlayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateHighlight(currentPlayer: PlayerType): void {
|
||||||
|
const isWhiteTurn = currentPlayer === 'white';
|
||||||
|
|
||||||
|
this.scene.tweens.killTweensOf(this.whiteContainer);
|
||||||
|
this.scene.tweens.killTweensOf(this.blackContainer);
|
||||||
|
|
||||||
|
if (isWhiteTurn) {
|
||||||
|
this.whiteBg.setFillStyle(0xfbbf24);
|
||||||
|
this.whiteContainer.setScale(1);
|
||||||
|
this.scene.tweens.add({
|
||||||
|
targets: this.whiteContainer,
|
||||||
|
scale: 1.15,
|
||||||
|
duration: 200,
|
||||||
|
ease: 'Back.easeOut',
|
||||||
|
yoyo: true,
|
||||||
|
hold: 400,
|
||||||
|
onComplete: () => {
|
||||||
|
this.scene.tweens.add({
|
||||||
|
targets: this.whiteContainer,
|
||||||
|
alpha: 0.6,
|
||||||
|
duration: 600,
|
||||||
|
ease: 'Sine.easeInOut',
|
||||||
|
yoyo: true,
|
||||||
|
repeat: -1,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.blackBg.setFillStyle(0x333333);
|
||||||
|
this.blackContainer.setAlpha(1);
|
||||||
|
this.blackContainer.setScale(1);
|
||||||
|
} else {
|
||||||
|
this.blackBg.setFillStyle(0xfbbf24);
|
||||||
|
this.blackContainer.setScale(1);
|
||||||
|
this.scene.tweens.add({
|
||||||
|
targets: this.blackContainer,
|
||||||
|
scale: 1.15,
|
||||||
|
duration: 200,
|
||||||
|
ease: 'Back.easeOut',
|
||||||
|
yoyo: true,
|
||||||
|
hold: 400,
|
||||||
|
onComplete: () => {
|
||||||
|
this.scene.tweens.add({
|
||||||
|
targets: this.blackContainer,
|
||||||
|
alpha: 0.6,
|
||||||
|
duration: 600,
|
||||||
|
ease: 'Sine.easeInOut',
|
||||||
|
yoyo: true,
|
||||||
|
repeat: -1,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.whiteBg.setFillStyle(0x000000);
|
||||||
|
this.whiteContainer.setAlpha(1);
|
||||||
|
this.whiteContainer.setScale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
this.whiteContainer.destroy();
|
||||||
|
this.blackContainer.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
import Phaser from 'phaser';
|
||||||
|
import type { BoopState, WinnerType } from '@/game/boop';
|
||||||
|
import { BOARD_OFFSET, CELL_SIZE, BOARD_SIZE } from './BoardRenderer';
|
||||||
|
|
||||||
|
export class WinnerOverlay {
|
||||||
|
private overlay?: Phaser.GameObjects.Container;
|
||||||
|
private tweens: Phaser.Tweens.TweenManager;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private scene: Phaser.Scene,
|
||||||
|
private onRestart: () => void
|
||||||
|
) {
|
||||||
|
this.tweens = scene.tweens;
|
||||||
|
}
|
||||||
|
|
||||||
|
show(winner: WinnerType): void {
|
||||||
|
if (this.overlay) {
|
||||||
|
this.overlay.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.overlay = this.scene.add.container();
|
||||||
|
|
||||||
|
const text = winner === 'draw' ? "It's a draw!" : winner ? `${winner.toUpperCase()} wins!` : '';
|
||||||
|
|
||||||
|
const bg = this.scene.add.rectangle(
|
||||||
|
BOARD_OFFSET.x + (BOARD_SIZE * CELL_SIZE) / 2,
|
||||||
|
BOARD_OFFSET.y + (BOARD_SIZE * CELL_SIZE) / 2,
|
||||||
|
BOARD_SIZE * CELL_SIZE,
|
||||||
|
BOARD_SIZE * CELL_SIZE,
|
||||||
|
0x000000,
|
||||||
|
0.6,
|
||||||
|
).setInteractive({ useHandCursor: true });
|
||||||
|
|
||||||
|
bg.on('pointerdown', () => {
|
||||||
|
this.onRestart();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.overlay.add(bg);
|
||||||
|
|
||||||
|
const winText = this.scene.add.text(
|
||||||
|
BOARD_OFFSET.x + (BOARD_SIZE * CELL_SIZE) / 2,
|
||||||
|
BOARD_OFFSET.y + (BOARD_SIZE * CELL_SIZE) / 2,
|
||||||
|
text,
|
||||||
|
{
|
||||||
|
fontSize: '40px',
|
||||||
|
fontFamily: 'Arial',
|
||||||
|
color: '#fbbf24',
|
||||||
|
},
|
||||||
|
).setOrigin(0.5);
|
||||||
|
|
||||||
|
this.overlay.add(winText);
|
||||||
|
|
||||||
|
this.tweens.add({
|
||||||
|
targets: winText,
|
||||||
|
scale: 1.2,
|
||||||
|
duration: 500,
|
||||||
|
yoyo: true,
|
||||||
|
repeat: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
hide(): void {
|
||||||
|
if (this.overlay) {
|
||||||
|
this.overlay.destroy();
|
||||||
|
this.overlay = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
this.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
export { GameScene } from './GameScene';
|
||||||
|
export { BoardRenderer, BOARD_SIZE, CELL_SIZE, BOARD_OFFSET } from './BoardRenderer';
|
||||||
|
export { createPieceSpawner } from './PieceSpawner';
|
||||||
|
export { SupplyUI } from './SupplyUI';
|
||||||
|
export { PieceTypeSelector } from './PieceTypeSelector';
|
||||||
|
export { WinnerOverlay } from './WinnerOverlay';
|
||||||
Loading…
Reference in New Issue