diff --git a/src/animation/PitAnimator.ts b/src/animation/PitAnimator.ts new file mode 100644 index 0000000..377e46f --- /dev/null +++ b/src/animation/PitAnimator.ts @@ -0,0 +1,194 @@ +import { + MancalaGame, + GameStep, + HistoryItem, + GAME_STEP_GAME_MOVE, + GAME_STEP_LAST_STONE_IN_EMPTY_PIT, + GAME_STEP_BOARD_CLEARED, + GAME_STEP_LAST_STONE_IN_BANK, +} from "mancala.js"; +import { v4 } from "uuid"; +import { Context } from "../context"; +import BoardViewModelFactory from "../factory/BoardViewModelFactory"; +import { PitViewModelFactory } from "../factory/PitViewModelFactory"; +import BoardViewModel from "../viewmodel/BoardViewModel"; + +const animationUpdateInterval = 300; + +export default class PitAnimator { + context: Context; + game: MancalaGame; + oldGame: MancalaGame; + currentIntervalID: number; + onBoardViewModelUpdate: (boardViewModel: BoardViewModel) => void; + boardViewModel: BoardViewModel; + oldBoardViewModel: BoardViewModel; + animationIndex: number = 0; + currentHistoryItem: HistoryItem; + + constructor( + context: Context, + onBoardViewModelUpdate: (boardViewModel: BoardViewModel) => void + ) { + this.context = context; + this.onBoardViewModelUpdate = onBoardViewModelUpdate; + } + + public setNewGame(game: MancalaGame) { + this.reset(); + this.game = game; + this.updateBoardViewModel(this.getBoardViewModelFromGame(this.game)); + } + + public setUpdatedGame(game: MancalaGame, forceClear = false) { + this.resetAnimationState(); + if (!this.game) { + this.setNewGame(game); + } else { + this.oldGame = this.game; + this.game = game; + this.onGameMoveAnimationStart(); + } + } + + onGameMoveAnimationStart() { + this.stopCurrentAnimation(); + if (this.game.history.length > 0) { + const lastHistoryItem = this.game.history[this.game.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.animationIndex === this.currentHistoryItem.gameSteps.length) { + this.clearCurrentInterval(); + this.updateBoardViewModel(this.getBoardViewModelFromGame(this.game)); + } else { + const gameStep = this.currentHistoryItem.gameSteps[this.animationIndex]; + const index = this.game.board.getPitIndexCircularly(gameStep.index); + this.animatePit(index, this.oldBoardViewModel, gameStep); + this.updateBoardViewModel(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 + ) { + const pitViewModel = boardViewModel.pits[index]; + if (this.animationIndex === 0) { + //This 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.context.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.game.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.game.board.getPitIndexCircularly(index); + const oppositePitViewModel = boardViewModel.pits[oppositeIndex]; + oppositePitViewModel.pitColor = theme.pitGetRivalStonePitAnimateColor; + oppositePitViewModel.stoneCount = 0; + } + } + } + + startAnimationUpdateCyle() { + this.clearCurrentInterval(); + this.currentIntervalID = setInterval( + () => this.onAnimate(), + animationUpdateInterval + ); + } + + stopCurrentAnimation() { + this.clearCurrentInterval(); + if (this.oldGame) { + this.updateBoardViewModel(this.getBoardViewModelFromGame(this.oldGame)); + } + this.resetAnimationState(); + } + + clearCurrentInterval() { + if (this.currentIntervalID) { + clearInterval(this.currentIntervalID); + } + } + + updateBoardViewModel(boardViewModel: BoardViewModel) { + boardViewModel.id = v4(); + this.onBoardViewModelUpdate?.(boardViewModel); + } + + private getBoardViewModelFromGame(game: MancalaGame): BoardViewModel { + const pitViewModels = this.createPitViewModelsFromGame(game); + return BoardViewModelFactory.create(v4(), pitViewModels); + } + + private createPitViewModelsFromGame(game: MancalaGame) { + return game.board.pits.map((pit) => { + const theme = this.context.themeManager.theme; + const stoneCount = pit.stoneCount; + const stoneColor = theme.ballColor; + const pitColor = theme.holeColor; + const id = pit.index.toString(); + return PitViewModelFactory.create({ + id, + stoneCount, + stoneColor, + pitColor, + }); + }); + } + + public resetAnimationState() { + this.animationIndex = -1; + this.currentHistoryItem = null; + this.boardViewModel = null; + this.oldBoardViewModel = null; + } + + public reset() { + this.resetAnimationState(); + this.game = null; + this.oldGame = null; + } + + public dispose() { + this.resetAnimationState(); + this.clearCurrentInterval(); + } +}