import { Application, Container, EventEmitter, Graphics, Point, Sprite, Texture, Ticker, } from 'pixi.js' import { Tile } from '@/game/Tile' import type { MatchSessionOptions, PlayerDto, TileDto } from '@/common/interfaces' import { GlowFilter } from 'pixi-filters' import { Scale, type ScaleFunction } from './utilities/scale' import { LoggingService } from '@/services/LoggingService' import { createContainer } from '@/common/helpers' import { createText, playerNameText, whiteStyle } from './utilities/fonts' import Config from '@/game/Config' import { TimerText } from './TimerText' import { Button } from './Button' export class Hand extends EventEmitter { tiles: Tile[] = [] container: Container = new Container() buttonPass!: Button scoreLayer: Container = new Container() activeLayer: Container = new Container() height: number width: number ticker: Ticker lastTimeClicked: number = 0 doubleClickThreshold: number = 300 initialized: boolean = false _canMove: boolean = false scale: number = 1 scaleY!: ScaleFunction scaleX!: ScaleFunction grain: number = 25 logger: LoggingService = new LoggingService() availableTiles: Tile[] = [] tilesLayer!: Container interactionsLayer!: Container score: number = 0 active: boolean = false private player!: PlayerDto private timer: TimerText constructor( app: Application, private options: MatchSessionOptions, ) { super() app.stage.addChild(this.container) this.ticker = app.ticker this.height = 130 * this.scale 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.buttonPass = this.createPassButton() this.timer = this.createTimer(this.options.turnWaitSeconds) this.calculateScale() this.initLayers() this.render() } 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.scoreLayer) this.container.addChild(this.activeLayer) this.interactionsLayer.addChild(this.buttonPass) this.interactionsLayer.addChild(this.timer) } gameFinished() { this.tiles = [] this.initialized = false } get canMove() { return this._canMove } 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]) } prepareForMove(isFirstMove: boolean, freeEnds?: [number, number]) { this.availableTiles = isFirstMove ? this.tiles : this.tiles.filter((tile) => this.hasMoves(tile.toPlain(), freeEnds)) if (this.availableTiles.length === 0) { this.buttonPass.disabled = false } this.availableTiles.forEach((tile) => { tile.animateTo({ x: tile.x, y: tile.y - 10, }) tile.interactive = true }) if (this.timer) { this.timer.reset() this.timer.start() } } createTimer(seconds: number): TimerText { const timer = new TimerText(seconds, new Point(this.width - 60, 45)) // timer.animation = false timer.on('timeout', () => { this.timerTimeout() }) return timer } timerTimeout() { if (this.timer === undefined) return this.timer.reset() if (this.availableTiles.length === 0) { this.emit('game:timer-timeout', null) return } const randomTile = this.availableTiles[Math.floor(Math.random() * this.availableTiles.length)] // pick random tile randomTile.alpha = 1 this.emit('game:timer-timeout', randomTile.toPlain()) } 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.timer && this.timer.reset() } hasMoves(tile: TileDto, freeEnds?: [number, number]): boolean { if (tile === undefined || freeEnds === undefined) return false let hasMoves: boolean = false if (tile.pips != undefined) { hasMoves = tile.pips.includes(freeEnds[0]) || tile.pips.includes(freeEnds[1]) } return hasMoves } initialize(playerState: PlayerDto) { this.tiles = this.createTiles(playerState) this.initialized = this.tiles.length > 0 this.render() this.emit('hand-updated', this.tiles) } 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 onTileClick(tile: Tile) { // if (Date.now() - this.lastTimeClicked < this.doubleClickThreshold) { // this.emit('tileDoubleClick', { id: tile.id }) // return // } const selected = this.tiles.find((t) => t.selected) if (selected) { this.deselectTile(selected) if (selected.id === tile.id) { this.emit('game:tile-click') return } } tile.selected = true tile.alpha = 1 this.emit('game:tile-click', tile.toPlain()) } private deselectTile(selected: Tile) { selected.selected = false selected.alpha = 0.7 } public tileMoved(tileDto: TileDto): Tile | undefined { const tile = this.tiles.find((t) => t.id === tileDto.id) if (!tile) return this.afterMove() this.tiles = this.tiles.filter((t) => t.id !== tileDto.id) tile.interactive = false tile.clearFilters() tile.off('pointerdown') tile.off('pointerover') tile.off('pointerout') this.tilesLayer.removeChild(tile.getSprite()) return tile } private createPassButton(): Button { const x = this.width - 100 const y = this.height - 45 const btn = new Button('PASS', new Point(x, y), { width: 80, height: 30 }, () => { this.timer.reset() this.buttonPass.disabled = true this.emit('game:button-pass-click') }) btn.disabledColor = 0xbabf95 btn.disabled = true this.buttonPass = btn return btn } disablePassButton() { this.buttonPass.disabled = true } update(playerState: PlayerDto) { this.tilesLayer.removeChildren() if (!this.initialized) { this.initialize(playerState) return } const missing: Tile | undefined = this.tiles.find( (tile: Tile) => !playerState.hand.find((t) => t.id === tile.id), ) if (missing) { this.tilesLayer.removeChild(missing.getSprite()) this.tiles = this.tiles.filter((tile) => tile.id !== missing.id) this.emit('hand-updated', this.tiles) } 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) newTile.alpha = 0.7 newTile.anchor = 0.5 newTile.addTo(this.tilesLayer) newTile.on('pointerdown', () => this.onTileClick(newTile)) newTile.on('pointerover', () => { this.emit('tileHover', newTile.toPlain()) newTile.alpha = 1 newTile.setFilters([ new GlowFilter({ distance: 10, outerStrength: 2, innerStrength: 1, color: 0xffffff, quality: 0.5, }), ]) }) newTile.on('pointerout', () => { if (!newTile.selected) { this.emit('tileHover') newTile.alpha = 0.7 newTile.getSprite().filters = [] } }) return newTile }) } renderTiles() { const deltaX = this.width / 2 - (this.tiles.length * 50 - 5) / 2 this.tiles.forEach((tile, i) => { tile.setPosition(deltaX + tile.width / 2 + i * (tile.width + 5), tile.height / 2 + 20) }) } renderScore() { 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() } }