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'

View File

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

View File

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

9
package-lock.json generated
View File

@ -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",

View File

@ -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",

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) {
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()

View File

@ -34,7 +34,7 @@ export interface MatchSessionDto {
maxPlayers: number
numPlayers: number
waitingSeconds: number
scoreboard: Map<string, number>
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[]
}

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 { 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(

View File

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

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 { 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<HTMLCanvasElement> {
@ -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) {

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[] = []
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()
}
}

View File

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

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

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 = {
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 })
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

View File

@ -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 {

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

View File

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

View File

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