working flow

This commit is contained in:
Jose Conde 2024-07-14 21:35:03 +02:00
parent 1b058db6c0
commit 3755f2857a
13 changed files with 375 additions and 214 deletions

View File

@ -1,6 +1,7 @@
import { Graphics, Container, Text } from 'pixi.js' import { Graphics, Container, Text } from 'pixi.js'
import type { ContainerOptions, Dimension, TileDto } from './interfaces' import type { ContainerOptions, Dimension, TileDto } from './interfaces'
import { DEFAULT_CONTAINER_OPTIONS } from './constants' import { DEFAULT_CONTAINER_OPTIONS } from './constants'
import useClipboard from 'vue-clipboard3'
export function getColorBackground(container: Container, colorName: string, alpha: number = 0.5) { export function getColorBackground(container: Container, colorName: string, alpha: number = 0.5) {
const graphics = new Graphics() const graphics = new Graphics()
@ -22,6 +23,10 @@ export function createContainer(options: ContainerOptions) {
if (opts.color) { if (opts.color) {
rect.fill(opts.color) rect.fill(opts.color)
} }
if (opts.alpha) {
rect.alpha = opts.alpha
}
rect.visible = opts.visible rect.visible = opts.visible
container.addChild(rect) container.addChild(rect)
if (opts.parent) { if (opts.parent) {
@ -78,6 +83,15 @@ export function createButton(
return container return container
} }
export function createCrosshair(container: Container, color: number = 0xff0000, d: Dimension) {
const verticalLine = new Graphics().moveTo(d.x, 0).lineTo(d.x, d.height).stroke(color)
const horizontalLine = new Graphics().moveTo(0, d.y).lineTo(d.width, d.y).stroke(color)
verticalLine.alpha = 0.2
horizontalLine.alpha = 0.2
container.addChild(verticalLine)
container.addChild(horizontalLine)
}
export async function wait(ms: number) { export async function wait(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms)) return new Promise((resolve) => setTimeout(resolve, ms))
} }
@ -91,3 +105,8 @@ export function isTilePair(tile: TileDto): boolean {
export function isTileVertical(tile: TileDto): boolean { export function isTileVertical(tile: TileDto): boolean {
return tile.orientation === 'north' || tile.orientation === 'south' return tile.orientation === 'north' || tile.orientation === 'south'
} }
export function copyToclipboard(value: string) {
const { toClipboard } = useClipboard()
toClipboard(value)
}

View File

@ -74,6 +74,7 @@ export interface ContainerOptions {
color?: number color?: number
visible?: boolean visible?: boolean
parent?: Container parent?: Container
alpha?: number
} }
export interface Dimension { export interface Dimension {

View File

@ -1,31 +1,38 @@
<script setup lang="ts"> <script setup lang="ts">
import type { MatchSessionDto, GameDto, PlayerDto } from '@/common/interfaces' import type { GameDto, PlayerDto } from '@/common/interfaces'
import { onMounted, onUnmounted, ref, watch, inject } from 'vue' import { onMounted, onUnmounted, ref, inject, toRaw } from 'vue'
import { Game } from '@/game/Game' import { Game } from '@/game/Game'
import { useGameStore } from '@/stores/game' import { useGameStore } from '@/stores/game'
import { useEventBusStore } from '@/stores/eventBus' import { useEventBusStore } from '@/stores/eventBus'
import type { LoggingService } from '@/services/LoggingService'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
const logger: LoggingService = inject<LoggingService>('logger') as LoggingService
const emit = defineEmits(['move'])
const socketService: any = inject('socket') const socketService: any = inject('socket')
const gameStore = useGameStore() const gameStore = useGameStore()
const eventBus = useEventBusStore() const eventBus = useEventBusStore()
const { playerState, sessionState, canMakeMove } = storeToRefs(gameStore) const { playerState, sessionState } = storeToRefs(gameStore)
const { updateSessionState, updatePlayerState, updateGameState } = gameStore const { updateGameState } = gameStore
const minScreenWidth = 800
const minScreenHeight = 700
let screenWidth = window.innerWidth - 10
let screenHeight = window.innerHeight - 10
if (screenWidth < minScreenWidth) screenWidth = minScreenWidth
if (screenHeight < minScreenHeight) screenHeight = minScreenHeight
const boardScale = screenWidth > 1200 ? 0.8 : screenWidth > 1200 ? 0.7 : 0.6
let appEl = ref<HTMLElement | null>(null) let appEl = ref<HTMLElement | null>(null)
const game = new Game( const game = new Game(
{ {
width: 1200, width: screenWidth,
height: 650, height: screenHeight,
boardScale: 0.7, boardScale,
handScale: 1 handScale: 1
}, },
emit,
socketService, socketService,
playerState.value?.id || '', playerState.value?.id || '',
sessionState.value?.id || '' sessionState.value?.id || ''
@ -61,36 +68,31 @@ onMounted(async () => {
const canvas = await game.setup() const canvas = await game.setup()
appEl.value.appendChild(canvas) appEl.value.appendChild(canvas)
await game.preload() await game.preload()
await game.start() await game.start(sessionState?.value?.players)
eventBus.subscribe('server:game-finished', (data) => { eventBus.subscribe('server:game-finished', (data) => {
console.log('server:game-finished :>> ', data)
game.gameFinished(data) game.gameFinished(data)
}) })
eventBus.subscribe('server:server-player-move', (data) => { eventBus.subscribe('server:server-player-move', (data) => {
console.log('server:player-move :>> ', data)
game.serverPlayerMove(data, playerState.value?.id ?? '') game.serverPlayerMove(data, playerState.value?.id ?? '')
}) })
eventBus.subscribe('game:player-turn-started', (data: any) => { eventBus.subscribe('game:player-turn-started', (data: any) => {
console.log('game:player-turn-started :>> ', data)
// game.setCanMakeMove(true) // game.setCanMakeMove(true)
}) })
eventBus.subscribe('server:hand-dealt', (playerState: PlayerDto) => { eventBus.subscribe('server:hand-dealt', (data: { player: PlayerDto; gameState: GameDto }) => {
console.log('server:hand-dealt :>> ', playerState) game.hand.update(data.player)
game.hand.update(playerState) game.updateOtherHands(data.gameState)
}) })
eventBus.subscribe('server:next-turn', (gameState: GameDto) => { eventBus.subscribe('server:next-turn', (gameState: GameDto) => {
console.log('server:next-turn :>> ', gameState)
updateGameState(gameState) updateGameState(gameState)
game.setNextPlayer(gameState) game.setNextPlayer(gameState)
}) })
eventBus.subscribe('server:match-finished', (data) => { eventBus.subscribe('server:match-finished', (data) => {
console.log('server:match-finished :>> ', data)
game.matchFinished(data) game.matchFinished(data)
}) })
@ -115,7 +117,7 @@ onMounted(async () => {
}) })
onUnmounted(() => { onUnmounted(() => {
game.destroy() //game.destroy()
}) })
</script> </script>

View File

@ -1,27 +1,18 @@
import { import { Application, Assets, Container, EventEmitter, Sprite, Text, Ticker } from 'pixi.js'
Application,
Assets,
Container,
EventEmitter,
Graphics,
Sprite,
Text,
Ticker
} from 'pixi.js'
import { Scale, type ScaleFunction } from '@/game/utilities/scale' import { Scale, type ScaleFunction } from '@/game/utilities/scale'
import type { AnimationOptions, Movement, PlayerDto, TileDto } from '@/common/interfaces' import type { AnimationOptions, Movement, PlayerDto, TileDto } from '@/common/interfaces'
import { Tile } from '@/game/Tile' import { Tile } from '@/game/Tile'
import { DIRECTIONS, createContainer, isTilePair } from '@/common/helpers' import { DIRECTIONS, createContainer, createCrosshair, isTilePair } from '@/common/helpers'
import { createText } from '@/game/utilities/fonts' import { createText } from '@/game/utilities/fonts'
import { LoggingService } from '@/services/LoggingService' import { LoggingService } from '@/services/LoggingService'
import { inject } from 'vue'
import { GlowFilter } from 'pixi-filters' import { GlowFilter } from 'pixi-filters'
import { ORIENTATION_ANGLES } from '@/common/constants' import { ORIENTATION_ANGLES } from '@/common/constants'
import type { OtherHand } from './OtherHand'
export class Board extends EventEmitter { export class Board extends EventEmitter {
private _scale: number = 1 private _scale: number = 1
private _canMove: boolean = false private _canMove: boolean = false
private logger = inject<LoggingService>('logger')! private logger: LoggingService = new LoggingService()
ticker: Ticker ticker: Ticker
height: number height: number
@ -50,6 +41,7 @@ export class Board extends EventEmitter {
playerHand: Tile[] = [] playerHand: Tile[] = []
firstTile?: Tile firstTile?: Tile
currentPlayer!: PlayerDto currentPlayer!: PlayerDto
otherPlayerHands: OtherHand[] = []
constructor(app: Application) { constructor(app: Application) {
super() super()
@ -93,18 +85,19 @@ export class Board extends EventEmitter {
visible: false visible: false
}) })
const verticalLine = new Graphics() createCrosshair(this.tilesContainer, 0xff0000, {
.moveTo(this.scaleX(0), 0) width: this.width,
.lineTo(this.scaleX(0), this.height) height: this.height,
.stroke(0xff0000) x: this.scaleX(0),
const horizontalLine = new Graphics() y: this.scaleY(0)
.moveTo(0, this.scaleY(0)) })
.lineTo(this.width, this.scaleY(0))
.stroke(0xff0000) createCrosshair(this.interactionContainer, 0xffff00, {
verticalLine.alpha = 0.2 width: this.width,
horizontalLine.alpha = 0.2 height: this.height,
this.tilesContainer.addChild(verticalLine) x: this.scaleX(0),
this.tilesContainer.addChild(horizontalLine) y: this.scaleY(0)
})
this.textContainer = createContainer({ this.textContainer = createContainer({
width: this.width, width: this.width,
@ -135,15 +128,6 @@ export class Board extends EventEmitter {
this.calculateScale() this.calculateScale()
} }
get canMove() {
return this._canMove
}
set canMove(value: boolean) {
this._canMove = value
this.updateCanMoveText()
}
setPlayerHand(tiles: Tile[]) { setPlayerHand(tiles: Tile[]) {
this.playerHand = tiles this.playerHand = tiles
} }
@ -153,12 +137,8 @@ export class Board extends EventEmitter {
this.textContainer.addChild(createText(text, this.scaleX(0), 100)) this.textContainer.addChild(createText(text, this.scaleX(0), 100))
} }
private updateCanMoveText() { async setPlayerTurn(player: PlayerDto) {
if (this.canMove) {
this.showText('Your turn!') this.showText('Your turn!')
} else {
this.showText('Waiting for players')
}
} }
async setServerPlayerTurn(currentPlayer: PlayerDto) { async setServerPlayerTurn(currentPlayer: PlayerDto) {
@ -168,6 +148,9 @@ export class Board extends EventEmitter {
async playerMove(move: any, playerId: string) { async playerMove(move: any, playerId: string) {
const { move: lastMove } = move const { move: lastMove } = move
if (lastMove === null) { if (lastMove === null) {
setTimeout(() => {
this.emit('game:tile-animation-ended')
}, 500)
return return
} }
if ( if (
@ -252,21 +235,46 @@ export class Board extends EventEmitter {
tile.reScale(this.scale) tile.reScale(this.scale)
this.tiles.push(tile) this.tiles.push(tile)
await this.animateTile(tile, x, y, orientation, move)
this.emit('game:tile-animation-ended', tile.toPlain())
}
async animateTile(tile: Tile, x: number, y: number, orientation: string, move: Movement) {
const targetX = this.scaleX(x)
const targetY = this.scaleY(y)
const animation: AnimationOptions = { const animation: AnimationOptions = {
x: this.scaleX(x), x: targetX,
y: this.scaleY(y), y: targetY,
rotation: ORIENTATION_ANGLES[orientation], rotation: ORIENTATION_ANGLES[orientation],
duration: 20 duration: 20
} }
const tempAlpha = tile.alpha
tile.setPosition(this.scaleX(x), this.scaleY(y)) tile.alpha = 0
const clonedTile = tile.clone()
clonedTile.addTo(this.tilesContainer)
const pos = this.getAnimationInitialPoosition(move)
clonedTile.setPosition(this.scaleX(pos.x), this.scaleY(pos.y))
await clonedTile.animateTo(animation)
clonedTile.removeFromParent()
tile.setOrientation(orientation) tile.setOrientation(orientation)
tile.setPosition(targetX, targetY)
tile.alpha = tempAlpha
}
// tile.setPosition(this.scaleX(0), this.height + tile.height / 2) getAnimationInitialPoosition(move: Movement): { x: number; y: number } {
// console.log('going to animate', tile.pips) const otherHand = this.otherPlayerHands.find((h) => h.player?.id === move.playerId)
// await tile.animateTo(animation) if (otherHand === undefined) {
// console.log('animated', tile.pips) return { x: 0, y: this.scaleY.inverse(this.height + 50) }
this.emit('game:tile-animation-ended', tile.toPlain()) }
const position = otherHand.position
switch (position) {
case 'left':
return { x: this.scaleX.inverse(100), y: this.scaleY.inverse(100) }
case 'right':
return { x: 0, y: this.scaleY.inverse(100) }
case 'top':
return { x: this.scaleX.inverse(this.width - 100), y: this.scaleY.inverse(20) }
}
} }
getPlayedTile(id: string): Tile | undefined { getPlayedTile(id: string): Tile | undefined {
@ -315,16 +323,14 @@ export class Board extends EventEmitter {
} }
} }
async updateBoard(move: Movement) { async updateBoard(move: Movement, tile: Tile | undefined) {
try { try {
const { tile: tileDto } = move // const { tileDto: tileDto } = move
const tile = this.getTileInHand(tileDto?.id ?? '') // const tile = this.getTileInHand(tileDto?.id ?? '')
this.movements.push(move)
if (tile === undefined) { if (tile === undefined) {
return return
} }
this.movements.push(move)
await this.addTile(tile, move) await this.addTile(tile, move)
this.setFreeEnd(move) this.setFreeEnd(move)
} catch (error) { } catch (error) {

View File

@ -5,17 +5,17 @@ import { Tile } from '@/game/Tile'
import { Hand } from '@/game/Hand' import { Hand } from '@/game/Hand'
import type { GameDto, Movement, PlayerDto, TileDto } from '@/common/interfaces' import type { GameDto, Movement, PlayerDto, TileDto } from '@/common/interfaces'
import type { SocketIoClientService } from '@/services/SocketIoClientService' import type { SocketIoClientService } from '@/services/SocketIoClientService'
import { useEventBusStore } from '@/stores/eventBus'
import { wait } from '@/common/helpers' import { wait } from '@/common/helpers'
import { Actions } from 'pixi-actions' import { Actions } from 'pixi-actions'
import { OtherHand } from './OtherHand'
export class Game { export class Game {
public board!: Board public board!: Board
public hand!: Hand public hand!: Hand
private app: Application = new Application() private app: Application = new Application()
private selectedTile: TileDto | undefined private selectedTile: TileDto | undefined
private eventBus: any = useEventBusStore()
private currentMove: Movement | undefined private currentMove: Movement | undefined
private otherHands: OtherHand[] = []
constructor( constructor(
private options: { boardScale: number; handScale: number; width: number; height: number } = { private options: { boardScale: number; handScale: number; width: number; height: number } = {
@ -24,24 +24,29 @@ export class Game {
width: 1200, width: 1200,
height: 800 height: 800
}, },
private emit: any,
private socketService: SocketIoClientService, private socketService: SocketIoClientService,
private playerId: string, private playerId: string,
private sessionId: string private sessionId: string
) {} ) {}
async setup(): Promise<HTMLCanvasElement> { async setup(): Promise<HTMLCanvasElement> {
const width = 1200 const width = this.options.width || 1200
const height = 800 const height = this.options.height || 800
await this.app.init({ width, height }) await this.app.init({ width, height })
this.app.ticker.add((tick) => Actions.tick(tick.deltaTime / 60)) this.app.ticker.add((tick) => Actions.tick(tick.deltaTime / 60))
return this.app.canvas return this.app.canvas
} }
async start() { async start(players: PlayerDto[] = []) {
this.board = new Board(this.app) this.board = new Board(this.app)
this.hand = new Hand(this.app) this.hand = new Hand(this.app)
this.otherHands = [
new OtherHand(this.app, 'left'),
new OtherHand(this.app, 'top'),
new OtherHand(this.app, 'right')
]
this.initOtherHands(players)
this.hand.scale = this.options.handScale this.hand.scale = this.options.handScale
this.board.scale = this.options.boardScale this.board.scale = this.options.boardScale
this.setBoardEvents() this.setBoardEvents()
@ -54,6 +59,32 @@ export class Game {
}) })
} }
initOtherHands(players: PlayerDto[]) {
const myIndex = players.findIndex((player) => player.id === this.playerId)
const copy = [...players]
const cut = copy.splice(myIndex)
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.board.otherPlayerHands = this.otherHands
}
updateOtherHands(gameState: GameDto) {
const players = gameState.players
players.forEach((player) => {
const hand = this.otherHands.find((hand) => hand.player?.id === player.id)
if (hand) {
hand.setHand(player.hand)
}
})
}
async preload() { async preload() {
await Assets.load(assets) await Assets.load(assets)
} }
@ -82,11 +113,11 @@ export class Game {
sessionId: this.sessionId, sessionId: this.sessionId,
move: move move: move
}) })
await this.board.updateBoard(move) await this.board.updateBoard(move, undefined)
}) })
this.hand.on('nextClick', () => { this.hand.on('nextClick', () => {
this.socketService.sendMessage('client:set-player-ready', { this.socketService.sendMessage('client:set-client-ready-for-next-game', {
userId: this.playerId, userId: this.playerId,
sessionId: this.sessionId sessionId: this.sessionId
}) })
@ -120,24 +151,18 @@ export class Game {
return validEnds return validEnds
} }
public setCanMakeMove(value: boolean) {
this.hand.setCanMove(value, this.board.count === 0, this.board.freeEnds)
this.board.canMove = value
}
async setNextPlayer(state: GameDto) { async setNextPlayer(state: GameDto) {
const currentPlayer = state?.currentPlayer! const currentPlayer = state?.currentPlayer!
if (currentPlayer.id !== this.playerId) { if (currentPlayer.id === this.playerId) {
this.setCanMakeMove(false) this.hand.prepareForMove(this.board.count === 0, this.board.freeEnds)
this.board.setServerPlayerTurn(currentPlayer) this.board.setPlayerTurn(currentPlayer)
} else { } else {
this.setCanMakeMove(true) this.board.setServerPlayerTurn(currentPlayer)
} }
} }
private setBoardEvents() { private setBoardEvents() {
this.board.on('game:board-left-action-click', async (data) => { this.board.on('game:board-left-action-click', async (data) => {
console.log('left data :>> ', data)
if (this.selectedTile === undefined) return if (this.selectedTile === undefined) return
const move: Movement = { const move: Movement = {
tile: this.selectedTile, tile: this.selectedTile,
@ -146,12 +171,11 @@ export class Game {
...data ...data
} }
this.currentMove = move this.currentMove = move
this.hand.tileMoved(this.selectedTile) const tile = this.hand.tileMoved(this.selectedTile)
await this.board.updateBoard({ ...move, tile: this.selectedTile }) await this.board.updateBoard({ ...move, tile: this.selectedTile }, tile)
}) })
this.board.on('game:board-right-action-click', async (data) => { this.board.on('game:board-right-action-click', async (data) => {
console.log('right data :>> ', data)
if (this.selectedTile === undefined) return if (this.selectedTile === undefined) return
const move: Movement = { const move: Movement = {
tile: this.selectedTile, tile: this.selectedTile,
@ -160,31 +184,25 @@ export class Game {
...data ...data
} }
this.currentMove = move this.currentMove = move
this.hand.tileMoved(this.selectedTile) const tile = this.hand.tileMoved(this.selectedTile)
await this.board.updateBoard({ ...move, tile: this.selectedTile }) await this.board.updateBoard({ ...move, tile: this.selectedTile }, tile)
}) })
this.board.on('game:tile-animation-ended', async (tile) => { this.board.on('game:tile-animation-ended', async (tile) => {
console.log('animation ended', tile) if (tile !== null && tile !== undefined && tile.playerId === this.playerId) {
if (tile.playerId === this.playerId) {
this.socketService.sendMessage('client:player-move', { this.socketService.sendMessage('client:player-move', {
sessionId: this.sessionId, sessionId: this.sessionId,
move: this.currentMove move: this.currentMove
}) })
} else {
this.socketService.sendMessage('client:animation-ended', {
sessionId: this.sessionId,
userId: this.playerId
})
} }
}) })
} }
// sendMoveEvent(move: Movement) {
// this.board.on('game:tile-animation-ended', async (tile) => {
// this.eventBus.publish('game:tile-animation-ended', tile)
// // this.socketService.sendMessageWithAck('client:tile-animation-ended', {
// // sessionId: this.sessionId,
// // tile
// // })
// })
// }
gameFinished(data: any) { gameFinished(data: any) {
this.hand.gameFinished() this.hand.gameFinished()
this.board.gameFinished(data) this.board.gameFinished(data)
@ -197,6 +215,13 @@ export class Game {
serverPlayerMove(data: any, playerId: string) { serverPlayerMove(data: any, playerId: string) {
this.board.playerMove(data, playerId) this.board.playerMove(data, playerId)
if (!(data.move === undefined || data.move === null)) {
const otherHand = this.otherHands.find((hand) => hand.player?.id === data.move.playerId)
if (otherHand) {
otherHand.update(data.move)
}
}
} }
private removeBoardEvents() { private removeBoardEvents() {

View File

@ -88,30 +88,31 @@ export class Hand extends EventEmitter {
this.scaleY = Scale([-scaleYSteps, scaleYSteps], [0, this.height]) this.scaleY = Scale([-scaleYSteps, scaleYSteps], [0, this.height])
} }
setCanMove(value: boolean, isFirstMove: boolean, freeEnds?: [number, number]) { prepareForMove(isFirstMove: boolean, freeEnds?: [number, number]) {
console.log('this.tiles :>> ', this.tiles.length) this.availableTiles = isFirstMove
this.availableTiles =
!value || isFirstMove
? this.tiles ? this.tiles
: this.tiles.filter((tile) => this.hasMoves(tile.toPlain(), freeEnds)) : this.tiles.filter((tile) => this.hasMoves(tile.toPlain(), freeEnds))
if (this.availableTiles.length === 0) {
console.log('this.availableTiles :>> ', this.availableTiles.length)
if (value && this.availableTiles.length === 0) {
this.createPassButton() this.createPassButton()
} else {
this.interactionsLayer.removeChild(this.buttonPass)
} }
this.availableTiles.forEach((tile) => { this.availableTiles.forEach((tile) => {
if (value) {
tile.animateTo({ tile.animateTo({
x: tile.x, x: tile.x,
y: tile.y - 10 y: tile.y - 10
}) })
// const action: Action = Actions.moveTo(tile.getSprite(), tile.x, tile.y - 10, 1,).play() tile.interactive = true
} })
tile.interactive = value }
afterMove() {
this.availableTiles.forEach((tile) => {
tile.animateTo({
x: tile.x,
y: tile.y + 10
})
tile.setPosition(tile.x, tile.y + 10)
tile.interactive = false
}) })
this._canMove = value
} }
hasMoves(tile: TileDto, freeEnds?: [number, number]): boolean { hasMoves(tile: TileDto, freeEnds?: [number, number]): boolean {
@ -166,20 +167,12 @@ export class Hand extends EventEmitter {
selected.alpha = 0.7 selected.alpha = 0.7
} }
public tileMoved(tileDto: TileDto) { public tileMoved(tileDto: TileDto): Tile | undefined {
const tile = this.tiles.find((t) => t.id === tileDto.id) const tile = this.tiles.find((t) => t.id === tileDto.id)
if (!tile) return if (!tile) return
this.availableTiles this.afterMove()
.filter((t) => t.id !== tileDto.id)
.forEach((t) => {
t.animateTo({
x: t.x,
y: t.y + 10
})
})
this.tiles = this.tiles.filter((t) => t.id !== tileDto.id) this.tiles = this.tiles.filter((t) => t.id !== tileDto.id)
tile.interactive = false tile.interactive = false
@ -187,6 +180,8 @@ export class Hand extends EventEmitter {
tile.off('pointerdown') tile.off('pointerdown')
tile.off('pointerover') tile.off('pointerover')
tile.off('pointerout') tile.off('pointerout')
this.tilesLayer.removeChild(tile.getSprite())
return tile
} }
private createPassButton() { private createPassButton() {
@ -195,7 +190,10 @@ export class Hand extends EventEmitter {
this.buttonPass = createButton( this.buttonPass = createButton(
'PASS', 'PASS',
{ x, y: this.height / 2, width: 50, height: 20 }, { x, y: this.height / 2, width: 50, height: 20 },
() => this.emit('game:button-pass-click'), () => {
this.interactionsLayer.removeChild(this.buttonPass)
this.emit('game:button-pass-click')
},
this.interactionsLayer this.interactionsLayer
) )
} }

118
src/game/OtherHand.ts Normal file
View File

@ -0,0 +1,118 @@
import { LoggingService } from '@/services/LoggingService'
import { Application, Container, Sprite, Texture } from 'pixi.js'
import { Scale, type ScaleFunction } from './utilities/scale'
import { Tile } from './Tile'
import type { Movement, PlayerDto, TileDto } from '@/common/interfaces'
import { createContainer } from '@/common/helpers'
import { createText, playerNameText } from './utilities/fonts'
export class OtherHand {
tilesInitialNumber: number = 7
player?: PlayerDto
hand: Tile[] = []
container: Container = new Container()
height: number
width: number
scale: number = 0.5
scaleY!: ScaleFunction
scaleX!: ScaleFunction
x: number = 0
y: number = 0
grain: number = 25
logger: LoggingService = new LoggingService()
tilesLayer!: Container
interactionsLayer!: Container
constructor(
private app: Application,
public position: 'left' | 'right' | 'top' = 'left'
) {
this.height = 100
this.width = 300
app.stage.addChild(this.container)
const { x, y } = this.getPosition()
this.container.width = this.width
this.container.height = this.height
this.container.x = x
this.container.y = y
this.calculateScale()
this.initLayers()
}
setPlayer(player: PlayerDto) {
this.player = player
this.container.addChild(createText(`${player.name}`, this.width / 2, 12, playerNameText))
}
setHand(tiles: TileDto[]) {
this.hand = tiles.map((tile) => new Tile(tile.id, this.app.ticker, undefined, this.scale))
this.render()
}
update(move: Movement) {
this.hand = this.hand.filter((tile) => tile.id !== move?.tile?.id)
this.render()
}
private render() {
this.tilesLayer.removeChildren()
const x = -9
this.hand.forEach((tile, index) => {
tile.setPosition(this.scaleX(x + index * 2), this.height / 2)
this.tilesLayer.addChild(tile.getSprite())
})
}
private addBg() {
const bg = new Sprite(Texture.WHITE)
bg.alpha = 0.08
bg.width = this.width
bg.height = this.height
this.container.addChild(bg)
}
private getPosition() {
let x = 0
let y = 0
if (this.position === 'left') {
x = 0
y = 30
} else if (this.position === 'right') {
x = this.app.canvas.width - this.width
y = 30
} else {
x = (this.app.canvas.width - this.width) / 2
y = 0
}
return { x, y }
}
private initLayers() {
this.container.removeChildren()
this.addBg()
this.tilesLayer = createContainer({
width: this.width,
height: this.height,
x: 0,
y: 0,
parent: this.container
})
this.interactionsLayer = createContainer({
width: this.width,
height: this.height,
x: 0,
y: 0,
parent: this.container
})
this.container.addChild(this.tilesLayer)
this.container.addChild(this.interactionsLayer)
}
private calculateScale() {
const scaleXSteps = Math.floor(this.width / (this.grain * this.scale)) / 2
const scaleYSteps = Math.floor(this.height / (this.grain * this.scale)) / 2
this.scaleX = Scale([-scaleXSteps, scaleXSteps], [0, this.width])
this.scaleY = Scale([-scaleYSteps, scaleYSteps], [0, this.height])
}
}

View File

@ -1,10 +1,11 @@
import type { AnimationOptions } from '@/common/interfaces' import type { AnimationOptions } from '@/common/interfaces'
import { Sprite, Texture, Ticker } from 'pixi.js' import { Container, Sprite, Texture, Ticker } from 'pixi.js'
import { Tile } from './Tile' import { Tile } from './Tile'
export abstract class SpriteBase { export abstract class SpriteBase {
private _interactive: boolean = false private _interactive: boolean = false
protected sprite: Sprite = new Sprite() protected sprite: Sprite = new Sprite()
private container?: Container
constructor( constructor(
protected ticker?: Ticker, protected ticker?: Ticker,
@ -136,6 +137,11 @@ export abstract class SpriteBase {
} }
addTo(container: any) { addTo(container: any) {
this.container = container
container.addChild(this.sprite) container.addChild(this.sprite)
} }
removeFromParent() {
this.container?.removeChild(this.sprite)
}
} }

View File

@ -86,4 +86,11 @@ export class Tile extends SpriteBase {
} }
this.orientation = value this.orientation = value
} }
clone(): Tile {
const copy = new Tile(this.id, this.ticker, this.pips, this.scale, this.playerId)
copy.selected = this.selected
copy.orientation = this.orientation
return copy
}
} }

View File

@ -16,6 +16,16 @@ export const mainText = new TextStyle({
stroke: '#658f56' stroke: '#658f56'
}) })
export const playerNameText = new TextStyle({
dropShadow: dropShadowStyle,
fill: '#a2a2a2',
fontFamily: 'Arial, Helvetica, sans-serif',
letterSpacing: 1,
stroke: '#565656',
fontSize: 15,
fontWeight: 'bold'
})
export function createText(str: string, x: number, y: number, style: TextStyle = mainText) { export function createText(str: string, x: number, y: number, style: TextStyle = mainText) {
const text = new Text({ text: str, style }) const text = new Text({ text: str, style })
text.anchor.set(0.5, 0.5) text.anchor.set(0.5, 0.5)

View File

@ -31,13 +31,15 @@ onBeforeUnmount(() => {
}) })
function copySeed() { function copySeed() {
if (sessionState?.value?.seed) toClipboard(sessionState.value.seed) if (sessionState?.value?.seed) {
toClipboard(sessionState.value.seed)
}
} }
</script> </script>
<template> <template>
<div class="block"> <div class="block">
<section class="block"> <section class="block info">
<p> <p>
Running: {{ sessionState?.sessionInProgress }} Seed: {{ sessionState?.seed }} Running: {{ sessionState?.sessionInProgress }} Seed: {{ sessionState?.seed }}
<button @click="copySeed">Copy!</button> <button @click="copySeed">Copy!</button>
@ -99,56 +101,15 @@ function copySeed() {
* { * {
box-sizing: border-box; box-sizing: border-box;
} }
.action-select {
display: flex;
gap: 16px;
align-items: start;
justify-items: center;
}
.game-container { .game-container {
display: flex; display: flex;
align-items: start; align-items: start;
justify-content: center; justify-content: center;
} }
.info {
.control {
padding: 8px;
margin-right: 4px;
}
textarea {
width: 100%;
height: 110px;
resize: none;
}
#response,
#status {
height: 180px;
}
.tiles-container {
display: flex;
gap: 4px;
flex-flow: row wrap;
align-content: flex-start;
}
.board-container {
position: absolute; position: absolute;
height: 600px; top: 0;
padding: 10px; left: 0;
width: calc(50% - 50px); z-index: 20;
background-color: var(--pico-form-element-background-color);
}
.hand-container {
position: fixed;
bottom: 0;
height: 200px;
padding: 10px;
width: calc(50% - 50px);
background-color: var(--pico-form-element-background-color);
} }
</style> </style>

View File

@ -8,8 +8,9 @@ import type { GameService } from '@/services/GameService'
import type { MatchSessionDto } from '@/common/interfaces' import type { MatchSessionDto } from '@/common/interfaces'
import { useEventBusStore } from '@/stores/eventBus' import { useEventBusStore } from '@/stores/eventBus'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import { copyToclipboard } from '@/common/helpers'
let seed = ref('') let seed = ref<string>('')
let sessionName = ref('Test Value') let sessionName = ref('Test Value')
let sessionId = ref('') let sessionId = ref('')
let matchSessions = ref<MatchSessionDto[]>([]) let matchSessions = ref<MatchSessionDto[]>([])
@ -79,16 +80,19 @@ async function joinMatch(id: string) {
router.push({ name: 'match', params: { id } }) router.push({ name: 'match', params: { id } })
} }
} }
async function deleteMatch(id: string) {
async function startMatch() { if (id) {
if (sessionState.value && sessionState.value.id) { await socketService.connect()
router.push({ name: 'game' }) await gameService.cancelMatchSession(id)
loadData()
} }
} }
async function loadData() { async function loadData() {
matchSessions.value = await gameService.listMatchSessions() const listResponse = await gameService.listMatchSessions()
sessionName.value = `Test #${matchSessions.value.length + 1}` console.log('listResponse :>> ', listResponse)
matchSessions.value = listResponse.data
sessionName.value = `Test #${listResponse.pagination.total + 1}`
} }
onMounted(() => { onMounted(() => {
@ -101,6 +105,11 @@ onUnmounted(() => {
logger.debug('Home view unmounted') logger.debug('Home view unmounted')
clearInterval(dataInterval) clearInterval(dataInterval)
}) })
function copy(sessionSeed: string) {
seed.value = sessionSeed
copyToclipboard(sessionSeed)
}
</script> </script>
<template> <template>
@ -132,16 +141,6 @@ onUnmounted(() => {
<button class="button" @click="createMatch" v-if="!isSessionStarted"> <button class="button" @click="createMatch" v-if="!isSessionStarted">
Create Match Session Create Match Session
</button> </button>
<!-- <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="readyForStart">
<span>Start</span>
</button>
<button class="button" @click="cancelMatch" v-if="isSessionStarted">
<span>Cancel</span>
</button> -->
</section> </section>
<section class="section available-sessions" v-if="!isSessionStarted"> <section class="section available-sessions" v-if="!isSessionStarted">
<h2 class="title is-4">Available Sessions</h2> <h2 class="title is-4">Available Sessions</h2>
@ -149,12 +148,19 @@ onUnmounted(() => {
<div v-if="matchSessions.length === 0"> <div v-if="matchSessions.length === 0">
<p>No sessions available</p> <p>No sessions available</p>
</div> </div>
<div v-else v-for="session in matchSessions" :key="session.id"> <div v-else class="grid is-col-min-12">
<p>{{ session.name }}</p> <div class="cell" v-for="session in matchSessions" :key="session.id">
<p>{{ session }}</p> <p class="title is-6">{{ session.name }}</p>
<button class="button" @click="() => joinMatch(session._id)"> <p>ID: {{ session._id }}</p>
Join ({{ session._id }}) <p>Players: {{ session.players.length }}</p>
</button> <p>
Seed: {{ session.seed }}
<button @click="() => copy(session.seed)">Copy</button>
</p>
<p>Status: {{ session.status }}</p>
<button class="button" @click="() => joinMatch(session._id)">Join</button>
<button class="button" @click="() => deleteMatch(session._id)">Delete</button>
</div>
</div> </div>
</div> </div>
</section> </section>

View File

@ -70,7 +70,9 @@ eventBus.subscribe('window-before-unload', async () => {
await cancelMatch() await cancelMatch()
}) })
eventBus.subscribe('server:match-starting', () => { eventBus.subscribe('server:match-starting', (data) => {
const session = data.sessionState as MatchSessionDto
updateSessionState(session)
logger.debug('Match starting') logger.debug('Match starting')
router.push({ name: 'game' }) router.push({ name: 'game' })
}) })