import { GameStep, HistoryItem, GAME_STEP_GAME_MOVE, GAME_STEP_LAST_STONE_IN_EMPTY_PIT, GAME_STEP_BOARD_CLEARED, GAME_STEP_LAST_STONE_IN_BANK, GAME_STEP_DOUBLE_STONE_IN_PIT, MancalaGame, } from "mancala.js"; import { v4 } from "uuid"; import { BoardViewModel } from "../viewmodel/BoardViewModel"; import { PitViewModelFactory, BoardViewModelFactory } from "../factory"; import { Game } from "../models"; import { getColorByBrightness } from "../util"; import { Theme, ThemeManager } from "../theme"; export const DEFAULT_ANIMATION_INTERVAL = 300; const animationUpdateInterval = DEFAULT_ANIMATION_INTERVAL; export class PitAnimator { themeManager: ThemeManager; game: Game | undefined; oldGame: Game | undefined; currentIntervalID: number | undefined; onBoardViewModelUpdate: (boardViewModel: BoardViewModel) => void; boardViewModel: BoardViewModel | undefined; oldBoardViewModel: BoardViewModel | undefined; animationIndex: number = 0; currentHistoryItem: HistoryItem | undefined; // Time between animation steps, in milliseconds. animationInterval: number; constructor( themeManager: ThemeManager, onBoardViewModelUpdate: (boardViewModel: BoardViewModel) => void, animationInterval: number = animationUpdateInterval ) { this.themeManager = themeManager; this.onBoardViewModelUpdate = onBoardViewModelUpdate; this.animationInterval = animationInterval; this.themeManager.on("themechange", this.onThemeChange.bind(this)); } get mancalaGame(): MancalaGame | undefined { return this.game?.mancalaGame; } public setNewGame(game: Game) { this.reset(); this.game = game; this.onBoardViewModelUpdate?.(this.getBoardViewModelFromGame(this.game)); } public setUpdatedGame(game: Game) { this.resetAnimationState(); if (!this.game) { this.setNewGame(game); } else { this.oldGame = this.game; this.game = game; this.onGameMoveAnimationStart(); } } onGameMoveAnimationStart() { this.stopCurrentAnimation(); if (this.game && this.oldGame && this.mancalaGame && this.mancalaGame?.history.length > 0) { const lastHistoryItem = this.mancalaGame.history[this.mancalaGame.history.length - 1]; if (lastHistoryItem.gameSteps.length > 0) { this.animationIndex = 0; this.currentHistoryItem = lastHistoryItem; this.boardViewModel = this.getBoardViewModelFromGame(this.game); this.oldBoardViewModel = this.getBoardViewModelFromGame(this.oldGame); this.startAnimationUpdateCyle(); } } } onAnimate() { if (!this.currentHistoryItem || !this.game || !this.oldBoardViewModel || !this.mancalaGame) return; if (this.animationIndex === this.currentHistoryItem.gameSteps.length) { this.clearCurrentInterval(); this.onBoardViewModelUpdate?.(this.getBoardViewModelFromGame(this.game)); } else { const gameStep = this.currentHistoryItem.gameSteps[this.animationIndex]; const index = this.mancalaGame.board.getPitIndexCircularly(gameStep.index); this.animatePit(index, this.oldBoardViewModel, gameStep); this.onBoardViewModelUpdate?.(this.oldBoardViewModel); } this.animationIndex++; } getGameMoveStepCount(historyItem: HistoryItem) { return historyItem.gameSteps.filter( (gameStep) => gameStep.type === GAME_STEP_GAME_MOVE ).length; } animatePit( index: number, boardViewModel: BoardViewModel, gameStep: GameStep ) { if (!this.currentHistoryItem || !this.game || !this.mancalaGame) return; const pitViewModel = boardViewModel.pits[index]; if (this.animationIndex === 0) { //This is one stone move case, TODO: beautify it later if (this.getGameMoveStepCount(this.currentHistoryItem) === 1) { const previousPitIndex = gameStep.index - 1; if (previousPitIndex > 0) { boardViewModel.pits[previousPitIndex].stoneCount = 0; } } else { pitViewModel.stoneCount = 0; } } const theme = this.themeManager.theme; if (gameStep.type === GAME_STEP_GAME_MOVE) { pitViewModel.stoneCount += 1; pitViewModel.pitColor = theme.pitGameMoveAnimateColor; } else if (gameStep.type === GAME_STEP_LAST_STONE_IN_EMPTY_PIT) { pitViewModel.pitColor = theme.pitGetRivalStonePitAnimateColor; pitViewModel.stoneCount = 0; const oppositeIndex = this.mancalaGame.board.getPitIndexCircularly( gameStep.data.oppositeIndex ); const oppositePitViewModel = boardViewModel.pits[oppositeIndex]; oppositePitViewModel.pitColor = theme.pitGetRivalStonePitAnimateColor; oppositePitViewModel.stoneCount = 0; } else if (gameStep.type === GAME_STEP_LAST_STONE_IN_BANK) { pitViewModel.pitColor = theme.pitLastStoneInBankPitAnimateColor; } else if (gameStep.type === GAME_STEP_BOARD_CLEARED) { for (const index of gameStep.data.pitIndexesThatHasStone) { const oppositeIndex = this.mancalaGame.board.getPitIndexCircularly(index); const oppositePitViewModel = boardViewModel.pits[oppositeIndex]; oppositePitViewModel.pitColor = theme.pitGetRivalStonePitAnimateColor; oppositePitViewModel.stoneCount = 0; } } else if (gameStep.type === GAME_STEP_DOUBLE_STONE_IN_PIT) { const _index = this.mancalaGame.board.getPitIndexCircularly(index); const pitViewModel = boardViewModel.pits[_index]; pitViewModel.pitColor = theme.pitGetRivalStonePitAnimateColor; pitViewModel.stoneCount = 0; } pitViewModel.stoneColor = getColorByBrightness( pitViewModel.pitColor, theme.stoneColor, theme.stoneLightColor ); } startAnimationUpdateCyle() { this.clearCurrentInterval(); //@ts-ignore this.currentIntervalID = setInterval( () => this.onAnimate(), this.animationInterval ); } stopCurrentAnimation() { this.clearCurrentInterval(); if (this.oldGame) { this.onBoardViewModelUpdate?.( this.getBoardViewModelFromGame(this.oldGame) ); } this.resetAnimationState(); } clearCurrentInterval() { if (this.currentIntervalID) { clearInterval(this.currentIntervalID); } } public getBoardViewModelFromGame(game: Game): BoardViewModel { const pitViewModels = this.createPitViewModelsFromGame(game); return BoardViewModelFactory.create(v4(), pitViewModels); } private createPitViewModelsFromGame(game: Game) { return game.mancalaGame.board.pits.map((pit) => { const theme = this.themeManager.theme; const stoneCount = pit.stoneCount; const stoneColor = theme.stoneColor; const pitColor = theme.pitColor; const id = pit.index.toString(); return PitViewModelFactory.create({ id, stoneCount, stoneColor, pitColor, }); }); } private onThemeChange(theme: Theme) { if (!this.game) return; this.onBoardViewModelUpdate?.(this.getBoardViewModelFromGame(this.game)); } public resetAnimationState() { this.animationIndex = -1; this.currentHistoryItem = undefined; this.boardViewModel = undefined; this.oldBoardViewModel = undefined; } public reset() { this.resetAnimationState(); this.game = undefined; this.oldGame = undefined; } public dispose() { this.themeManager.off("themechange", this.onThemeChange.bind(this)); this.resetAnimationState(); this.clearCurrentInterval(); } }