Jose Conde 498a8253fd v0.2.0
2024-07-25 16:29:14 +02:00

354 lines
9.2 KiB
TypeScript

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