diff --git a/packages/client/src/modules/play/Components/Synthesis/SynthesisBudget/SynthesisBudget.tsx b/packages/client/src/modules/play/Components/Synthesis/SynthesisBudget/SynthesisBudget.tsx index 51fdd084..5c337185 100644 --- a/packages/client/src/modules/play/Components/Synthesis/SynthesisBudget/SynthesisBudget.tsx +++ b/packages/client/src/modules/play/Components/Synthesis/SynthesisBudget/SynthesisBudget.tsx @@ -3,13 +3,14 @@ import { formatBudget } from "../../../../../lib/formatter"; import { useTranslation } from "../../../../translations"; import { synthesisConstants } from "../../../playerActions/constants/synthesis"; import { Icon } from "../../../../common/components/Icon"; -import { usePlay, useTeamValues } from "../../../context/playContext"; +import { usePlay } from "../../../context/playContext"; import { ITeam } from "../../../../../utils/types"; import { getDaysToEnergyShiftTargetYear } from "../../../../../lib/time"; import { Typography } from "../../../../common/components/Typography"; import { Tag } from "../../../../common/components/Tag"; import { ENERGY_SHIFT_TARGET_YEAR } from "../../../../common/constants"; import { CardStyled } from "../Synthesis.common.styles"; +import { useTeamValuesForTeam } from "../../../context/hooks/shared"; export default SynthesisBudget; @@ -17,8 +18,7 @@ function SynthesisBudget({ team }: { team: ITeam | null }) { const { t } = useTranslation(["common", "countries"]); const { game } = usePlay(); const daysTo2050 = getDaysToEnergyShiftTargetYear(new Date(game.date)); - const { getTeamById } = useTeamValues(); - const teamValues = getTeamById(team?.id); + const { teamValues } = useTeamValuesForTeam({ teamId: team?.id }); const budget = teamValues?.budgetSpent || 0; diff --git a/packages/client/src/modules/play/Components/Synthesis/SynthesisCarbon/SynthesisCarbon.tsx b/packages/client/src/modules/play/Components/Synthesis/SynthesisCarbon/SynthesisCarbon.tsx index 8c52a53d..d5e38b54 100644 --- a/packages/client/src/modules/play/Components/Synthesis/SynthesisCarbon/SynthesisCarbon.tsx +++ b/packages/client/src/modules/play/Components/Synthesis/SynthesisCarbon/SynthesisCarbon.tsx @@ -6,12 +6,12 @@ import { import { useTranslation } from "../../../../translations"; import { synthesisConstants } from "../../../playerActions/constants/synthesis"; import { Icon } from "../../../../common/components/Icon"; -import { useTeamValues } from "../../../context/playContext"; import { ITeam } from "../../../../../utils/types"; import { Typography } from "../../../../common/components/Typography"; import { TagNumber } from "../../../../common/components/TagNumber"; import { Tag } from "../../../../common/components/Tag"; import { CardStyled } from "../Synthesis.common.styles"; +import { useTeamValuesForTeam } from "../../../context/hooks/shared"; const CARBON_FOOTPRINT_TONS_THRESHOLD = 2; @@ -19,8 +19,7 @@ export default SynthesisCarbon; function SynthesisCarbon({ team }: { team: ITeam | null }) { const { t } = useTranslation(); - const { getTeamById } = useTeamValues(); - const teamValues = getTeamById(team?.id); + const { teamValues } = useTeamValuesForTeam({ teamId: team?.id }); const teamCarbonFootprintInKgPerDay = teamValues?.carbonFootprint || 0; const carbonFootprintReduction = teamValues?.carbonFootprintReduction || 0; diff --git a/packages/client/src/modules/play/Components/Synthesis/SynthesisProduction/SynthesisProduction.tsx b/packages/client/src/modules/play/Components/Synthesis/SynthesisProduction/SynthesisProduction.tsx index 4490c393..77615a46 100644 --- a/packages/client/src/modules/play/Components/Synthesis/SynthesisProduction/SynthesisProduction.tsx +++ b/packages/client/src/modules/play/Components/Synthesis/SynthesisProduction/SynthesisProduction.tsx @@ -6,7 +6,7 @@ import { } from "../../../../../lib/formatter"; import { useTranslation } from "../../../../translations"; import { Icon } from "../../../../common/components/Icon"; -import { usePlay, useTeamValues } from "../../../context/playContext"; +import { usePlay } from "../../../context/playContext"; import { ITeam } from "../../../../../utils/types"; import { Typography } from "../../../../common/components/Typography"; import { TagNumber } from "../../../../common/components/TagNumber"; @@ -15,14 +15,14 @@ import { ProductionDatum } from "../../../../persona/production"; import { isDecarbonatedEnergyProduction } from "../../../utils/production"; import { TagEnergy } from "../../../../common/components/TagEnergy"; import { CardStyled } from "../Synthesis.common.styles"; +import { useTeamValuesForTeam } from "../../../context/hooks/shared"; export default SynthesisProduction; function SynthesisProduction({ team }: { team: ITeam | null }) { const { t } = useTranslation(); const { productionOfCountryToday } = usePlay(); - const { getTeamById } = useTeamValues(); - const teamValues = getTeamById(team?.id); + const { teamValues } = useTeamValuesForTeam({ teamId: team?.id }); const computeRenewableEnergyProduction = useCallback( (production: ProductionDatum[] = []) => diff --git a/packages/client/src/modules/play/GameConsole/StatsConsole.tsx b/packages/client/src/modules/play/GameConsole/StatsConsole.tsx index 4cec38ff..23965e1b 100644 --- a/packages/client/src/modules/play/GameConsole/StatsConsole.tsx +++ b/packages/client/src/modules/play/GameConsole/StatsConsole.tsx @@ -1,6 +1,6 @@ import { Box, Grid, useTheme } from "@mui/material"; import { PlayBox } from "../Components"; -import { TeamIdToValues, usePlay, useTeamValues } from "../context/playContext"; +import { TeamIdToValues, usePlay } from "../context/playContext"; import { ConsumptionStats, ProductionStats } from "./ProdStats"; import { Typography } from "../../common/components/Typography"; import { Icon } from "../../common/components/Icon"; @@ -13,6 +13,7 @@ import { import { useTranslation } from "../../translations/useTranslation"; import { I18nTranslateFunction } from "../../translations"; import { IEnrichedGame, ITeam } from "../../../utils/types"; +import { useTeamValues } from "../context/hooks/shared"; export { StatsConsole }; diff --git a/packages/client/src/modules/play/GameConsole/TeamConsoleContent.tsx b/packages/client/src/modules/play/GameConsole/TeamConsoleContent.tsx index fd3af5b5..9423394a 100644 --- a/packages/client/src/modules/play/GameConsole/TeamConsoleContent.tsx +++ b/packages/client/src/modules/play/GameConsole/TeamConsoleContent.tsx @@ -1,4 +1,5 @@ import { Box, Rating, useTheme } from "@mui/material"; +import { ReactNode, useMemo } from "react"; import { PlayerChart } from "./PlayerChart"; import { PlayBox } from "../Components"; import { Icon } from "../../common/components/Icon"; @@ -17,29 +18,142 @@ import { import { TeamActionsRecap } from "../Components/TeamActionsRecap"; import { getTeamActionsAtCurrentStep } from "../utils/teamActions"; import { sumAllValues } from "../../persona"; -import { useMemo } from "react"; import SynthesisRecapForTeacher from "../Components/Synthesis/SynthesisRecapForTeacher"; export { TeamConsoleContent }; function TeamConsoleContent({ team }: { team: ITeam }) { - const { game, players, productionActionById } = usePlay(); const currentStep = useCurrentStep(); + + const isProductionStep = currentStep?.type === "production"; + const isSynthesisStep = currentStep?.id === "final-situation"; + + if (isSynthesisStep) { + return ; + } else if (isProductionStep) { + return ; + } else { + return ; + } +} + +function TeamConsoleContentConsumption({ team }: { team: ITeam }) { + const { players } = usePlay(); const playersInTeam = useMemo( () => players.filter((p) => p.teamId === team.id), [players, team] ); - const isProductionStep = currentStep?.type === "production"; - const isSynthesisStep = currentStep?.id === "final-situation"; + return ( + + + + {playersInTeam.map((player) => ( + + ))} + + + + + + + + + } + /> + ); +} + +function TeamConsoleContentProduction({ team }: { team: ITeam }) { + const { game, players, productionActionById } = usePlay(); + const playersInTeam = useMemo( + () => players.filter((p) => p.teamId === team.id), + [players, team] + ); const teamActionsAtCurrentStep = getTeamActionsAtCurrentStep( game.step, team.actions, productionActionById ); - const PlayerComponent = getPlayerComponent(isProductionStep, isSynthesisStep); + return ( + + + + {playersInTeam.map((player) => ( + + ))} + + + + + + + + + + + + + + } + /> + ); +} + +function TeamConsoleContentSynthesis({ team }: { team: ITeam }) { + const { players } = usePlay(); + const playersInTeam = useMemo( + () => players.filter((p) => p.teamId === team.id), + [players, team] + ); + + return ( + + + + + + + + {playersInTeam.map((player) => ( + + ))} + + + + + + + + + } + /> + ); +} + +function TeamConsoleContentLayout({ + team, + content, +}: { + team: ITeam; + content: ReactNode; +}) { return ( - {isSynthesisStep && ( - - - - )} - - - - {playersInTeam.map((player) => ( - - ))} - - - {isProductionStep && ( - - - - - - )} - - - - + + {content} ); } -function getPlayerComponent( - isProductionStep: boolean, - isSynthesisStep: boolean -) { - if (isSynthesisStep) { - return PlayerSynthesis; - } else if (isProductionStep) { - return PlayerProduction; - } - return PlayerConsumption; -} - function PlayerSynthesis({ player }: { player: Player }) { const { latestPersona } = usePersonaByUserId(player.userId); diff --git a/packages/client/src/modules/play/PlayerPersona/PlayerHeader.tsx b/packages/client/src/modules/play/PlayerPersona/PlayerHeader.tsx index 770afd7d..ce2abc66 100644 --- a/packages/client/src/modules/play/PlayerPersona/PlayerHeader.tsx +++ b/packages/client/src/modules/play/PlayerPersona/PlayerHeader.tsx @@ -11,7 +11,7 @@ import { Link } from "react-router-dom"; import { useAuth } from "../../auth/authProvider"; import GameStepper from "../../common/components/Stepper"; import { PlayBox } from "../Components"; -import { useCurrentStep, usePlay, useTeamValues } from "../context/playContext"; +import { useCurrentStep, usePlay } from "../context/playContext"; import { Icon } from "../../common/components/Icon"; import { formatPoints, @@ -26,6 +26,7 @@ import { useTranslation } from "../../translations/useTranslation"; import { useCurrentPlayer, usePersona } from "../context/hooks/player"; import { Button } from "../../common/components/Button"; import { useMemo } from "react"; +import { useTeamValues } from "../context/hooks/shared"; export { PlayerHeader }; diff --git a/packages/client/src/modules/play/context/hooks/shared/index.ts b/packages/client/src/modules/play/context/hooks/shared/index.ts new file mode 100644 index 00000000..268c39d8 --- /dev/null +++ b/packages/client/src/modules/play/context/hooks/shared/index.ts @@ -0,0 +1,2 @@ +export { useTeamValues } from "./useTeamValues"; +export { useTeamValuesForTeam } from "./useTeamValuesForTeam"; diff --git a/packages/client/src/modules/play/context/hooks/shared/useTeamValues.ts b/packages/client/src/modules/play/context/hooks/shared/useTeamValues.ts new file mode 100644 index 00000000..41da3b6a --- /dev/null +++ b/packages/client/src/modules/play/context/hooks/shared/useTeamValues.ts @@ -0,0 +1,39 @@ +import { useMemo } from "react"; +import { TeamValues, usePersonaByUserId, usePlay } from "../../playContext"; +import { buildTeamValues } from "./utils"; + +export { useTeamValues }; + +function useTeamValues(): { + teamValues: TeamValues[]; + getTeamById: (id: number | undefined) => TeamValues | undefined; +} { + const { game, players, teams } = usePlay(); + + const userIds: number[] = useMemo( + () => players.map((p) => p.userId), + [players] + ); + const personaByUserId = usePersonaByUserId(userIds); + + const teamValues = useMemo(() => { + return teams.map((team) => { + return buildTeamValues({ + game, + personaByUserId, + players, + team, + }); + }); + // TODO: check `personaByUserId` in deps doesn't trigger infinite renders. + }, [game, personaByUserId, players, teams]); + + const getTeamById = (id: number | undefined) => { + return teamValues.find((t) => t.id === id); + }; + + return { + teamValues, + getTeamById, + }; +} diff --git a/packages/client/src/modules/play/context/hooks/shared/useTeamValuesForTeam.ts b/packages/client/src/modules/play/context/hooks/shared/useTeamValuesForTeam.ts new file mode 100644 index 00000000..20d752ff --- /dev/null +++ b/packages/client/src/modules/play/context/hooks/shared/useTeamValuesForTeam.ts @@ -0,0 +1,26 @@ +import { useMemo } from "react"; +import { TeamValues, usePersonaByUserId, usePlay } from "../../playContext"; +import { buildTeamValues } from "./utils"; + +export { useTeamValuesForTeam }; + +function useTeamValuesForTeam({ teamId }: { teamId?: number }): { + teamValues: TeamValues; +} { + const { game, players, teams } = usePlay(); + + const userIds: number[] = useMemo( + () => players.filter((p) => p.teamId === teamId).map((p) => p.userId), + [players, teamId] + ); + const personaByUserId = usePersonaByUserId(userIds); + + const teamValues = useMemo(() => { + const team = teams.find((t) => t.id === teamId); + return buildTeamValues({ game, personaByUserId, players, team }); + }, [game, personaByUserId, players, teamId, teams]); + + return { + teamValues, + }; +} diff --git a/packages/client/src/modules/play/context/hooks/shared/utils.ts b/packages/client/src/modules/play/context/hooks/shared/utils.ts new file mode 100644 index 00000000..13a6570f --- /dev/null +++ b/packages/client/src/modules/play/context/hooks/shared/utils.ts @@ -0,0 +1,112 @@ +import range from "lodash/range"; +import { IGame, ITeam, Player } from "../../../../../utils/types"; +import { GameStepType, isStepOfType } from "../../../constants"; +import { TeamValues, usePersonaByUserId } from "../../playContext"; +import { mean } from "../../../../../lib/math"; +import { sumAllValues } from "../../../../persona"; + +export { buildStepToData, buildTeamValues }; + +type PersonaByUserId = ReturnType; + +function buildStepToData( + dataType: GameStepType, + game: IGame, + players: Player[], + personaByUserId: PersonaByUserId +) { + return Object.fromEntries( + range(0, game.lastFinishedStep + 1) + .filter((step) => isStepOfType(step, dataType)) + .map((step: number) => [ + step, + buildStepData(dataType, step, players, personaByUserId), + ]) + ); +} + +function buildStepData( + dataType: GameStepType, + step: number, + players: Player[], + personaByUserId: PersonaByUserId +) { + return mean( + players + .map((p) => personaByUserId[p.userId].getPersonaAtStep(step)[dataType]) + .map((data) => + parseInt(sumAllValues(data as { type: string; value: number }[])) + ) + ); +} + +function buildTeamValues({ + game, + personaByUserId, + players, + team, +}: { + game: IGame; + personaByUserId: PersonaByUserId; + players: Player[]; + team?: ITeam; +}): TeamValues { + const playersInTeam = players.filter((p) => p.teamId === team?.id); + const playerRepresentingTeam = playersInTeam[0] || null; + const personaRepresentingTeam = + personaByUserId[playerRepresentingTeam?.userId || -1] || null; + const currentPersonaRepresentingTeam = + personaRepresentingTeam?.getPersonaAtStep?.(game.step) || null; + + return { + id: team?.id || 0, + playerCount: playersInTeam.length, + points: mean( + playersInTeam.map( + ({ userId }) => personaByUserId[userId].currentPersona.points + ) + ), + budget: mean( + playersInTeam.map( + ({ userId }) => personaByUserId[userId].currentPersona.budget + ) + ), + budgetSpent: mean( + playersInTeam + .map(({ userId }) => personaByUserId[userId]) + .map( + (persona) => + persona.getPersonaAtStep(0).budget - persona.currentPersona.budget + ) + ), + carbonFootprint: mean( + playersInTeam.map( + ({ userId }) => personaByUserId[userId].currentPersona.carbonFootprint + ) + ), + carbonFootprintReduction: mean( + playersInTeam + .map(({ userId }) => personaByUserId[userId]) + .map( + (persona) => + (1 - + persona.currentPersona.carbonFootprint / + persona.getPersonaAtStep(0).carbonFootprint) * + 100 + ) + ), + stepToConsumption: buildStepToData( + "consumption", + game, + playersInTeam, + personaByUserId + ), + stepToProduction: buildStepToData( + "production", + game, + playersInTeam, + personaByUserId + ), + productionCurrent: currentPersonaRepresentingTeam?.production || [], + }; +} diff --git a/packages/client/src/modules/play/context/playContext.tsx b/packages/client/src/modules/play/context/playContext.tsx index 5633b894..60933a11 100644 --- a/packages/client/src/modules/play/context/playContext.tsx +++ b/packages/client/src/modules/play/context/playContext.tsx @@ -12,11 +12,8 @@ import { ProductionAction, } from "../../../utils/types"; import { useAuth } from "../../auth/authProvider"; -import { GameStep, GameStepType, isStepOfType, STEPS } from "../constants"; +import { GameStep, STEPS } from "../constants"; import { buildPersona } from "../utils/persona"; -import { mean } from "../../../lib/math"; -import { range } from "lodash"; -import { sumAllValues } from "../../persona"; import { buildInitialPersona } from "../../persona/persona"; import { WEB_SOCKET_URL } from "../../common/constants"; import { usePlayStore } from "./usePlayStore"; @@ -26,12 +23,11 @@ import { ProductionDatum } from "../../persona/production"; export { RootPlayProvider, useCurrentStep, - useTeamValues, useLoadedPlay as usePlay, usePersonaByUserId, }; -export type { TeamIdToValues }; +export type { TeamIdToValues, TeamValues }; interface TeamIdToValues { [k: string]: TeamValues; @@ -239,125 +235,6 @@ function useLoadedPlay(): IPlayContext { return playValue; } -function useTeamValues(): { - teamValues: TeamValues[]; - getTeamById: (id: number | undefined) => TeamValues | undefined; -} { - const { game, players, teams } = useLoadedPlay(); - - const userIds: number[] = useMemo( - () => players.map((p) => p.userId), - [players] - ); - const personaByUserId = usePersonaByUserId(userIds); - - const teamValues = useMemo(() => { - return teams.map((team) => { - const playersInTeam = players.filter((p) => p.teamId === team.id); - const playerRepresentingTeam = playersInTeam[0] || null; - const personaRepresentingTeam = - personaByUserId[playerRepresentingTeam?.userId || -1] || null; - const currentPersonaRepresentingTeam = - personaRepresentingTeam?.getPersonaAtStep?.(game.step) || null; - - return { - id: team.id, - playerCount: playersInTeam.length, - points: mean( - playersInTeam.map( - ({ userId }) => personaByUserId[userId].currentPersona.points - ) - ), - budget: mean( - playersInTeam.map( - ({ userId }) => personaByUserId[userId].currentPersona.budget - ) - ), - budgetSpent: mean( - playersInTeam - .map(({ userId }) => personaByUserId[userId]) - .map( - (persona) => - persona.getPersonaAtStep(0).budget - - persona.currentPersona.budget - ) - ), - carbonFootprint: mean( - playersInTeam.map( - ({ userId }) => - personaByUserId[userId].currentPersona.carbonFootprint - ) - ), - carbonFootprintReduction: mean( - playersInTeam - .map(({ userId }) => personaByUserId[userId]) - .map( - (persona) => - (1 - - persona.currentPersona.carbonFootprint / - persona.getPersonaAtStep(0).carbonFootprint) * - 100 - ) - ), - stepToConsumption: buildStepToData( - "consumption", - game, - playersInTeam, - personaByUserId - ), - stepToProduction: buildStepToData( - "production", - game, - playersInTeam, - personaByUserId - ), - productionCurrent: currentPersonaRepresentingTeam?.production || [], - }; - }); - // TODO: check `personaByUserId` in deps doesn't trigger infinite renders. - }, [game, personaByUserId, players, teams]); - - const getTeamById = (id: number | undefined) => { - return teamValues.find((t) => t.id === id); - }; - - return { - teamValues, - getTeamById, - }; -} - -function buildStepToData( - dataType: GameStepType, - game: IGame, - players: Player[], - personaByUserId: ReturnType -) { - return Object.fromEntries( - range(0, game.lastFinishedStep + 1) - .filter((step) => isStepOfType(step, dataType)) - .map((step: number) => [ - step, - buildStepData(dataType, step, players, personaByUserId), - ]) - ); -} - -function buildStepData( - dataType: GameStepType, - step: number, - players: Player[], - personaByUserId: ReturnType -) { - return mean( - players - .map((p) => personaByUserId[p.userId].getPersonaAtStep(step)[dataType]) - .map((data) => - parseInt(sumAllValues(data as { type: string; value: number }[])) - ) - ); -} - function useCurrentStep(): GameStep | null { const playValue = usePlay(); if (!playValue) {