match page (initial) i18n
This commit is contained in:
		@@ -38,6 +38,7 @@ export interface MatchSessionDto {
 | 
			
		||||
  matchWinner: PlayerDto | null
 | 
			
		||||
  matchInProgress: boolean
 | 
			
		||||
  playersReady: number
 | 
			
		||||
  gameSummaries: GameSummary[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface GameDto {
 | 
			
		||||
@@ -115,4 +116,11 @@ export interface GameSummary {
 | 
			
		||||
  winner: PlayerDto
 | 
			
		||||
  score: { id: string; name: string; score: number }[]
 | 
			
		||||
  players?: PlayerDto[]
 | 
			
		||||
  board: TileDto[]
 | 
			
		||||
  boneyard: TileDto[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Config {
 | 
			
		||||
  waitMillisToShowSummary: number
 | 
			
		||||
  activeHandStrokeColor: number
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +1,21 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
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 { useGameStore } from '@/stores/game'
 | 
			
		||||
import { useEventBusStore } from '@/stores/eventBus'
 | 
			
		||||
import { storeToRefs } from 'pinia'
 | 
			
		||||
import { useGameOptionsStore } from '@/stores/gameOptions'
 | 
			
		||||
import { useRoute, useRouter } from 'vue-router'
 | 
			
		||||
 | 
			
		||||
let sessionId: string
 | 
			
		||||
const socketService: any = inject('socket')
 | 
			
		||||
 | 
			
		||||
const gameStore = useGameStore()
 | 
			
		||||
const gameOptionsStore = useGameOptionsStore()
 | 
			
		||||
const eventBus = useEventBusStore()
 | 
			
		||||
const router = useRouter()
 | 
			
		||||
const route = useRoute()
 | 
			
		||||
const { playerState, sessionState } = storeToRefs(gameStore)
 | 
			
		||||
const { updateGameState } = gameStore
 | 
			
		||||
const { gameOptions } = storeToRefs(gameOptionsStore)
 | 
			
		||||
@@ -68,12 +72,18 @@ const game = new Game(
 | 
			
		||||
// )
 | 
			
		||||
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
  sessionId = route.params.id as string
 | 
			
		||||
  if (appEl.value === null) return
 | 
			
		||||
  const canvas = await game.setup()
 | 
			
		||||
  appEl.value.appendChild(canvas)
 | 
			
		||||
  await game.preload()
 | 
			
		||||
  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) => {
 | 
			
		||||
    game.gameFinished(data)
 | 
			
		||||
  })
 | 
			
		||||
@@ -88,7 +98,7 @@ onMounted(async () => {
 | 
			
		||||
 | 
			
		||||
  eventBus.subscribe('server:hand-dealt', (data: { player: PlayerDto; gameState: GameDto }) => {
 | 
			
		||||
    game.hand.update(data.player)
 | 
			
		||||
    game.updateOtherHands(data.gameState)
 | 
			
		||||
    game.updateOtherHands(data.gameState.players)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  eventBus.subscribe('server:next-turn', (gameState: GameDto) => {
 | 
			
		||||
@@ -99,25 +109,6 @@ onMounted(async () => {
 | 
			
		||||
  eventBus.subscribe('server:match-finished', (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(() => {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,41 +0,0 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
defineProps<{
 | 
			
		||||
  msg: string
 | 
			
		||||
}>()
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="greetings">
 | 
			
		||||
    <h1 class="green">{{ msg }}</h1>
 | 
			
		||||
    <h3>
 | 
			
		||||
      You’ve 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>
 | 
			
		||||
@@ -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>
 | 
			
		||||
 | 
			
		||||
    Vue’s
 | 
			
		||||
    <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>
 | 
			
		||||
@@ -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>
 | 
			
		||||
@@ -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 type { AnimationOptions, Movement, PlayerDto, TileDto } from '@/common/interfaces'
 | 
			
		||||
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 { LoggingService } from '@/services/LoggingService'
 | 
			
		||||
import { GlowFilter } from 'pixi-filters'
 | 
			
		||||
@@ -85,19 +85,12 @@ export class Board extends EventEmitter {
 | 
			
		||||
      visible: false,
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    createCrosshair(this.tilesContainer, 0xff0000, {
 | 
			
		||||
      width: this.width,
 | 
			
		||||
      height: this.height,
 | 
			
		||||
      x: this.scaleX(0),
 | 
			
		||||
      y: this.scaleY(0),
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    createCrosshair(this.interactionContainer, 0xffff00, {
 | 
			
		||||
      width: this.width,
 | 
			
		||||
      height: this.height,
 | 
			
		||||
      x: this.scaleX(0),
 | 
			
		||||
      y: this.scaleY(0),
 | 
			
		||||
    })
 | 
			
		||||
    // createCrosshair(this.tilesContainer, 0xff0000, {
 | 
			
		||||
    //   width: this.width,
 | 
			
		||||
    //   height: this.height,
 | 
			
		||||
    //   x: this.scaleX(0),
 | 
			
		||||
    //   y: this.scaleY(0),
 | 
			
		||||
    // })
 | 
			
		||||
 | 
			
		||||
    this.textContainer = createContainer({
 | 
			
		||||
      width: this.width,
 | 
			
		||||
@@ -105,7 +98,7 @@ export class Board extends EventEmitter {
 | 
			
		||||
      parent: this.container,
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    this.showText(t('starting_game'))
 | 
			
		||||
    this.showText(t('game.starting_game'))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private calculateScale() {
 | 
			
		||||
@@ -144,11 +137,11 @@ export class Board extends EventEmitter {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async setPlayerTurn(player: PlayerDto) {
 | 
			
		||||
    this.showText('Your turn!')
 | 
			
		||||
    this.showText(t('game.your-turn'))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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) {
 | 
			
		||||
@@ -599,8 +592,7 @@ export class Board extends EventEmitter {
 | 
			
		||||
    return [canPlayNorth, canPlayEast, canPlaySouth, canPlayWest]
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  gameFinished(data: any) {
 | 
			
		||||
    const { lastGame, gameState } = data
 | 
			
		||||
  clean() {
 | 
			
		||||
    this.tiles = []
 | 
			
		||||
    this.boneyard = []
 | 
			
		||||
    this.movements = []
 | 
			
		||||
@@ -614,10 +606,13 @@ export class Board extends EventEmitter {
 | 
			
		||||
    this.firstTile = undefined
 | 
			
		||||
    this.tilesContainer.removeChildren()
 | 
			
		||||
    this.interactionContainer.removeChildren()
 | 
			
		||||
    this.showText(`Game finished \n Winner ${lastGame?.winner?.name ?? 'No winner'}`)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  matchFinished(data: any) {
 | 
			
		||||
    // this.showText(`Game finished \n Winner ${lastGame?.winner?.name ?? 'No winner'}`)
 | 
			
		||||
  gameFinished() {
 | 
			
		||||
    this.showText(t('game.round-finished'))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  matchFinished() {
 | 
			
		||||
    this.showText(t('game.match-finished'))
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										8
									
								
								src/game/Config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/game/Config.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
import type { Config } from '@/common/interfaces'
 | 
			
		||||
 | 
			
		||||
const config: Config = {
 | 
			
		||||
  waitMillisToShowSummary: 1000,
 | 
			
		||||
  activeHandStrokeColor: 0xb39c4d,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default config
 | 
			
		||||
@@ -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 { assets } from '@/game/utilities/assets'
 | 
			
		||||
import { Tile } from '@/game/Tile'
 | 
			
		||||
@@ -9,6 +9,7 @@ import { wait } from '@/common/helpers'
 | 
			
		||||
import { Actions } from 'pixi-actions'
 | 
			
		||||
import { OtherHand } from './OtherHand'
 | 
			
		||||
import { GameSummayView } from './GameSummayView'
 | 
			
		||||
import Config from './Config'
 | 
			
		||||
 | 
			
		||||
interface GameOptions {
 | 
			
		||||
  boardScale: number
 | 
			
		||||
@@ -18,7 +19,7 @@ interface GameOptions {
 | 
			
		||||
  background: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class Game {
 | 
			
		||||
export class Game extends EventEmitter {
 | 
			
		||||
  public board!: Board
 | 
			
		||||
  public hand!: Hand
 | 
			
		||||
  private app: Application = new Application()
 | 
			
		||||
@@ -27,6 +28,7 @@ export class Game {
 | 
			
		||||
  private otherHands: OtherHand[] = []
 | 
			
		||||
  private backgroundLayer: Container = new Container()
 | 
			
		||||
  private gameSummaryView!: GameSummayView
 | 
			
		||||
  private players: PlayerDto[] = []
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private options: GameOptions = {
 | 
			
		||||
@@ -39,7 +41,9 @@ export class Game {
 | 
			
		||||
    private socketService: SocketIoClientService,
 | 
			
		||||
    private playerId: string,
 | 
			
		||||
    private sessionId: string,
 | 
			
		||||
  ) {}
 | 
			
		||||
  ) {
 | 
			
		||||
    super()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async setup(): Promise<HTMLCanvasElement> {
 | 
			
		||||
    const width = this.options.width || 1200
 | 
			
		||||
@@ -59,7 +63,8 @@ export class Game {
 | 
			
		||||
      new OtherHand(this.app, 'top'),
 | 
			
		||||
      new OtherHand(this.app, 'right'),
 | 
			
		||||
    ]
 | 
			
		||||
    this.initOtherHands(players)
 | 
			
		||||
    this.initPlayers(players)
 | 
			
		||||
    this.players = players
 | 
			
		||||
    this.gameSummaryView = new GameSummayView(this.app)
 | 
			
		||||
    this.hand.scale = this.options.handScale
 | 
			
		||||
    this.board.scale = this.options.boardScale
 | 
			
		||||
@@ -81,23 +86,23 @@ export class Game {
 | 
			
		||||
    this.backgroundLayer.addChild(background)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  initOtherHands(players: PlayerDto[]) {
 | 
			
		||||
  initPlayers(players: PlayerDto[]) {
 | 
			
		||||
    const myIndex = players.findIndex((player) => player.id === this.playerId)
 | 
			
		||||
    const copy = [...players]
 | 
			
		||||
    const cut = copy.splice(myIndex)
 | 
			
		||||
    cut.shift()
 | 
			
		||||
    const player = cut.shift()
 | 
			
		||||
    const final = cut.concat(copy)
 | 
			
		||||
 | 
			
		||||
    for (let i = 0; i < final.length; i++) {
 | 
			
		||||
      const hand = this.otherHands[i]
 | 
			
		||||
      hand.setPlayer(final[i])
 | 
			
		||||
    }
 | 
			
		||||
    this.hand.setPlayer(player)
 | 
			
		||||
 | 
			
		||||
    this.board.otherPlayerHands = this.otherHands
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  updateOtherHands(gameState: GameDto) {
 | 
			
		||||
    const players = gameState.players
 | 
			
		||||
  updateOtherHands(players: PlayerDto[]) {
 | 
			
		||||
    players.forEach((player) => {
 | 
			
		||||
      const hand = this.otherHands.find((hand) => hand.player?.id === player.id)
 | 
			
		||||
      if (hand) {
 | 
			
		||||
@@ -115,6 +120,7 @@ export class Game {
 | 
			
		||||
 | 
			
		||||
  setPlayersInactive() {
 | 
			
		||||
    this.otherHands.forEach((hand) => hand.setActive(false))
 | 
			
		||||
    this.hand.setActive(false)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async preload() {
 | 
			
		||||
@@ -148,7 +154,12 @@ export class Game {
 | 
			
		||||
      await this.board.updateBoard(move, undefined)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    this.gameSummaryView.on('finishClick', (data) => {
 | 
			
		||||
      this.emit('game:finish-click', data)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    this.gameSummaryView.on('nextClick', (data) => {
 | 
			
		||||
      this.board.clean()
 | 
			
		||||
      this.updateScoreboard(data.sessionState)
 | 
			
		||||
      this.socketService.sendMessage('client:set-client-ready-for-next-game', {
 | 
			
		||||
        userId: this.playerId,
 | 
			
		||||
@@ -161,11 +172,13 @@ export class Game {
 | 
			
		||||
 | 
			
		||||
  private updateScoreboard(sessionState: MatchSessionDto) {
 | 
			
		||||
    const scoreboard = sessionState.scoreboard
 | 
			
		||||
    this.otherHands.forEach((hand) => {
 | 
			
		||||
      const player: PlayerDto | undefined = hand.player
 | 
			
		||||
    const myScore = scoreboard.find((d) => d.id === this.playerId)?.score || 0
 | 
			
		||||
    this.hand.setScore(myScore)
 | 
			
		||||
    this.otherHands.forEach((otherHand) => {
 | 
			
		||||
      const player: PlayerDto | undefined = otherHand.player
 | 
			
		||||
      const name: string = player?.name || ''
 | 
			
		||||
      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!
 | 
			
		||||
    this.setPlayersInactive()
 | 
			
		||||
    if (currentPlayer.id === this.playerId) {
 | 
			
		||||
      this.hand.setActive(true)
 | 
			
		||||
      this.hand.prepareForMove(this.board.count === 0, this.board.freeEnds)
 | 
			
		||||
      this.board.setPlayerTurn(currentPlayer)
 | 
			
		||||
    } 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.board.gameFinished(data)
 | 
			
		||||
    this.board.gameFinished()
 | 
			
		||||
    this.setPlayersInactive()
 | 
			
		||||
    this.gameSummaryView.setGameSummary(data, 'round')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  matchFinished(data: any) {
 | 
			
		||||
    // this.hand.matchFinished()
 | 
			
		||||
    this.board.matchFinished(data)
 | 
			
		||||
  async matchFinished(data: any) {
 | 
			
		||||
    await wait(Config.waitMillisToShowSummary)
 | 
			
		||||
    this.updateOtherHands(data.lastGame.players)
 | 
			
		||||
    this.board.matchFinished()
 | 
			
		||||
    this.gameSummaryView.setGameSummary(data, 'match')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -30,8 +30,6 @@ export class GameSummayView extends EventEmitter {
 | 
			
		||||
      height: this.height,
 | 
			
		||||
      parent: this.container,
 | 
			
		||||
    })
 | 
			
		||||
    console.log('GameSummaryView created!')
 | 
			
		||||
 | 
			
		||||
    this.container.visible = false
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -67,7 +65,7 @@ export class GameSummayView extends EventEmitter {
 | 
			
		||||
 | 
			
		||||
    if (this.gameSummary.isBlocked) {
 | 
			
		||||
      line += 30
 | 
			
		||||
      this.container.addChild(
 | 
			
		||||
      this.layer.addChild(
 | 
			
		||||
        createText({
 | 
			
		||||
          text: '(Blocked)',
 | 
			
		||||
          x: this.width / 2,
 | 
			
		||||
 
 | 
			
		||||
@@ -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 type { PlayerDto, TileDto } from '@/common/interfaces'
 | 
			
		||||
import { GlowFilter } from 'pixi-filters'
 | 
			
		||||
import { Scale, type ScaleFunction } from './utilities/scale'
 | 
			
		||||
import { LoggingService } from '@/services/LoggingService'
 | 
			
		||||
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 {
 | 
			
		||||
  tiles: Tile[] = []
 | 
			
		||||
  container: Container = new Container()
 | 
			
		||||
  buttonPass: Container = new Container()
 | 
			
		||||
  scoreLayer: Container = new Container()
 | 
			
		||||
  activeLayer: Container = new Container()
 | 
			
		||||
  height: number
 | 
			
		||||
  width: number
 | 
			
		||||
  ticker: Ticker
 | 
			
		||||
@@ -27,18 +30,22 @@ export class Hand extends EventEmitter {
 | 
			
		||||
  tilesLayer!: Container
 | 
			
		||||
  interactionsLayer!: Container
 | 
			
		||||
  score: number = 0
 | 
			
		||||
  active: boolean = false
 | 
			
		||||
  private player!: PlayerDto
 | 
			
		||||
 | 
			
		||||
  constructor(app: Application) {
 | 
			
		||||
    super()
 | 
			
		||||
    app.stage.addChild(this.container)
 | 
			
		||||
    this.ticker = app.ticker
 | 
			
		||||
    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.x = app.canvas.width / 2 - this.width / 2
 | 
			
		||||
    this.container.width = this.width
 | 
			
		||||
    this.container.height = this.height
 | 
			
		||||
    this.calculateScale()
 | 
			
		||||
    this.initLayers()
 | 
			
		||||
    this.render()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  initLayers() {
 | 
			
		||||
@@ -58,12 +65,12 @@ export class Hand extends EventEmitter {
 | 
			
		||||
      y: 0,
 | 
			
		||||
      parent: this.container,
 | 
			
		||||
    })
 | 
			
		||||
    this.container.addChild(this.scoreLayer)
 | 
			
		||||
    this.container.addChild(this.activeLayer)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  gameFinished() {
 | 
			
		||||
    this.logger.debug('gameFinished')
 | 
			
		||||
    this.tiles = []
 | 
			
		||||
 | 
			
		||||
    this.initialized = false
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -205,6 +212,22 @@ export class Hand extends EventEmitter {
 | 
			
		||||
    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) {
 | 
			
		||||
    return playerState.hand.map((tile: TileDto) => {
 | 
			
		||||
      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() {
 | 
			
		||||
    //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() {
 | 
			
		||||
    this.renderTiles()
 | 
			
		||||
    this.renderScore()
 | 
			
		||||
    this.renderActive()
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,8 @@ 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, scoreText } from './utilities/fonts'
 | 
			
		||||
import { createText, playerNameText, whiteStyle } from './utilities/fonts'
 | 
			
		||||
import Config from '@/game/Config'
 | 
			
		||||
 | 
			
		||||
export class OtherHand {
 | 
			
		||||
  tilesInitialNumber: number = 7
 | 
			
		||||
@@ -62,10 +63,11 @@ export class OtherHand {
 | 
			
		||||
 | 
			
		||||
  setScore(score: number) {
 | 
			
		||||
    this.score = score
 | 
			
		||||
    this.render()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -80,7 +82,7 @@ export class OtherHand {
 | 
			
		||||
      text: `${this.score}`,
 | 
			
		||||
      x: this.width - 5,
 | 
			
		||||
      y: 50,
 | 
			
		||||
      style: scoreText,
 | 
			
		||||
      style: whiteStyle(36, 'bold'),
 | 
			
		||||
    })
 | 
			
		||||
    text.anchor.set(1, 0.5)
 | 
			
		||||
    this.scoreLayer.addChild(text)
 | 
			
		||||
@@ -90,7 +92,7 @@ export class OtherHand {
 | 
			
		||||
    this.tilesLayer.removeChildren()
 | 
			
		||||
    const x = -9
 | 
			
		||||
    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())
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
@@ -98,7 +100,9 @@ export class OtherHand {
 | 
			
		||||
  private renderActive() {
 | 
			
		||||
    this.interactionsLayer.removeChildren()
 | 
			
		||||
    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)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										48
									
								
								src/i18n/en.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/i18n/en.json
									
									
									
									
									
										Normal 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!"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,3 +0,0 @@
 | 
			
		||||
export const en = {
 | 
			
		||||
  starting_game: 'Starting game...',
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										48
									
								
								src/i18n/es.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/i18n/es.json
									
									
									
									
									
										Normal 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"
 | 
			
		||||
}
 | 
			
		||||
@@ -1,3 +0,0 @@
 | 
			
		||||
export const es = {
 | 
			
		||||
  starting_game: 'Iniciando la partida...',
 | 
			
		||||
}
 | 
			
		||||
@@ -1,8 +1,9 @@
 | 
			
		||||
import { createI18n } from 'vue-i18n'
 | 
			
		||||
import { en } from './en'
 | 
			
		||||
import { es } from './es'
 | 
			
		||||
import en from './en.json'
 | 
			
		||||
import es from './es.json'
 | 
			
		||||
 | 
			
		||||
const i18n = createI18n({
 | 
			
		||||
  legacy: false,
 | 
			
		||||
  locale: 'es',
 | 
			
		||||
  messages: {
 | 
			
		||||
    en,
 | 
			
		||||
@@ -10,9 +11,11 @@ const i18n = createI18n({
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const translate = (key: string) => {
 | 
			
		||||
  return i18n.global.t(key)
 | 
			
		||||
}
 | 
			
		||||
// const translate = (key: string, context: any, plural: number = 1) => {
 | 
			
		||||
//   return i18n.global.t(key, plural)
 | 
			
		||||
// }
 | 
			
		||||
 | 
			
		||||
export default i18n
 | 
			
		||||
export { translate, translate as t }
 | 
			
		||||
 | 
			
		||||
const { t } = i18n.global
 | 
			
		||||
export { t, t as $t }
 | 
			
		||||
 
 | 
			
		||||
@@ -14,9 +14,9 @@ const router = createRouter({
 | 
			
		||||
        {
 | 
			
		||||
          path: '',
 | 
			
		||||
          component: LandingView,
 | 
			
		||||
          name: 'landing'
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
          name: 'landing',
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      path: '/home',
 | 
			
		||||
@@ -27,10 +27,10 @@ const router = createRouter({
 | 
			
		||||
          name: 'home',
 | 
			
		||||
          component: HomeView,
 | 
			
		||||
          meta: {
 | 
			
		||||
            requiresAuth: true
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
            requiresAuth: true,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
      // route level code-splitting
 | 
			
		||||
      // this generates a separate chunk (About.[hash].js) for this route
 | 
			
		||||
      // which is lazy-loaded when the route is visited.
 | 
			
		||||
@@ -45,13 +45,13 @@ const router = createRouter({
 | 
			
		||||
          name: 'match',
 | 
			
		||||
          component: () => import('@/views/MatchView.vue'),
 | 
			
		||||
          meta: {
 | 
			
		||||
            requiresAuth: true
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
            requiresAuth: true,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      path: '/game',
 | 
			
		||||
      path: '/game:id',
 | 
			
		||||
      component: AuthenticatedLayout,
 | 
			
		||||
      children: [
 | 
			
		||||
        {
 | 
			
		||||
@@ -59,12 +59,12 @@ const router = createRouter({
 | 
			
		||||
          name: 'game',
 | 
			
		||||
          component: () => import('@/views/GameView.vue'),
 | 
			
		||||
          meta: {
 | 
			
		||||
            requiresAuth: true
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
            requiresAuth: true,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
router.beforeEach((to, from, next) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -4,11 +4,11 @@ import { ServiceBase } from './ServiceBase'
 | 
			
		||||
export class GameService extends ServiceBase {
 | 
			
		||||
  private networkService = new NetworkService()
 | 
			
		||||
 | 
			
		||||
  async createMatchSession(sessionName: string, seed: string) {
 | 
			
		||||
  async createMatchSession(sessionName: string, seed: string, options: any) {
 | 
			
		||||
    const response = await this.networkService.post({
 | 
			
		||||
      uri: '/game/match',
 | 
			
		||||
      body: { sessionName, seed },
 | 
			
		||||
      auth: true
 | 
			
		||||
      body: { sessionName, seed, options },
 | 
			
		||||
      auth: true,
 | 
			
		||||
    })
 | 
			
		||||
    const { sessionId } = response
 | 
			
		||||
    return sessionId
 | 
			
		||||
@@ -17,7 +17,7 @@ export class GameService extends ServiceBase {
 | 
			
		||||
  async cancelMatchSession(sessionId: string) {
 | 
			
		||||
    const response = await this.networkService.delete({
 | 
			
		||||
      uri: `/game/match/${sessionId}`,
 | 
			
		||||
      auth: true
 | 
			
		||||
      auth: true,
 | 
			
		||||
    })
 | 
			
		||||
    return response
 | 
			
		||||
  }
 | 
			
		||||
@@ -25,7 +25,7 @@ export class GameService extends ServiceBase {
 | 
			
		||||
  async joinMatchSession(sessionId: string) {
 | 
			
		||||
    const response = await this.networkService.put({
 | 
			
		||||
      uri: `/game/match/${sessionId}`,
 | 
			
		||||
      auth: true
 | 
			
		||||
      auth: true,
 | 
			
		||||
    })
 | 
			
		||||
    return response
 | 
			
		||||
  }
 | 
			
		||||
@@ -33,7 +33,7 @@ export class GameService extends ServiceBase {
 | 
			
		||||
  async listMatchSessions() {
 | 
			
		||||
    const response = await this.networkService.get({
 | 
			
		||||
      uri: '/game/match',
 | 
			
		||||
      auth: true
 | 
			
		||||
      auth: true,
 | 
			
		||||
    })
 | 
			
		||||
    return response
 | 
			
		||||
  }
 | 
			
		||||
@@ -41,7 +41,7 @@ export class GameService extends ServiceBase {
 | 
			
		||||
  async getMatchSession(sessionId: string) {
 | 
			
		||||
    const response = await this.networkService.get({
 | 
			
		||||
      uri: `/game/match/${sessionId}`,
 | 
			
		||||
      auth: true
 | 
			
		||||
      auth: true,
 | 
			
		||||
    })
 | 
			
		||||
    return response
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import { createColors } from 'colorette'
 | 
			
		||||
import { createColors, gray, redBright, yellow } from 'colorette'
 | 
			
		||||
import dayjs from 'dayjs'
 | 
			
		||||
import pino, { type BaseLogger } from 'pino'
 | 
			
		||||
import { isProxy, toRaw } from 'vue'
 | 
			
		||||
@@ -11,7 +11,7 @@ export class LoggingService {
 | 
			
		||||
  constructor() {
 | 
			
		||||
    this._logger = pino({
 | 
			
		||||
      browser: {
 | 
			
		||||
        asObject: true,
 | 
			
		||||
        asObject: false,
 | 
			
		||||
        transmit: {
 | 
			
		||||
          level: import.meta.env.VITE_LOG_LEVEL || 'error',
 | 
			
		||||
          send: (level, logEvent) => {
 | 
			
		||||
@@ -31,14 +31,14 @@ export class LoggingService {
 | 
			
		||||
            if (messages.length > 0) {
 | 
			
		||||
              console.log(
 | 
			
		||||
                `${logStr.join(' ')}:`,
 | 
			
		||||
                ...messages.filter((m) => m !== undefined && m !== null)
 | 
			
		||||
                ...messages.filter((m) => m !== undefined && m !== null),
 | 
			
		||||
              )
 | 
			
		||||
            } else {
 | 
			
		||||
              console.log(logStr.join(' '))
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -46,7 +46,10 @@ export class LoggingService {
 | 
			
		||||
    return {
 | 
			
		||||
      info: green,
 | 
			
		||||
      debug: blue,
 | 
			
		||||
      error: red
 | 
			
		||||
      error: red,
 | 
			
		||||
      trace: gray,
 | 
			
		||||
      warn: yellow,
 | 
			
		||||
      fatal: redBright,
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -55,11 +58,11 @@ export class LoggingService {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  info(message: string, data?: any) {
 | 
			
		||||
    this._logger.info(this._getMessageWidthObject(message, data))
 | 
			
		||||
    this._logger.info(message, data)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  warn(message: string, data?: any) {
 | 
			
		||||
    this._logger.warn(this._getMessageWidthObject(message, data))
 | 
			
		||||
    this._logger.warn(message, data)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  error(error: any, message?: string) {
 | 
			
		||||
@@ -67,11 +70,11 @@ export class LoggingService {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  fatal(message: string, data?: any) {
 | 
			
		||||
    this._logger.fatal(this._getMessageWidthObject(message, data))
 | 
			
		||||
    this._logger.fatal(message, data)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  trace(message: string, data?: any) {
 | 
			
		||||
    this._logger.trace(this._getMessageWidthObject(message, data))
 | 
			
		||||
    this._logger.trace(message, data)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  object(message: any) {
 | 
			
		||||
 
 | 
			
		||||
@@ -28,9 +28,9 @@ export class SocketIoClientService extends ServiceBase {
 | 
			
		||||
      })
 | 
			
		||||
      this.socket.on('connect', () => {
 | 
			
		||||
        if (this.socket && this.socket.recovered) {
 | 
			
		||||
          console.log('socket recovered succesfully')
 | 
			
		||||
          this.logger.debug('SOCKET: socket recovered succesfully')
 | 
			
		||||
        } else {
 | 
			
		||||
          console.log('socket connected')
 | 
			
		||||
          this.logger.debug('SOCKET: socket connected')
 | 
			
		||||
        }
 | 
			
		||||
        this.isConnected = true
 | 
			
		||||
        this.addEvents()
 | 
			
		||||
@@ -42,21 +42,20 @@ export class SocketIoClientService extends ServiceBase {
 | 
			
		||||
  addEvents(): void {
 | 
			
		||||
    this.socket.on('disconnect', () => {
 | 
			
		||||
      this.isConnected = false
 | 
			
		||||
      console.log('Disconnected from server')
 | 
			
		||||
      console.log('SOCKET: Disconnected from server')
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    this.socket.on('reconnect', () => {
 | 
			
		||||
      this.isConnected = true
 | 
			
		||||
      console.log('Reconnected to server')
 | 
			
		||||
      this.logger.debug('SOCKET: Reconnected to server')
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    this.socket.on('reconnect_error', () => {
 | 
			
		||||
      this.isConnected = false
 | 
			
		||||
      console.log('Failed to reconnect to server')
 | 
			
		||||
      this.logger.debug('SOCKET: Failed to reconnect to server')
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    this.socket.on('ping', () => {
 | 
			
		||||
      console.log('Ping received from server')
 | 
			
		||||
      this.socket.emit('pong') // Send pong response
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
@@ -73,7 +72,7 @@ export class SocketIoClientService extends ServiceBase {
 | 
			
		||||
    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)
 | 
			
		||||
        this.logger.trace(`SOCKET: Received event: ${event}`, data)
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
@@ -81,24 +80,24 @@ export class SocketIoClientService extends ServiceBase {
 | 
			
		||||
  sendMessage(event: string, data: any): void {
 | 
			
		||||
    if (this.isConnected) {
 | 
			
		||||
      this.socket?.emit('client:event', { event, data })
 | 
			
		||||
      console.log('sendMessage :>> ', event, data)
 | 
			
		||||
      this.logger.trace(`SOCKET: sendMessage :>> ${event}`, data)
 | 
			
		||||
    } else {
 | 
			
		||||
      console.log('Not connected to server')
 | 
			
		||||
      this.logger.trace('Not connected to server')
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async sendMessageWithAck(event: string, data: any): Promise<any> {
 | 
			
		||||
    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 })
 | 
			
		||||
    } else {
 | 
			
		||||
      console.log('Not connected to server')
 | 
			
		||||
      this.logger.trace('SOCKET: Not connected to server')
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  disconnect(): void {
 | 
			
		||||
    this.socket?.disconnect()
 | 
			
		||||
    this.isConnected = false
 | 
			
		||||
    console.log('Disconnected from server')
 | 
			
		||||
    this.logger.debug('SOCKET: Disconnected from server')
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -12,16 +12,16 @@ const ioOpts = {
 | 
			
		||||
  reconnectionDelay: 1000, // Time between each attempt (in ms)
 | 
			
		||||
  reconnectionDelayMax: 5000, // Maximum time between attempts (in ms)
 | 
			
		||||
  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 = io('http://localhost:3000', ioOpts)
 | 
			
		||||
 | 
			
		||||
socket.on('connect', () => {
 | 
			
		||||
  if (socket.recovered) {
 | 
			
		||||
    console.log('socket recovered succesfully')
 | 
			
		||||
    console.log('SOCKET: socket recovered succesfully')
 | 
			
		||||
  } else {
 | 
			
		||||
    console.log('socket connected')
 | 
			
		||||
    console.log('SOCKET: socket connected')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // setTimeout(() => {
 | 
			
		||||
@@ -42,5 +42,5 @@ export default {
 | 
			
		||||
  },
 | 
			
		||||
  disconnect() {
 | 
			
		||||
    socket.disconnect()
 | 
			
		||||
  }
 | 
			
		||||
  },
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -28,9 +28,11 @@ const socketService: any = inject('socket')
 | 
			
		||||
const gameService: GameService = inject<GameService>('game') as GameService
 | 
			
		||||
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 { gameOptions } = storeToRefs(gameOptionsStore)
 | 
			
		||||
const { updateSessionState, updatePlayerState, updateGameState } = gameStore
 | 
			
		||||
 | 
			
		||||
// function setPlayerReady() {
 | 
			
		||||
//   logger.debug('Starting game')
 | 
			
		||||
@@ -57,11 +59,60 @@ async function createMatch() {
 | 
			
		||||
  logger.debug('Creating match')
 | 
			
		||||
  await socketService.connect()
 | 
			
		||||
  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')
 | 
			
		||||
  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) {
 | 
			
		||||
  if (id) {
 | 
			
		||||
    await socketService.connect()
 | 
			
		||||
@@ -80,7 +131,7 @@ async function deleteMatch(id: string) {
 | 
			
		||||
async function loadData() {
 | 
			
		||||
  const listResponse = await gameService.listMatchSessions()
 | 
			
		||||
  matchSessions.value = listResponse.data
 | 
			
		||||
  sessionName.value = `Test #${listResponse.pagination.total + 1}`
 | 
			
		||||
  sessionName.value = `Test #${Date.now()}`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
@@ -99,41 +150,47 @@ function copy(sessionSeed: string) {
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="block home">
 | 
			
		||||
  <div class="container home">
 | 
			
		||||
    <section class="section">
 | 
			
		||||
      <h1 class="title is-2">Welcome to the {{ user.username }}'s Home Page</h1>
 | 
			
		||||
      <div class="block">
 | 
			
		||||
      <h1 class="title is-2">
 | 
			
		||||
        {{ $t('welcome-to-the-user-username-s-home-page', [user.username]) }}
 | 
			
		||||
      </h1>
 | 
			
		||||
      <div class="block" v-if="!isSessionStarted">
 | 
			
		||||
        <div class="field">
 | 
			
		||||
          <label class="label">Name</label>
 | 
			
		||||
          <label class="label">{{ $t('name') }}</label>
 | 
			
		||||
          <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 class="field">
 | 
			
		||||
          <label class="label">Seed</label>
 | 
			
		||||
          <label class="label">{{ $t('seed') }}</label>
 | 
			
		||||
          <div class="control">
 | 
			
		||||
            <input
 | 
			
		||||
              type="text"
 | 
			
		||||
              class="input"
 | 
			
		||||
              style="margin-bottom: 0"
 | 
			
		||||
              v-model="seed"
 | 
			
		||||
              placeholder="Type the session seed here!"
 | 
			
		||||
              placeholder="$t('seed-placeholder')"
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="grid">
 | 
			
		||||
          <div class="cell">
 | 
			
		||||
            <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="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>
 | 
			
		||||
                    <option value="green">{{ $t('green-fabric') }}</option>
 | 
			
		||||
                    <option value="gray">{{ $t('gray-fabric') }}</option>
 | 
			
		||||
                    <option value="blue">{{ $t('blue-fabric') }}</option>
 | 
			
		||||
                    <option value="yellow">{{ $t('yellow-fabric') }}</option>
 | 
			
		||||
                    <option value="red">{{ $t('red-fabric') }}</option>
 | 
			
		||||
                  </select>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
@@ -142,14 +199,14 @@ function copy(sessionSeed: string) {
 | 
			
		||||
              <div class="control">
 | 
			
		||||
                <label for="teamed" class="checkbox">
 | 
			
		||||
                  <input v-model="teamed" name="teamed" type="checkbox" />
 | 
			
		||||
                  Crossed game ({{ teamed }})
 | 
			
		||||
                  {{ $t('crossed-game-teamed', [teamed]) }}
 | 
			
		||||
                </label>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="cell">
 | 
			
		||||
            <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="select">
 | 
			
		||||
                  <select v-model="pointsToWin" name="pointsToWin">
 | 
			
		||||
@@ -164,37 +221,58 @@ function copy(sessionSeed: string) {
 | 
			
		||||
            </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 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 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 v-if="matchSessions.length === 0">
 | 
			
		||||
          <p>No sessions available</p>
 | 
			
		||||
          <p>{{ $t('no-sessions-available') }}</p>
 | 
			
		||||
        </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>{{ $t('id-session-_id', [session._id]) }}</p>
 | 
			
		||||
                <p>{{ $t('players-session-players-length', [session.players.length]) }}</p>
 | 
			
		||||
                <p>
 | 
			
		||||
                  Seed: {{ session.seed }}
 | 
			
		||||
                  <button class="button is-small" @click="() => copy(session.seed)">Copy</button>
 | 
			
		||||
                  {{ $t('seed-session-seed', [session.seed]) }}
 | 
			
		||||
                  <button class="button is-small" @click="() => copy(session.seed)">
 | 
			
		||||
                    {{ $t('copy') }}
 | 
			
		||||
                  </button>
 | 
			
		||||
                </p>
 | 
			
		||||
                <p>Status: {{ session.status }}</p>
 | 
			
		||||
                <p>{{ $t('status-session-status', [session.status]) }}</p>
 | 
			
		||||
                <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>
 | 
			
		||||
                  <a href="#" @click.once.prevent="() => joinMatch(session._id)">
 | 
			
		||||
                    {{ $t('join') }}
 | 
			
		||||
                  </a>
 | 
			
		||||
                </p>
 | 
			
		||||
                <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>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -2,10 +2,12 @@
 | 
			
		||||
import { AuthenticationService } from '@/services/AuthenticationService'
 | 
			
		||||
import { inject, ref } from 'vue'
 | 
			
		||||
import { useRouter } from 'vue-router'
 | 
			
		||||
import { useI18n } from 'vue-i18n'
 | 
			
		||||
 | 
			
		||||
const router = useRouter()
 | 
			
		||||
const username = ref('')
 | 
			
		||||
const password = ref('')
 | 
			
		||||
const { t } = useI18n()
 | 
			
		||||
 | 
			
		||||
const authService = inject<AuthenticationService>('auth')
 | 
			
		||||
 | 
			
		||||
@@ -14,7 +16,7 @@ async function login() {
 | 
			
		||||
    await authService?.login(username.value, password.value)
 | 
			
		||||
    router.push({ name: 'home' })
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    alert('Invalid username or password')
 | 
			
		||||
    alert(t('invalid-username-or-password'))
 | 
			
		||||
  }
 | 
			
		||||
  // if (username.value === 'admin' && password.value === 'password') {
 | 
			
		||||
  // localStorage.setItem('token', 'true')
 | 
			
		||||
@@ -27,24 +29,34 @@ async function login() {
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="login">
 | 
			
		||||
    <h1>Login</h1>
 | 
			
		||||
    <h1 class="title">{{ $t('login') }}</h1>
 | 
			
		||||
    <form class="form" @submit.prevent="login">
 | 
			
		||||
      <div class="field">
 | 
			
		||||
        <label class="label">Username</label>
 | 
			
		||||
        <label class="label">{{ $t('username') }}</label>
 | 
			
		||||
        <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 class="field">
 | 
			
		||||
        <label class="label">Username</label>
 | 
			
		||||
        <label class="label">{{ $t('password') }}</label>
 | 
			
		||||
        <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 class="field is-grouped">
 | 
			
		||||
        <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 class="control">
 | 
			
		||||
          <button class="button">Cancel</button>
 | 
			
		||||
 
 | 
			
		||||
@@ -2,88 +2,28 @@
 | 
			
		||||
import type { MatchSessionDto } from '@/common/interfaces'
 | 
			
		||||
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 { inject, onBeforeMount, ref, toRaw } from 'vue'
 | 
			
		||||
import { useRoute, useRouter } from 'vue-router'
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
let sessionId: string
 | 
			
		||||
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() {
 | 
			
		||||
  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(() => {
 | 
			
		||||
  sessionId = route.params.id as string
 | 
			
		||||
  console.log('sessionId :>> ', sessionId)
 | 
			
		||||
  loadData()
 | 
			
		||||
  if (sessionId) {
 | 
			
		||||
    setInterval(loadData, 5000)
 | 
			
		||||
    // setInterval(loadData, 5000)
 | 
			
		||||
  } else {
 | 
			
		||||
    router.push({ name: 'home' })
 | 
			
		||||
  }
 | 
			
		||||
@@ -91,27 +31,50 @@ onBeforeMount(() => {
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <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>
 | 
			
		||||
      <p>Session started: {{ isSessionStarted }}</p>
 | 
			
		||||
      <p>Host: {{ amIHost }}</p>
 | 
			
		||||
      <p>{{ sessionState || 'No session' }}</p>
 | 
			
		||||
  <div class="container">
 | 
			
		||||
    <h1 class="title is-1">{{ $t('match-page') }}</h1>
 | 
			
		||||
    <h2 class="title is-3">{{ matchSession?.name }}</h2>
 | 
			
		||||
    <div class="block mt-6">
 | 
			
		||||
      <p class="mb-4">
 | 
			
		||||
        <span class="title is-5">{{ $t('winner') }}</span>
 | 
			
		||||
        <span class="is-size-5 ml-4">{{ matchSession?.matchWinner?.name }}</span>
 | 
			
		||||
      </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">
 | 
			
		||||
      <p v-if="!amIHost && !readyForStart">Waiting for host to start session</p>
 | 
			
		||||
      <button class="button" @click="setPlayerReady" v-if="isSessionStarted">
 | 
			
		||||
        <span v-if="!readyForStart">Ready</span><span v-else>Unready</span>
 | 
			
		||||
      </button>
 | 
			
		||||
      <button class="button" @click="startMatch" v-if="amIHost && readyForStart">
 | 
			
		||||
        <span>Start</span>
 | 
			
		||||
      </button>
 | 
			
		||||
 | 
			
		||||
      <button class="button" @click="cancelMatch" v-if="isSessionStarted">
 | 
			
		||||
        <span>Cancel</span>
 | 
			
		||||
      </button>
 | 
			
		||||
    <div class="grid">
 | 
			
		||||
      <div
 | 
			
		||||
        class="cell"
 | 
			
		||||
        v-bind:key="$index"
 | 
			
		||||
        v-for="(summary, $index) in matchSession?.gameSummaries"
 | 
			
		||||
      >
 | 
			
		||||
        <div class="block mt-6">
 | 
			
		||||
          <h3 class="title is-5">{{ $t('round-index-1', [$index + 1]) }}</h3>
 | 
			
		||||
          <p class="mb-4">
 | 
			
		||||
            <span class="title is-5">{{ $t('winner') }}</span>
 | 
			
		||||
            <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>
 | 
			
		||||
</template>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user