import { Application, Assets, Container, EventEmitter, TilingSprite } from 'pixi.js' import { Board } from '@/game/Board' import { assets } from '@/game/utilities/assets' import { Tile } from '@/game/Tile' import { Hand } from '@/game/Hand' import type { GameDto, MatchSessionDto, MatchSessionOptions, Movement, PlayerDto, TileDto, } from '@/common/interfaces' import type { SocketIoClientService } from '@/services/SocketIoClientService' import { wait } from '@/common/helpers' import { Actions } from 'pixi-actions' import { OtherHand } from './OtherHand' import { GameSummayView } from './GameSummayView' import Config from './Config' import { createText, grayStyle } from './utilities/fonts' import { t } from '@/i18n' import { DIRECTION_INDEXES, DIRECTIONS } from '@/common/constants' export class Game extends EventEmitter { public board!: Board public hand!: Hand private app: Application = new Application() private selectedTile: TileDto | undefined private currentMove: Movement | undefined private otherHands: OtherHand[] = [] private backgroundLayer: Container = new Container() private gameSummaryView!: GameSummayView private players: PlayerDto[] = [] constructor( private options: MatchSessionOptions, private socketService: SocketIoClientService | undefined, private playerId: string, private sessionId: string, ) { super() this.options.screen = { width: 1280, height: 720, handScale: 1, boardScale: 0.7, ...this.options.screen, } } get stage() { return this.app.stage } async setup(): Promise { const width = this.options.screen?.width || 1280 const height = this.options.screen?.height || 720 await this.app.init({ width, height }) this.app.ticker.add((tick) => Actions.tick(tick.deltaTime / 60)) return this.app.canvas } async start(players: PlayerDto[] = []) { this.iniialStuff(this.app) this.board = new Board(this.app, this.options) this.hand = new Hand(this.app, this.options) this.otherHands = [ new OtherHand(this.app, 'left'), new OtherHand(this.app, 'top'), new OtherHand(this.app, 'right'), ] this.initPlayers(players) this.players = players this.gameSummaryView = new GameSummayView(this.app, this.options) this.hand.scale = this.options.screen?.handScale || 1 this.board.scale = this.options.screen?.boardScale || 0.7 this.setBoardEvents() this.setHandEvents() this.initEventBus() await wait(500) this.socketService && this.socketService.sendMessage('client:set-client-ready', { sessionId: this.sessionId, userId: this.playerId, }) } iniialStuff(app: Application) { app.stage.addChild(this.backgroundLayer) const background = new TilingSprite({ texture: Assets.get(`bg-${this.options.background}`), width: app.canvas.width, height: app.canvas.height, }) this.backgroundLayer.addChild(background) const actor = this.options.teamed ? t('team') : t('player') const type = this.options.winType === 'points' ? t('n-points', this.options.winTarget) : t('n-rounds', this.options.winTarget) const helptext = t('first-actor-to-win-this-options-wintarget-this-options-wintype', [ actor.toLowerCase(), type.toLowerCase(), ]) this.backgroundLayer.addChild( createText({ text: `${helptext}`, x: this.app.canvas.width / 2, y: 120, style: grayStyle(14, 'lighter', false), }), ) } initPlayers(players: PlayerDto[]) { const myIndex = players.findIndex((player) => player.id === this.playerId) const copy = [...players] const cut = copy.splice(myIndex) const player = cut.shift() const final = cut.concat(copy) for (let i = 0; i < final.length; i++) { const hand = this.otherHands[i] hand.setPlayer(final[i]) } this.hand.setPlayer(player) this.board.otherPlayerHands = this.otherHands } updateOtherHands(players: PlayerDto[]) { players.forEach((player) => { const hand = this.otherHands.find((hand) => hand.player?.id === player.id) if (hand) { hand.setHand(player.hand) } }) } highLightPlayer(player: PlayerDto) { const hand = this.otherHands.find((hand) => hand.player?.id === player.id) if (hand) { hand.setActive(true) } } setPlayersInactive() { this.otherHands.forEach((hand) => hand.setActive(false)) this.hand.setActive(false) } async preload() { await Assets.load(assets) } destroy() { this.removeBoardEvents() this.removeHandEvents() this.app.destroy() } private initEventBus() {} private setHandEvents() { this.hand.on('hand-updated', (tiles: Tile[]) => { this.board.setPlayerHand(tiles) }) this.hand.on('game:tile-click', (tile: TileDto) => this.highlightMoves(tile)) this.hand.on('game:timer-timeout', this.handleHandPlayerTimeout.bind(this)) this.hand.on('game:button-pass-click', this.sendPassEvent.bind(this)) this.gameSummaryView.on('finishClick', (data) => { this.emit('game:finish-click', data) }) this.gameSummaryView.on('nextClick', (data) => { this.board.clean() this.updateScoreboard(data.sessionState) this.socketService && this.socketService.sendMessage('client:set-client-ready-for-next-game', { userId: this.playerId, sessionId: this.sessionId, }) }) this.hand.on('hand-initialized', () => {}) } private handleHandPlayerTimeout(tile: TileDto | null) { if (tile === null) { this.sendPassEvent() this.hand.disablePassButton() return } const freeEnds = this.board.freeEnds console.log('freeEnds :>> ', freeEnds, this.playerId) const move: Movement = { id: '', tile, type: 'right', playerId: this.playerId, } if (freeEnds !== undefined) { const side = tile.pips?.includes(freeEnds[0]) ? 'left' : 'right' const direction = side === 'left' ? this.board.leftDirection : this.board.rightDirection let dirIndex = DIRECTION_INDEXES[direction] const validMoves = this.board.nextTileValidMoves(tile, side) const validPoints = this.board.nextTileValidPoints(tile, side, validMoves) let safeCount = 4 while (safeCount > 0 && !validMoves[dirIndex % 4]) { dirIndex += 1 safeCount -= 1 } console.log('validMoves :>> ', validMoves) console.log('validPoints :>> ', validPoints) const validPoint = validPoints[dirIndex % 4] if (validPoint !== undefined) { move.x = validPoint[0] move.y = validPoint[1] } move.direction = DIRECTIONS[dirIndex % 4] move.type = side } this.currentMove = move this.board.updateBoard(move, this.hand.tileMoved(tile)) } private async sendPassEvent() { const move: Movement = { id: '', type: 'pass', playerId: this.playerId, } this.socketService && this.socketService.sendMessage('client:player-move', { sessionId: this.sessionId, move: move, }) await this.board.updateBoard(move, undefined) } private updateScoreboard(sessionState: MatchSessionDto) { const scoreboard = sessionState.scoreboard const myScore = scoreboard.find((d) => d.id === this.playerId)?.score || 0 this.hand.setScore(myScore) this.otherHands.forEach((otherHand) => { const player: PlayerDto | undefined = otherHand.player const name: string = player?.name || '' const score = scoreboard.find((d) => d.name === name)?.score || 0 otherHand.setScore(score) }) } highlightMoves(tile: TileDto) { this.selectedTile = tile if (tile !== undefined) { this.board.setMovesForTile(this.getMoves(tile), tile) } else { this.board.cleanInteractions() } } getMoves(tile: any): [boolean, boolean] { if (tile === undefined) return [false, false] if (this.board.count === 0) return [false, true] const validEnds: [boolean, boolean] = [false, false] const freeEnds = this.board.freeEnds if (freeEnds !== undefined) { if (tile.pips != undefined) { validEnds[0] = tile.pips.includes(freeEnds[0]) validEnds[1] = tile.pips.includes(freeEnds[1]) } } return validEnds } async setNextPlayer(state: GameDto) { const currentPlayer = state?.currentPlayer! this.setPlayersInactive() if (currentPlayer.id === this.playerId) { this.hand.setActive(true) this.hand.prepareForMove(this.board.count === 0, this.board.freeEnds) this.board.setPlayerTurn(currentPlayer) } else { this.board.setServerPlayerTurn(currentPlayer) this.highLightPlayer(currentPlayer) } } private setBoardEvents() { this.board.on('game:board-left-action-click', async (data) => { if (this.selectedTile === undefined) return const move: Movement = { tile: this.selectedTile, type: 'left', playerId: this.playerId, ...data, } this.currentMove = move const tile = this.hand.tileMoved(this.selectedTile) await this.board.updateBoard({ ...move, tile: this.selectedTile }, tile) }) this.board.on('game:board-right-action-click', async (data) => { if (this.selectedTile === undefined) return const move: Movement = { tile: this.selectedTile, type: 'right', playerId: this.playerId, ...data, } this.currentMove = move const tile = this.hand.tileMoved(this.selectedTile) await this.board.updateBoard({ ...move, tile: this.selectedTile }, tile) }) this.board.on('game:tile-animation-ended', async (tile) => { if (tile !== null && tile !== undefined && tile.playerId === this.playerId) { this.socketService && this.socketService.sendMessage('client:player-move', { sessionId: this.sessionId, move: this.currentMove, }) } else { this.socketService && this.socketService.sendMessage('client:animation-ended', { sessionId: this.sessionId, userId: this.playerId, }) } }) } async gameFinished(data: any) { await wait(Config.waitMillisToShowSummary) this.updateOtherHands(data.lastGame.players) this.hand.gameFinished() this.board.gameFinished() this.setPlayersInactive() this.gameSummaryView.setGameSummary(data, 'round') } async matchFinished(data: any) { await wait(Config.waitMillisToShowSummary) this.updateOtherHands(data.lastGame.players) this.board.matchFinished() this.gameSummaryView.setGameSummary(data, 'match') } serverPlayerMove(data: any, playerId: string) { this.board.playerMove(data, playerId) if (!(data.move === undefined || data.move === null)) { const otherHand = this.otherHands.find((hand) => hand.player?.id === data.move.playerId) if (otherHand) { otherHand.update(data.move) } } else { // ShowPassed } } private removeBoardEvents() { this.board.off('game:board-left-action-click') this.board.off('game:board-right-action-click') } private removeHandEvents() { this.hand.off('game:tile-click') this.hand.off('game:button-pass-click') } }