354 lines
9.2 KiB
TypeScript
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()
|
|
}
|
|
}
|