import { Application, Container, EventEmitter, Graphics, Sprite, Text, Texture, Ticker } from 'pixi.js' import { Tile } from '@/game/Tile' import type { Dimension, PlayerDto, TileDto } from '@/common/interfaces' import { GlowFilter } from 'pixi-filters' import { Scale, type ScaleFunction } from './utilities/scale' import { LoggingService } from '@/services/LoggingService' export class Hand extends EventEmitter { tiles: Tile[] = [] container: Container = new Container() buttonPass: Container = new Container() buttonNext: 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() constructor(app: Application) { super() app.stage.addChild(this.container) this.ticker = app.ticker this.height = 130 this.width = app.canvas.width this.container.y = app.canvas.height - this.height this.container.width = this.width this.container.height = this.height this.calculateScale() this.addBg() } gameFinished() { this.logger.debug('gameFinished') this.tiles = [] this.container.removeChildren() this.initialized = false this.buttonNext = this.createButton( 'NEXT', { x: this.width / 2 - 25, y: this.height / 2, width: 50, height: 20 }, 'nextClick' ) } 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]) } set canMove(value: boolean) { this._canMove = value if (value) { this.createPassButton() } else { this.container.removeChild(this.buttonPass) } this.tiles.forEach((tile) => { tile.interactive = value }) } initialize(playerState: PlayerDto) { this.tiles = this.createTiles(playerState) this.emit('handUpdated', this.tiles) this.initialized = this.tiles.length > 0 this.renderTiles() } 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('tileClick') return } } tile.selected = true tile.alpha = 1 tile.animateTo(tile.x, tile.y - 10) this.emit('tileClick', tile.toPlain()) } private deselectTile(selected: Tile) { selected.animateTo(selected.x, selected.y + 10) selected.selected = false selected.alpha = 0.7 } public tileMoved(tileDto: TileDto) { const tile = this.tiles.find((t) => t.id === tileDto.id) if (!tile) return tile.interactive = false tile.clearFilters() tile.off('pointerdown') tile.off('pointerover') tile.off('pointerout') } private createButton( textStr: string, dimension: Dimension, action: string | Function ): Container { const { x, y, width, height } = dimension const rectangle = new Graphics().roundRect(x, y, width + 4, height + 4, 5).fill(0xffff00) const text = new Text({ text: textStr, style: { fontFamily: 'Arial', fontSize: 12, fontWeight: 'bold', fill: 0x121212, align: 'center' } }) text.anchor = 0.5 const container = new Container() container.addChild(rectangle) container.addChild(text) text.y = y + height / 2 text.x = x + width / 2 container.eventMode = 'static' container.cursor = 'pointer' rectangle.alpha = 0.7 text.alpha = 0.7 container.on('pointerdown', () => { action instanceof Function ? action() : this.emit(action) }) container.on('pointerover', () => { rectangle.alpha = 1 text.alpha = 1 }) container.on('pointerout', () => { rectangle.alpha = 0.7 text.alpha = 0.7 }) this.container.addChild(container) return container } private createPassButton() { const lastTile = this.tiles[this.tiles.length - 1] const x = lastTile ? lastTile.x + lastTile.width : this.scaleX(0) this.buttonPass = this.createButton( 'PASS', { x, y: this.height / 2, width: 50, height: 20 }, 'passClick' ) } update(playerState: PlayerDto) { 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.container.removeChild(missing.getSprite()) this.tiles = this.tiles.filter((tile) => tile.id !== missing.id) this.emit('handUpdated', this.tiles) } this.renderTiles() } private createTiles(playerState: PlayerDto) { return playerState.hand.map((tile) => { const newTile: Tile = new Tile(tile.id, this.ticker, tile.pips, this.scale) newTile.alpha = 0.7 newTile.anchor = 0.5 newTile.addTo(this.container) newTile.on('pointerdown', () => this.onTileClick(newTile)) newTile.on('pointerover', () => { newTile.alpha = 1 newTile.setFilters([ new GlowFilter({ distance: 10, outerStrength: 2, innerStrength: 1, color: 0xffffff, quality: 0.5 }) ]) }) newTile.on('pointerout', () => { if (!newTile.selected) { 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) }) } }