From 3755f2857af76ac4ef1e61cad5929d9e9d2ff9e7 Mon Sep 17 00:00:00 2001 From: Jose Conde Date: Sun, 14 Jul 2024 21:35:03 +0200 Subject: [PATCH] working flow --- src/common/helpers.ts | 19 +++++ src/common/interfaces.ts | 1 + src/components/GameComponent.vue | 44 ++++++------ src/game/Board.ts | 116 ++++++++++++++++-------------- src/game/Game.ts | 95 ++++++++++++++++--------- src/game/Hand.ts | 60 ++++++++-------- src/game/OtherHand.ts | 118 +++++++++++++++++++++++++++++++ src/game/SpriteBase.ts | 8 ++- src/game/Tile.ts | 7 ++ src/game/utilities/fonts.ts | 10 +++ src/views/GameView.vue | 55 +++----------- src/views/HomeView.vue | 52 ++++++++------ src/views/MatchView.vue | 4 +- 13 files changed, 375 insertions(+), 214 deletions(-) create mode 100644 src/game/OtherHand.ts diff --git a/src/common/helpers.ts b/src/common/helpers.ts index 32bfe42..4c7a65c 100644 --- a/src/common/helpers.ts +++ b/src/common/helpers.ts @@ -1,6 +1,7 @@ import { Graphics, Container, Text } from 'pixi.js' import type { ContainerOptions, Dimension, TileDto } from './interfaces' import { DEFAULT_CONTAINER_OPTIONS } from './constants' +import useClipboard from 'vue-clipboard3' export function getColorBackground(container: Container, colorName: string, alpha: number = 0.5) { const graphics = new Graphics() @@ -22,6 +23,10 @@ export function createContainer(options: ContainerOptions) { if (opts.color) { rect.fill(opts.color) } + if (opts.alpha) { + rect.alpha = opts.alpha + } + rect.visible = opts.visible container.addChild(rect) if (opts.parent) { @@ -78,6 +83,15 @@ export function createButton( return container } +export function createCrosshair(container: Container, color: number = 0xff0000, d: Dimension) { + const verticalLine = new Graphics().moveTo(d.x, 0).lineTo(d.x, d.height).stroke(color) + const horizontalLine = new Graphics().moveTo(0, d.y).lineTo(d.width, d.y).stroke(color) + verticalLine.alpha = 0.2 + horizontalLine.alpha = 0.2 + container.addChild(verticalLine) + container.addChild(horizontalLine) +} + export async function wait(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)) } @@ -91,3 +105,8 @@ export function isTilePair(tile: TileDto): boolean { export function isTileVertical(tile: TileDto): boolean { return tile.orientation === 'north' || tile.orientation === 'south' } + +export function copyToclipboard(value: string) { + const { toClipboard } = useClipboard() + toClipboard(value) +} diff --git a/src/common/interfaces.ts b/src/common/interfaces.ts index 64de1dd..a886238 100644 --- a/src/common/interfaces.ts +++ b/src/common/interfaces.ts @@ -74,6 +74,7 @@ export interface ContainerOptions { color?: number visible?: boolean parent?: Container + alpha?: number } export interface Dimension { diff --git a/src/components/GameComponent.vue b/src/components/GameComponent.vue index 59d5ba9..66df8cc 100644 --- a/src/components/GameComponent.vue +++ b/src/components/GameComponent.vue @@ -1,31 +1,38 @@ diff --git a/src/game/Board.ts b/src/game/Board.ts index fdb6d83..b1292ed 100644 --- a/src/game/Board.ts +++ b/src/game/Board.ts @@ -1,27 +1,18 @@ -import { - Application, - Assets, - Container, - EventEmitter, - Graphics, - Sprite, - Text, - Ticker -} from 'pixi.js' +import { Application, Assets, Container, EventEmitter, Sprite, Text, Ticker } from 'pixi.js' import { Scale, type ScaleFunction } from '@/game/utilities/scale' import type { AnimationOptions, Movement, PlayerDto, TileDto } from '@/common/interfaces' import { Tile } from '@/game/Tile' -import { DIRECTIONS, createContainer, isTilePair } from '@/common/helpers' +import { DIRECTIONS, createContainer, createCrosshair, isTilePair } from '@/common/helpers' import { createText } from '@/game/utilities/fonts' import { LoggingService } from '@/services/LoggingService' -import { inject } from 'vue' import { GlowFilter } from 'pixi-filters' import { ORIENTATION_ANGLES } from '@/common/constants' +import type { OtherHand } from './OtherHand' export class Board extends EventEmitter { private _scale: number = 1 private _canMove: boolean = false - private logger = inject('logger')! + private logger: LoggingService = new LoggingService() ticker: Ticker height: number @@ -50,6 +41,7 @@ export class Board extends EventEmitter { playerHand: Tile[] = [] firstTile?: Tile currentPlayer!: PlayerDto + otherPlayerHands: OtherHand[] = [] constructor(app: Application) { super() @@ -93,18 +85,19 @@ export class Board extends EventEmitter { visible: false }) - const verticalLine = new Graphics() - .moveTo(this.scaleX(0), 0) - .lineTo(this.scaleX(0), this.height) - .stroke(0xff0000) - const horizontalLine = new Graphics() - .moveTo(0, this.scaleY(0)) - .lineTo(this.width, this.scaleY(0)) - .stroke(0xff0000) - verticalLine.alpha = 0.2 - horizontalLine.alpha = 0.2 - this.tilesContainer.addChild(verticalLine) - this.tilesContainer.addChild(horizontalLine) + createCrosshair(this.tilesContainer, 0xff0000, { + width: this.width, + height: this.height, + x: this.scaleX(0), + y: this.scaleY(0) + }) + + createCrosshair(this.interactionContainer, 0xffff00, { + width: this.width, + height: this.height, + x: this.scaleX(0), + y: this.scaleY(0) + }) this.textContainer = createContainer({ width: this.width, @@ -135,15 +128,6 @@ export class Board extends EventEmitter { this.calculateScale() } - get canMove() { - return this._canMove - } - - set canMove(value: boolean) { - this._canMove = value - this.updateCanMoveText() - } - setPlayerHand(tiles: Tile[]) { this.playerHand = tiles } @@ -153,12 +137,8 @@ export class Board extends EventEmitter { this.textContainer.addChild(createText(text, this.scaleX(0), 100)) } - private updateCanMoveText() { - if (this.canMove) { - this.showText('Your turn!') - } else { - this.showText('Waiting for players') - } + async setPlayerTurn(player: PlayerDto) { + this.showText('Your turn!') } async setServerPlayerTurn(currentPlayer: PlayerDto) { @@ -168,6 +148,9 @@ export class Board extends EventEmitter { async playerMove(move: any, playerId: string) { const { move: lastMove } = move if (lastMove === null) { + setTimeout(() => { + this.emit('game:tile-animation-ended') + }, 500) return } if ( @@ -252,21 +235,46 @@ export class Board extends EventEmitter { tile.reScale(this.scale) this.tiles.push(tile) + await this.animateTile(tile, x, y, orientation, move) + this.emit('game:tile-animation-ended', tile.toPlain()) + } + + async animateTile(tile: Tile, x: number, y: number, orientation: string, move: Movement) { + const targetX = this.scaleX(x) + const targetY = this.scaleY(y) const animation: AnimationOptions = { - x: this.scaleX(x), - y: this.scaleY(y), + x: targetX, + y: targetY, rotation: ORIENTATION_ANGLES[orientation], duration: 20 } - - tile.setPosition(this.scaleX(x), this.scaleY(y)) + const tempAlpha = tile.alpha + tile.alpha = 0 + const clonedTile = tile.clone() + clonedTile.addTo(this.tilesContainer) + const pos = this.getAnimationInitialPoosition(move) + clonedTile.setPosition(this.scaleX(pos.x), this.scaleY(pos.y)) + await clonedTile.animateTo(animation) + clonedTile.removeFromParent() tile.setOrientation(orientation) + tile.setPosition(targetX, targetY) + tile.alpha = tempAlpha + } - // tile.setPosition(this.scaleX(0), this.height + tile.height / 2) - // console.log('going to animate', tile.pips) - // await tile.animateTo(animation) - // console.log('animated', tile.pips) - this.emit('game:tile-animation-ended', tile.toPlain()) + getAnimationInitialPoosition(move: Movement): { x: number; y: number } { + const otherHand = this.otherPlayerHands.find((h) => h.player?.id === move.playerId) + if (otherHand === undefined) { + return { x: 0, y: this.scaleY.inverse(this.height + 50) } + } + const position = otherHand.position + switch (position) { + case 'left': + return { x: this.scaleX.inverse(100), y: this.scaleY.inverse(100) } + case 'right': + return { x: 0, y: this.scaleY.inverse(100) } + case 'top': + return { x: this.scaleX.inverse(this.width - 100), y: this.scaleY.inverse(20) } + } } getPlayedTile(id: string): Tile | undefined { @@ -315,16 +323,14 @@ export class Board extends EventEmitter { } } - async updateBoard(move: Movement) { + async updateBoard(move: Movement, tile: Tile | undefined) { try { - const { tile: tileDto } = move - const tile = this.getTileInHand(tileDto?.id ?? '') - + // const { tileDto: tileDto } = move + // const tile = this.getTileInHand(tileDto?.id ?? '') + this.movements.push(move) if (tile === undefined) { return } - - this.movements.push(move) await this.addTile(tile, move) this.setFreeEnd(move) } catch (error) { diff --git a/src/game/Game.ts b/src/game/Game.ts index 8c38ef8..46e3cff 100644 --- a/src/game/Game.ts +++ b/src/game/Game.ts @@ -5,17 +5,17 @@ import { Tile } from '@/game/Tile' import { Hand } from '@/game/Hand' import type { GameDto, Movement, PlayerDto, TileDto } from '@/common/interfaces' import type { SocketIoClientService } from '@/services/SocketIoClientService' -import { useEventBusStore } from '@/stores/eventBus' import { wait } from '@/common/helpers' import { Actions } from 'pixi-actions' +import { OtherHand } from './OtherHand' export class Game { public board!: Board public hand!: Hand private app: Application = new Application() private selectedTile: TileDto | undefined - private eventBus: any = useEventBusStore() private currentMove: Movement | undefined + private otherHands: OtherHand[] = [] constructor( private options: { boardScale: number; handScale: number; width: number; height: number } = { @@ -24,24 +24,29 @@ export class Game { width: 1200, height: 800 }, - private emit: any, private socketService: SocketIoClientService, private playerId: string, private sessionId: string ) {} async setup(): Promise { - const width = 1200 - const height = 800 + const width = this.options.width || 1200 + const height = this.options.height || 800 await this.app.init({ width, height }) this.app.ticker.add((tick) => Actions.tick(tick.deltaTime / 60)) return this.app.canvas } - async start() { + async start(players: PlayerDto[] = []) { this.board = new Board(this.app) this.hand = new Hand(this.app) + this.otherHands = [ + new OtherHand(this.app, 'left'), + new OtherHand(this.app, 'top'), + new OtherHand(this.app, 'right') + ] + this.initOtherHands(players) this.hand.scale = this.options.handScale this.board.scale = this.options.boardScale this.setBoardEvents() @@ -54,6 +59,32 @@ export class Game { }) } + initOtherHands(players: PlayerDto[]) { + const myIndex = players.findIndex((player) => player.id === this.playerId) + const copy = [...players] + const cut = copy.splice(myIndex) + 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.board.otherPlayerHands = this.otherHands + } + + updateOtherHands(gameState: GameDto) { + const players = gameState.players + + players.forEach((player) => { + const hand = this.otherHands.find((hand) => hand.player?.id === player.id) + if (hand) { + hand.setHand(player.hand) + } + }) + } + async preload() { await Assets.load(assets) } @@ -82,11 +113,11 @@ export class Game { sessionId: this.sessionId, move: move }) - await this.board.updateBoard(move) + await this.board.updateBoard(move, undefined) }) this.hand.on('nextClick', () => { - this.socketService.sendMessage('client:set-player-ready', { + this.socketService.sendMessage('client:set-client-ready-for-next-game', { userId: this.playerId, sessionId: this.sessionId }) @@ -120,24 +151,18 @@ export class Game { return validEnds } - public setCanMakeMove(value: boolean) { - this.hand.setCanMove(value, this.board.count === 0, this.board.freeEnds) - this.board.canMove = value - } - async setNextPlayer(state: GameDto) { const currentPlayer = state?.currentPlayer! - if (currentPlayer.id !== this.playerId) { - this.setCanMakeMove(false) - this.board.setServerPlayerTurn(currentPlayer) + if (currentPlayer.id === this.playerId) { + this.hand.prepareForMove(this.board.count === 0, this.board.freeEnds) + this.board.setPlayerTurn(currentPlayer) } else { - this.setCanMakeMove(true) + this.board.setServerPlayerTurn(currentPlayer) } } private setBoardEvents() { this.board.on('game:board-left-action-click', async (data) => { - console.log('left data :>> ', data) if (this.selectedTile === undefined) return const move: Movement = { tile: this.selectedTile, @@ -146,12 +171,11 @@ export class Game { ...data } this.currentMove = move - this.hand.tileMoved(this.selectedTile) - await this.board.updateBoard({ ...move, tile: this.selectedTile }) + 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) => { - console.log('right data :>> ', data) if (this.selectedTile === undefined) return const move: Movement = { tile: this.selectedTile, @@ -160,31 +184,25 @@ export class Game { ...data } this.currentMove = move - this.hand.tileMoved(this.selectedTile) - await this.board.updateBoard({ ...move, tile: this.selectedTile }) + 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) => { - console.log('animation ended', tile) - if (tile.playerId === this.playerId) { + if (tile !== null && tile !== undefined && tile.playerId === this.playerId) { this.socketService.sendMessage('client:player-move', { sessionId: this.sessionId, move: this.currentMove }) + } else { + this.socketService.sendMessage('client:animation-ended', { + sessionId: this.sessionId, + userId: this.playerId + }) } }) } - // sendMoveEvent(move: Movement) { - // this.board.on('game:tile-animation-ended', async (tile) => { - // this.eventBus.publish('game:tile-animation-ended', tile) - // // this.socketService.sendMessageWithAck('client:tile-animation-ended', { - // // sessionId: this.sessionId, - // // tile - // // }) - // }) - // } - gameFinished(data: any) { this.hand.gameFinished() this.board.gameFinished(data) @@ -197,6 +215,13 @@ export class Game { 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) + } + } } private removeBoardEvents() { diff --git a/src/game/Hand.ts b/src/game/Hand.ts index 21d75fc..519c15c 100644 --- a/src/game/Hand.ts +++ b/src/game/Hand.ts @@ -88,30 +88,31 @@ export class Hand extends EventEmitter { this.scaleY = Scale([-scaleYSteps, scaleYSteps], [0, this.height]) } - setCanMove(value: boolean, isFirstMove: boolean, freeEnds?: [number, number]) { - console.log('this.tiles :>> ', this.tiles.length) - this.availableTiles = - !value || isFirstMove - ? this.tiles - : this.tiles.filter((tile) => this.hasMoves(tile.toPlain(), freeEnds)) - - console.log('this.availableTiles :>> ', this.availableTiles.length) - if (value && this.availableTiles.length === 0) { + prepareForMove(isFirstMove: boolean, freeEnds?: [number, number]) { + this.availableTiles = isFirstMove + ? this.tiles + : this.tiles.filter((tile) => this.hasMoves(tile.toPlain(), freeEnds)) + if (this.availableTiles.length === 0) { this.createPassButton() - } else { - this.interactionsLayer.removeChild(this.buttonPass) } this.availableTiles.forEach((tile) => { - if (value) { - tile.animateTo({ - x: tile.x, - y: tile.y - 10 - }) - // const action: Action = Actions.moveTo(tile.getSprite(), tile.x, tile.y - 10, 1,).play() - } - tile.interactive = value + tile.animateTo({ + x: tile.x, + y: tile.y - 10 + }) + tile.interactive = true + }) + } + + afterMove() { + this.availableTiles.forEach((tile) => { + tile.animateTo({ + x: tile.x, + y: tile.y + 10 + }) + tile.setPosition(tile.x, tile.y + 10) + tile.interactive = false }) - this._canMove = value } hasMoves(tile: TileDto, freeEnds?: [number, number]): boolean { @@ -166,20 +167,12 @@ export class Hand extends EventEmitter { selected.alpha = 0.7 } - public tileMoved(tileDto: TileDto) { + public tileMoved(tileDto: TileDto): Tile | undefined { const tile = this.tiles.find((t) => t.id === tileDto.id) if (!tile) return - this.availableTiles - .filter((t) => t.id !== tileDto.id) - .forEach((t) => { - t.animateTo({ - x: t.x, - y: t.y + 10 - }) - }) - + this.afterMove() this.tiles = this.tiles.filter((t) => t.id !== tileDto.id) tile.interactive = false @@ -187,6 +180,8 @@ export class Hand extends EventEmitter { tile.off('pointerdown') tile.off('pointerover') tile.off('pointerout') + this.tilesLayer.removeChild(tile.getSprite()) + return tile } private createPassButton() { @@ -195,7 +190,10 @@ export class Hand extends EventEmitter { this.buttonPass = createButton( 'PASS', { x, y: this.height / 2, width: 50, height: 20 }, - () => this.emit('game:button-pass-click'), + () => { + this.interactionsLayer.removeChild(this.buttonPass) + this.emit('game:button-pass-click') + }, this.interactionsLayer ) } diff --git a/src/game/OtherHand.ts b/src/game/OtherHand.ts new file mode 100644 index 0000000..c4b829c --- /dev/null +++ b/src/game/OtherHand.ts @@ -0,0 +1,118 @@ +import { LoggingService } from '@/services/LoggingService' +import { Application, Container, Sprite, Texture } from 'pixi.js' +import { Scale, type ScaleFunction } from './utilities/scale' +import { Tile } from './Tile' +import type { Movement, PlayerDto, TileDto } from '@/common/interfaces' +import { createContainer } from '@/common/helpers' +import { createText, playerNameText } from './utilities/fonts' + +export class OtherHand { + tilesInitialNumber: number = 7 + player?: PlayerDto + hand: Tile[] = [] + container: Container = new Container() + height: number + width: number + scale: number = 0.5 + scaleY!: ScaleFunction + scaleX!: ScaleFunction + x: number = 0 + y: number = 0 + grain: number = 25 + logger: LoggingService = new LoggingService() + tilesLayer!: Container + interactionsLayer!: Container + + constructor( + private app: Application, + public position: 'left' | 'right' | 'top' = 'left' + ) { + this.height = 100 + this.width = 300 + app.stage.addChild(this.container) + const { x, y } = this.getPosition() + this.container.width = this.width + this.container.height = this.height + this.container.x = x + this.container.y = y + this.calculateScale() + this.initLayers() + } + + setPlayer(player: PlayerDto) { + this.player = player + this.container.addChild(createText(`${player.name}`, this.width / 2, 12, playerNameText)) + } + + setHand(tiles: TileDto[]) { + this.hand = tiles.map((tile) => new Tile(tile.id, this.app.ticker, undefined, this.scale)) + this.render() + } + + update(move: Movement) { + this.hand = this.hand.filter((tile) => tile.id !== move?.tile?.id) + this.render() + } + + private render() { + this.tilesLayer.removeChildren() + const x = -9 + this.hand.forEach((tile, index) => { + tile.setPosition(this.scaleX(x + index * 2), this.height / 2) + this.tilesLayer.addChild(tile.getSprite()) + }) + } + + private addBg() { + const bg = new Sprite(Texture.WHITE) + bg.alpha = 0.08 + bg.width = this.width + bg.height = this.height + this.container.addChild(bg) + } + + private getPosition() { + let x = 0 + let y = 0 + + if (this.position === 'left') { + x = 0 + y = 30 + } else if (this.position === 'right') { + x = this.app.canvas.width - this.width + y = 30 + } else { + x = (this.app.canvas.width - this.width) / 2 + y = 0 + } + return { x, y } + } + + private initLayers() { + this.container.removeChildren() + this.addBg() + this.tilesLayer = createContainer({ + width: this.width, + height: this.height, + x: 0, + y: 0, + parent: this.container + }) + this.interactionsLayer = createContainer({ + width: this.width, + height: this.height, + x: 0, + y: 0, + parent: this.container + }) + this.container.addChild(this.tilesLayer) + this.container.addChild(this.interactionsLayer) + } + + private calculateScale() { + const scaleXSteps = Math.floor(this.width / (this.grain * this.scale)) / 2 + const scaleYSteps = Math.floor(this.height / (this.grain * this.scale)) / 2 + this.scaleX = Scale([-scaleXSteps, scaleXSteps], [0, this.width]) + this.scaleY = Scale([-scaleYSteps, scaleYSteps], [0, this.height]) + } +} diff --git a/src/game/SpriteBase.ts b/src/game/SpriteBase.ts index 0245091..a8a3d6d 100644 --- a/src/game/SpriteBase.ts +++ b/src/game/SpriteBase.ts @@ -1,10 +1,11 @@ import type { AnimationOptions } from '@/common/interfaces' -import { Sprite, Texture, Ticker } from 'pixi.js' +import { Container, Sprite, Texture, Ticker } from 'pixi.js' import { Tile } from './Tile' export abstract class SpriteBase { private _interactive: boolean = false protected sprite: Sprite = new Sprite() + private container?: Container constructor( protected ticker?: Ticker, @@ -136,6 +137,11 @@ export abstract class SpriteBase { } addTo(container: any) { + this.container = container container.addChild(this.sprite) } + + removeFromParent() { + this.container?.removeChild(this.sprite) + } } diff --git a/src/game/Tile.ts b/src/game/Tile.ts index 67a908c..2b19b0f 100644 --- a/src/game/Tile.ts +++ b/src/game/Tile.ts @@ -86,4 +86,11 @@ export class Tile extends SpriteBase { } this.orientation = value } + + clone(): Tile { + const copy = new Tile(this.id, this.ticker, this.pips, this.scale, this.playerId) + copy.selected = this.selected + copy.orientation = this.orientation + return copy + } } diff --git a/src/game/utilities/fonts.ts b/src/game/utilities/fonts.ts index 5d428c5..720d90b 100644 --- a/src/game/utilities/fonts.ts +++ b/src/game/utilities/fonts.ts @@ -16,6 +16,16 @@ export const mainText = new TextStyle({ stroke: '#658f56' }) +export const playerNameText = new TextStyle({ + dropShadow: dropShadowStyle, + fill: '#a2a2a2', + fontFamily: 'Arial, Helvetica, sans-serif', + letterSpacing: 1, + stroke: '#565656', + fontSize: 15, + fontWeight: 'bold' +}) + export function createText(str: string, x: number, y: number, style: TextStyle = mainText) { const text = new Text({ text: str, style }) text.anchor.set(0.5, 0.5) diff --git a/src/views/GameView.vue b/src/views/GameView.vue index 67b1dbe..2fdc554 100644 --- a/src/views/GameView.vue +++ b/src/views/GameView.vue @@ -31,13 +31,15 @@ onBeforeUnmount(() => { }) function copySeed() { - if (sessionState?.value?.seed) toClipboard(sessionState.value.seed) + if (sessionState?.value?.seed) { + toClipboard(sessionState.value.seed) + } }