diff --git a/.env b/.env index 2a01ab5..abd394e 100644 --- a/.env +++ b/.env @@ -1,2 +1,2 @@ -VITE_LOG_LEVEL= 'error' +VITE_LOG_LEVEL= 'trace' VITE_API_URL= 'http://localhost:3000/api' \ No newline at end of file diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 6f40582..9340ac4 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -3,13 +3,13 @@ require('@rushstack/eslint-patch/modern-module-resolution') module.exports = { root: true, - 'extends': [ + extends: [ 'plugin:vue/vue3-essential', 'eslint:recommended', '@vue/eslint-config-typescript', - '@vue/eslint-config-prettier/skip-formatting' + '@vue/eslint-config-prettier/skip-formatting', ], parserOptions: { - ecmaVersion: 'latest' - } + ecmaVersion: 'latest', + }, } diff --git a/.prettierrc.json b/.prettierrc.json index ecdf3e0..70fe929 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -4,5 +4,5 @@ "tabWidth": 2, "singleQuote": true, "printWidth": 100, - "trailingComma": "none" + "trailingComma": "all" } diff --git a/package-lock.json b/package-lock.json index 7f44a77..38f4034 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "domino-client", "version": "0.0.0", "dependencies": { + "@pixi/sound": "^6.0.0", "bulma": "^1.0.1", "colorette": "^2.0.20", "dayjs": "^1.11.11", @@ -664,6 +665,14 @@ "resolved": "https://registry.npmjs.org/@pixi/colord/-/colord-2.9.6.tgz", "integrity": "sha512-nezytU2pw587fQstUu1AsJZDVEynjskwOL+kibwcdxsMBFqPsFFNA7xl0ii/gXuDi6M0xj3mfRJj8pBSc2jCfA==" }, + "node_modules/@pixi/sound": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@pixi/sound/-/sound-6.0.0.tgz", + "integrity": "sha512-fEaCs2JmyYT1qqouFS3DydSccI35dyYD0pKK2hEbIGVDKUTvl224x0p4qme2YU9l465WRtM7gspLzP5fFf1mxQ==", + "peerDependencies": { + "pixi.js": "^8.0.0" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", diff --git a/package.json b/package.json index 7edcfd6..bcaa4d8 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "format": "prettier --write src/" }, "dependencies": { + "@pixi/sound": "^6.0.0", "bulma": "^1.0.1", "colorette": "^2.0.20", "dayjs": "^1.11.11", diff --git a/src/assets/images/backgrounds/bg-blue.png b/src/assets/images/backgrounds/bg-blue.png new file mode 100644 index 0000000..8c4f1f0 Binary files /dev/null and b/src/assets/images/backgrounds/bg-blue.png differ diff --git a/src/assets/images/backgrounds/bg-red.png b/src/assets/images/backgrounds/bg-red.png new file mode 100644 index 0000000..ba9ca00 Binary files /dev/null and b/src/assets/images/backgrounds/bg-red.png differ diff --git a/src/assets/images/backgrounds/bg-yellow.png b/src/assets/images/backgrounds/bg-yellow.png new file mode 100644 index 0000000..9cef4d5 Binary files /dev/null and b/src/assets/images/backgrounds/bg-yellow.png differ diff --git a/src/assets/sounds/intro.mp3 b/src/assets/sounds/intro.mp3 new file mode 100644 index 0000000..8c658b8 Binary files /dev/null and b/src/assets/sounds/intro.mp3 differ diff --git a/src/assets/sounds/move-1.mp3 b/src/assets/sounds/move-1.mp3 new file mode 100644 index 0000000..319edba Binary files /dev/null and b/src/assets/sounds/move-1.mp3 differ diff --git a/src/assets/sounds/move-2.mp3 b/src/assets/sounds/move-2.mp3 new file mode 100644 index 0000000..3da7e73 Binary files /dev/null and b/src/assets/sounds/move-2.mp3 differ diff --git a/src/assets/sounds/move-3.mp3 b/src/assets/sounds/move-3.mp3 new file mode 100644 index 0000000..76f1890 Binary files /dev/null and b/src/assets/sounds/move-3.mp3 differ diff --git a/src/assets/sounds/move-4.mp3 b/src/assets/sounds/move-4.mp3 new file mode 100644 index 0000000..5537860 Binary files /dev/null and b/src/assets/sounds/move-4.mp3 differ diff --git a/src/assets/sounds/move-sprite.mp3 b/src/assets/sounds/move-sprite.mp3 new file mode 100644 index 0000000..30da94f Binary files /dev/null and b/src/assets/sounds/move-sprite.mp3 differ diff --git a/src/assets/sounds/move-sprite.ogg b/src/assets/sounds/move-sprite.ogg new file mode 100644 index 0000000..f5fcd81 Binary files /dev/null and b/src/assets/sounds/move-sprite.ogg differ diff --git a/src/common/helpers.ts b/src/common/helpers.ts index 4c7a65c..1bbd7ac 100644 --- a/src/common/helpers.ts +++ b/src/common/helpers.ts @@ -17,8 +17,13 @@ export function getColorBackground(container: Container, colorName: string, alph export function createContainer(options: ContainerOptions) { const opts = { ...DEFAULT_CONTAINER_OPTIONS, ...options } const container = new Container() + container.x = opts.x + container.y = opts.y + if (opts.parent) { + opts.parent.addChild(container) + } - const rect = new Graphics().rect(opts.x, opts.y, opts.width, opts.height) + const rect = new Graphics().rect(0, 0, opts.width, opts.height) if (opts.color) { rect.fill(opts.color) @@ -29,19 +34,17 @@ export function createContainer(options: ContainerOptions) { rect.visible = opts.visible container.addChild(rect) - if (opts.parent) { - opts.parent.addChild(container) - } return container } -export function createButton( - textStr: string, - dimension: Dimension, - action: Function, +export function createButton(options: { + text: string + dimension: Dimension + action: Function parent?: Container -): Container { - const { x, y, width, height } = dimension +}): Container { + const { text: textStr, dimension, action, parent } = options + const { x, y, width, height = 25 } = dimension const rectangle = new Graphics().roundRect(x, y, width + 4, height + 4, 5).fill(0xffff00) const text = new Text({ text: textStr, @@ -50,8 +53,8 @@ export function createButton( fontSize: 12, fontWeight: 'bold', fill: 0x121212, - align: 'center' - } + align: 'center', + }, }) text.anchor = 0.5 const container = new Container() diff --git a/src/common/interfaces.ts b/src/common/interfaces.ts index a886238..8b6f9cf 100644 --- a/src/common/interfaces.ts +++ b/src/common/interfaces.ts @@ -34,7 +34,7 @@ export interface MatchSessionDto { maxPlayers: number numPlayers: number waitingSeconds: number - scoreboard: Map + scoreboard: { id: string; name: string; score: number }[] matchWinner: PlayerDto | null matchInProgress: boolean playersReady: number @@ -98,3 +98,20 @@ export interface AnimationOptions { width?: number height?: number } + +export interface GameOptions { + boardScale?: number + handScale?: number + width?: number + height?: number + background?: string + teamed?: boolean +} +export interface GameSummary { + gameId: string + isBlocked: boolean + isTied: boolean + winner: PlayerDto + score: { id: string; name: string; score: number }[] + players?: PlayerDto[] +} diff --git a/src/common/summarymock.ts b/src/common/summarymock.ts new file mode 100644 index 0000000..40a2582 --- /dev/null +++ b/src/common/summarymock.ts @@ -0,0 +1,269 @@ +export const summaryMock = { + lastGame: { + gameId: '6cd2b32c-185e-4869-a3f9-7a1a718a00f7', + isBlocked: true, + isTied: false, + winner: { + id: '23ed7e37-84e2-4e4e-b833-e97aef351f18', + name: 'Bob (AI)', + score: 14, + hand: [ + { + id: '69c6e996-7dd4-4565-a49c-96b7ba35db7d', + flipped: false, + revealed: true, + playerId: '23ed7e37-84e2-4e4e-b833-e97aef351f18', + }, + ], + teamedWith: null, + ready: true, + }, + score: [ + { + id: '668977662eb15e2ef3bdac3f', + name: 'arhuako', + score: 0, + }, + { + id: '3494d8b9-6b15-49e5-8fec-15e2d6539f71', + name: 'Alice (AI)', + score: 0, + }, + { + id: '23ed7e37-84e2-4e4e-b833-e97aef351f18', + name: 'Bob (AI)', + score: 14, + }, + { + id: '2eda2a20-a97a-4310-a431-3aaef916e602', + name: 'Charlie (AI)', + score: 0, + }, + ], + players: [ + { + id: '668977662eb15e2ef3bdac3f', + name: 'arhuako', + score: 0, + hand: [ + { + id: '93d523a2-ae4d-4b51-a7a7-2e740be2da14', + flipped: false, + revealed: true, + playerId: '668977662eb15e2ef3bdac3f', + }, + ], + teamedWith: null, + ready: true, + }, + { + id: '3494d8b9-6b15-49e5-8fec-15e2d6539f71', + name: 'Alice (AI)', + score: 0, + hand: [ + { + id: 'cf9a04f9-3d8e-4feb-afa6-34a8a61ec014', + flipped: false, + revealed: true, + playerId: '3494d8b9-6b15-49e5-8fec-15e2d6539f71', + }, + ], + teamedWith: null, + ready: true, + }, + { + id: '23ed7e37-84e2-4e4e-b833-e97aef351f18', + name: 'Bob (AI)', + score: 14, + hand: [ + { + id: '69c6e996-7dd4-4565-a49c-96b7ba35db7d', + flipped: false, + revealed: true, + playerId: '23ed7e37-84e2-4e4e-b833-e97aef351f18', + }, + ], + teamedWith: null, + ready: true, + }, + { + id: '2eda2a20-a97a-4310-a431-3aaef916e602', + name: 'Charlie (AI)', + score: 0, + hand: [ + { + id: '22d0a60c-b9e9-43f6-804b-6899221de04d', + flipped: false, + revealed: true, + playerId: '2eda2a20-a97a-4310-a431-3aaef916e602', + }, + ], + teamedWith: null, + ready: true, + }, + ], + }, + sessionState: { + id: '669524c8b78825b8f9ede908', + name: 'Test #26', + creator: '668977662eb15e2ef3bdac3f', + players: [ + { + id: '668977662eb15e2ef3bdac3f', + name: 'arhuako', + score: 0, + hand: [], + teamedWith: null, + ready: false, + }, + { + id: '3494d8b9-6b15-49e5-8fec-15e2d6539f71', + name: 'Alice (AI)', + score: 0, + hand: [], + teamedWith: null, + ready: false, + }, + { + id: '23ed7e37-84e2-4e4e-b833-e97aef351f18', + name: 'Bob (AI)', + score: 0, + hand: [], + teamedWith: null, + ready: false, + }, + { + id: '2eda2a20-a97a-4310-a431-3aaef916e602', + name: 'Charlie (AI)', + score: 0, + hand: [], + teamedWith: null, + ready: false, + }, + ], + playersReady: 0, + sessionInProgress: true, + maxPlayers: 4, + numPlayers: 4, + waitingForPlayers: false, + waitingSeconds: 0, + seed: '1721050312717-840b4s07gb8-8d980dd8', + mode: 'classic', + pointsToWin: 50, + status: 'in progress', + scoreboard: [ + ['arhuako', 0], + ['Alice (AI)', 0], + ['Bob (AI)', 14], + ['Charlie (AI)', 0], + ], + matchWinner: null, + matchInProgress: true, + gameSummaries: [ + { + gameId: '6cd2b32c-185e-4869-a3f9-7a1a718a00f7', + isBlocked: true, + isTied: false, + winner: { + id: '23ed7e37-84e2-4e4e-b833-e97aef351f18', + name: 'Bob (AI)', + score: 14, + hand: [ + { + id: '69c6e996-7dd4-4565-a49c-96b7ba35db7d', + flipped: false, + revealed: true, + playerId: '23ed7e37-84e2-4e4e-b833-e97aef351f18', + }, + ], + teamedWith: null, + ready: true, + }, + score: [ + { + id: '668977662eb15e2ef3bdac3f', + name: 'arhuako', + score: 0, + }, + { + id: '3494d8b9-6b15-49e5-8fec-15e2d6539f71', + name: 'Alice (AI)', + score: 0, + }, + { + id: '23ed7e37-84e2-4e4e-b833-e97aef351f18', + name: 'Bob (AI)', + score: 14, + }, + { + id: '2eda2a20-a97a-4310-a431-3aaef916e602', + name: 'Charlie (AI)', + score: 0, + }, + ], + players: [ + { + id: '668977662eb15e2ef3bdac3f', + name: 'arhuako', + score: 0, + hand: [ + { + id: '93d523a2-ae4d-4b51-a7a7-2e740be2da14', + flipped: false, + revealed: true, + playerId: '668977662eb15e2ef3bdac3f', + }, + ], + teamedWith: null, + ready: true, + }, + { + id: '3494d8b9-6b15-49e5-8fec-15e2d6539f71', + name: 'Alice (AI)', + score: 0, + hand: [ + { + id: 'cf9a04f9-3d8e-4feb-afa6-34a8a61ec014', + flipped: false, + revealed: true, + playerId: '3494d8b9-6b15-49e5-8fec-15e2d6539f71', + }, + ], + teamedWith: null, + ready: true, + }, + { + id: '23ed7e37-84e2-4e4e-b833-e97aef351f18', + name: 'Bob (AI)', + score: 14, + hand: [ + { + id: '69c6e996-7dd4-4565-a49c-96b7ba35db7d', + flipped: false, + revealed: true, + playerId: '23ed7e37-84e2-4e4e-b833-e97aef351f18', + }, + ], + teamedWith: null, + ready: true, + }, + { + id: '2eda2a20-a97a-4310-a431-3aaef916e602', + name: 'Charlie (AI)', + score: 0, + hand: [ + { + id: '22d0a60c-b9e9-43f6-804b-6899221de04d', + flipped: false, + revealed: true, + playerId: '2eda2a20-a97a-4310-a431-3aaef916e602', + }, + ], + teamedWith: null, + ready: true, + }, + ], + }, + ], + }, +} diff --git a/src/components/GameComponent.vue b/src/components/GameComponent.vue index 66df8cc..de0d3f1 100644 --- a/src/components/GameComponent.vue +++ b/src/components/GameComponent.vue @@ -5,13 +5,16 @@ import { Game } from '@/game/Game' import { useGameStore } from '@/stores/game' import { useEventBusStore } from '@/stores/eventBus' import { storeToRefs } from 'pinia' +import { useGameOptionsStore } from '@/stores/gameOptions' const socketService: any = inject('socket') const gameStore = useGameStore() +const gameOptionsStore = useGameOptionsStore() const eventBus = useEventBusStore() const { playerState, sessionState } = storeToRefs(gameStore) const { updateGameState } = gameStore +const { gameOptions } = storeToRefs(gameOptionsStore) const minScreenWidth = 800 const minScreenHeight = 700 @@ -31,11 +34,12 @@ const game = new Game( width: screenWidth, height: screenHeight, boardScale, - handScale: 1 + handScale: 1, + background: gameOptions.value?.background || 'green', }, socketService, playerState.value?.id || '', - sessionState.value?.id || '' + sessionState.value?.id || '', ) // watch( diff --git a/src/game/Board.ts b/src/game/Board.ts index b1292ed..49eaa11 100644 --- a/src/game/Board.ts +++ b/src/game/Board.ts @@ -8,6 +8,7 @@ import { LoggingService } from '@/services/LoggingService' import { GlowFilter } from 'pixi-filters' import { ORIENTATION_ANGLES } from '@/common/constants' import type { OtherHand } from './OtherHand' +import { sound } from '@pixi/sound' export class Board extends EventEmitter { private _scale: number = 1 @@ -46,63 +47,61 @@ export class Board extends EventEmitter { constructor(app: Application) { super() this.ticker = app.ticker - this.height = app.canvas.height - 130 + this.height = app.canvas.height - 130 - 130 this.width = app.canvas.width this.scaleX = Scale([0, this.width], [0, this.width]) this.scaleY = Scale([0, this.height], [0, this.height]) this.calculateScale() this.container = createContainer({ + y: 130, width: this.width, height: this.height, - parent: app.stage + parent: app.stage, }) - const background = new Sprite(Assets.get('bg-green')) - // background.width = this.width - // background.height = this.height - this.container.addChild(background) + // this.container.y = 130 this.initialContainer = createContainer({ width: this.width, height: this.height, // color: 0x1e2f23, visible: false, - parent: this.container + parent: this.container, }) this.tilesContainer = createContainer({ width: this.width, height: this.height, // color: 0x1e2f23, - parent: this.container + parent: this.container, }) this.interactionContainer = createContainer({ width: this.width, height: this.height, parent: this.container, - visible: false + visible: false, }) createCrosshair(this.tilesContainer, 0xff0000, { width: this.width, height: this.height, x: this.scaleX(0), - y: this.scaleY(0) + y: this.scaleY(0), }) createCrosshair(this.interactionContainer, 0xffff00, { width: this.width, height: this.height, x: this.scaleX(0), - y: this.scaleY(0) + y: this.scaleY(0), }) this.textContainer = createContainer({ width: this.width, height: this.height, - parent: this.container + parent: this.container, }) this.showText('Starting game...') @@ -134,7 +133,13 @@ export class Board extends EventEmitter { showText(text: string) { this.textContainer.removeChildren() - this.textContainer.addChild(createText(text, this.scaleX(0), 100)) + this.textContainer.addChild( + createText({ + text, + x: this.scaleX(0), + y: -10, + }), + ) } async setPlayerTurn(player: PlayerDto) { @@ -169,7 +174,6 @@ export class Board extends EventEmitter { } async addTile(tile: Tile, move: Movement) { - console.log('adding tile', tile.pips) let orientation = '' let x: number = move.type === 'left' @@ -234,11 +238,18 @@ export class Board extends EventEmitter { tile.addTo(this.tilesContainer) tile.reScale(this.scale) this.tiles.push(tile) - + const moveSound = this.getRandomClickSound() await this.animateTile(tile, x, y, orientation, move) + sound.play(moveSound) this.emit('game:tile-animation-ended', tile.toPlain()) } + getRandomClickSound() { + const sounds = ['snd-move-1', 'snd-move-2', 'snd-move-3', 'snd-move-4'] + const index = Math.floor(Math.random() * sounds.length) + return sounds[index] + } + async animateTile(tile: Tile, x: number, y: number, orientation: string, move: Movement) { const targetX = this.scaleX(x) const targetY = this.scaleY(y) @@ -246,13 +257,13 @@ export class Board extends EventEmitter { x: targetX, y: targetY, rotation: ORIENTATION_ANGLES[orientation], - duration: 20 + duration: 20, } const tempAlpha = tile.alpha tile.alpha = 0 const clonedTile = tile.clone() clonedTile.addTo(this.tilesContainer) - const pos = this.getAnimationInitialPoosition(move) + const pos = this.getAnimationInitialPosition(move) clonedTile.setPosition(this.scaleX(pos.x), this.scaleY(pos.y)) await clonedTile.animateTo(animation) clonedTile.removeFromParent() @@ -261,7 +272,7 @@ export class Board extends EventEmitter { tile.alpha = tempAlpha } - getAnimationInitialPoosition(move: Movement): { x: number; y: number } { + getAnimationInitialPosition(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) } @@ -271,9 +282,9 @@ export class Board extends EventEmitter { 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) } + case 'top': + return { x: 0, y: this.scaleY.inverse(100) } } } @@ -365,7 +376,7 @@ export class Board extends EventEmitter { nextTileValidPoints( tile: TileDto, side: string, - validMoves: boolean[] + validMoves: boolean[], ): ([number, number] | undefined)[] { const isLeft = side === 'left' const end = isLeft ? this.leftTile : this.rightTile @@ -450,8 +461,8 @@ export class Board extends EventEmitter { outerStrength: 1, innerStrength: 0, color: 0xffffff, - quality: 0.5 - }) + quality: 0.5, + }), ]) dot.setOrientation(direction ?? 'north') // const dot = new Dot(this.ticker, this.scale) diff --git a/src/game/Game.ts b/src/game/Game.ts index 46e3cff..69cb753 100644 --- a/src/game/Game.ts +++ b/src/game/Game.ts @@ -1,13 +1,30 @@ -import { Application, Assets } from 'pixi.js' +import { Application, Assets, Container, Sprite } 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, Movement, PlayerDto, TileDto } from '@/common/interfaces' +import type { + GameDto, + GameSummary, + MatchSessionDto, + 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 { summaryMock } from '@/common/summarymock' + +interface GameOptions { + boardScale: number + handScale: number + width: number + height: number + background: string +} export class Game { public board!: Board @@ -16,17 +33,20 @@ export class Game { private selectedTile: TileDto | undefined private currentMove: Movement | undefined private otherHands: OtherHand[] = [] + private backgroundLayer: Container = new Container() + private gameSummaryView!: GameSummayView constructor( - private options: { boardScale: number; handScale: number; width: number; height: number } = { + private options: GameOptions = { boardScale: 1, handScale: 1, width: 1200, - height: 800 + height: 800, + background: 'bg-green', }, private socketService: SocketIoClientService, private playerId: string, - private sessionId: string + private sessionId: string, ) {} async setup(): Promise { @@ -39,14 +59,16 @@ export class Game { } async start(players: PlayerDto[] = []) { + this.iniialStuff(this.app) 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') + new OtherHand(this.app, 'right'), ] this.initOtherHands(players) + this.gameSummaryView = new GameSummayView(this.app) this.hand.scale = this.options.handScale this.board.scale = this.options.boardScale this.setBoardEvents() @@ -55,10 +77,18 @@ export class Game { wait(3000) this.socketService.sendMessage('client:set-client-ready', { sessionId: this.sessionId, - userId: this.playerId + userId: this.playerId, }) } + iniialStuff(app: Application) { + app.stage.addChild(this.backgroundLayer) + const background = new Sprite(Assets.get(`bg-${this.options.background}`)) + background.width = this.app.canvas.width + background.height = this.app.canvas.height + this.backgroundLayer.addChild(background) + } + initOtherHands(players: PlayerDto[]) { const myIndex = players.findIndex((player) => player.id === this.playerId) const copy = [...players] @@ -107,25 +137,36 @@ export class Game { const move: Movement = { id: '', type: 'pass', - playerId: this.playerId + playerId: this.playerId, } this.socketService.sendMessage('client:player-move', { sessionId: this.sessionId, - move: move + move: move, }) await this.board.updateBoard(move, undefined) }) - this.hand.on('nextClick', () => { + this.gameSummaryView.on('nextClick', (data) => { + this.updateScoreboard(data.sessionState) this.socketService.sendMessage('client:set-client-ready-for-next-game', { userId: this.playerId, - sessionId: this.sessionId + sessionId: this.sessionId, }) }) this.hand.on('hand-initialized', () => {}) } + private updateScoreboard(sessionState: MatchSessionDto) { + const scoreboard = sessionState.scoreboard + this.otherHands.forEach((hand) => { + const player: PlayerDto | undefined = hand.player + const name: string = player?.name || '' + const score = scoreboard.find((d) => d.name === name)?.score || 0 + hand.setScore(score) + }) + } + highlightMoves(tile: TileDto) { this.selectedTile = tile if (tile !== undefined) { @@ -168,7 +209,7 @@ export class Game { tile: this.selectedTile, type: 'left', playerId: this.playerId, - ...data + ...data, } this.currentMove = move const tile = this.hand.tileMoved(this.selectedTile) @@ -181,7 +222,7 @@ export class Game { tile: this.selectedTile, type: 'right', playerId: this.playerId, - ...data + ...data, } this.currentMove = move const tile = this.hand.tileMoved(this.selectedTile) @@ -192,12 +233,12 @@ export class Game { if (tile !== null && tile !== undefined && tile.playerId === this.playerId) { this.socketService.sendMessage('client:player-move', { sessionId: this.sessionId, - move: this.currentMove + move: this.currentMove, }) } else { this.socketService.sendMessage('client:animation-ended', { sessionId: this.sessionId, - userId: this.playerId + userId: this.playerId, }) } }) @@ -206,11 +247,13 @@ export class Game { gameFinished(data: any) { this.hand.gameFinished() this.board.gameFinished(data) + this.gameSummaryView.setGameSummary(data, 'round') } matchFinished(data: any) { // this.hand.matchFinished() this.board.matchFinished(data) + this.gameSummaryView.setGameSummary(data, 'match') } serverPlayerMove(data: any, playerId: string) { diff --git a/src/game/GameSummayView.ts b/src/game/GameSummayView.ts new file mode 100644 index 0000000..ad589ee --- /dev/null +++ b/src/game/GameSummayView.ts @@ -0,0 +1,168 @@ +import { createButton, createContainer } from '@/common/helpers' +import type { GameSummary, MatchSessionDto } from '@/common/interfaces' +import { EventEmitter, type Application, type Container } from 'pixi.js' +import { createText, whiteStyle, yellowStyle } from './utilities/fonts' + +export class GameSummayView extends EventEmitter { + public width: number + public height: number + container!: Container + layer!: Container + gameSummary!: GameSummary + matchState!: MatchSessionDto + type: 'round' | 'match' = 'round' + + constructor(app: Application) { + super() + this.width = 500 + this.height = 400 + this.container = createContainer({ + width: this.width, + height: this.height, + x: app.canvas.width / 2 - this.width / 2, + y: app.canvas.height / 2 - this.height / 2, + parent: app.stage, + alpha: 0.7, + color: 0x121212, + }) + this.layer = createContainer({ + width: this.width, + height: this.height, + parent: this.container, + }) + console.log('GameSummaryView created!') + + this.container.visible = false + } + + setGameSummary(data: any, type: 'round' | 'match') { + this.type = type + this.matchState = data.sessionState + this.gameSummary = data.lastGame + this.render() + this.container.visible = true + } + + renderTitle(y: number = 20, title: string): number { + const text = createText({ + text: title, + x: this.width / 2, + y, + style: yellowStyle(24), + }) + this.layer.addChild(text) + return y + 24 + } + + renderWinner(y: number): number { + let line = y + 12 + this.layer.addChild( + createText({ + text: `Winner: ${this.gameSummary.winner.name}`, + x: this.width / 2, + y: line, + style: whiteStyle(20), + }), + ) + + if (this.gameSummary.isBlocked) { + line += 30 + this.container.addChild( + createText({ + text: '(Blocked)', + x: this.width / 2, + y: line, + style: whiteStyle(), + }), + ) + } + + line += 30 + this.layer.addChild( + createText({ + text: `Points this round: ${this.gameSummary.winner.score}`, + x: this.width / 2, + y: line, + style: whiteStyle(20), + }), + ) + return line + 16 + } + + renderScores(y: number): number { + const scores = this.matchState.scoreboard + // this.type === 'round' + // ? this.gameSummary.score + // : this.matchState.scoreboard.map((d) => ({ name: d[0], score: d[1] })) + let line = y + 30 + scores.forEach((score: any) => { + line = line + 30 + this.layer.addChild( + createText({ + text: `${score.name}:`, + x: 130, + y: line, + style: whiteStyle(18), + align: 'left', + }), + ) + this.layer.addChild( + createText({ + text: `${score.score}`, + x: 330, + y: line, + style: whiteStyle(18), + align: 'right', + }), + ) + }) + return line + } + + renderButtons() { + if (this.type === 'round') { + this.layer.addChild( + createButton({ + text: 'Next', + dimension: { + x: this.width / 2 - 25, + y: this.height - 50, + width: 60, + height: 25, + }, + action: () => { + this.emit('nextClick', { sessionState: this.matchState }) + this.container.visible = false + }, + parent: this.layer, + }), + ) + } else { + this.layer.addChild( + createButton({ + text: 'Finish', + dimension: { + x: this.width / 2 - 25, + y: this.height - 50, + width: 60, + height: 25, + }, + action: () => { + this.emit('finishClick', this.gameSummary) + this.container.visible = false + }, + parent: this.layer, + }), + ) + } + } + + render() { + const title: string = this.type === 'round' ? 'Round Summary' : 'Match Finished!' + this.layer.removeChildren() + let y = this.renderTitle(30, title.toUpperCase()) + y = this.renderWinner(y) + this.renderScores(y) + this.renderButtons() + } +} diff --git a/src/game/Hand.ts b/src/game/Hand.ts index 519c15c..d349d76 100644 --- a/src/game/Hand.ts +++ b/src/game/Hand.ts @@ -11,7 +11,6 @@ export class Hand extends EventEmitter { tiles: Tile[] = [] container: Container = new Container() buttonPass: Container = new Container() - buttonNext: Container = new Container() height: number width: number ticker: Ticker @@ -27,6 +26,7 @@ export class Hand extends EventEmitter { availableTiles: Tile[] = [] tilesLayer!: Container interactionsLayer!: Container + score: number = 0 constructor(app: Application) { super() @@ -49,14 +49,14 @@ export class Hand extends EventEmitter { height: this.height, x: 0, y: 0, - parent: this.container + parent: this.container, }) this.interactionsLayer = createContainer({ width: this.width, height: this.height, x: 0, y: 0, - parent: this.container + parent: this.container, }) } @@ -65,16 +65,6 @@ export class Hand extends EventEmitter { this.tiles = [] this.initialized = false - this.buttonNext = createButton( - 'NEXT', - { x: this.width / 2 - 25, y: this.height / 2, width: 50, height: 20 }, - () => { - this.tilesLayer.removeChildren() - this.interactionsLayer.removeChild(this.buttonNext) - this.emit('nextClick') - }, - this.interactionsLayer - ) } get canMove() { @@ -98,7 +88,7 @@ export class Hand extends EventEmitter { this.availableTiles.forEach((tile) => { tile.animateTo({ x: tile.x, - y: tile.y - 10 + y: tile.y - 10, }) tile.interactive = true }) @@ -108,7 +98,7 @@ export class Hand extends EventEmitter { this.availableTiles.forEach((tile) => { tile.animateTo({ x: tile.x, - y: tile.y + 10 + y: tile.y + 10, }) tile.setPosition(tile.x, tile.y + 10) tile.interactive = false @@ -128,7 +118,7 @@ export class Hand extends EventEmitter { initialize(playerState: PlayerDto) { this.tiles = this.createTiles(playerState) this.initialized = this.tiles.length > 0 - this.renderTiles() + this.render() this.emit('hand-updated', this.tiles) } @@ -187,31 +177,32 @@ export class Hand extends EventEmitter { private createPassButton() { const lastTile = this.tiles[this.tiles.length - 1] const x = lastTile ? lastTile.x + lastTile.width : this.scaleX(0) - this.buttonPass = createButton( - 'PASS', - { x, y: this.height / 2, width: 50, height: 20 }, - () => { + this.buttonPass = createButton({ + text: 'PASS', + dimension: { x, y: this.height / 2, width: 50, height: 20 }, + action: () => { this.interactionsLayer.removeChild(this.buttonPass) this.emit('game:button-pass-click') }, - this.interactionsLayer - ) + parent: this.interactionsLayer, + }) } update(playerState: PlayerDto) { + this.tilesLayer.removeChildren() if (!this.initialized) { this.initialize(playerState) return } const missing: Tile | undefined = this.tiles.find( - (tile: Tile) => !playerState.hand.find((t) => t.id === tile.id) + (tile: Tile) => !playerState.hand.find((t) => t.id === tile.id), ) if (missing) { this.tilesLayer.removeChild(missing.getSprite()) this.tiles = this.tiles.filter((tile) => tile.id !== missing.id) this.emit('hand-updated', this.tiles) } - this.renderTiles() + this.render() } private createTiles(playerState: PlayerDto) { @@ -230,8 +221,8 @@ export class Hand extends EventEmitter { outerStrength: 2, innerStrength: 1, color: 0xffffff, - quality: 0.5 - }) + quality: 0.5, + }), ]) }) newTile.on('pointerout', () => { @@ -251,4 +242,13 @@ export class Hand extends EventEmitter { tile.setPosition(deltaX + tile.width / 2 + i * (tile.width + 5), tile.height / 2 + 20) }) } + + renderScore() { + //this.scoreLayer.removeChildren() + } + + render() { + this.renderTiles() + this.renderScore() + } } diff --git a/src/game/OtherHand.ts b/src/game/OtherHand.ts index c4b829c..b06b8d0 100644 --- a/src/game/OtherHand.ts +++ b/src/game/OtherHand.ts @@ -4,7 +4,7 @@ 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' +import { createText, playerNameText, scoreText } from './utilities/fonts' export class OtherHand { tilesInitialNumber: number = 7 @@ -22,10 +22,12 @@ export class OtherHand { logger: LoggingService = new LoggingService() tilesLayer!: Container interactionsLayer!: Container + scoreLayer: Container = new Container() + score: number = 0 constructor( private app: Application, - public position: 'left' | 'right' | 'top' = 'left' + public position: 'left' | 'right' | 'top' = 'left', ) { this.height = 100 this.width = 300 @@ -37,11 +39,23 @@ export class OtherHand { this.container.y = y this.calculateScale() this.initLayers() + this.render() } setPlayer(player: PlayerDto) { this.player = player - this.container.addChild(createText(`${player.name}`, this.width / 2, 12, playerNameText)) + this.container.addChild( + createText({ + text: `${player.name}`, + x: this.width / 2, + y: 12, + style: playerNameText, + }), + ) + } + + setScore(score: number) { + this.score = score } setHand(tiles: TileDto[]) { @@ -54,7 +68,19 @@ export class OtherHand { this.render() } - private render() { + private renderScore() { + this.scoreLayer.removeChildren() + const text = createText({ + text: `${this.score}`, + x: this.width - 5, + y: 50, + style: scoreText, + }) + text.anchor.set(1, 0.5) + this.scoreLayer.addChild(text) + } + + private renderTiles() { this.tilesLayer.removeChildren() const x = -9 this.hand.forEach((tile, index) => { @@ -63,6 +89,11 @@ export class OtherHand { }) } + private render() { + this.renderTiles() + this.renderScore() + } + private addBg() { const bg = new Sprite(Texture.WHITE) bg.alpha = 0.08 @@ -96,17 +127,17 @@ export class OtherHand { height: this.height, x: 0, y: 0, - parent: this.container + parent: this.container, }) this.interactionsLayer = createContainer({ width: this.width, height: this.height, x: 0, y: 0, - parent: this.container + parent: this.container, }) - this.container.addChild(this.tilesLayer) - this.container.addChild(this.interactionsLayer) + // this.container.addChild(this.tilesLayer) + this.container.addChild(this.scoreLayer) } private calculateScale() { diff --git a/src/game/utilities/assets.ts b/src/game/utilities/assets.ts index ccadeda..4e55974 100644 --- a/src/game/utilities/assets.ts +++ b/src/game/utilities/assets.ts @@ -27,10 +27,16 @@ import tile6_3 from '@/assets/images/tiles/6-3.png' import tile6_4 from '@/assets/images/tiles/6-4.png' import tile6_5 from '@/assets/images/tiles/6-5.png' import tile6_6 from '@/assets/images/tiles/6-6.png' -import dot from '@/assets/images/circle.png' import bgWood_1 from '@/assets/images/backgrounds/wood-1.jpg' import bg_1 from '@/assets/images/backgrounds/bg-1.png' import bg_green from '@/assets/images/backgrounds/bg-green.png' +import bg_red from '@/assets/images/backgrounds/bg-red.png' +import bg_yellow from '@/assets/images/backgrounds/bg-yellow.png' +import snd_move_1 from '@/assets/sounds/move-1.mp3' +import snd_move_2 from '@/assets/sounds/move-2.mp3' +import snd_move_3 from '@/assets/sounds/move-3.mp3' +import snd_move_4 from '@/assets/sounds/move-4.mp3' +import snd_intro from '@/assets/sounds/intro.mp3' export const assets = [ { alias: 'tile-back', src: tileBack }, @@ -62,8 +68,14 @@ export const assets = [ { alias: 'tile-6_4', src: tile6_4 }, { alias: 'tile-6_5', src: tile6_5 }, { alias: 'tile-6_6', src: tile6_6 }, - { alias: 'dot', src: dot }, { alias: 'bg-wood-1', src: bgWood_1 }, - { alias: 'bg-1', src: bg_1 }, - { alias: 'bg-green', src: bg_green } + { alias: 'bg-gray', src: bg_1 }, + { alias: 'bg-green', src: bg_green }, + { alias: 'bg-red', src: bg_red }, + { alias: 'bg-yellow', src: bg_yellow }, + { alias: 'snd-move-1', src: snd_move_1 }, + { alias: 'snd-move-2', src: snd_move_2 }, + { alias: 'snd-move-3', src: snd_move_3 }, + { alias: 'snd-move-4', src: snd_move_4 }, + { alias: 'snd-intro', src: snd_intro }, ] diff --git a/src/game/utilities/fonts.ts b/src/game/utilities/fonts.ts index 720d90b..1f65a3d 100644 --- a/src/game/utilities/fonts.ts +++ b/src/game/utilities/fonts.ts @@ -1,19 +1,27 @@ -import { Text, TextStyle } from 'pixi.js' +import { + Container, + Text, + TextStyle, + type TextStyleAlign, + type TextStyleFontStyle, + type TextStyleFontWeight, + type TextStyleOptions, +} from 'pixi.js' export const dropShadowStyle = { alpha: 0.5, angle: 0.3, blur: 5, - distance: 4 + distance: 4, } export const mainText = new TextStyle({ dropShadow: dropShadowStyle, - fill: '#b71a1a', + fill: '#aaaaaa', fontFamily: 'Arial, Helvetica, sans-serif', fontWeight: 'bold', letterSpacing: 1, - stroke: '#658f56' + stroke: '#565656', }) export const playerNameText = new TextStyle({ @@ -23,12 +31,104 @@ export const playerNameText = new TextStyle({ letterSpacing: 1, stroke: '#565656', fontSize: 15, - fontWeight: 'bold' + fontWeight: 'bold', }) -export function createText(str: string, x: number, y: number, style: TextStyle = mainText) { +export const summaryTitle = new TextStyle({ + dropShadow: dropShadowStyle, + fill: '#a2a2a2', + fontFamily: 'Arial, Helvetica, sans-serif', + letterSpacing: 1, + stroke: '#565656', + fontSize: 15, + fontWeight: 'bold', +}) + +export const scoreText = new TextStyle({ + dropShadow: dropShadowStyle, + fill: '#aaaaaa', + fontFamily: 'Arial, Helvetica, sans-serif', + letterSpacing: 1, + stroke: '#565656', + fontSize: 32, + fontWeight: 'bold', +}) + +function getStyle(styleOptions: TextStyleOptions = {}) { + const { + fill = 0xa2a2a2, + stroke = 0x565656, + fontSize = 15, + fontFamily = 'Arial, Helvetica, sans-serif', + fontWeight = 'normal', + fontStyle = 'normal', + dropShadow, + letterSpacing = 1, + } = styleOptions + const style = new TextStyle({ + fill, + fontFamily, + letterSpacing, + stroke, + fontSize, + fontStyle, + fontWeight: fontWeight as any, + dropShadow: dropShadow ? dropShadowStyle : undefined, + }) + return style +} + +export const whiteStyle = ( + fontSize: number = 15, + fontWeight: TextStyleFontWeight = 'normal', + dropShadow: boolean = true, +) => + getStyle({ + fontSize, + fontWeight, + dropShadow, + }) + +export const yellowStyle = ( + fontSize: number = 15, + fontWeight: TextStyleFontWeight = 'normal', + dropShadow: boolean = true, +) => + getStyle({ + fill: 0xffff00, + fontSize, + fontWeight, + dropShadow, + }) + +interface TextOptions { + text: string + x: number + y: number + style?: TextStyle + container?: Container + align?: 'left' | 'center' | 'right' + fontStyle?: TextStyleFontStyle +} + +export function createText(textOptions: TextOptions) { + const defaultOptions = { style: whiteStyle(), align: 'center' } + const { text: str, x, y, style, container, align } = { ...defaultOptions, ...textOptions } const text = new Text({ text: str, style }) - text.anchor.set(0.5, 0.5) + + switch (align) { + case 'center': + text.anchor.set(0.5, 0.5) + break + case 'left': + text.anchor.set(0, 0.5) + break + case 'right': + text.anchor.set(1, 0.5) + break + } + + if (container) container.addChild(text) text.x = x text.y = y return text diff --git a/src/services/SocketIoClientService.ts b/src/services/SocketIoClientService.ts index 890d1dd..d7c29dd 100644 --- a/src/services/SocketIoClientService.ts +++ b/src/services/SocketIoClientService.ts @@ -23,8 +23,8 @@ export class SocketIoClientService extends ServiceBase { } this.socket = io(this.url, { auth: { - token: jwt.value - } + token: jwt.value, + }, }) this.socket.on('connect', () => { if (this.socket && this.socket.recovered) { @@ -62,14 +62,6 @@ export class SocketIoClientService extends ServiceBase { // Custom events - // this.socket.on('makeMove', async (data: any, callback: any) => { - // callback(await this.gameEventManager.handleCanMakeMoveEvent(data)) - // }) - - // this.socket.on('chooseTile', async (data: any, callback: any) => { - // callback(await this.gameEventManager.handleCanSelectTileEvent()) - // }) - this.socket.on('server:game-event', (data: any) => { this.gameEventManager.handleGameEvent(data) }) @@ -77,6 +69,13 @@ export class SocketIoClientService extends ServiceBase { this.socket.on('server:game-event-ack', async (data: any, callback: any) => { await this.gameEventManager.handleGameEventAck(data, callback) }) + + this.socket.onAny((eventName, eventData) => { + if (eventName === 'server:game-event' || eventName === 'server:game-event-ack') { + const { event, data } = eventData + this.logger.debug(`Received event: ${event}`, data) + } + }) } sendMessage(event: string, data: any): void { diff --git a/src/stores/gameOptions.ts b/src/stores/gameOptions.ts new file mode 100644 index 0000000..26b7007 --- /dev/null +++ b/src/stores/gameOptions.ts @@ -0,0 +1,9 @@ +import type { GameOptions } from '@/common/interfaces' +import { defineStore } from 'pinia' +import { ref } from 'vue' + +export const useGameOptionsStore = defineStore('gameOptions', () => { + const gameOptions = ref() + + return { gameOptions } +}) diff --git a/src/views/GameView.vue b/src/views/GameView.vue index 2fdc554..9788a19 100644 --- a/src/views/GameView.vue +++ b/src/views/GameView.vue @@ -40,20 +40,16 @@ function copySeed() {