This commit is contained in:
Jose Conde 2024-07-16 02:14:50 +02:00
parent 3755f2857a
commit 5392dce264
31 changed files with 883 additions and 183 deletions

2
.env
View File

@ -1,2 +1,2 @@
VITE_LOG_LEVEL= 'error' VITE_LOG_LEVEL= 'trace'
VITE_API_URL= 'http://localhost:3000/api' VITE_API_URL= 'http://localhost:3000/api'

View File

@ -3,13 +3,13 @@ require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = { module.exports = {
root: true, root: true,
'extends': [ extends: [
'plugin:vue/vue3-essential', 'plugin:vue/vue3-essential',
'eslint:recommended', 'eslint:recommended',
'@vue/eslint-config-typescript', '@vue/eslint-config-typescript',
'@vue/eslint-config-prettier/skip-formatting' '@vue/eslint-config-prettier/skip-formatting',
], ],
parserOptions: { parserOptions: {
ecmaVersion: 'latest' ecmaVersion: 'latest',
} },
} }

View File

@ -4,5 +4,5 @@
"tabWidth": 2, "tabWidth": 2,
"singleQuote": true, "singleQuote": true,
"printWidth": 100, "printWidth": 100,
"trailingComma": "none" "trailingComma": "all"
} }

9
package-lock.json generated
View File

@ -8,6 +8,7 @@
"name": "domino-client", "name": "domino-client",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@pixi/sound": "^6.0.0",
"bulma": "^1.0.1", "bulma": "^1.0.1",
"colorette": "^2.0.20", "colorette": "^2.0.20",
"dayjs": "^1.11.11", "dayjs": "^1.11.11",
@ -664,6 +665,14 @@
"resolved": "https://registry.npmjs.org/@pixi/colord/-/colord-2.9.6.tgz", "resolved": "https://registry.npmjs.org/@pixi/colord/-/colord-2.9.6.tgz",
"integrity": "sha512-nezytU2pw587fQstUu1AsJZDVEynjskwOL+kibwcdxsMBFqPsFFNA7xl0ii/gXuDi6M0xj3mfRJj8pBSc2jCfA==" "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": { "node_modules/@pkgjs/parseargs": {
"version": "0.11.0", "version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",

View File

@ -14,6 +14,7 @@
"format": "prettier --write src/" "format": "prettier --write src/"
}, },
"dependencies": { "dependencies": {
"@pixi/sound": "^6.0.0",
"bulma": "^1.0.1", "bulma": "^1.0.1",
"colorette": "^2.0.20", "colorette": "^2.0.20",
"dayjs": "^1.11.11", "dayjs": "^1.11.11",

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

BIN
src/assets/sounds/intro.mp3 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -17,8 +17,13 @@ export function getColorBackground(container: Container, colorName: string, alph
export function createContainer(options: ContainerOptions) { export function createContainer(options: ContainerOptions) {
const opts = { ...DEFAULT_CONTAINER_OPTIONS, ...options } const opts = { ...DEFAULT_CONTAINER_OPTIONS, ...options }
const container = new Container() 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) { if (opts.color) {
rect.fill(opts.color) rect.fill(opts.color)
@ -29,19 +34,17 @@ export function createContainer(options: ContainerOptions) {
rect.visible = opts.visible rect.visible = opts.visible
container.addChild(rect) container.addChild(rect)
if (opts.parent) {
opts.parent.addChild(container)
}
return container return container
} }
export function createButton( export function createButton(options: {
textStr: string, text: string
dimension: Dimension, dimension: Dimension
action: Function, action: Function
parent?: Container parent?: Container
): Container { }): Container {
const { x, y, width, height } = dimension 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 rectangle = new Graphics().roundRect(x, y, width + 4, height + 4, 5).fill(0xffff00)
const text = new Text({ const text = new Text({
text: textStr, text: textStr,
@ -50,8 +53,8 @@ export function createButton(
fontSize: 12, fontSize: 12,
fontWeight: 'bold', fontWeight: 'bold',
fill: 0x121212, fill: 0x121212,
align: 'center' align: 'center',
} },
}) })
text.anchor = 0.5 text.anchor = 0.5
const container = new Container() const container = new Container()

View File

@ -34,7 +34,7 @@ export interface MatchSessionDto {
maxPlayers: number maxPlayers: number
numPlayers: number numPlayers: number
waitingSeconds: number waitingSeconds: number
scoreboard: Map<string, number> scoreboard: { id: string; name: string; score: number }[]
matchWinner: PlayerDto | null matchWinner: PlayerDto | null
matchInProgress: boolean matchInProgress: boolean
playersReady: number playersReady: number
@ -98,3 +98,20 @@ export interface AnimationOptions {
width?: number width?: number
height?: 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[]
}

269
src/common/summarymock.ts Normal file
View File

@ -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,
},
],
},
],
},
}

View File

@ -5,13 +5,16 @@ import { Game } from '@/game/Game'
import { useGameStore } from '@/stores/game' import { useGameStore } from '@/stores/game'
import { useEventBusStore } from '@/stores/eventBus' import { useEventBusStore } from '@/stores/eventBus'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { useGameOptionsStore } from '@/stores/gameOptions'
const socketService: any = inject('socket') const socketService: any = inject('socket')
const gameStore = useGameStore() const gameStore = useGameStore()
const gameOptionsStore = useGameOptionsStore()
const eventBus = useEventBusStore() const eventBus = useEventBusStore()
const { playerState, sessionState } = storeToRefs(gameStore) const { playerState, sessionState } = storeToRefs(gameStore)
const { updateGameState } = gameStore const { updateGameState } = gameStore
const { gameOptions } = storeToRefs(gameOptionsStore)
const minScreenWidth = 800 const minScreenWidth = 800
const minScreenHeight = 700 const minScreenHeight = 700
@ -31,11 +34,12 @@ const game = new Game(
width: screenWidth, width: screenWidth,
height: screenHeight, height: screenHeight,
boardScale, boardScale,
handScale: 1 handScale: 1,
background: gameOptions.value?.background || 'green',
}, },
socketService, socketService,
playerState.value?.id || '', playerState.value?.id || '',
sessionState.value?.id || '' sessionState.value?.id || '',
) )
// watch( // watch(

View File

@ -8,6 +8,7 @@ import { LoggingService } from '@/services/LoggingService'
import { GlowFilter } from 'pixi-filters' import { GlowFilter } from 'pixi-filters'
import { ORIENTATION_ANGLES } from '@/common/constants' import { ORIENTATION_ANGLES } from '@/common/constants'
import type { OtherHand } from './OtherHand' import type { OtherHand } from './OtherHand'
import { sound } from '@pixi/sound'
export class Board extends EventEmitter { export class Board extends EventEmitter {
private _scale: number = 1 private _scale: number = 1
@ -46,63 +47,61 @@ export class Board extends EventEmitter {
constructor(app: Application) { constructor(app: Application) {
super() super()
this.ticker = app.ticker this.ticker = app.ticker
this.height = app.canvas.height - 130 this.height = app.canvas.height - 130 - 130
this.width = app.canvas.width this.width = app.canvas.width
this.scaleX = Scale([0, this.width], [0, this.width]) this.scaleX = Scale([0, this.width], [0, this.width])
this.scaleY = Scale([0, this.height], [0, this.height]) this.scaleY = Scale([0, this.height], [0, this.height])
this.calculateScale() this.calculateScale()
this.container = createContainer({ this.container = createContainer({
y: 130,
width: this.width, width: this.width,
height: this.height, height: this.height,
parent: app.stage parent: app.stage,
}) })
const background = new Sprite(Assets.get('bg-green')) // this.container.y = 130
// background.width = this.width
// background.height = this.height
this.container.addChild(background)
this.initialContainer = createContainer({ this.initialContainer = createContainer({
width: this.width, width: this.width,
height: this.height, height: this.height,
// color: 0x1e2f23, // color: 0x1e2f23,
visible: false, visible: false,
parent: this.container parent: this.container,
}) })
this.tilesContainer = createContainer({ this.tilesContainer = createContainer({
width: this.width, width: this.width,
height: this.height, height: this.height,
// color: 0x1e2f23, // color: 0x1e2f23,
parent: this.container parent: this.container,
}) })
this.interactionContainer = createContainer({ this.interactionContainer = createContainer({
width: this.width, width: this.width,
height: this.height, height: this.height,
parent: this.container, parent: this.container,
visible: false visible: false,
}) })
createCrosshair(this.tilesContainer, 0xff0000, { createCrosshair(this.tilesContainer, 0xff0000, {
width: this.width, width: this.width,
height: this.height, height: this.height,
x: this.scaleX(0), x: this.scaleX(0),
y: this.scaleY(0) y: this.scaleY(0),
}) })
createCrosshair(this.interactionContainer, 0xffff00, { createCrosshair(this.interactionContainer, 0xffff00, {
width: this.width, width: this.width,
height: this.height, height: this.height,
x: this.scaleX(0), x: this.scaleX(0),
y: this.scaleY(0) y: this.scaleY(0),
}) })
this.textContainer = createContainer({ this.textContainer = createContainer({
width: this.width, width: this.width,
height: this.height, height: this.height,
parent: this.container parent: this.container,
}) })
this.showText('Starting game...') this.showText('Starting game...')
@ -134,7 +133,13 @@ export class Board extends EventEmitter {
showText(text: string) { showText(text: string) {
this.textContainer.removeChildren() 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) { async setPlayerTurn(player: PlayerDto) {
@ -169,7 +174,6 @@ export class Board extends EventEmitter {
} }
async addTile(tile: Tile, move: Movement) { async addTile(tile: Tile, move: Movement) {
console.log('adding tile', tile.pips)
let orientation = '' let orientation = ''
let x: number = let x: number =
move.type === 'left' move.type === 'left'
@ -234,11 +238,18 @@ export class Board extends EventEmitter {
tile.addTo(this.tilesContainer) tile.addTo(this.tilesContainer)
tile.reScale(this.scale) tile.reScale(this.scale)
this.tiles.push(tile) this.tiles.push(tile)
const moveSound = this.getRandomClickSound()
await this.animateTile(tile, x, y, orientation, move) await this.animateTile(tile, x, y, orientation, move)
sound.play(moveSound)
this.emit('game:tile-animation-ended', tile.toPlain()) 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) { async animateTile(tile: Tile, x: number, y: number, orientation: string, move: Movement) {
const targetX = this.scaleX(x) const targetX = this.scaleX(x)
const targetY = this.scaleY(y) const targetY = this.scaleY(y)
@ -246,13 +257,13 @@ export class Board extends EventEmitter {
x: targetX, x: targetX,
y: targetY, y: targetY,
rotation: ORIENTATION_ANGLES[orientation], rotation: ORIENTATION_ANGLES[orientation],
duration: 20 duration: 20,
} }
const tempAlpha = tile.alpha const tempAlpha = tile.alpha
tile.alpha = 0 tile.alpha = 0
const clonedTile = tile.clone() const clonedTile = tile.clone()
clonedTile.addTo(this.tilesContainer) clonedTile.addTo(this.tilesContainer)
const pos = this.getAnimationInitialPoosition(move) const pos = this.getAnimationInitialPosition(move)
clonedTile.setPosition(this.scaleX(pos.x), this.scaleY(pos.y)) clonedTile.setPosition(this.scaleX(pos.x), this.scaleY(pos.y))
await clonedTile.animateTo(animation) await clonedTile.animateTo(animation)
clonedTile.removeFromParent() clonedTile.removeFromParent()
@ -261,7 +272,7 @@ export class Board extends EventEmitter {
tile.alpha = tempAlpha 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) const otherHand = this.otherPlayerHands.find((h) => h.player?.id === move.playerId)
if (otherHand === undefined) { if (otherHand === undefined) {
return { x: 0, y: this.scaleY.inverse(this.height + 50) } return { x: 0, y: this.scaleY.inverse(this.height + 50) }
@ -271,9 +282,9 @@ export class Board extends EventEmitter {
case 'left': case 'left':
return { x: this.scaleX.inverse(100), y: this.scaleY.inverse(100) } return { x: this.scaleX.inverse(100), y: this.scaleY.inverse(100) }
case 'right': 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) } 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( nextTileValidPoints(
tile: TileDto, tile: TileDto,
side: string, side: string,
validMoves: boolean[] validMoves: boolean[],
): ([number, number] | undefined)[] { ): ([number, number] | undefined)[] {
const isLeft = side === 'left' const isLeft = side === 'left'
const end = isLeft ? this.leftTile : this.rightTile const end = isLeft ? this.leftTile : this.rightTile
@ -450,8 +461,8 @@ export class Board extends EventEmitter {
outerStrength: 1, outerStrength: 1,
innerStrength: 0, innerStrength: 0,
color: 0xffffff, color: 0xffffff,
quality: 0.5 quality: 0.5,
}) }),
]) ])
dot.setOrientation(direction ?? 'north') dot.setOrientation(direction ?? 'north')
// const dot = new Dot(this.ticker, this.scale) // const dot = new Dot(this.ticker, this.scale)

View File

@ -1,13 +1,30 @@
import { Application, Assets } from 'pixi.js' import { Application, Assets, Container, Sprite } from 'pixi.js'
import { Board } from '@/game/Board' import { Board } from '@/game/Board'
import { assets } from '@/game/utilities/assets' import { assets } from '@/game/utilities/assets'
import { Tile } from '@/game/Tile' import { Tile } from '@/game/Tile'
import { Hand } from '@/game/Hand' 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 type { SocketIoClientService } from '@/services/SocketIoClientService'
import { wait } from '@/common/helpers' import { wait } from '@/common/helpers'
import { Actions } from 'pixi-actions' import { Actions } from 'pixi-actions'
import { OtherHand } from './OtherHand' 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 { export class Game {
public board!: Board public board!: Board
@ -16,17 +33,20 @@ export class Game {
private selectedTile: TileDto | undefined private selectedTile: TileDto | undefined
private currentMove: Movement | undefined private currentMove: Movement | undefined
private otherHands: OtherHand[] = [] private otherHands: OtherHand[] = []
private backgroundLayer: Container = new Container()
private gameSummaryView!: GameSummayView
constructor( constructor(
private options: { boardScale: number; handScale: number; width: number; height: number } = { private options: GameOptions = {
boardScale: 1, boardScale: 1,
handScale: 1, handScale: 1,
width: 1200, width: 1200,
height: 800 height: 800,
background: 'bg-green',
}, },
private socketService: SocketIoClientService, private socketService: SocketIoClientService,
private playerId: string, private playerId: string,
private sessionId: string private sessionId: string,
) {} ) {}
async setup(): Promise<HTMLCanvasElement> { async setup(): Promise<HTMLCanvasElement> {
@ -39,14 +59,16 @@ export class Game {
} }
async start(players: PlayerDto[] = []) { async start(players: PlayerDto[] = []) {
this.iniialStuff(this.app)
this.board = new Board(this.app) this.board = new Board(this.app)
this.hand = new Hand(this.app) this.hand = new Hand(this.app)
this.otherHands = [ this.otherHands = [
new OtherHand(this.app, 'left'), new OtherHand(this.app, 'left'),
new OtherHand(this.app, 'top'), new OtherHand(this.app, 'top'),
new OtherHand(this.app, 'right') new OtherHand(this.app, 'right'),
] ]
this.initOtherHands(players) this.initOtherHands(players)
this.gameSummaryView = new GameSummayView(this.app)
this.hand.scale = this.options.handScale this.hand.scale = this.options.handScale
this.board.scale = this.options.boardScale this.board.scale = this.options.boardScale
this.setBoardEvents() this.setBoardEvents()
@ -55,10 +77,18 @@ export class Game {
wait(3000) wait(3000)
this.socketService.sendMessage('client:set-client-ready', { this.socketService.sendMessage('client:set-client-ready', {
sessionId: this.sessionId, 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[]) { initOtherHands(players: PlayerDto[]) {
const myIndex = players.findIndex((player) => player.id === this.playerId) const myIndex = players.findIndex((player) => player.id === this.playerId)
const copy = [...players] const copy = [...players]
@ -107,25 +137,36 @@ export class Game {
const move: Movement = { const move: Movement = {
id: '', id: '',
type: 'pass', type: 'pass',
playerId: this.playerId playerId: this.playerId,
} }
this.socketService.sendMessage('client:player-move', { this.socketService.sendMessage('client:player-move', {
sessionId: this.sessionId, sessionId: this.sessionId,
move: move move: move,
}) })
await this.board.updateBoard(move, undefined) 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', { this.socketService.sendMessage('client:set-client-ready-for-next-game', {
userId: this.playerId, userId: this.playerId,
sessionId: this.sessionId sessionId: this.sessionId,
}) })
}) })
this.hand.on('hand-initialized', () => {}) 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) { highlightMoves(tile: TileDto) {
this.selectedTile = tile this.selectedTile = tile
if (tile !== undefined) { if (tile !== undefined) {
@ -168,7 +209,7 @@ export class Game {
tile: this.selectedTile, tile: this.selectedTile,
type: 'left', type: 'left',
playerId: this.playerId, playerId: this.playerId,
...data ...data,
} }
this.currentMove = move this.currentMove = move
const tile = this.hand.tileMoved(this.selectedTile) const tile = this.hand.tileMoved(this.selectedTile)
@ -181,7 +222,7 @@ export class Game {
tile: this.selectedTile, tile: this.selectedTile,
type: 'right', type: 'right',
playerId: this.playerId, playerId: this.playerId,
...data ...data,
} }
this.currentMove = move this.currentMove = move
const tile = this.hand.tileMoved(this.selectedTile) const tile = this.hand.tileMoved(this.selectedTile)
@ -192,12 +233,12 @@ export class Game {
if (tile !== null && tile !== undefined && tile.playerId === this.playerId) { if (tile !== null && tile !== undefined && tile.playerId === this.playerId) {
this.socketService.sendMessage('client:player-move', { this.socketService.sendMessage('client:player-move', {
sessionId: this.sessionId, sessionId: this.sessionId,
move: this.currentMove move: this.currentMove,
}) })
} else { } else {
this.socketService.sendMessage('client:animation-ended', { this.socketService.sendMessage('client:animation-ended', {
sessionId: this.sessionId, sessionId: this.sessionId,
userId: this.playerId userId: this.playerId,
}) })
} }
}) })
@ -206,11 +247,13 @@ export class Game {
gameFinished(data: any) { gameFinished(data: any) {
this.hand.gameFinished() this.hand.gameFinished()
this.board.gameFinished(data) this.board.gameFinished(data)
this.gameSummaryView.setGameSummary(data, 'round')
} }
matchFinished(data: any) { matchFinished(data: any) {
// this.hand.matchFinished() // this.hand.matchFinished()
this.board.matchFinished(data) this.board.matchFinished(data)
this.gameSummaryView.setGameSummary(data, 'match')
} }
serverPlayerMove(data: any, playerId: string) { serverPlayerMove(data: any, playerId: string) {

168
src/game/GameSummayView.ts Normal file
View File

@ -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()
}
}

View File

@ -11,7 +11,6 @@ export class Hand extends EventEmitter {
tiles: Tile[] = [] tiles: Tile[] = []
container: Container = new Container() container: Container = new Container()
buttonPass: Container = new Container() buttonPass: Container = new Container()
buttonNext: Container = new Container()
height: number height: number
width: number width: number
ticker: Ticker ticker: Ticker
@ -27,6 +26,7 @@ export class Hand extends EventEmitter {
availableTiles: Tile[] = [] availableTiles: Tile[] = []
tilesLayer!: Container tilesLayer!: Container
interactionsLayer!: Container interactionsLayer!: Container
score: number = 0
constructor(app: Application) { constructor(app: Application) {
super() super()
@ -49,14 +49,14 @@ export class Hand extends EventEmitter {
height: this.height, height: this.height,
x: 0, x: 0,
y: 0, y: 0,
parent: this.container parent: this.container,
}) })
this.interactionsLayer = createContainer({ this.interactionsLayer = createContainer({
width: this.width, width: this.width,
height: this.height, height: this.height,
x: 0, x: 0,
y: 0, y: 0,
parent: this.container parent: this.container,
}) })
} }
@ -65,16 +65,6 @@ export class Hand extends EventEmitter {
this.tiles = [] this.tiles = []
this.initialized = false 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() { get canMove() {
@ -98,7 +88,7 @@ export class Hand extends EventEmitter {
this.availableTiles.forEach((tile) => { this.availableTiles.forEach((tile) => {
tile.animateTo({ tile.animateTo({
x: tile.x, x: tile.x,
y: tile.y - 10 y: tile.y - 10,
}) })
tile.interactive = true tile.interactive = true
}) })
@ -108,7 +98,7 @@ export class Hand extends EventEmitter {
this.availableTiles.forEach((tile) => { this.availableTiles.forEach((tile) => {
tile.animateTo({ tile.animateTo({
x: tile.x, x: tile.x,
y: tile.y + 10 y: tile.y + 10,
}) })
tile.setPosition(tile.x, tile.y + 10) tile.setPosition(tile.x, tile.y + 10)
tile.interactive = false tile.interactive = false
@ -128,7 +118,7 @@ export class Hand extends EventEmitter {
initialize(playerState: PlayerDto) { initialize(playerState: PlayerDto) {
this.tiles = this.createTiles(playerState) this.tiles = this.createTiles(playerState)
this.initialized = this.tiles.length > 0 this.initialized = this.tiles.length > 0
this.renderTiles() this.render()
this.emit('hand-updated', this.tiles) this.emit('hand-updated', this.tiles)
} }
@ -187,31 +177,32 @@ export class Hand extends EventEmitter {
private createPassButton() { private createPassButton() {
const lastTile = this.tiles[this.tiles.length - 1] const lastTile = this.tiles[this.tiles.length - 1]
const x = lastTile ? lastTile.x + lastTile.width : this.scaleX(0) const x = lastTile ? lastTile.x + lastTile.width : this.scaleX(0)
this.buttonPass = createButton( this.buttonPass = createButton({
'PASS', text: 'PASS',
{ x, y: this.height / 2, width: 50, height: 20 }, dimension: { x, y: this.height / 2, width: 50, height: 20 },
() => { action: () => {
this.interactionsLayer.removeChild(this.buttonPass) this.interactionsLayer.removeChild(this.buttonPass)
this.emit('game:button-pass-click') this.emit('game:button-pass-click')
}, },
this.interactionsLayer parent: this.interactionsLayer,
) })
} }
update(playerState: PlayerDto) { update(playerState: PlayerDto) {
this.tilesLayer.removeChildren()
if (!this.initialized) { if (!this.initialized) {
this.initialize(playerState) this.initialize(playerState)
return return
} }
const missing: Tile | undefined = this.tiles.find( 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) { if (missing) {
this.tilesLayer.removeChild(missing.getSprite()) this.tilesLayer.removeChild(missing.getSprite())
this.tiles = this.tiles.filter((tile) => tile.id !== missing.id) this.tiles = this.tiles.filter((tile) => tile.id !== missing.id)
this.emit('hand-updated', this.tiles) this.emit('hand-updated', this.tiles)
} }
this.renderTiles() this.render()
} }
private createTiles(playerState: PlayerDto) { private createTiles(playerState: PlayerDto) {
@ -230,8 +221,8 @@ export class Hand extends EventEmitter {
outerStrength: 2, outerStrength: 2,
innerStrength: 1, innerStrength: 1,
color: 0xffffff, color: 0xffffff,
quality: 0.5 quality: 0.5,
}) }),
]) ])
}) })
newTile.on('pointerout', () => { 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) tile.setPosition(deltaX + tile.width / 2 + i * (tile.width + 5), tile.height / 2 + 20)
}) })
} }
renderScore() {
//this.scoreLayer.removeChildren()
}
render() {
this.renderTiles()
this.renderScore()
}
} }

View File

@ -4,7 +4,7 @@ import { Scale, type ScaleFunction } from './utilities/scale'
import { Tile } from './Tile' import { Tile } from './Tile'
import type { Movement, PlayerDto, TileDto } from '@/common/interfaces' import type { Movement, PlayerDto, TileDto } from '@/common/interfaces'
import { createContainer } from '@/common/helpers' import { createContainer } from '@/common/helpers'
import { createText, playerNameText } from './utilities/fonts' import { createText, playerNameText, scoreText } from './utilities/fonts'
export class OtherHand { export class OtherHand {
tilesInitialNumber: number = 7 tilesInitialNumber: number = 7
@ -22,10 +22,12 @@ export class OtherHand {
logger: LoggingService = new LoggingService() logger: LoggingService = new LoggingService()
tilesLayer!: Container tilesLayer!: Container
interactionsLayer!: Container interactionsLayer!: Container
scoreLayer: Container = new Container()
score: number = 0
constructor( constructor(
private app: Application, private app: Application,
public position: 'left' | 'right' | 'top' = 'left' public position: 'left' | 'right' | 'top' = 'left',
) { ) {
this.height = 100 this.height = 100
this.width = 300 this.width = 300
@ -37,11 +39,23 @@ export class OtherHand {
this.container.y = y this.container.y = y
this.calculateScale() this.calculateScale()
this.initLayers() this.initLayers()
this.render()
} }
setPlayer(player: PlayerDto) { setPlayer(player: PlayerDto) {
this.player = player 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[]) { setHand(tiles: TileDto[]) {
@ -54,7 +68,19 @@ export class OtherHand {
this.render() 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() this.tilesLayer.removeChildren()
const x = -9 const x = -9
this.hand.forEach((tile, index) => { this.hand.forEach((tile, index) => {
@ -63,6 +89,11 @@ export class OtherHand {
}) })
} }
private render() {
this.renderTiles()
this.renderScore()
}
private addBg() { private addBg() {
const bg = new Sprite(Texture.WHITE) const bg = new Sprite(Texture.WHITE)
bg.alpha = 0.08 bg.alpha = 0.08
@ -96,17 +127,17 @@ export class OtherHand {
height: this.height, height: this.height,
x: 0, x: 0,
y: 0, y: 0,
parent: this.container parent: this.container,
}) })
this.interactionsLayer = createContainer({ this.interactionsLayer = createContainer({
width: this.width, width: this.width,
height: this.height, height: this.height,
x: 0, x: 0,
y: 0, y: 0,
parent: this.container parent: this.container,
}) })
this.container.addChild(this.tilesLayer) // this.container.addChild(this.tilesLayer)
this.container.addChild(this.interactionsLayer) this.container.addChild(this.scoreLayer)
} }
private calculateScale() { private calculateScale() {

View File

@ -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_4 from '@/assets/images/tiles/6-4.png'
import tile6_5 from '@/assets/images/tiles/6-5.png' import tile6_5 from '@/assets/images/tiles/6-5.png'
import tile6_6 from '@/assets/images/tiles/6-6.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 bgWood_1 from '@/assets/images/backgrounds/wood-1.jpg'
import bg_1 from '@/assets/images/backgrounds/bg-1.png' import bg_1 from '@/assets/images/backgrounds/bg-1.png'
import bg_green from '@/assets/images/backgrounds/bg-green.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 = [ export const assets = [
{ alias: 'tile-back', src: tileBack }, { alias: 'tile-back', src: tileBack },
@ -62,8 +68,14 @@ export const assets = [
{ alias: 'tile-6_4', src: tile6_4 }, { alias: 'tile-6_4', src: tile6_4 },
{ alias: 'tile-6_5', src: tile6_5 }, { alias: 'tile-6_5', src: tile6_5 },
{ alias: 'tile-6_6', src: tile6_6 }, { alias: 'tile-6_6', src: tile6_6 },
{ alias: 'dot', src: dot },
{ alias: 'bg-wood-1', src: bgWood_1 }, { alias: 'bg-wood-1', src: bgWood_1 },
{ alias: 'bg-1', src: bg_1 }, { alias: 'bg-gray', src: bg_1 },
{ alias: 'bg-green', src: bg_green } { 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 },
] ]

View File

@ -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 = { export const dropShadowStyle = {
alpha: 0.5, alpha: 0.5,
angle: 0.3, angle: 0.3,
blur: 5, blur: 5,
distance: 4 distance: 4,
} }
export const mainText = new TextStyle({ export const mainText = new TextStyle({
dropShadow: dropShadowStyle, dropShadow: dropShadowStyle,
fill: '#b71a1a', fill: '#aaaaaa',
fontFamily: 'Arial, Helvetica, sans-serif', fontFamily: 'Arial, Helvetica, sans-serif',
fontWeight: 'bold', fontWeight: 'bold',
letterSpacing: 1, letterSpacing: 1,
stroke: '#658f56' stroke: '#565656',
}) })
export const playerNameText = new TextStyle({ export const playerNameText = new TextStyle({
@ -23,12 +31,104 @@ export const playerNameText = new TextStyle({
letterSpacing: 1, letterSpacing: 1,
stroke: '#565656', stroke: '#565656',
fontSize: 15, 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 }) const text = new Text({ text: str, style })
switch (align) {
case 'center':
text.anchor.set(0.5, 0.5) 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.x = x
text.y = y text.y = y
return text return text

View File

@ -23,8 +23,8 @@ export class SocketIoClientService extends ServiceBase {
} }
this.socket = io(this.url, { this.socket = io(this.url, {
auth: { auth: {
token: jwt.value token: jwt.value,
} },
}) })
this.socket.on('connect', () => { this.socket.on('connect', () => {
if (this.socket && this.socket.recovered) { if (this.socket && this.socket.recovered) {
@ -62,14 +62,6 @@ export class SocketIoClientService extends ServiceBase {
// Custom events // 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.socket.on('server:game-event', (data: any) => {
this.gameEventManager.handleGameEvent(data) 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) => { this.socket.on('server:game-event-ack', async (data: any, callback: any) => {
await this.gameEventManager.handleGameEventAck(data, callback) 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 { sendMessage(event: string, data: any): void {

View File

@ -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<GameOptions>()
return { gameOptions }
})

View File

@ -40,20 +40,16 @@ function copySeed() {
<template> <template>
<div class="block"> <div class="block">
<section class="block info"> <section class="block info">
<p> <p>Running: {{ sessionState?.sessionInProgress }}</p>
Running: {{ sessionState?.sessionInProgress }} Seed: {{ sessionState?.seed }} <p>Seed: {{ sessionState?.seed }}</p>
<button @click="copySeed">Copy!</button>
</p>
<p> <p>
FreeEnds: {{ gameState?.boardFreeEnds }} - Current Player:{{ FreeEnds: {{ gameState?.boardFreeEnds }} - Current Player:{{
gameState?.currentPlayer?.name gameState?.currentPlayer?.name
}} }}
- Score: {{ sessionState?.scoreboard }}
</p>
<p v-if="sessionState?.id">
SessionID: {{ sessionState.id }} PlayerID: {{ playerState?.id }} - canMakeMove
{{ canMakeMove }}
</p> </p>
<p>Score: {{ sessionState?.scoreboard }}</p>
<p v-if="sessionState?.id">SessionID: {{ sessionState.id }}</p>
<p>PlayerID: {{ playerState?.id }}</p>
</section> </section>
<section class="block"> <section class="block">
<div class="game-container"> <div class="game-container">
@ -107,9 +103,12 @@ function copySeed() {
justify-content: center; justify-content: center;
} }
.info { .info {
position: absolute; color: white;
top: 0; opacity: 0.1;
left: 0; position: fixed;
top: 200px;
left: 10px;
z-index: 20; z-index: 20;
pointer-events: none;
} }
</style> </style>

View File

@ -9,25 +9,27 @@ import type { MatchSessionDto } from '@/common/interfaces'
import { useEventBusStore } from '@/stores/eventBus' import { useEventBusStore } from '@/stores/eventBus'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import { copyToclipboard } from '@/common/helpers' import { copyToclipboard } from '@/common/helpers'
import { useGameOptionsStore } from '@/stores/gameOptions'
let background = ref<string>('green')
let teamed = ref<boolean>(false)
let seed = ref<string>('') let seed = ref<string>('')
let sessionName = ref('Test Value') let sessionName = ref('Test Value')
let sessionId = ref('')
let matchSessions = ref<MatchSessionDto[]>([]) let matchSessions = ref<MatchSessionDto[]>([])
let dataInterval: any let dataInterval: any
const router = useRouter() const router = useRouter()
const gameStore = useGameStore() const gameStore = useGameStore()
const auth = useAuthStore() const auth = useAuthStore()
const gameOptionsStore = useGameOptionsStore()
const socketService: any = inject('socket') const socketService: any = inject('socket')
const gameService: GameService = inject<GameService>('game') as GameService const gameService: GameService = inject<GameService>('game') as GameService
const logger: LoggingService = inject<LoggingService>('logger') as LoggingService const logger: LoggingService = inject<LoggingService>('logger') as LoggingService
const { readyForStart, sessionState, isSessionStarted, playerState, amIHost } = const { sessionState, isSessionStarted, playerState, amIHost } = storeToRefs(gameStore)
storeToRefs(gameStore)
const { updateSessionState, updatePlayerState, updateGameState } = gameStore
const { user } = storeToRefs(auth) const { user } = storeToRefs(auth)
const { gameOptions } = storeToRefs(gameOptionsStore)
// function setPlayerReady() { // function setPlayerReady() {
// logger.debug('Starting game') // logger.debug('Starting game')
@ -53,26 +55,12 @@ eventBus.subscribe('window-before-unload', () => {
async function createMatch() { async function createMatch() {
logger.debug('Creating match') logger.debug('Creating match')
await socketService.connect() await socketService.connect()
gameOptions.value = { background: background.value }
const id = await gameService.createMatchSession(sessionName.value, seed.value) const id = await gameService.createMatchSession(sessionName.value, seed.value)
logger.debug('Match created successfully') logger.debug('Match created successfully')
router.push({ name: 'match', params: { id } }) router.push({ name: 'match', params: { id } })
} }
async function cancelMatch() {
logger.debug('Cancelling match')
await gameService.cancelMatchSession(sessionId.value)
await socketService.disconnect()
sessionId.value = ''
seed.value = ''
sessionName.value = ''
updateSessionState(undefined)
updatePlayerState(undefined)
updateGameState(undefined)
logger.debug('Match cancelled successfully')
loadData()
}
async function joinMatch(id: string) { async function joinMatch(id: string) {
if (id) { if (id) {
await socketService.connect() await socketService.connect()
@ -90,19 +78,16 @@ async function deleteMatch(id: string) {
async function loadData() { async function loadData() {
const listResponse = await gameService.listMatchSessions() const listResponse = await gameService.listMatchSessions()
console.log('listResponse :>> ', listResponse)
matchSessions.value = listResponse.data matchSessions.value = listResponse.data
sessionName.value = `Test #${listResponse.pagination.total + 1}` sessionName.value = `Test #${listResponse.pagination.total + 1}`
} }
onMounted(() => { onMounted(() => {
logger.debug('Home view mounted')
loadData() loadData()
dataInterval = setInterval(loadData, 5000) dataInterval = setInterval(loadData, 5000)
}) })
onUnmounted(() => { onUnmounted(() => {
logger.debug('Home view unmounted')
clearInterval(dataInterval) clearInterval(dataInterval)
}) })
@ -117,32 +102,58 @@ function copy(sessionSeed: string) {
<section class="section"> <section class="section">
<h1 class="title is-2">Welcome to the {{ user.username }}'s Home Page</h1> <h1 class="title is-2">Welcome to the {{ user.username }}'s Home Page</h1>
<div class="block"> <div class="block">
<p>This is a protected route.</p> <div class="field">
<p>{{ sessionState || 'No session' }}</p> <label class="label">Name</label>
<p>{{ playerState || 'No player state' }}</p> <div class="control">
<p>Session started: {{ isSessionStarted }}</p> <input type="text" class="input" v-model="sessionName" placeholder="Session Name" />
<p>Host: {{ amIHost }}</p>
</div> </div>
<div class="block" v-if="!isSessionStarted"> </div>
<div class="grid"> <div class="field">
<div class="cell"> <label class="label">Seed</label>
<div class="control">
<input <input
type="text"
class="input" class="input"
style="margin-bottom: 0" style="margin-bottom: 0"
v-model="sessionName" v-model="seed"
placeholder="Session Name" placeholder="Type the session seed here!"
/> />
</div> </div>
</div>
<div class="grid">
<div class="cell"> <div class="cell">
<input class="input" style="margin-bottom: 0" v-model="seed" placeholder="Seed" /> <div class="field">
<label for="background" class="label">Background color</label>
<div class="control">
<div class="select">
<select v-model="background" name="background">
<option value="green">Green Fabric</option>
<option value="gray">Gray Fabric</option>
<option value="blue">Blue Fabric</option>
<option value="yellow">Yellow Fabric</option>
<option value="red">Red Fabric</option>
</select>
</div> </div>
</div> </div>
</div> </div>
<button class="button" @click="createMatch" v-if="!isSessionStarted"> <div class="field">
Create Match Session <div class="control">
</button> <label for="teamed" class="checkbox">
<input v-model="teamed" name="teamed" type="checkbox" />
Crossed game ({{ teamed }})
</label>
</div>
</div>
</div>
<div class="cell"></div>
</div>
</div>
<div class="block" v-if="!isSessionStarted"></div>
<button class="button is-primary" @click.once="createMatch">Create Match Session</button>
</section> </section>
<section class="section available-sessions" v-if="!isSessionStarted"> <section class="section available-sessions">
<h2 class="title is-4">Available Sessions</h2> <h2 class="title is-4">Available Sessions</h2>
<div class="block"> <div class="block">
<div v-if="matchSessions.length === 0"> <div v-if="matchSessions.length === 0">
@ -150,16 +161,27 @@ function copy(sessionSeed: string) {
</div> </div>
<div v-else class="grid is-col-min-12"> <div v-else class="grid is-col-min-12">
<div class="cell" v-for="session in matchSessions" :key="session.id"> <div class="cell" v-for="session in matchSessions" :key="session.id">
<div class="card">
<div class="card-content">
<p class="title is-6">{{ session.name }}</p> <p class="title is-6">{{ session.name }}</p>
<p>ID: {{ session._id }}</p> <p>ID: {{ session._id }}</p>
<p>Players: {{ session.players.length }}</p> <p>Players: {{ session.players.length }}</p>
<p> <p>
Seed: {{ session.seed }} Seed: {{ session.seed }}
<button @click="() => copy(session.seed)">Copy</button> <button class="button is-small" @click="() => copy(session.seed)">Copy</button>
</p> </p>
<p>Status: {{ session.status }}</p> <p>Status: {{ session.status }}</p>
<button class="button" @click="() => joinMatch(session._id)">Join</button> <div class="buttons is-centered mt-4"></div>
<button class="button" @click="() => deleteMatch(session._id)">Delete</button> </div>
<div class="card-footer">
<p class="card-footer-item">
<a href="#" @click.once.prevent="() => joinMatch(session._id)"> Join </a>
</p>
<p class="card-footer-item">
<a href="#" @click.once.prevent="() => deleteMatch(session._id)"> Delete </a>
</p>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -4,6 +4,7 @@ import type { GameService } from '@/services/GameService'
import type { LoggingService } from '@/services/LoggingService' import type { LoggingService } from '@/services/LoggingService'
import { useEventBusStore } from '@/stores/eventBus' import { useEventBusStore } from '@/stores/eventBus'
import { useGameStore } from '@/stores/game' import { useGameStore } from '@/stores/game'
import { useGameOptionsStore } from '@/stores/gameOptions'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { inject, onBeforeMount, ref } from 'vue' import { inject, onBeforeMount, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
@ -12,6 +13,7 @@ const route = useRoute()
const router = useRouter() const router = useRouter()
const gameStore = useGameStore() const gameStore = useGameStore()
const eventBus = useEventBusStore() const eventBus = useEventBusStore()
const gameOptionsStore = useGameOptionsStore()
const socketService: any = inject('socket') const socketService: any = inject('socket')
const gameService: GameService = inject<GameService>('game') as GameService const gameService: GameService = inject<GameService>('game') as GameService
const logger: LoggingService = inject<LoggingService>('logger') as LoggingService const logger: LoggingService = inject<LoggingService>('logger') as LoggingService
@ -22,6 +24,7 @@ let matchSession = ref<MatchSessionDto | undefined>(undefined)
const { readyForStart, sessionState, isSessionStarted, playerState, amIHost } = const { readyForStart, sessionState, isSessionStarted, playerState, amIHost } =
storeToRefs(gameStore) storeToRefs(gameStore)
const { updateSessionState, updatePlayerState, updateGameState } = gameStore const { updateSessionState, updatePlayerState, updateGameState } = gameStore
const { gameOptions } = storeToRefs(gameOptionsStore)
async function setPlayerReady() { async function setPlayerReady() {
logger.debug('Starting game') logger.debug('Starting game')
@ -35,7 +38,7 @@ async function setPlayerReady() {
} }
await socketService.sendMessage('client:set-player-ready', { await socketService.sendMessage('client:set-player-ready', {
userId: playerState.value.id, userId: playerState.value.id,
sessionId: sessionState.value.id sessionId: sessionState.value.id,
}) })
} }
@ -45,7 +48,7 @@ async function startMatch() {
if (sessionId) { if (sessionId) {
await socketService.sendMessageWithAck('client:start-session', { await socketService.sendMessageWithAck('client:start-session', {
sessionId: sessionId, sessionId: sessionId,
playerId: playerId playerId: playerId,
}) })
} }
} }
@ -89,7 +92,7 @@ onBeforeMount(() => {
<template> <template>
<div> <div>
<h1 class="title is-2">Match Page</h1> <h1 class="title is-2">Match Page {{ isSessionStarted }}</h1>
<div class="block" v-if="matchSession"> <div class="block" v-if="matchSession">
<p>Session ID: {{ matchSession._id }}</p> <p>Session ID: {{ matchSession._id }}</p>
<p>Session Name: {{ matchSession.name }}</p> <p>Session Name: {{ matchSession.name }}</p>