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
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
}

View File

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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