From 696872d5d78cdb3392e7967faf30fc34a606a69e Mon Sep 17 00:00:00 2001 From: hypercross Date: Sat, 4 Apr 2026 14:22:35 +0800 Subject: [PATCH] feat: that's boop for ya --- packages/boop-game/index.html | 12 + packages/boop-game/package.json | 26 ++ packages/boop-game/src/game/boop.ts | 380 +++++++++++++++++++++ packages/boop-game/src/main.tsx | 13 + packages/boop-game/src/scenes/GameScene.ts | 254 ++++++++++++++ packages/boop-game/src/style.css | 9 + packages/boop-game/src/ui/App.tsx | 38 +++ packages/boop-game/tsconfig.json | 16 + packages/boop-game/vite.config.ts | 13 + pnpm-lock.yaml | 40 +++ 10 files changed, 801 insertions(+) create mode 100644 packages/boop-game/index.html create mode 100644 packages/boop-game/package.json create mode 100644 packages/boop-game/src/game/boop.ts create mode 100644 packages/boop-game/src/main.tsx create mode 100644 packages/boop-game/src/scenes/GameScene.ts create mode 100644 packages/boop-game/src/style.css create mode 100644 packages/boop-game/src/ui/App.tsx create mode 100644 packages/boop-game/tsconfig.json create mode 100644 packages/boop-game/vite.config.ts diff --git a/packages/boop-game/index.html b/packages/boop-game/index.html new file mode 100644 index 0000000..8a8645e --- /dev/null +++ b/packages/boop-game/index.html @@ -0,0 +1,12 @@ + + + + + + Boop Game + + +
+ + + diff --git a/packages/boop-game/package.json b/packages/boop-game/package.json new file mode 100644 index 0000000..dab1c25 --- /dev/null +++ b/packages/boop-game/package.json @@ -0,0 +1,26 @@ +{ + "name": "boop-game", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@preact/signals-core": "^1.5.1", + "boardgame-core": "link:../../../boardgame-core", + "boardgame-phaser": "workspace:*", + "mutative": "^1.3.0", + "phaser": "^3.80.1", + "preact": "^10.19.3" + }, + "devDependencies": { + "@preact/preset-vite": "^2.8.1", + "@preact/signals": "^2.9.0", + "@tailwindcss/vite": "^4.0.0", + "tailwindcss": "^4.0.0", + "typescript": "^5.3.3", + "vite": "^5.1.0" + } +} diff --git a/packages/boop-game/src/game/boop.ts b/packages/boop-game/src/game/boop.ts new file mode 100644 index 0000000..6cc32bd --- /dev/null +++ b/packages/boop-game/src/game/boop.ts @@ -0,0 +1,380 @@ +import { + createGameCommandRegistry, + Part, + MutableSignal, + createRegion, + createPart, + isCellOccupied as isCellOccupiedUtil, + getPartAtPosition, +} from 'boardgame-core'; + +const BOARD_SIZE = 6; +const MAX_PIECES_PER_PLAYER = 8; +const WIN_LENGTH = 3; + +export type PlayerType = 'white' | 'black'; +export type PieceType = 'kitten' | 'cat'; +export type WinnerType = PlayerType | 'draw' | null; + +export type BoopPart = Part<{ player: PlayerType; pieceType: PieceType }>; + +type PieceSupply = { supply: number; placed: number }; + +type Player = { + id: PlayerType; + kitten: PieceSupply; + cat: PieceSupply; +}; + +type PlayerData = Record; + +export function createInitialState() { + return { + board: createRegion('board', [ + { name: 'x', min: 0, max: BOARD_SIZE - 1 }, + { name: 'y', min: 0, max: BOARD_SIZE - 1 }, + ]), + pieces: {} as Record, + currentPlayer: 'white' as PlayerType, + winner: null as WinnerType, + players: { + white: createPlayer('white'), + black: createPlayer('black'), + }, + }; +} + +function createPlayer(id: PlayerType): Player { + return { + id, + kitten: { supply: MAX_PIECES_PER_PLAYER, placed: 0 }, + cat: { supply: 0, placed: 0 }, + }; +} + +export type BoopState = ReturnType; +const registration = createGameCommandRegistry(); +export const registry = registration.registry; + +export function getPlayer(host: MutableSignal, player: PlayerType): 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() { + const {context} = this; + while (true) { + const currentPlayer = context.value.currentPlayer; + const turnOutput = await this.run<{winner: WinnerType}>(`turn ${currentPlayer}`); + if (!turnOutput.success) throw new Error(turnOutput.error); + + context.produce(state => { + state.winner = turnOutput.result.winner; + if (!state.winner) { + state.currentPlayer = state.currentPlayer === 'white' ? 'black' : 'white'; + } + }); + if (context.value.winner) break; + } + + return context.value; +}); + +registration.add('turn ', async function(cmd) { + const [turnPlayer] = cmd.params as [PlayerType]; + + const playCmd = await this.prompt( + 'play [type:string]', + (command) => { + const [player, row, col, type] = command.params as [PlayerType, number, number, PieceType?]; + const pieceType = type === 'cat' ? 'cat' : 'kitten'; + + if (player !== turnPlayer) { + return `Invalid player: ${player}. Expected ${turnPlayer}.`; + } + if (!isValidMove(row, col)) { + return `Invalid position: (${row}, ${col}). Must be between 0 and ${BOARD_SIZE - 1}.`; + } + if (isCellOccupied(this.context, row, col)) { + return `Cell (${row}, ${col}) is already occupied.`; + } + + const playerData = getPlayer(this.context, player); + const supply = playerData[pieceType].supply; + if (supply <= 0) { + return `No ${pieceType}s left in ${player}'s supply.`; + } + return null; + }, + this.context.value.currentPlayer + ); + const [player, row, col, type] = playCmd.params as [PlayerType, number, number, PieceType?]; + const pieceType = type === 'cat' ? 'cat' : 'kitten'; + + placePiece(this.context, row, col, turnPlayer, pieceType); + applyBoops(this.context, row, col, pieceType); + + const graduatedLines = checkGraduation(this.context, turnPlayer); + if (graduatedLines.length > 0) { + processGraduation(this.context, turnPlayer, graduatedLines); + } + + if (countPiecesOnBoard(this.context, turnPlayer) >= MAX_PIECES_PER_PLAYER) { + const pieces = this.context.value.pieces; + const availableKittens = Object.values(pieces).filter( + p => p.player === turnPlayer && p.pieceType === 'kitten' + ); + + if (availableKittens.length > 0) { + const graduateCmd = await this.prompt( + 'graduate ', + (command) => { + const [row, col] = command.params as [number, number]; + const posKey = `${row},${col}`; + const part = availableKittens.find(p => `${p.position[0]},${p.position[1]}` === posKey); + if (!part) return `No kitten at (${row}, ${col}).`; + return null; + }, + this.context.value.currentPlayer + ); + const [row, col] = graduateCmd.params as [number, number]; + const part = availableKittens.find(p => p.position[0] === row && p.position[1] === col)!; + removePieceFromBoard(this.context, part); + const playerData = getPlayer(this.context, turnPlayer); + incrementSupply(playerData, 'cat', 1); + } + } + + const winner = checkWinner(this.context); + if (winner) return { winner }; + + return { winner: null }; +}); + +function isValidMove(row: number, col: number): boolean { + return !isNaN(row) && !isNaN(col) && row >= 0 && row < BOARD_SIZE && col >= 0 && col < BOARD_SIZE; +} + +export function getBoardRegion(host: MutableSignal) { + return host.value.board; +} + +export function isCellOccupied(host: MutableSignal, row: number, col: number): boolean { + return isCellOccupiedUtil(host.value.pieces, 'board', [row, col]); +} + +export function getPartAt(host: MutableSignal, row: number, col: number): BoopPart | null { + return getPartAtPosition(host.value.pieces, 'board', [row, col]) || null; +} + +export function placePiece(host: MutableSignal, row: number, col: number, player: PlayerType, pieceType: PieceType) { + const board = getBoardRegion(host); + const playerData = getPlayer(host, player); + const count = playerData[pieceType].placed + 1; + + const piece = createPart<{ player: PlayerType; pieceType: PieceType }>( + { regionId: 'board', position: [row, col], player, pieceType }, + `${player}-${pieceType}-${count}` + ); + host.produce(s => { + s.pieces[piece.id] = piece; + board.childIds.push(piece.id); + board.partMap[`${row},${col}`] = piece.id; + }); + decrementSupply(playerData, pieceType); +} + +export function applyBoops(host: MutableSignal, placedRow: number, placedCol: number, placedType: PieceType) { + const board = getBoardRegion(host); + const pieces = host.value.pieces; + const piecesArray = Object.values(pieces); + + const piecesToBoop: { part: BoopPart; dr: number; dc: number }[] = []; + + for (const part of piecesArray) { + const [r, c] = part.position; + if (r === placedRow && c === placedCol) continue; + + const dr = Math.sign(r - placedRow); + const dc = Math.sign(c - placedCol); + + if (Math.abs(r - placedRow) <= 1 && Math.abs(c - placedCol) <= 1) { + const booperIsKitten = placedType === 'kitten'; + const targetIsCat = part.pieceType === 'cat'; + + if (booperIsKitten && targetIsCat) continue; + + piecesToBoop.push({ part, dr, dc }); + } + } + + for (const { part, dr, dc } of piecesToBoop) { + const [r, c] = part.position; + const newRow = r + dr; + const newCol = c + dc; + + if (newRow < 0 || newRow >= BOARD_SIZE || newCol < 0 || newCol >= BOARD_SIZE) { + const pt = part.pieceType; + const pl = part.player; + const playerData = getPlayer(host, pl); + removePieceFromBoard(host, part); + incrementSupply(playerData, pt); + continue; + } + + if (isCellOccupied(host, newRow, newCol)) continue; + + part.position = [newRow, newCol]; + board.partMap = Object.fromEntries( + board.childIds.map(id => { + const p = pieces[id]; + return [p.position.join(','), id]; + }) + ); + } +} + +export function removePieceFromBoard(host: MutableSignal, part: BoopPart) { + const board = getBoardRegion(host); + const playerData = getPlayer(host, part.player); + board.childIds = board.childIds.filter(id => id !== part.id); + delete board.partMap[part.position.join(',')]; + delete host.value.pieces[part.id]; + playerData[part.pieceType].placed--; +} + +const DIRECTIONS: [number, number][] = [ + [0, 1], + [1, 0], + [1, 1], + [1, -1], +]; + +export function* linesThrough(r: number, c: number): Generator { + for (const [dr, dc] of DIRECTIONS) { + const minStart = -(WIN_LENGTH - 1); + for (let offset = minStart; offset <= 0; offset++) { + const startR = r + offset * dr; + const startC = c + offset * dc; + const endR = startR + (WIN_LENGTH - 1) * dr; + const endC = startC + (WIN_LENGTH - 1) * dc; + + if (startR < 0 || startR >= BOARD_SIZE || startC < 0 || startC >= BOARD_SIZE) continue; + if (endR < 0 || endR >= BOARD_SIZE || endC < 0 || endC >= BOARD_SIZE) continue; + + const line: number[][] = []; + for (let i = 0; i < WIN_LENGTH; i++) { + line.push([startR + i * dr, startC + i * dc]); + } + yield line; + } + } +} + +export function* allLines(): Generator { + const seen = new Set(); + for (let r = 0; r < BOARD_SIZE; r++) { + for (let c = 0; c < BOARD_SIZE; c++) { + for (const line of linesThrough(r, c)) { + const key = line.map(p => p.join(',')).join(';'); + if (!seen.has(key)) { + seen.add(key); + yield line; + } + } + } + } +} + +export function hasWinningLine(positions: number[][]): boolean { + const posSet = new Set(positions.map(p => `${p[0]},${p[1]}`)); + for (const line of allLines()) { + if (line.every(([lr, lc]) => posSet.has(`${lr},${lc}`))) return true; + } + return false; +} + +export function checkGraduation(host: MutableSignal, player: PlayerType): number[][][] { + const pieces = host.value.pieces; + const piecesArray = Object.values(pieces); + const posSet = new Set(); + + for (const part of piecesArray) { + if (part.player === player && part.pieceType === 'kitten') { + posSet.add(`${part.position[0]},${part.position[1]}`); + } + } + + const winningLines: number[][][] = []; + for (const line of allLines()) { + if (line.every(([lr, lc]) => posSet.has(`${lr},${lc}`))) { + winningLines.push(line); + } + } + return winningLines; +} + +export function processGraduation(host: MutableSignal, player: PlayerType, lines: number[][][]) { + const allPositions = new Set(); + for (const line of lines) { + for (const [r, c] of line) { + allPositions.add(`${r},${c}`); + } + } + + const board = getBoardRegion(host); + const pieces = host.value.pieces; + const partsToRemove = Object.values(pieces).filter( + p => p.player === player && p.pieceType === 'kitten' && allPositions.has(`${p.position[0]},${p.position[1]}`) + ); + + for (const part of partsToRemove) { + removePieceFromBoard(host, part); + } + + const count = partsToRemove.length; + const playerData = getPlayer(host, player); + incrementSupply(playerData, 'cat', count); +} + +export function countPiecesOnBoard(host: MutableSignal, player: PlayerType): number { + const pieces = host.value.pieces; + return Object.values(pieces).filter(p => p.player === player).length; +} + +export function checkWinner(host: MutableSignal): WinnerType { + const pieces = host.value.pieces; + const piecesArray = Object.values(pieces); + + for (const player of ['white', 'black'] as PlayerType[]) { + const positions = piecesArray + .filter(p => p.player === player && p.pieceType === 'cat') + .map(p => p.position); + if (hasWinningLine(positions)) return player; + } + + return null; +} + +// 命令构建器 +export const commands = { + play: (player: PlayerType, row: number, col: number, type?: PieceType) => + `play ${player} ${row} ${col}${type ? ` ${type}` : ''}`, + turn: (player: PlayerType) => `turn ${player}`, + graduate: (row: number, col: number) => `graduate ${row} ${col}`, +} as const; + +// 导出游戏模块 +export const gameModule = { + createInitialState, + registry, + commands, +}; diff --git a/packages/boop-game/src/main.tsx b/packages/boop-game/src/main.tsx new file mode 100644 index 0000000..7edaef3 --- /dev/null +++ b/packages/boop-game/src/main.tsx @@ -0,0 +1,13 @@ +import { h } from 'preact'; +import { GameUI } from 'boardgame-phaser'; +import { gameModule } from './game/boop'; +import './style.css'; +import App from "@/ui/App"; +import {GameScene} from "@/scenes/GameScene"; + +const ui = new GameUI({ + container: document.getElementById('ui-root')!, + root: , +}); + +ui.mount(); diff --git a/packages/boop-game/src/scenes/GameScene.ts b/packages/boop-game/src/scenes/GameScene.ts new file mode 100644 index 0000000..f327174 --- /dev/null +++ b/packages/boop-game/src/scenes/GameScene.ts @@ -0,0 +1,254 @@ +import Phaser from 'phaser'; +import type {BoopState, BoopPart, PlayerType, PieceType} from '@/game/boop'; +import { GameHostScene } from 'boardgame-phaser'; +import { spawnEffect, type Spawner } from 'boardgame-phaser'; +import type { ReadonlySignal } from '@preact/signals-core'; +import {commands} from "@/game/boop"; + +const BOARD_SIZE = 6; +const CELL_SIZE = 80; +const BOARD_OFFSET = { x: 80, y: 100 }; + +export class GameScene extends GameHostScene { + private boardContainer!: Phaser.GameObjects.Container; + private gridGraphics!: Phaser.GameObjects.Graphics; + private turnText!: Phaser.GameObjects.Text; + private infoText!: Phaser.GameObjects.Text; + private winnerOverlay?: Phaser.GameObjects.Container; + + constructor() { + super('GameScene'); + } + + create(): void { + super.create(); + + this.boardContainer = this.add.container(0, 0); + this.gridGraphics = this.add.graphics(); + this.drawGrid(); + + this.disposables.add(spawnEffect(new BoopPartSpawner(this, this.gameHost.state))); + + this.watch(() => { + const winner = this.state.winner; + if (winner) { + this.showWinner(winner); + } else if (this.winnerOverlay) { + this.winnerOverlay.destroy(); + this.winnerOverlay = undefined; + } + }); + + this.watch(() => { + const currentPlayer = this.state.currentPlayer; + this.updateTurnText(currentPlayer); + }); + + this.setupInput(); + } + + private setupInput(): 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.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, 'kitten'); + const error = this.gameHost.onInput(cmd); + if (error) { + console.warn('Invalid move:', error); + } + }); + } + } + } + + 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.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 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 { + constructor(public readonly scene: GameScene, public readonly state: ReadonlySignal) {} + + *getData() { + for (const part of Object.values(this.state.value.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; + obj.x = x; + obj.y = y; + } + + 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.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(), + }); + } +} diff --git a/packages/boop-game/src/style.css b/packages/boop-game/src/style.css new file mode 100644 index 0000000..28b2bb7 --- /dev/null +++ b/packages/boop-game/src/style.css @@ -0,0 +1,9 @@ +@import "tailwindcss"; + +#ui-root { + pointer-events: none; +} + +#ui-root * { + pointer-events: auto; +} diff --git a/packages/boop-game/src/ui/App.tsx b/packages/boop-game/src/ui/App.tsx new file mode 100644 index 0000000..182cfba --- /dev/null +++ b/packages/boop-game/src/ui/App.tsx @@ -0,0 +1,38 @@ +import {useComputed} from '@preact/signals'; +import { createGameHost, type GameModule } from 'boardgame-core'; +import Phaser from 'phaser'; +import { h } from 'preact'; +import { PhaserGame, PhaserScene } from 'boardgame-phaser'; + +export default function App>(props: { gameModule: GameModule, gameScene: { new(): Phaser.Scene } }) { + + const gameHost = useComputed(() => { + const gameHost = createGameHost(props.gameModule); + return { gameHost }; + }); + + const scene = useComputed(() => new props.gameScene()); + + const handleReset = async () => { + gameHost.value.gameHost.setup('setup'); + }; + const label = useComputed(() => gameHost.value.gameHost.status.value === 'running' ? 'Restart' : 'Start'); + + return ( +
+
+ + + +
+
+ +
+
+ ); +} diff --git a/packages/boop-game/tsconfig.json b/packages/boop-game/tsconfig.json new file mode 100644 index 0000000..d7f3323 --- /dev/null +++ b/packages/boop-game/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + }, + "jsx": "react-jsx", + "jsxImportSource": "preact", + "noEmit": true, + "declaration": false, + "declarationMap": false, + "sourceMap": false + }, + "include": ["src/**/*"] +} diff --git a/packages/boop-game/vite.config.ts b/packages/boop-game/vite.config.ts new file mode 100644 index 0000000..acb8595 --- /dev/null +++ b/packages/boop-game/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite'; +import preact from '@preact/preset-vite'; +import tailwindcss from '@tailwindcss/vite'; +import path from 'path'; + +export default defineConfig({ + plugins: [preact(), tailwindcss()], + resolve: { + alias: { + '@': path.resolve(__dirname, 'src'), + }, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 809f8d5..6bb93e7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,6 +15,46 @@ importers: specifier: ^3.2.4 version: 3.2.4(lightningcss@1.32.0) + packages/boop-game: + dependencies: + '@preact/signals-core': + specifier: ^1.5.1 + version: 1.14.1 + boardgame-core: + specifier: link:../../../boardgame-core + version: link:../../../boardgame-core + boardgame-phaser: + specifier: workspace:* + version: link:../framework + mutative: + specifier: ^1.3.0 + version: 1.3.0 + phaser: + specifier: ^3.80.1 + version: 3.90.0 + preact: + specifier: ^10.19.3 + version: 10.29.0 + devDependencies: + '@preact/preset-vite': + specifier: ^2.8.1 + version: 2.10.5(@babel/core@7.29.0)(preact@10.29.0)(rollup@4.60.1)(vite@5.4.21(lightningcss@1.32.0)) + '@preact/signals': + specifier: ^2.9.0 + version: 2.9.0(preact@10.29.0) + '@tailwindcss/vite': + specifier: ^4.0.0 + version: 4.2.2(vite@5.4.21(lightningcss@1.32.0)) + tailwindcss: + specifier: ^4.0.0 + version: 4.2.2 + typescript: + specifier: ^5.3.3 + version: 5.9.3 + vite: + specifier: ^5.1.0 + version: 5.4.21(lightningcss@1.32.0) + packages/framework: devDependencies: '@preact/signals':