match page (initial) i18n

This commit is contained in:
Jose Conde 2024-07-16 19:22:06 +02:00
parent c3ba84a815
commit 8f2c492278
24 changed files with 482 additions and 478 deletions

View File

@ -38,6 +38,7 @@ export interface MatchSessionDto {
matchWinner: PlayerDto | null matchWinner: PlayerDto | null
matchInProgress: boolean matchInProgress: boolean
playersReady: number playersReady: number
gameSummaries: GameSummary[]
} }
export interface GameDto { export interface GameDto {
@ -115,4 +116,11 @@ export interface GameSummary {
winner: PlayerDto winner: PlayerDto
score: { id: string; name: string; score: number }[] score: { id: string; name: string; score: number }[]
players?: PlayerDto[] players?: PlayerDto[]
board: TileDto[]
boneyard: TileDto[]
}
export interface Config {
waitMillisToShowSummary: number
activeHandStrokeColor: number
} }

View File

@ -1,17 +1,21 @@
<script setup lang="ts"> <script setup lang="ts">
import type { GameDto, PlayerDto } from '@/common/interfaces' import type { GameDto, PlayerDto } from '@/common/interfaces'
import { onMounted, onUnmounted, ref, inject, toRaw } from 'vue' import { onMounted, onUnmounted, ref, inject } from 'vue'
import { Game } from '@/game/Game' 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' import { useGameOptionsStore } from '@/stores/gameOptions'
import { useRoute, useRouter } from 'vue-router'
let sessionId: string
const socketService: any = inject('socket') const socketService: any = inject('socket')
const gameStore = useGameStore() const gameStore = useGameStore()
const gameOptionsStore = useGameOptionsStore() const gameOptionsStore = useGameOptionsStore()
const eventBus = useEventBusStore() const eventBus = useEventBusStore()
const router = useRouter()
const route = useRoute()
const { playerState, sessionState } = storeToRefs(gameStore) const { playerState, sessionState } = storeToRefs(gameStore)
const { updateGameState } = gameStore const { updateGameState } = gameStore
const { gameOptions } = storeToRefs(gameOptionsStore) const { gameOptions } = storeToRefs(gameOptionsStore)
@ -68,12 +72,18 @@ const game = new Game(
// ) // )
onMounted(async () => { onMounted(async () => {
sessionId = route.params.id as string
if (appEl.value === null) return if (appEl.value === null) return
const canvas = await game.setup() const canvas = await game.setup()
appEl.value.appendChild(canvas) appEl.value.appendChild(canvas)
await game.preload() await game.preload()
await game.start(sessionState?.value?.players) await game.start(sessionState?.value?.players)
game.on('game:finish-click', () => {
game.destroy()
router.push({ name: 'match', params: { id: sessionId } })
})
eventBus.subscribe('server:game-finished', (data) => { eventBus.subscribe('server:game-finished', (data) => {
game.gameFinished(data) game.gameFinished(data)
}) })
@ -88,7 +98,7 @@ onMounted(async () => {
eventBus.subscribe('server:hand-dealt', (data: { player: PlayerDto; gameState: GameDto }) => { eventBus.subscribe('server:hand-dealt', (data: { player: PlayerDto; gameState: GameDto }) => {
game.hand.update(data.player) game.hand.update(data.player)
game.updateOtherHands(data.gameState) game.updateOtherHands(data.gameState.players)
}) })
eventBus.subscribe('server:next-turn', (gameState: GameDto) => { eventBus.subscribe('server:next-turn', (gameState: GameDto) => {
@ -99,25 +109,6 @@ onMounted(async () => {
eventBus.subscribe('server:match-finished', (data) => { eventBus.subscribe('server:match-finished', (data) => {
game.matchFinished(data) game.matchFinished(data)
}) })
// eventBus.subscribe('server:game-state', (data) => {
// console.log('server:game-state :>> ', data)
// game.serverGameState(data)
// })
// mockMove(game, [6, 6], 'left')
// mockMove(game, [6, 4], 'left')
// mockMove(game, [4, 4], 'left')
// mockMove(game, [4, 0], 'left')
// mockMove(game, [0, 0], 'left')
// mockMove(game, [2, 0], 'left')
// mockMove(game, [2, 2], 'left')
// mockMove(game, [6, 5], 'right')
// mockMove(game, [5, 5], 'right')
// mockMove(game, [5, 1], 'right')
// mockMove(game, [3, 1], 'right')
// mockMove(game, [3, 0], 'right')
// mockMove(game, [5, 0], 'right')
}) })
onUnmounted(() => { onUnmounted(() => {

View File

@ -1,41 +0,0 @@
<script setup lang="ts">
defineProps<{
msg: string
}>()
</script>
<template>
<div class="greetings">
<h1 class="green">{{ msg }}</h1>
<h3>
Youve successfully created a project with
<a href="https://vitejs.dev/" target="_blank" rel="noopener">Vite</a> +
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>. What's next?
</h3>
</div>
</template>
<style scoped>
h1 {
font-weight: 500;
font-size: 2.6rem;
position: relative;
top: -10px;
}
h3 {
font-size: 1.2rem;
}
.greetings h1,
.greetings h3 {
text-align: center;
}
@media (min-width: 1024px) {
.greetings h1,
.greetings h3 {
text-align: left;
}
}
</style>

View File

@ -1,88 +0,0 @@
<script setup lang="ts">
import WelcomeItem from './WelcomeItem.vue'
import DocumentationIcon from './icons/IconDocumentation.vue'
import ToolingIcon from './icons/IconTooling.vue'
import EcosystemIcon from './icons/IconEcosystem.vue'
import CommunityIcon from './icons/IconCommunity.vue'
import SupportIcon from './icons/IconSupport.vue'
</script>
<template>
<WelcomeItem>
<template #icon>
<DocumentationIcon />
</template>
<template #heading>Documentation</template>
Vues
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
provides you with all information you need to get started.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<ToolingIcon />
</template>
<template #heading>Tooling</template>
This project is served and bundled with
<a href="https://vitejs.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
recommended IDE setup is
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a> +
<a href="https://github.com/johnsoncodehk/volar" target="_blank" rel="noopener">Volar</a>. If
you need to test your components and web pages, check out
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a> and
<a href="https://on.cypress.io/component" target="_blank" rel="noopener"
>Cypress Component Testing</a
>.
<br />
More instructions are available in <code>README.md</code>.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<EcosystemIcon />
</template>
<template #heading>Ecosystem</template>
Get official tools and libraries for your project:
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
you need more resources, we suggest paying
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
a visit.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<CommunityIcon />
</template>
<template #heading>Community</template>
Got stuck? Ask your question on
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>, our official
Discord server, or
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
>StackOverflow</a
>. You should also subscribe to
<a href="https://news.vuejs.org" target="_blank" rel="noopener">our mailing list</a> and follow
the official
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
twitter account for latest news in the Vue world.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<SupportIcon />
</template>
<template #heading>Support Vue</template>
As an independent project, Vue relies on community backing for its sustainability. You can help
us by
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
</WelcomeItem>
</template>

View File

@ -1,87 +0,0 @@
<template>
<div class="item">
<i>
<slot name="icon"></slot>
</i>
<div class="details">
<h3>
<slot name="heading"></slot>
</h3>
<slot></slot>
</div>
</div>
</template>
<style scoped>
.item {
margin-top: 2rem;
display: flex;
position: relative;
}
.details {
flex: 1;
margin-left: 1rem;
}
i {
display: flex;
place-items: center;
place-content: center;
width: 32px;
height: 32px;
color: var(--color-text);
}
h3 {
font-size: 1.2rem;
font-weight: 500;
margin-bottom: 0.4rem;
color: var(--color-heading);
}
@media (min-width: 1024px) {
.item {
margin-top: 0;
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
}
i {
top: calc(50% - 25px);
left: -26px;
position: absolute;
border: 1px solid var(--color-border);
background: var(--color-background);
border-radius: 8px;
width: 50px;
height: 50px;
}
.item:before {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
bottom: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:after {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
top: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:first-of-type:before {
display: none;
}
.item:last-of-type:after {
display: none;
}
}
</style>

View File

@ -1,8 +1,8 @@
import { Application, Assets, Container, EventEmitter, Sprite, Text, Ticker } from 'pixi.js' import { Application, Container, EventEmitter, Text, Ticker } from 'pixi.js'
import { Scale, type ScaleFunction } from '@/game/utilities/scale' import { Scale, type ScaleFunction } from '@/game/utilities/scale'
import type { AnimationOptions, Movement, PlayerDto, TileDto } from '@/common/interfaces' import type { AnimationOptions, Movement, PlayerDto, TileDto } from '@/common/interfaces'
import { Tile } from '@/game/Tile' import { Tile } from '@/game/Tile'
import { DIRECTIONS, createContainer, createCrosshair, isTilePair } from '@/common/helpers' import { DIRECTIONS, createContainer, isTilePair } from '@/common/helpers'
import { createText } from '@/game/utilities/fonts' import { createText } from '@/game/utilities/fonts'
import { LoggingService } from '@/services/LoggingService' import { LoggingService } from '@/services/LoggingService'
import { GlowFilter } from 'pixi-filters' import { GlowFilter } from 'pixi-filters'
@ -85,19 +85,12 @@ export class Board extends EventEmitter {
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, {
width: this.width,
height: this.height,
x: this.scaleX(0),
y: this.scaleY(0),
})
this.textContainer = createContainer({ this.textContainer = createContainer({
width: this.width, width: this.width,
@ -105,7 +98,7 @@ export class Board extends EventEmitter {
parent: this.container, parent: this.container,
}) })
this.showText(t('starting_game')) this.showText(t('game.starting_game'))
} }
private calculateScale() { private calculateScale() {
@ -144,11 +137,11 @@ export class Board extends EventEmitter {
} }
async setPlayerTurn(player: PlayerDto) { async setPlayerTurn(player: PlayerDto) {
this.showText('Your turn!') this.showText(t('game.your-turn'))
} }
async setServerPlayerTurn(currentPlayer: PlayerDto) { async setServerPlayerTurn(currentPlayer: PlayerDto) {
this.showText(`${currentPlayer.name}'s turn!\n Please wait...`) this.showText(t('game.player-turn', [currentPlayer.name]))
} }
async playerMove(move: any, playerId: string) { async playerMove(move: any, playerId: string) {
@ -599,8 +592,7 @@ export class Board extends EventEmitter {
return [canPlayNorth, canPlayEast, canPlaySouth, canPlayWest] return [canPlayNorth, canPlayEast, canPlaySouth, canPlayWest]
} }
gameFinished(data: any) { clean() {
const { lastGame, gameState } = data
this.tiles = [] this.tiles = []
this.boneyard = [] this.boneyard = []
this.movements = [] this.movements = []
@ -614,10 +606,13 @@ export class Board extends EventEmitter {
this.firstTile = undefined this.firstTile = undefined
this.tilesContainer.removeChildren() this.tilesContainer.removeChildren()
this.interactionContainer.removeChildren() this.interactionContainer.removeChildren()
this.showText(`Game finished \n Winner ${lastGame?.winner?.name ?? 'No winner'}`)
} }
matchFinished(data: any) { gameFinished() {
// this.showText(`Game finished \n Winner ${lastGame?.winner?.name ?? 'No winner'}`) this.showText(t('game.round-finished'))
}
matchFinished() {
this.showText(t('game.match-finished'))
} }
} }

8
src/game/Config.ts Normal file
View File

@ -0,0 +1,8 @@
import type { Config } from '@/common/interfaces'
const config: Config = {
waitMillisToShowSummary: 1000,
activeHandStrokeColor: 0xb39c4d,
}
export default config

View File

@ -1,4 +1,4 @@
import { Application, Assets, Container, Sprite } from 'pixi.js' import { Application, Assets, Container, EventEmitter, 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'
@ -9,6 +9,7 @@ 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 { GameSummayView } from './GameSummayView'
import Config from './Config'
interface GameOptions { interface GameOptions {
boardScale: number boardScale: number
@ -18,7 +19,7 @@ interface GameOptions {
background: string background: string
} }
export class Game { export class Game extends EventEmitter {
public board!: Board public board!: Board
public hand!: Hand public hand!: Hand
private app: Application = new Application() private app: Application = new Application()
@ -27,6 +28,7 @@ export class Game {
private otherHands: OtherHand[] = [] private otherHands: OtherHand[] = []
private backgroundLayer: Container = new Container() private backgroundLayer: Container = new Container()
private gameSummaryView!: GameSummayView private gameSummaryView!: GameSummayView
private players: PlayerDto[] = []
constructor( constructor(
private options: GameOptions = { private options: GameOptions = {
@ -39,7 +41,9 @@ export class Game {
private socketService: SocketIoClientService, private socketService: SocketIoClientService,
private playerId: string, private playerId: string,
private sessionId: string, private sessionId: string,
) {} ) {
super()
}
async setup(): Promise<HTMLCanvasElement> { async setup(): Promise<HTMLCanvasElement> {
const width = this.options.width || 1200 const width = this.options.width || 1200
@ -59,7 +63,8 @@ export class Game {
new OtherHand(this.app, 'top'), new OtherHand(this.app, 'top'),
new OtherHand(this.app, 'right'), new OtherHand(this.app, 'right'),
] ]
this.initOtherHands(players) this.initPlayers(players)
this.players = players
this.gameSummaryView = new GameSummayView(this.app) 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
@ -81,23 +86,23 @@ export class Game {
this.backgroundLayer.addChild(background) this.backgroundLayer.addChild(background)
} }
initOtherHands(players: PlayerDto[]) { initPlayers(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]
const cut = copy.splice(myIndex) const cut = copy.splice(myIndex)
cut.shift() const player = cut.shift()
const final = cut.concat(copy) const final = cut.concat(copy)
for (let i = 0; i < final.length; i++) { for (let i = 0; i < final.length; i++) {
const hand = this.otherHands[i] const hand = this.otherHands[i]
hand.setPlayer(final[i]) hand.setPlayer(final[i])
} }
this.hand.setPlayer(player)
this.board.otherPlayerHands = this.otherHands this.board.otherPlayerHands = this.otherHands
} }
updateOtherHands(gameState: GameDto) { updateOtherHands(players: PlayerDto[]) {
const players = gameState.players
players.forEach((player) => { players.forEach((player) => {
const hand = this.otherHands.find((hand) => hand.player?.id === player.id) const hand = this.otherHands.find((hand) => hand.player?.id === player.id)
if (hand) { if (hand) {
@ -115,6 +120,7 @@ export class Game {
setPlayersInactive() { setPlayersInactive() {
this.otherHands.forEach((hand) => hand.setActive(false)) this.otherHands.forEach((hand) => hand.setActive(false))
this.hand.setActive(false)
} }
async preload() { async preload() {
@ -148,7 +154,12 @@ export class Game {
await this.board.updateBoard(move, undefined) await this.board.updateBoard(move, undefined)
}) })
this.gameSummaryView.on('finishClick', (data) => {
this.emit('game:finish-click', data)
})
this.gameSummaryView.on('nextClick', (data) => { this.gameSummaryView.on('nextClick', (data) => {
this.board.clean()
this.updateScoreboard(data.sessionState) 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,
@ -161,11 +172,13 @@ export class Game {
private updateScoreboard(sessionState: MatchSessionDto) { private updateScoreboard(sessionState: MatchSessionDto) {
const scoreboard = sessionState.scoreboard const scoreboard = sessionState.scoreboard
this.otherHands.forEach((hand) => { const myScore = scoreboard.find((d) => d.id === this.playerId)?.score || 0
const player: PlayerDto | undefined = hand.player this.hand.setScore(myScore)
this.otherHands.forEach((otherHand) => {
const player: PlayerDto | undefined = otherHand.player
const name: string = player?.name || '' const name: string = player?.name || ''
const score = scoreboard.find((d) => d.name === name)?.score || 0 const score = scoreboard.find((d) => d.name === name)?.score || 0
hand.setScore(score) otherHand.setScore(score)
}) })
} }
@ -198,6 +211,7 @@ export class Game {
const currentPlayer = state?.currentPlayer! const currentPlayer = state?.currentPlayer!
this.setPlayersInactive() this.setPlayersInactive()
if (currentPlayer.id === this.playerId) { if (currentPlayer.id === this.playerId) {
this.hand.setActive(true)
this.hand.prepareForMove(this.board.count === 0, this.board.freeEnds) this.hand.prepareForMove(this.board.count === 0, this.board.freeEnds)
this.board.setPlayerTurn(currentPlayer) this.board.setPlayerTurn(currentPlayer)
} else { } else {
@ -248,15 +262,19 @@ export class Game {
}) })
} }
gameFinished(data: any) { async gameFinished(data: any) {
await wait(Config.waitMillisToShowSummary)
this.updateOtherHands(data.lastGame.players)
this.hand.gameFinished() this.hand.gameFinished()
this.board.gameFinished(data) this.board.gameFinished()
this.setPlayersInactive()
this.gameSummaryView.setGameSummary(data, 'round') this.gameSummaryView.setGameSummary(data, 'round')
} }
matchFinished(data: any) { async matchFinished(data: any) {
// this.hand.matchFinished() await wait(Config.waitMillisToShowSummary)
this.board.matchFinished(data) this.updateOtherHands(data.lastGame.players)
this.board.matchFinished()
this.gameSummaryView.setGameSummary(data, 'match') this.gameSummaryView.setGameSummary(data, 'match')
} }

View File

@ -30,8 +30,6 @@ export class GameSummayView extends EventEmitter {
height: this.height, height: this.height,
parent: this.container, parent: this.container,
}) })
console.log('GameSummaryView created!')
this.container.visible = false this.container.visible = false
} }
@ -67,7 +65,7 @@ export class GameSummayView extends EventEmitter {
if (this.gameSummary.isBlocked) { if (this.gameSummary.isBlocked) {
line += 30 line += 30
this.container.addChild( this.layer.addChild(
createText({ createText({
text: '(Blocked)', text: '(Blocked)',
x: this.width / 2, x: this.width / 2,

View File

@ -1,16 +1,19 @@
import { Application, Container, EventEmitter, Sprite, Texture, Ticker } from 'pixi.js' import { Application, Container, EventEmitter, Graphics, Sprite, Texture, Ticker } from 'pixi.js'
import { Tile } from '@/game/Tile' import { Tile } from '@/game/Tile'
import type { PlayerDto, TileDto } from '@/common/interfaces' import type { PlayerDto, TileDto } from '@/common/interfaces'
import { GlowFilter } from 'pixi-filters' import { GlowFilter } from 'pixi-filters'
import { Scale, type ScaleFunction } from './utilities/scale' import { Scale, type ScaleFunction } from './utilities/scale'
import { LoggingService } from '@/services/LoggingService' import { LoggingService } from '@/services/LoggingService'
import { createButton, createContainer } from '@/common/helpers' import { createButton, createContainer } from '@/common/helpers'
import { Action, Actions } from 'pixi-actions' import { createText, playerNameText, whiteStyle } from './utilities/fonts'
import Config from '@/game/Config'
export class Hand extends EventEmitter { 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()
scoreLayer: Container = new Container()
activeLayer: Container = new Container()
height: number height: number
width: number width: number
ticker: Ticker ticker: Ticker
@ -27,18 +30,22 @@ export class Hand extends EventEmitter {
tilesLayer!: Container tilesLayer!: Container
interactionsLayer!: Container interactionsLayer!: Container
score: number = 0 score: number = 0
active: boolean = false
private player!: PlayerDto
constructor(app: Application) { constructor(app: Application) {
super() super()
app.stage.addChild(this.container) app.stage.addChild(this.container)
this.ticker = app.ticker this.ticker = app.ticker
this.height = 130 * this.scale this.height = 130 * this.scale
this.width = app.canvas.width this.width = 800 // app.canvas.width
this.container.y = app.canvas.height - this.height this.container.y = app.canvas.height - this.height
this.container.x = app.canvas.width / 2 - this.width / 2
this.container.width = this.width this.container.width = this.width
this.container.height = this.height this.container.height = this.height
this.calculateScale() this.calculateScale()
this.initLayers() this.initLayers()
this.render()
} }
initLayers() { initLayers() {
@ -58,12 +65,12 @@ export class Hand extends EventEmitter {
y: 0, y: 0,
parent: this.container, parent: this.container,
}) })
this.container.addChild(this.scoreLayer)
this.container.addChild(this.activeLayer)
} }
gameFinished() { gameFinished() {
this.logger.debug('gameFinished')
this.tiles = [] this.tiles = []
this.initialized = false this.initialized = false
} }
@ -205,6 +212,22 @@ export class Hand extends EventEmitter {
this.render() this.render()
} }
setPlayer(player: PlayerDto | undefined) {
if (!player) return
this.player = player
this.render()
}
setScore(score: number) {
this.score = score
this.render()
}
setActive(active: boolean) {
this.active = active
this.render()
}
private createTiles(playerState: PlayerDto) { private createTiles(playerState: PlayerDto) {
return playerState.hand.map((tile: TileDto) => { return playerState.hand.map((tile: TileDto) => {
const newTile: Tile = new Tile(tile.id, this.ticker, tile.pips, this.scale, tile.playerId) const newTile: Tile = new Tile(tile.id, this.ticker, tile.pips, this.scale, tile.playerId)
@ -244,11 +267,38 @@ export class Hand extends EventEmitter {
} }
renderScore() { renderScore() {
//this.scoreLayer.removeChildren() this.scoreLayer.removeChildren()
const name = createText({
text: this.player?.name ?? '-',
x: 100,
y: 50,
style: playerNameText,
})
const text = createText({
text: `${this.score}`,
x: 100,
// x: this.width - 5,
y: 80,
style: whiteStyle(36, 'bold'),
})
text.anchor.set(1, 0.5)
this.scoreLayer.addChild(name)
this.scoreLayer.addChild(text)
}
renderActive() {
this.activeLayer.removeChildren()
if (this.active) {
const rectangle = new Graphics()
.roundRect(0, 0, this.width, this.height - 1, 5)
.stroke(Config.activeHandStrokeColor)
this.activeLayer.addChild(rectangle)
}
} }
render() { render() {
this.renderTiles() this.renderTiles()
this.renderScore() this.renderScore()
this.renderActive()
} }
} }

View File

@ -4,7 +4,8 @@ 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, scoreText } from './utilities/fonts' import { createText, playerNameText, whiteStyle } from './utilities/fonts'
import Config from '@/game/Config'
export class OtherHand { export class OtherHand {
tilesInitialNumber: number = 7 tilesInitialNumber: number = 7
@ -62,10 +63,11 @@ export class OtherHand {
setScore(score: number) { setScore(score: number) {
this.score = score this.score = score
this.render()
} }
setHand(tiles: TileDto[]) { setHand(tiles: TileDto[]) {
this.hand = tiles.map((tile) => new Tile(tile.id, this.app.ticker, undefined, this.scale)) this.hand = tiles.map((tile) => new Tile(tile.id, this.app.ticker, tile.pips, this.scale))
this.render() this.render()
} }
@ -80,7 +82,7 @@ export class OtherHand {
text: `${this.score}`, text: `${this.score}`,
x: this.width - 5, x: this.width - 5,
y: 50, y: 50,
style: scoreText, style: whiteStyle(36, 'bold'),
}) })
text.anchor.set(1, 0.5) text.anchor.set(1, 0.5)
this.scoreLayer.addChild(text) this.scoreLayer.addChild(text)
@ -90,7 +92,7 @@ export class OtherHand {
this.tilesLayer.removeChildren() this.tilesLayer.removeChildren()
const x = -9 const x = -9
this.hand.forEach((tile, index) => { this.hand.forEach((tile, index) => {
tile.setPosition(this.scaleX(x + index * 2), this.height / 2) tile.setPosition(this.scaleX(x + index * 2), this.height / 2 + 10)
this.tilesLayer.addChild(tile.getSprite()) this.tilesLayer.addChild(tile.getSprite())
}) })
} }
@ -98,7 +100,9 @@ export class OtherHand {
private renderActive() { private renderActive() {
this.interactionsLayer.removeChildren() this.interactionsLayer.removeChildren()
if (this.active) { if (this.active) {
const rectangle = new Graphics().roundRect(0, 0, this.width, this.height, 5).stroke(0xffff00) const rectangle = new Graphics()
.roundRect(0, 0, this.width, this.height, 5)
.stroke(Config.activeHandStrokeColor)
this.interactionsLayer.addChild(rectangle) this.interactionsLayer.addChild(rectangle)
} }
} }

48
src/i18n/en.json Normal file
View File

@ -0,0 +1,48 @@
{
"match-page": "Match Page",
"login": "Login",
"username": "Username",
"username-placeholder": "Username",
"password": "Password",
"password-placeholder": "Password",
"login-button": "Login",
"invalid-username-or-password": "Invalid username or password",
"winner": "Winner",
"points-to-win": "Points to win",
"final-scoreboard": "Final Scoreboard",
"round-index-1": "Round {0}",
"scoreboard": "Scoreboard",
"available-sessions": "Available Sessions",
"no-sessions-available": "No sessions available",
"id-session-_id": "ID: {0}",
"players-session-players-length": "Players: {0}",
"copy": "Copy",
"seed-session-seed": "Seed: {0}",
"status-session-status": "Status: {0}",
"delete": "Delete",
"join": "Join",
"welcome-to-the-user-username-s-home-page": "Welcome to the {0}'s Home Page",
"name": "Name",
"session-name-placeholder": "Session Name",
"seed": "Seed",
"seed-placeholder": "Type the session seed here!",
"background-color": "Background color",
"green-fabric": "Green Fabric",
"gray-fabric": "Gray Fabric",
"blue-fabric": "Blue Fabric",
"yellow-fabric": "Yellow Fabric",
"red-fabric": "Red Fabric",
"crossed-game-teamed": "Crossed game ({0})",
"create-match-session": "Create Match Session",
"ready": "Ready",
"unready": "Unready",
"start": "Start",
"cancel": "Cancel",
"game": {
"match-finished": "Match finished",
"round-finished": "Round finished",
"starting_game": "Starting game...",
"your-turn": "Your turn!",
"player-turn": "{0}'s turn!"
}
}

View File

@ -1,3 +0,0 @@
export const en = {
starting_game: 'Starting game...',
}

48
src/i18n/es.json Normal file
View File

@ -0,0 +1,48 @@
{
"match-page": "Página de partido",
"login": "Acceso",
"username": "Nombre de usuario",
"username-placeholder": "Nombre de usuario",
"login-button": "Acceso",
"password": "Contraseña",
"password-placeholder": "Contraseña",
"invalid-username-or-password": "usuario o contraseña invalido",
"points-to-win": "Puntos para ganar",
"round-index-1": "Ronda {0}",
"scoreboard": "Marcador",
"winner": "Ganador",
"final-scoreboard": "Puntaje Final",
"id-session-_id": "ID: {0}",
"seed-placeholder": "¡Escriba la semilla de la sesión aquí!",
"session-name-placeholder": "Nombre de la sesión",
"status-session-status": "Estado: {0}",
"unready": "No preparado",
"welcome-to-the-user-username-s-home-page": "Bienvenido a la página de inicio de {0}",
"available-sessions": "Sesiones disponibles",
"background-color": "Color de fondo",
"blue-fabric": "Fabrica azul",
"cancel": "Cancelar",
"copy": "Copiar",
"game": {
"starting_game": "Iniciando la partida...",
"match-finished": "Partido terminado",
"player-turn": "¡Es el turno de {0}!",
"round-finished": "Ronda terminada",
"your-turn": "¡Tu turno!"
},
"create-match-session": "Crear sesión de partido",
"crossed-game-teamed": "Juego cruzado ({0})",
"delete": "Borrar",
"gray-fabric": "Tela gris",
"green-fabric": "Tela verde",
"join": "Unirse",
"name": "Nombre",
"no-sessions-available": "No hay sesiones disponibles",
"players-session-players-length": "Jugadores: {0}",
"ready": "Listo",
"red-fabric": "Tela roja",
"seed": "Semilla",
"seed-session-seed": "Semilla: {0}",
"start": "Comenzar",
"yellow-fabric": "Tela amarilla"
}

View File

@ -1,3 +0,0 @@
export const es = {
starting_game: 'Iniciando la partida...',
}

View File

@ -1,8 +1,9 @@
import { createI18n } from 'vue-i18n' import { createI18n } from 'vue-i18n'
import { en } from './en' import en from './en.json'
import { es } from './es' import es from './es.json'
const i18n = createI18n({ const i18n = createI18n({
legacy: false,
locale: 'es', locale: 'es',
messages: { messages: {
en, en,
@ -10,9 +11,11 @@ const i18n = createI18n({
}, },
}) })
const translate = (key: string) => { // const translate = (key: string, context: any, plural: number = 1) => {
return i18n.global.t(key) // return i18n.global.t(key, plural)
} // }
export default i18n export default i18n
export { translate, translate as t }
const { t } = i18n.global
export { t, t as $t }

View File

@ -14,9 +14,9 @@ const router = createRouter({
{ {
path: '', path: '',
component: LandingView, component: LandingView,
name: 'landing' name: 'landing',
} },
] ],
}, },
{ {
path: '/home', path: '/home',
@ -27,10 +27,10 @@ const router = createRouter({
name: 'home', name: 'home',
component: HomeView, component: HomeView,
meta: { meta: {
requiresAuth: true requiresAuth: true,
} },
} },
] ],
// route level code-splitting // route level code-splitting
// this generates a separate chunk (About.[hash].js) for this route // this generates a separate chunk (About.[hash].js) for this route
// which is lazy-loaded when the route is visited. // which is lazy-loaded when the route is visited.
@ -45,13 +45,13 @@ const router = createRouter({
name: 'match', name: 'match',
component: () => import('@/views/MatchView.vue'), component: () => import('@/views/MatchView.vue'),
meta: { meta: {
requiresAuth: true requiresAuth: true,
} },
} },
] ],
}, },
{ {
path: '/game', path: '/game:id',
component: AuthenticatedLayout, component: AuthenticatedLayout,
children: [ children: [
{ {
@ -59,12 +59,12 @@ const router = createRouter({
name: 'game', name: 'game',
component: () => import('@/views/GameView.vue'), component: () => import('@/views/GameView.vue'),
meta: { meta: {
requiresAuth: true requiresAuth: true,
} },
} },
] ],
} },
] ],
}) })
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {

View File

@ -4,11 +4,11 @@ import { ServiceBase } from './ServiceBase'
export class GameService extends ServiceBase { export class GameService extends ServiceBase {
private networkService = new NetworkService() private networkService = new NetworkService()
async createMatchSession(sessionName: string, seed: string) { async createMatchSession(sessionName: string, seed: string, options: any) {
const response = await this.networkService.post({ const response = await this.networkService.post({
uri: '/game/match', uri: '/game/match',
body: { sessionName, seed }, body: { sessionName, seed, options },
auth: true auth: true,
}) })
const { sessionId } = response const { sessionId } = response
return sessionId return sessionId
@ -17,7 +17,7 @@ export class GameService extends ServiceBase {
async cancelMatchSession(sessionId: string) { async cancelMatchSession(sessionId: string) {
const response = await this.networkService.delete({ const response = await this.networkService.delete({
uri: `/game/match/${sessionId}`, uri: `/game/match/${sessionId}`,
auth: true auth: true,
}) })
return response return response
} }
@ -25,7 +25,7 @@ export class GameService extends ServiceBase {
async joinMatchSession(sessionId: string) { async joinMatchSession(sessionId: string) {
const response = await this.networkService.put({ const response = await this.networkService.put({
uri: `/game/match/${sessionId}`, uri: `/game/match/${sessionId}`,
auth: true auth: true,
}) })
return response return response
} }
@ -33,7 +33,7 @@ export class GameService extends ServiceBase {
async listMatchSessions() { async listMatchSessions() {
const response = await this.networkService.get({ const response = await this.networkService.get({
uri: '/game/match', uri: '/game/match',
auth: true auth: true,
}) })
return response return response
} }
@ -41,7 +41,7 @@ export class GameService extends ServiceBase {
async getMatchSession(sessionId: string) { async getMatchSession(sessionId: string) {
const response = await this.networkService.get({ const response = await this.networkService.get({
uri: `/game/match/${sessionId}`, uri: `/game/match/${sessionId}`,
auth: true auth: true,
}) })
return response return response
} }

View File

@ -1,4 +1,4 @@
import { createColors } from 'colorette' import { createColors, gray, redBright, yellow } from 'colorette'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import pino, { type BaseLogger } from 'pino' import pino, { type BaseLogger } from 'pino'
import { isProxy, toRaw } from 'vue' import { isProxy, toRaw } from 'vue'
@ -11,7 +11,7 @@ export class LoggingService {
constructor() { constructor() {
this._logger = pino({ this._logger = pino({
browser: { browser: {
asObject: true, asObject: false,
transmit: { transmit: {
level: import.meta.env.VITE_LOG_LEVEL || 'error', level: import.meta.env.VITE_LOG_LEVEL || 'error',
send: (level, logEvent) => { send: (level, logEvent) => {
@ -31,14 +31,14 @@ export class LoggingService {
if (messages.length > 0) { if (messages.length > 0) {
console.log( console.log(
`${logStr.join(' ')}:`, `${logStr.join(' ')}:`,
...messages.filter((m) => m !== undefined && m !== null) ...messages.filter((m) => m !== undefined && m !== null),
) )
} else { } else {
console.log(logStr.join(' ')) console.log(logStr.join(' '))
} }
} },
} },
} },
}) })
} }
@ -46,7 +46,10 @@ export class LoggingService {
return { return {
info: green, info: green,
debug: blue, debug: blue,
error: red error: red,
trace: gray,
warn: yellow,
fatal: redBright,
} }
} }
@ -55,11 +58,11 @@ export class LoggingService {
} }
info(message: string, data?: any) { info(message: string, data?: any) {
this._logger.info(this._getMessageWidthObject(message, data)) this._logger.info(message, data)
} }
warn(message: string, data?: any) { warn(message: string, data?: any) {
this._logger.warn(this._getMessageWidthObject(message, data)) this._logger.warn(message, data)
} }
error(error: any, message?: string) { error(error: any, message?: string) {
@ -67,11 +70,11 @@ export class LoggingService {
} }
fatal(message: string, data?: any) { fatal(message: string, data?: any) {
this._logger.fatal(this._getMessageWidthObject(message, data)) this._logger.fatal(message, data)
} }
trace(message: string, data?: any) { trace(message: string, data?: any) {
this._logger.trace(this._getMessageWidthObject(message, data)) this._logger.trace(message, data)
} }
object(message: any) { object(message: any) {

View File

@ -28,9 +28,9 @@ export class SocketIoClientService extends ServiceBase {
}) })
this.socket.on('connect', () => { this.socket.on('connect', () => {
if (this.socket && this.socket.recovered) { if (this.socket && this.socket.recovered) {
console.log('socket recovered succesfully') this.logger.debug('SOCKET: socket recovered succesfully')
} else { } else {
console.log('socket connected') this.logger.debug('SOCKET: socket connected')
} }
this.isConnected = true this.isConnected = true
this.addEvents() this.addEvents()
@ -42,21 +42,20 @@ export class SocketIoClientService extends ServiceBase {
addEvents(): void { addEvents(): void {
this.socket.on('disconnect', () => { this.socket.on('disconnect', () => {
this.isConnected = false this.isConnected = false
console.log('Disconnected from server') console.log('SOCKET: Disconnected from server')
}) })
this.socket.on('reconnect', () => { this.socket.on('reconnect', () => {
this.isConnected = true this.isConnected = true
console.log('Reconnected to server') this.logger.debug('SOCKET: Reconnected to server')
}) })
this.socket.on('reconnect_error', () => { this.socket.on('reconnect_error', () => {
this.isConnected = false this.isConnected = false
console.log('Failed to reconnect to server') this.logger.debug('SOCKET: Failed to reconnect to server')
}) })
this.socket.on('ping', () => { this.socket.on('ping', () => {
console.log('Ping received from server')
this.socket.emit('pong') // Send pong response this.socket.emit('pong') // Send pong response
}) })
@ -73,7 +72,7 @@ export class SocketIoClientService extends ServiceBase {
this.socket.onAny((eventName, eventData) => { this.socket.onAny((eventName, eventData) => {
if (eventName === 'server:game-event' || eventName === 'server:game-event-ack') { if (eventName === 'server:game-event' || eventName === 'server:game-event-ack') {
const { event, data } = eventData const { event, data } = eventData
this.logger.debug(`Received event: ${event}`, data) this.logger.trace(`SOCKET: Received event: ${event}`, data)
} }
}) })
} }
@ -81,24 +80,24 @@ export class SocketIoClientService extends ServiceBase {
sendMessage(event: string, data: any): void { sendMessage(event: string, data: any): void {
if (this.isConnected) { if (this.isConnected) {
this.socket?.emit('client:event', { event, data }) this.socket?.emit('client:event', { event, data })
console.log('sendMessage :>> ', event, data) this.logger.trace(`SOCKET: sendMessage :>> ${event}`, data)
} else { } else {
console.log('Not connected to server') this.logger.trace('Not connected to server')
} }
} }
async sendMessageWithAck(event: string, data: any): Promise<any> { async sendMessageWithAck(event: string, data: any): Promise<any> {
if (this.isConnected) { if (this.isConnected) {
console.log('sendMessageWithAck :>> ', event, data) this.logger.trace(`SOCKET: sendMessageWithAck :>> ${event}}`, data)
return await this.socket?.emitWithAck('client:event-with-ack', { event, data }) return await this.socket?.emitWithAck('client:event-with-ack', { event, data })
} else { } else {
console.log('Not connected to server') this.logger.trace('SOCKET: Not connected to server')
} }
} }
disconnect(): void { disconnect(): void {
this.socket?.disconnect() this.socket?.disconnect()
this.isConnected = false this.isConnected = false
console.log('Disconnected from server') this.logger.debug('SOCKET: Disconnected from server')
} }
} }

View File

@ -12,16 +12,16 @@ const ioOpts = {
reconnectionDelay: 1000, // Time between each attempt (in ms) reconnectionDelay: 1000, // Time between each attempt (in ms)
reconnectionDelayMax: 5000, // Maximum time between attempts (in ms) reconnectionDelayMax: 5000, // Maximum time between attempts (in ms)
randomizationFactor: 0.5, // Randomization factor for the delay randomizationFactor: 0.5, // Randomization factor for the delay
timeout: 20000 // Connection timeout (in ms) timeout: 20000, // Connection timeout (in ms)
} }
// const socket = URL === undefined ? io(ioOpts) : io(URL, ioOpts) // const socket = URL === undefined ? io(ioOpts) : io(URL, ioOpts)
const socket = io('http://localhost:3000', ioOpts) const socket = io('http://localhost:3000', ioOpts)
socket.on('connect', () => { socket.on('connect', () => {
if (socket.recovered) { if (socket.recovered) {
console.log('socket recovered succesfully') console.log('SOCKET: socket recovered succesfully')
} else { } else {
console.log('socket connected') console.log('SOCKET: socket connected')
} }
// setTimeout(() => { // setTimeout(() => {
@ -42,5 +42,5 @@ export default {
}, },
disconnect() { disconnect() {
socket.disconnect() socket.disconnect()
} },
} }

View File

@ -28,9 +28,11 @@ 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 { sessionState, isSessionStarted, playerState, amIHost } = storeToRefs(gameStore) const { sessionState, isSessionStarted, playerState, amIHost, readyForStart } =
storeToRefs(gameStore)
const { user } = storeToRefs(auth) const { user } = storeToRefs(auth)
const { gameOptions } = storeToRefs(gameOptionsStore) const { gameOptions } = storeToRefs(gameOptionsStore)
const { updateSessionState, updatePlayerState, updateGameState } = gameStore
// function setPlayerReady() { // function setPlayerReady() {
// logger.debug('Starting game') // logger.debug('Starting game')
@ -57,11 +59,60 @@ async function createMatch() {
logger.debug('Creating match') logger.debug('Creating match')
await socketService.connect() await socketService.connect()
gameOptions.value = { background: background.value } gameOptions.value = { background: background.value }
const id = await gameService.createMatchSession(sessionName.value, seed.value) const sessionOptions = {
pointsToWin: pointsToWin.value,
}
const id = await gameService.createMatchSession(sessionName.value, seed.value, sessionOptions)
logger.debug('Match created successfully') logger.debug('Match created successfully')
router.push({ name: 'match', params: { id } }) // router.push({ name: 'match', params: { id } })
} }
async function setPlayerReady() {
logger.debug('Starting game')
if (!sessionState.value) {
logger.error('No session found')
return
}
if (!playerState.value) {
logger.error('No player found')
return
}
await socketService.sendMessage('client:set-player-ready', {
userId: playerState.value.id,
sessionId: sessionState.value.id,
})
}
async function startMatch() {
const sessionId = sessionState?.value?.id
const playerId = playerState?.value?.id
if (sessionId) {
await socketService.sendMessageWithAck('client:start-session', {
sessionId: sessionId,
playerId: playerId,
})
}
}
async function cancelMatch() {
logger.debug('Cancelling match')
if (sessionState?.value?.id) {
await gameService.cancelMatchSession(sessionState?.value?.id)
updateSessionState(undefined)
updatePlayerState(undefined)
updateGameState(undefined)
logger.debug('Match cancelled successfully')
router.push({ name: 'home' })
}
}
eventBus.subscribe('server:match-starting', (data) => {
const session = data.sessionState as MatchSessionDto
updateSessionState(session)
router.push({ name: 'game', params: { id: session.id } })
})
async function joinMatch(id: string) { async function joinMatch(id: string) {
if (id) { if (id) {
await socketService.connect() await socketService.connect()
@ -80,7 +131,7 @@ async function deleteMatch(id: string) {
async function loadData() { async function loadData() {
const listResponse = await gameService.listMatchSessions() const listResponse = await gameService.listMatchSessions()
matchSessions.value = listResponse.data matchSessions.value = listResponse.data
sessionName.value = `Test #${listResponse.pagination.total + 1}` sessionName.value = `Test #${Date.now()}`
} }
onMounted(() => { onMounted(() => {
@ -99,41 +150,47 @@ function copy(sessionSeed: string) {
</script> </script>
<template> <template>
<div class="block home"> <div class="container home">
<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">
<div class="block"> {{ $t('welcome-to-the-user-username-s-home-page', [user.username]) }}
</h1>
<div class="block" v-if="!isSessionStarted">
<div class="field"> <div class="field">
<label class="label">Name</label> <label class="label">{{ $t('name') }}</label>
<div class="control"> <div class="control">
<input type="text" class="input" v-model="sessionName" placeholder="Session Name" /> <input
type="text"
class="input"
v-model="sessionName"
placeholder="$t('session-name-placeholder')"
/>
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<label class="label">Seed</label> <label class="label">{{ $t('seed') }}</label>
<div class="control"> <div class="control">
<input <input
type="text" type="text"
class="input" class="input"
style="margin-bottom: 0" style="margin-bottom: 0"
v-model="seed" v-model="seed"
placeholder="Type the session seed here!" placeholder="$t('seed-placeholder')"
/> />
</div> </div>
</div> </div>
<div class="grid"> <div class="grid">
<div class="cell"> <div class="cell">
<div class="field"> <div class="field">
<label for="background" class="label">Background color</label> <label for="background" class="label">{{ $t('background-color') }}</label>
<div class="control"> <div class="control">
<div class="select"> <div class="select">
<select v-model="background" name="background"> <select v-model="background" name="background">
<option value="green">Green Fabric</option> <option value="green">{{ $t('green-fabric') }}</option>
<option value="gray">Gray Fabric</option> <option value="gray">{{ $t('gray-fabric') }}</option>
<option value="blue">Blue Fabric</option> <option value="blue">{{ $t('blue-fabric') }}</option>
<option value="yellow">Yellow Fabric</option> <option value="yellow">{{ $t('yellow-fabric') }}</option>
<option value="red">Red Fabric</option> <option value="red">{{ $t('red-fabric') }}</option>
</select> </select>
</div> </div>
</div> </div>
@ -142,14 +199,14 @@ function copy(sessionSeed: string) {
<div class="control"> <div class="control">
<label for="teamed" class="checkbox"> <label for="teamed" class="checkbox">
<input v-model="teamed" name="teamed" type="checkbox" /> <input v-model="teamed" name="teamed" type="checkbox" />
Crossed game ({{ teamed }}) {{ $t('crossed-game-teamed', [teamed]) }}
</label> </label>
</div> </div>
</div> </div>
</div> </div>
<div class="cell"> <div class="cell">
<div class="field"> <div class="field">
<label for="pointsToWin" class="label">Points to win</label> <label for="pointsToWin" class="label">{{ $t('points-to-win') }}</label>
<div class="control"> <div class="control">
<div class="select"> <div class="select">
<select v-model="pointsToWin" name="pointsToWin"> <select v-model="pointsToWin" name="pointsToWin">
@ -164,37 +221,58 @@ function copy(sessionSeed: string) {
</div> </div>
</div> </div>
</div> </div>
<div class="buttons">
<button class="button is-primary" @click.once="createMatch">
{{ $t('create-match-session') }}
</button>
</div> </div>
<div class="block" v-if="!isSessionStarted"></div> </div>
<div class="buttons" v-if="isSessionStarted">
<button class="button" @click="setPlayerReady">
<span v-if="!readyForStart">{{ $t('ready') }}</span
><span v-else>{{ $t('unready') }}</span>
</button>
<button class="button" @click="startMatch" v-if="amIHost && readyForStart">
<span>{{ $t('start') }}</span>
</button>
<button class="button is-primary" @click.once="createMatch">Create Match Session</button> <button class="button" @click="cancelMatch">
<span>{{ $t('cancel') }}</span>
</button>
</div>
</section> </section>
<section class="section available-sessions"> <section class="section available-sessions">
<h2 class="title is-4">Available Sessions</h2> <h2 class="title is-4">{{ $t('available-sessions') }}</h2>
<div class="block"> <div class="block">
<div v-if="matchSessions.length === 0"> <div v-if="matchSessions.length === 0">
<p>No sessions available</p> <p>{{ $t('no-sessions-available') }}</p>
</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">
<div class="card-content"> <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>{{ $t('id-session-_id', [session._id]) }}</p>
<p>Players: {{ session.players.length }}</p> <p>{{ $t('players-session-players-length', [session.players.length]) }}</p>
<p> <p>
Seed: {{ session.seed }} {{ $t('seed-session-seed', [session.seed]) }}
<button class="button is-small" @click="() => copy(session.seed)">Copy</button> <button class="button is-small" @click="() => copy(session.seed)">
{{ $t('copy') }}
</button>
</p> </p>
<p>Status: {{ session.status }}</p> <p>{{ $t('status-session-status', [session.status]) }}</p>
<div class="buttons is-centered mt-4"></div> <div class="buttons is-centered mt-4"></div>
</div> </div>
<div class="card-footer"> <div class="card-footer">
<p class="card-footer-item"> <p class="card-footer-item">
<a href="#" @click.once.prevent="() => joinMatch(session._id)"> Join </a> <a href="#" @click.once.prevent="() => joinMatch(session._id)">
{{ $t('join') }}
</a>
</p> </p>
<p class="card-footer-item"> <p class="card-footer-item">
<a href="#" @click.once.prevent="() => deleteMatch(session._id)"> Delete </a> <a href="#" @click.once.prevent="() => deleteMatch(session._id)">
{{ $t('delete') }}
</a>
</p> </p>
</div> </div>
</div> </div>

View File

@ -2,10 +2,12 @@
import { AuthenticationService } from '@/services/AuthenticationService' import { AuthenticationService } from '@/services/AuthenticationService'
import { inject, ref } from 'vue' import { inject, ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
const router = useRouter() const router = useRouter()
const username = ref('') const username = ref('')
const password = ref('') const password = ref('')
const { t } = useI18n()
const authService = inject<AuthenticationService>('auth') const authService = inject<AuthenticationService>('auth')
@ -14,7 +16,7 @@ async function login() {
await authService?.login(username.value, password.value) await authService?.login(username.value, password.value)
router.push({ name: 'home' }) router.push({ name: 'home' })
} catch (error) { } catch (error) {
alert('Invalid username or password') alert(t('invalid-username-or-password'))
} }
// if (username.value === 'admin' && password.value === 'password') { // if (username.value === 'admin' && password.value === 'password') {
// localStorage.setItem('token', 'true') // localStorage.setItem('token', 'true')
@ -27,24 +29,34 @@ async function login() {
<template> <template>
<div class="login"> <div class="login">
<h1>Login</h1> <h1 class="title">{{ $t('login') }}</h1>
<form class="form" @submit.prevent="login"> <form class="form" @submit.prevent="login">
<div class="field"> <div class="field">
<label class="label">Username</label> <label class="label">{{ $t('username') }}</label>
<div class="control"> <div class="control">
<input class="input" type="text" v-model="username" placeholder="Username" /> <input
class="input"
type="text"
v-model="username"
:placeholder="t('username-placeholder')"
/>
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<label class="label">Username</label> <label class="label">{{ $t('password') }}</label>
<div class="control"> <div class="control">
<input class="input" type="password" v-model="password" placeholder="Password" /> <input
class="input"
type="password"
v-model="password"
:placeholder="t('password-placeholder')"
/>
</div> </div>
</div> </div>
<div class="field is-grouped"> <div class="field is-grouped">
<div class="control"> <div class="control">
<button class="button is-primary" type="submit">Login</button> <button class="button is-primary" type="submit">{{ $t('login-button') }}</button>
</div> </div>
<!-- <div class="control"> <!-- <div class="control">
<button class="button">Cancel</button> <button class="button">Cancel</button>

View File

@ -2,88 +2,28 @@
import type { MatchSessionDto } from '@/common/interfaces' import type { MatchSessionDto } from '@/common/interfaces'
import type { GameService } from '@/services/GameService' import type { GameService } from '@/services/GameService'
import type { LoggingService } from '@/services/LoggingService' import type { LoggingService } from '@/services/LoggingService'
import { useEventBusStore } from '@/stores/eventBus' import { inject, onBeforeMount, ref, toRaw } from 'vue'
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' import { useRoute, useRouter } from 'vue-router'
const route = useRoute() const route = useRoute()
const router = useRouter() 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 gameService: GameService = inject<GameService>('game') as GameService
const logger: LoggingService = inject<LoggingService>('logger') as LoggingService const logger: LoggingService = inject<LoggingService>('logger') as LoggingService
let sessionId: string let sessionId: string
let matchSession = ref<MatchSessionDto | undefined>(undefined) 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')
if (!sessionState.value) {
logger.error('No session found')
return
}
if (!playerState.value) {
logger.error('No player found')
return
}
await socketService.sendMessage('client:set-player-ready', {
userId: playerState.value.id,
sessionId: sessionState.value.id,
})
}
async function startMatch() {
const sessionId = sessionState?.value?.id
const playerId = playerState?.value?.id
if (sessionId) {
await socketService.sendMessageWithAck('client:start-session', {
sessionId: sessionId,
playerId: playerId,
})
}
}
async function cancelMatch() {
logger.debug('Cancelling match')
await gameService.cancelMatchSession(sessionId)
updateSessionState(undefined)
updatePlayerState(undefined)
updateGameState(undefined)
logger.debug('Match cancelled successfully')
router.push({ name: 'home' })
}
async function loadData() { async function loadData() {
await gameService.getMatchSession(sessionId) matchSession.value = (await gameService.getMatchSession(sessionId)) as MatchSessionDto
console.log('matchSession.value :>> ', toRaw(matchSession.value))
} }
eventBus.subscribe('window-before-unload', async () => {
logger.debug('Window before unload')
await cancelMatch()
})
eventBus.subscribe('server:match-starting', (data) => {
const session = data.sessionState as MatchSessionDto
updateSessionState(session)
logger.debug('Match starting')
router.push({ name: 'game' })
})
onBeforeMount(() => { onBeforeMount(() => {
sessionId = route.params.id as string sessionId = route.params.id as string
console.log('sessionId :>> ', sessionId)
loadData()
if (sessionId) { if (sessionId) {
setInterval(loadData, 5000) // setInterval(loadData, 5000)
} else { } else {
router.push({ name: 'home' }) router.push({ name: 'home' })
} }
@ -91,27 +31,50 @@ onBeforeMount(() => {
</script> </script>
<template> <template>
<div> <div class="container">
<h1 class="title is-2">Match Page {{ isSessionStarted }}</h1> <h1 class="title is-1">{{ $t('match-page') }}</h1>
<div class="block" v-if="matchSession"> <h2 class="title is-3">{{ matchSession?.name }}</h2>
<p>Session ID: {{ matchSession._id }}</p> <div class="block mt-6">
<p>Session Name: {{ matchSession.name }}</p> <p class="mb-4">
<p>Session started: {{ isSessionStarted }}</p> <span class="title is-5">{{ $t('winner') }}</span>
<p>Host: {{ amIHost }}</p> <span class="is-size-5 ml-4">{{ matchSession?.matchWinner?.name }}</span>
<p>{{ sessionState || 'No session' }}</p> </p>
<p class="mb-4">
<span class="title is-5">{{ $t('points-to-win') }}</span>
<span class="is-size-5 ml-4">{{ matchSession?.pointsToWin }}</span>
</p>
<h3 class="title is-5">{{ $t('final-scoreboard') }}</h3>
<div v-bind:key="$index" v-for="(score, $index) in matchSession?.scoreboard">
<p class="">
<span class="title is-5">{{ score.name }}</span>
<span class="is-size-5 ml-4">{{ score.score }}</span>
</p>
</div> </div>
<div class="block"> </div>
<p v-if="!amIHost && !readyForStart">Waiting for host to start session</p> <div class="grid">
<button class="button" @click="setPlayerReady" v-if="isSessionStarted"> <div
<span v-if="!readyForStart">Ready</span><span v-else>Unready</span> class="cell"
</button> v-bind:key="$index"
<button class="button" @click="startMatch" v-if="amIHost && readyForStart"> v-for="(summary, $index) in matchSession?.gameSummaries"
<span>Start</span> >
</button> <div class="block mt-6">
<h3 class="title is-5">{{ $t('round-index-1', [$index + 1]) }}</h3>
<button class="button" @click="cancelMatch" v-if="isSessionStarted"> <p class="mb-4">
<span>Cancel</span> <span class="title is-5">{{ $t('winner') }}</span>
</button> <span class="is-size-5 ml-4">{{ summary.winner?.name }}</span>
</p>
<h4 class="title is-6">{{ $t('scoreboard') }}</h4>
<div v-bind:key="$index" v-for="(gameScore, $index) in summary.score">
<p class="">
<span class="title is-5">{{ gameScore.name }}</span>
<span class="is-size-5 ml-4">{{ gameScore.score }}</span>
</p>
</div>
</div>
</div>
</div>
<div class="section">
<!-- <div>{{ matchSession }}</div> -->
</div> </div>
</div> </div>
</template> </template>