diff --git a/package.json b/package.json index 23ae830..8a6b98e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mancala-frontend", - "version": "0.1.3-beta.2", + "version": "0.1.3-beta.3", "description": "", "main": "index.js", "scripts": { @@ -14,9 +14,11 @@ "author": "", "license": "ISC", "dependencies": { + "@types/uuid": "^8.3.4", "mancala.js": "^0.0.2-beta.1", "react": "^17.0.2", - "react-dom": "^17.0.2" + "react-dom": "^17.0.2", + "uuid": "^8.3.2" }, "devDependencies": { "@types/react": "^17.0.11", diff --git a/src/Home.tsx b/src/Home.tsx index 3e95d96..a4e225a 100644 --- a/src/Home.tsx +++ b/src/Home.tsx @@ -6,6 +6,8 @@ import { RTMTWS } from "./rtmt/rtmt_websocket"; import { channel_game_move, channel_leave_game, + channel_on_game_crashed, + channel_on_game_start, channel_on_game_update, channel_on_game_user_leave, } from "./channel_names"; @@ -13,6 +15,8 @@ import Button from "./components/Button"; import InfoPanel from "./components/InfoPanel"; import { CommonMancalaGame, MancalaGame, Pit } from "mancala.js"; import { GameMove } from "./models/GameMove"; +import PitAnimator from "./animation/PitAnimator"; +import BoardViewModel from "./viewmodel/BoardViewModel"; type ConnectionState = "connecting" | "error" | "connected" | "reconnecting"; @@ -25,19 +29,16 @@ const Home: FunctionComponent<{ initial?: number }> = ({ initial = 0 }) => { const [searchingOpponent, setSearchingOpponent] = useState(false); const [game, setGame] = useState(undefined); - const gameRef = React.useRef(game); const [crashMessage, setCrashMessage] = useState(undefined); const [userKeyWhoLeave, setUserKeyWhoLeave] = useState(undefined); - const [animationPitIndex, setAnimationPitIndex] = useState(-1); + const [boardViewModel, setBoardViewModel] = useState(null); - const [intervalId, setIntervalId] = useState(-1); + const [boardId, setBoardId] = useState(); - useEffect(() => { - gameRef.current = game; - }); + const [pitAnimator, setPitAnimator] = useState(); const onConnectionDone = () => { setConnetionState("connected"); @@ -69,44 +70,23 @@ const Home: FunctionComponent<{ initial?: number }> = ({ initial = 0 }) => { }); }; - const listenMessages = () => { + const listenMessages = (pitAnimator: PitAnimator) => { + context.rtmt.listenMessage(channel_on_game_start, (message: Object) => { + const newGame: CommonMancalaGame = message as CommonMancalaGame; + const mancalaGame = MancalaGame.createFromMancalaGame(newGame); + setSearchingOpponent(false); + setGame(mancalaGame); + pitAnimator.setNewGame(mancalaGame); + }); + context.rtmt.listenMessage(channel_on_game_update, (message: Object) => { const newGame: CommonMancalaGame = message as CommonMancalaGame; const mancalaGame = MancalaGame.createFromMancalaGame(newGame); - if (gameRef.current && mancalaGame.history.length > 0) { - const lastHistoryItem = - mancalaGame.history[mancalaGame.history.length - 1]; - if (lastHistoryItem.gameSteps.length > 0) { - let stepIndex = 0; - if (intervalId) { - clearInterval(intervalId); - } - const id = setInterval(() => { - if (stepIndex === lastHistoryItem.gameSteps.length) { - clearInterval(id); - setAnimationPitIndex(-1); - setGame(mancalaGame); - } else { - const gameStep = lastHistoryItem.gameSteps[stepIndex]; - const index = mancalaGame.board.getPitIndexCircularly( - gameStep.index - ); - setAnimationPitIndex(index); - } - stepIndex++; - }, 250); - setIntervalId(intervalId); - } - } + setGame(mancalaGame); + pitAnimator.setUpdatedGame(mancalaGame); }); - context.rtmt.listenMessage("on_game_start", (message: Object) => { - const newGame: CommonMancalaGame = message as CommonMancalaGame; - setSearchingOpponent(false); - setGame(MancalaGame.createFromMancalaGame(newGame)); - }); - - context.rtmt.listenMessage("on_game_crashed", (message: any) => { + context.rtmt.listenMessage(channel_on_game_crashed, (message: any) => { const newCrashMessage = message as string; console.error("on_game_crash"); console.error(newCrashMessage); @@ -119,9 +99,19 @@ const Home: FunctionComponent<{ initial?: number }> = ({ initial = 0 }) => { }); }; + const updateBoardViewModel = (boardViewModel: BoardViewModel) => { + setBoardId(boardViewModel.id); + setBoardViewModel(boardViewModel); + }; + React.useEffect(() => { - listenMessages(); + const pitAnimator = new PitAnimator(context, updateBoardViewModel); + setPitAnimator(pitAnimator); + listenMessages(pitAnimator); connectToServer("connecting"); + return () => { + pitAnimator.dispose(); + }; }, []); const resetGameState = () => { @@ -140,7 +130,12 @@ const Home: FunctionComponent<{ initial?: number }> = ({ initial = 0 }) => { context.rtmt.sendMessage(channel_leave_game, {}); }; - const onHoleSelect = (index: number, hole: Pit) => { + const onHoleSelect = (index: number, pit: Pit) => { + //TODO : stoneCount comes from view model! + if (pit.stoneCount === 0) { + //TODO : warn user + return; + } const gameMove: GameMove = { index: index }; context.rtmt.sendMessage(channel_game_move, gameMove); }; @@ -240,12 +235,14 @@ const Home: FunctionComponent<{ initial?: number }> = ({ initial = 0 }) => { userKeyWhoLeave={userKeyWhoLeave} searchingOpponent={searchingOpponent} /> - {game && ( + {game && boardViewModel && ( )} 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(); + } +} diff --git a/src/components/BoardView.tsx b/src/components/BoardView.tsx index 9feab8c..a64cf3f 100644 --- a/src/components/BoardView.tsx +++ b/src/components/BoardView.tsx @@ -1,30 +1,9 @@ import { Bank, MancalaGame, Pit } from "mancala.js"; import * as React from "react"; import { FunctionComponent, useState } from "react"; - -type Theme = { - background: string; - boardColor: string; - boardColorWhenPlayerTurn: string; - storeColor: string; - storeColorWhenPlayerTurn: string; - holeColor: string; - ballColor: string; - ballLightColor: string; - holeAnimateColor: string; -}; - -const theme: Theme = { - background: "#EEEEEE", - boardColor: "#4D606E", - boardColorWhenPlayerTurn: "#84b8a6", - storeColor: "#3FBAC2", - storeColorWhenPlayerTurn: "#6cab94", - holeColor: "#D3D4D8", - ballColor: "#393E46", - ballLightColor: "#393E46", - holeAnimateColor: "#afb3a4", -}; +import { Context } from "../context"; +import BoardViewModel from "../viewmodel/BoardViewModel"; +import PitViewModel from "../viewmodel/PitViewModel"; const BallView: FunctionComponent<{ color: string }> = ({ color }) => { return ( @@ -48,41 +27,19 @@ function range(size: number) { return ans; } -const PitContainer: FunctionComponent<{ - pit: Pit; - isAnimating: boolean; - onClick: () => void; -}> = ({ pit, isAnimating, onClick }) => { - if (isAnimating) { - pit.stoneCount += 1; - } - return ( - <> - - - ); -}; - const HoleView: FunctionComponent<{ - hole: Pit; - color: string; - stoneColor: string; + pitViewModel: PitViewModel; onClick: () => void; -}> = ({ hole, color, stoneColor, onClick }) => { - const balls = [...range(hole.stoneCount)].map((i) => ( - +}> = ({ pitViewModel, onClick }) => { + const balls = [...range(pitViewModel.stoneCount)].map((i) => ( + )); return (
= ({ store, color, stoneColor, gridColumn, gridRow }) => { - const balls = [...range(store.stoneCount)].map((i) => ( - +}> = ({ pitViewModel, gridColumn, gridRow }) => { + const balls = [...range(pitViewModel.stoneCount)].map((i) => ( + )); return (
void; - animationPitIndex: number; -}> = ({ game, userKey, onHoleSelect, animationPitIndex }) => { +}> = ({ game, context, boardId, boardViewModel, userKey, onHoleSelect }) => { + const createPitView = (pitViewModel: PitViewModel, onClick: () => void) => { + return ; + }; const player1Pits = game?.board.player1Pits.map((pit) => { - const isAnimating = pit.index === animationPitIndex; - return ( - { - if (game.turnPlayerId === game.player1Id) - onHoleSelect(game.board.player1Pits.indexOf(pit), pit); - }} - /> - ); + const pitViewModel = boardViewModel.pits[pit.index]; + return createPitView(pitViewModel, () => { + if (game.turnPlayerId === game.player1Id) + onHoleSelect(game.board.player1Pits.indexOf(pit), pit); + }); }); - const player2Pits = game!!.board.player2Pits.map((pit) => { - const isAnimating = pit.index === animationPitIndex; - return ( - { - if (game.turnPlayerId === game.player2Id) - onHoleSelect(game.board.player2Pits.indexOf(pit), pit); - }} - /> - ); + const pitViewModel = boardViewModel.pits[pit.index]; + return createPitView(pitViewModel, () => { + if (game.turnPlayerId === game.player2Id) + onHoleSelect(game.board.player2Pits.indexOf(pit), pit); + }); }); - const isUserTurn = game.checkIsPlayerTurn(userKey); - - const animatingPlayer1Bank = - game.board.player1Bank.index === animationPitIndex; - const animatingPlayer2Bank = - game.board.player2Bank.index === animationPitIndex; - - const storeColorPlayer1 = animatingPlayer1Bank - ? theme.holeAnimateColor - : isUserTurn - ? theme.storeColor - : theme.storeColorWhenPlayerTurn; - - const storeStoneColorPlayer1 = animatingPlayer1Bank - ? theme.ballLightColor - : theme.ballColor; - - const storeColorPlayer2 = animatingPlayer2Bank - ? theme.holeAnimateColor - : isUserTurn - ? theme.storeColor - : theme.storeColorWhenPlayerTurn; - - const storeStoneColorPlayer2 = animatingPlayer2Bank - ? theme.ballLightColor - : theme.ballColor; + const theme = context.themeManager.theme; + const player1BankViewModel = + boardViewModel.pits[game.board.player1BankIndex()]; + const player2BankViewModel = + boardViewModel.pits[game.board.player2BankIndex()]; + const player1Bank = ( + + ); + const player2Bank = ( + + ); return (
@@ -225,17 +161,13 @@ const BoardView: FunctionComponent<{ ) : ( <> {player2Pits.reverse()} diff --git a/src/context.tsx b/src/context.tsx index 3a51286..140c840 100644 --- a/src/context.tsx +++ b/src/context.tsx @@ -1,23 +1,28 @@ -import { Texts, TrTr } from "./const/texts" -import { RTMT } from "./rtmt/rtmt" -import { RTMTWS } from "./rtmt/rtmt_websocket" -import { UserKeyStore, UserKeyStoreImpl } from "./store/key_store" +import { Texts, TrTr } from "./const/texts"; +import { RTMT } from "./rtmt/rtmt"; +import { RTMTWS } from "./rtmt/rtmt_websocket"; +import { UserKeyStore, UserKeyStoreImpl } from "./store/key_store"; +import defaultTheme from "./theme/DefaultTheme"; +import ThemeManager from "./theme/ThemeManager"; -type Context = { - rtmt : RTMT - userKeyStore : UserKeyStore - texts : Texts -} +export type Context = { + rtmt: RTMT; + userKeyStore: UserKeyStore; + texts: Texts; + themeManager: ThemeManager; +}; -export const initContext = ()=> { - const rtmt = new RTMTWS() - const userKeyStore = new UserKeyStoreImpl() - const texts = TrTr - return { - rtmt : rtmt, - userKeyStore : userKeyStore, - texts : texts, - } -} +export const initContext = () => { + const rtmt = new RTMTWS(); + const userKeyStore = new UserKeyStoreImpl(); + const texts = TrTr; + const themeManager = new ThemeManager(defaultTheme); + return { + rtmt: rtmt, + userKeyStore: userKeyStore, + texts: texts, + themeManager, + }; +}; -export const context : Context = initContext() +export const context: Context = initContext(); diff --git a/src/factory/BoardViewModelFactory.ts b/src/factory/BoardViewModelFactory.ts new file mode 100644 index 0000000..cd8e74f --- /dev/null +++ b/src/factory/BoardViewModelFactory.ts @@ -0,0 +1,11 @@ +import BoardViewModel from "../viewmodel/BoardViewModel"; +import PitViewModel from "../viewmodel/PitViewModel"; + +export default class BoardViewModelFactory { + public static create( + id: string, + pitViewModels: PitViewModel[] + ): BoardViewModel { + return new BoardViewModel(id, pitViewModels); + } +} diff --git a/src/factory/PitViewModelFactory.ts b/src/factory/PitViewModelFactory.ts new file mode 100644 index 0000000..00d33b2 --- /dev/null +++ b/src/factory/PitViewModelFactory.ts @@ -0,0 +1,13 @@ +import PitViewModel from "../viewmodel/PitViewModel"; + +export class PitViewModelFactory { + public static create(params: { + id: string; + stoneCount: number; + stoneColor: string; + pitColor: string; + }): PitViewModel { + const { id, stoneCount, stoneColor, pitColor } = params; + return new PitViewModel(id, stoneCount, stoneColor, pitColor); + } +} diff --git a/src/theme/DefaultTheme.ts b/src/theme/DefaultTheme.ts new file mode 100644 index 0000000..57e7649 --- /dev/null +++ b/src/theme/DefaultTheme.ts @@ -0,0 +1,18 @@ +import { Theme } from "./Theme"; + +const defaultTheme: Theme = { + background: "#EEEEEE", + boardColor: "#4D606E", + boardColorWhenPlayerTurn: "#84b8a6", + storeColor: "#3FBAC2", + storeColorWhenPlayerTurn: "#6cab94", + holeColor: "#D3D4D8", + ballColor: "#393E46", + ballLightColor: "#393E46", + pitGameMoveAnimateColor: "#c9b43c", + pitEmptyPitAnimateColor: "#5d7322", + pitLastStoneInBankPitAnimateColor: "#9463f7", + pitGetRivalStonePitAnimateColor: "#ff3d44", +}; + +export default defaultTheme; diff --git a/src/theme/Theme.ts b/src/theme/Theme.ts new file mode 100644 index 0000000..3434c11 --- /dev/null +++ b/src/theme/Theme.ts @@ -0,0 +1,14 @@ +export type Theme = { + background: string; + boardColor: string; + boardColorWhenPlayerTurn: string; + storeColor: string; + storeColorWhenPlayerTurn: string; + holeColor: string; + ballColor: string; + ballLightColor: string; + pitGameMoveAnimateColor: string; + pitEmptyPitAnimateColor: string; + pitLastStoneInBankPitAnimateColor: string; + pitGetRivalStonePitAnimateColor: string; +}; diff --git a/src/theme/ThemeManager.ts b/src/theme/ThemeManager.ts new file mode 100644 index 0000000..8d391aa --- /dev/null +++ b/src/theme/ThemeManager.ts @@ -0,0 +1,18 @@ +import { Theme } from "./Theme"; + +export default class ThemeManager { + _theme: Theme; + onThemeChange: (theme: Theme) => void; + constructor(theme: Theme) { + this._theme = theme; + } + + public get theme() { + return this._theme; + } + + public set theme(value) { + this._theme = value; + this.onThemeChange?.(value); + } +} diff --git a/src/viewmodel/BoardViewModel.ts b/src/viewmodel/BoardViewModel.ts new file mode 100644 index 0000000..02b59c7 --- /dev/null +++ b/src/viewmodel/BoardViewModel.ts @@ -0,0 +1,10 @@ +import PitViewModel from "./PitViewModel"; + +export default class BoardViewModel { + id: string; + pits: PitViewModel[]; + constructor(id: string, pits: PitViewModel[]) { + this.id = id; + this.pits = pits; + } +} diff --git a/src/viewmodel/PitViewModel.ts b/src/viewmodel/PitViewModel.ts new file mode 100644 index 0000000..090469e --- /dev/null +++ b/src/viewmodel/PitViewModel.ts @@ -0,0 +1,18 @@ +export default class PitViewModel { + id: string; + stoneCount: number; + stoneColor: string; + pitColor: string; + + constructor( + id: string, + stoneCount: number, + stoneColor: string, + pitColor: string + ) { + this.id = id; + this.stoneCount = stoneCount; + this.stoneColor = stoneColor; + this.pitColor = pitColor; + } +} diff --git a/yarn.lock b/yarn.lock index 411f8c5..ff9cc96 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1042,6 +1042,11 @@ resolved "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz" integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== +"@types/uuid@^8.3.4": + version "8.3.4" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" + integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw== + abab@^2.0.0: version "2.0.6" resolved "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz" @@ -5377,6 +5382,11 @@ uuid@^3.3.2: resolved "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + v8-compile-cache@^2.0.0: version "2.3.0" resolved "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz"