Compare commits
	
		
			4 Commits
		
	
	
		
			291e7ff8a7
			...
			04f30e3ac2
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 04f30e3ac2 | |||
| 9d0b5cb254 | |||
| db8a8b4b7b | |||
| ac20baad23 | 
| @@ -1,37 +1,85 @@ | ||||
| import ChallengeCard from '@/components/ChallengeCard' | ||||
| import PenaltyBanner from '@/components/PenalyBanner' | ||||
| import { useDrawRandomChallengeMutation } from '@/hooks/mutations/useChallengeMutation' | ||||
| import { useAuth } from '@/hooks/useAuth' | ||||
| import { useChallengeActions } from '@/hooks/useChallengeActions' | ||||
| import { useChallenges } from '@/hooks/useChallenges' | ||||
| import { useGame } from '@/hooks/useGame' | ||||
| import { FontAwesome6 } from '@expo/vector-icons' | ||||
| import { useEffect, useMemo, useState } from 'react' | ||||
| import { View } from 'react-native' | ||||
| import { Appbar, Button, Surface, Text } from 'react-native-paper' | ||||
| import { ActivityIndicator, Appbar, Banner, FAB, MD3Colors, Surface, Text, TouchableRipple } from 'react-native-paper' | ||||
|  | ||||
| function ChallengeScreenHeader() { | ||||
|   return <> | ||||
|     <Appbar.Header> | ||||
|       <Appbar.Content title={"Défis"} /> | ||||
|       <Appbar.Action icon='format-list-bulleted' /> | ||||
|     </Appbar.Header> | ||||
|     <PenaltyBanner /> | ||||
|   </> | ||||
| } | ||||
|  | ||||
| function ChallengeScreenBody() { | ||||
|   const auth = useAuth() | ||||
|   const game = useGame() | ||||
|   const challengeActions = useChallengeActions() | ||||
|   const challenges = useChallenges() | ||||
|   const currentChallengeAction = useMemo(() => { | ||||
|     if (!game.activeChallengeId) | ||||
|       return null | ||||
|     return challengeActions.challengeActions.find((action) => action.id === game.activeChallengeId) | ||||
|   }, [game, challengeActions]) | ||||
|   const currentChallenge = useMemo(() => { | ||||
|     if (!currentChallengeAction) | ||||
|       return null | ||||
|     return challenges.challenges.find((challenge) => challenge.id === currentChallengeAction.challengeId) | ||||
|   }, [currentChallengeAction, challenges]) | ||||
|   const [loading, setLoading] = useState(false) | ||||
|   const drawRandomChallengeMutation = useDrawRandomChallengeMutation({ | ||||
|     auth, | ||||
|     onPostSuccess: () => setLoading(true), | ||||
|   }) | ||||
|   useEffect(() => { | ||||
|     if (challengeActions) | ||||
|       setLoading(false) | ||||
|   }, [challengeActions]) | ||||
|  | ||||
|   return <> | ||||
|     {currentChallenge && <ChallengeCard challenge={currentChallenge} />} | ||||
|     {!currentChallenge && game.currentRunner && <> | ||||
|       <Banner | ||||
|           visible={!currentChallenge && game.currentRunner && !loading} | ||||
|           icon='cancel' | ||||
|           style={{ backgroundColor: MD3Colors.error40 }}> | ||||
|         <Text variant='titleMedium' style={{ textAlign: 'center' }}>Aucun défi en cours.</Text> | ||||
|       </Banner> | ||||
|       <View style={{ flexGrow: 1, justifyContent: 'center', alignItems: 'center' }}> | ||||
|         <FAB | ||||
|             label='Tirer un défi' | ||||
|             icon='cards' | ||||
|             disabled={drawRandomChallengeMutation.isPending} | ||||
|             visible={!currentChallenge && game.currentRunner && !loading} | ||||
|             onPress={() => drawRandomChallengeMutation.mutate()} | ||||
|             variant='tertiary' | ||||
|             customSize={64} /> | ||||
|         {loading && <ActivityIndicator size={'large'} />} | ||||
|       </View> | ||||
|     </>} | ||||
|     <Banner | ||||
|         visible={game.gameStarted && !game.currentRunner} | ||||
|         icon={({ size }) => <FontAwesome6 name='cat' size={size} color={'pink'} />} | ||||
|         style={{ backgroundColor: MD3Colors.secondary30 }}> | ||||
|       Vous êtes poursuiveuse, et n'avez donc pas de défi à accomplir. | ||||
|     </Banner> | ||||
|   </> | ||||
| } | ||||
|  | ||||
| export default function ChallengesScreen() { | ||||
|   return ( | ||||
|     <Surface style={{ flex: 1 }}> | ||||
|       <Appbar.Header> | ||||
|         <Appbar.Content title={"Défis"} /> | ||||
|         <Appbar.Action icon='format-list-bulleted' /> | ||||
|       </Appbar.Header> | ||||
|       <PenaltyBanner /> | ||||
|       <Surface elevation={2} style={{ flex: 1, margin: 20, borderRadius: 20 }}> | ||||
|         <View style={{ padding: 10 }}> | ||||
|           <Text variant='headlineMedium' style={{ textAlign: 'center' }}>Titre</Text> | ||||
|         </View> | ||||
|         <View style={{ flexGrow: 1 }}> | ||||
|           <Surface elevation={5} mode='flat' style={{ flexGrow: 1, padding: 15 }}> | ||||
|             <Text variant='bodyLarge' style={{ flexGrow: 1 }}>Description</Text> | ||||
|             <Text variant='titleMedium'> | ||||
|               Récompense : 500 <FontAwesome6 name='coins' /> | ||||
|             </Text> | ||||
|           </Surface> | ||||
|         </View> | ||||
|         <View style={{ flexWrap: 'wrap', flexDirection: 'row', justifyContent: 'space-around', padding: 15 }}> | ||||
|           <Button mode='outlined' icon='cancel'> | ||||
|             Passer | ||||
|           </Button> | ||||
|           <Button mode='contained' icon='check'> | ||||
|             Terminer | ||||
|           </Button> | ||||
|         </View> | ||||
|       </Surface> | ||||
|       <ChallengeScreenHeader /> | ||||
|       <ChallengeScreenBody /> | ||||
|     </Surface> | ||||
|   ) | ||||
| } | ||||
|   | ||||
							
								
								
									
										30
									
								
								client/components/ChallengeCard.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								client/components/ChallengeCard.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| import { Challenge } from "@/utils/features/challenges/challengesSlice" | ||||
| import { FontAwesome6 } from "@expo/vector-icons" | ||||
| import { View } from "react-native" | ||||
| import { Button, Surface, Text } from "react-native-paper" | ||||
|  | ||||
| export default function ChallengeCard({ challenge }: { challenge: Challenge }) { | ||||
|   return ( | ||||
|     <Surface elevation={2} style={{ flex: 1, margin: 20, borderRadius: 20 }}> | ||||
|     <View style={{ padding: 10 }}> | ||||
|       <Text variant='headlineMedium' style={{ textAlign: 'center' }}>{challenge.title}</Text> | ||||
|     </View> | ||||
|     <View style={{ flexGrow: 1 }}> | ||||
|       <Surface elevation={5} mode='flat' style={{ flexGrow: 1, padding: 15 }}> | ||||
|         <Text variant='bodyLarge' style={{ flexGrow: 1 }}>{challenge.description}</Text> | ||||
|         <Text variant='titleMedium'> | ||||
|           Récompense : {challenge.reward} <FontAwesome6 name='coins' /> | ||||
|         </Text> | ||||
|       </Surface> | ||||
|     </View> | ||||
|     <View style={{ flexWrap: 'wrap', flexDirection: 'row', justifyContent: 'space-around', padding: 15 }}> | ||||
|       <Button mode='outlined' icon='cancel'> | ||||
|         Passer | ||||
|       </Button> | ||||
|       <Button mode='contained' icon='check'> | ||||
|         Terminer | ||||
|       </Button> | ||||
|     </View> | ||||
|   </Surface> | ||||
|   ) | ||||
| } | ||||
| @@ -1,7 +1,11 @@ | ||||
| import { useAuth } from '@/hooks/useAuth' | ||||
| import { useGame, useUpdateGameState, useUpdateMoney } from '@/hooks/useGame' | ||||
| import { useDownloadChallengeActions } from '@/hooks/useChallengeActions' | ||||
| import { useDownloadChallenges } from '@/hooks/useChallenges' | ||||
| import { useGame, useUpdateActiveChallengeId, useUpdateGameState, useUpdateMoney } from '@/hooks/useGame' | ||||
| import { useDownloadTrains } from '@/hooks/useTrain' | ||||
| import { isAuthValid } from '@/utils/features/auth/authSlice' | ||||
| import { ChallengeActionPayload } from '@/utils/features/challengeActions/challengeActionsSlice' | ||||
| import { Challenge } from '@/utils/features/challenges/challengesSlice' | ||||
| import { useQuery } from '@tanstack/react-query' | ||||
| import { ReactNode, useEffect } from 'react' | ||||
|  | ||||
| @@ -10,7 +14,10 @@ export default function GameProvider({ children }: { children: ReactNode }) { | ||||
|   const game = useGame() | ||||
|   const updateGameState = useUpdateGameState() | ||||
|   const updateMoney = useUpdateMoney() | ||||
|   const updateActiveChallengeId = useUpdateActiveChallengeId() | ||||
|   const downloadTrains = useDownloadTrains() | ||||
|   const downloadChallenges = useDownloadChallenges() | ||||
|   const downloadChallengeActions = useDownloadChallengeActions() | ||||
|  | ||||
|   const gameQuery = useQuery({ | ||||
|     queryKey: ['get-game', auth.token], | ||||
| @@ -34,8 +41,10 @@ export default function GameProvider({ children }: { children: ReactNode }) { | ||||
|     refetchInterval: 5000, | ||||
|   }) | ||||
|   useEffect(() => { | ||||
|     if (playerQuery.isSuccess && playerQuery.data) | ||||
|     if (playerQuery.isSuccess && playerQuery.data) { | ||||
|       updateMoney(playerQuery.data.money) | ||||
|       updateActiveChallengeId(playerQuery.data.activeChallengeId) | ||||
|     } | ||||
|   }, [playerQuery.status, playerQuery.dataUpdatedAt]) | ||||
|  | ||||
|   const trainsQuery = useQuery({ | ||||
| @@ -47,10 +56,27 @@ export default function GameProvider({ children }: { children: ReactNode }) { | ||||
|     refetchInterval: 5000, | ||||
|   }) | ||||
|   useEffect(() => { | ||||
|     if (trainsQuery.isSuccess && trainsQuery.data && trainsQuery) | ||||
|     if (trainsQuery.isSuccess && trainsQuery.data) | ||||
|       downloadTrains(trainsQuery.data) | ||||
|   }, [trainsQuery.status, trainsQuery.dataUpdatedAt]) | ||||
|  | ||||
|   const challengesQuery = useQuery({ | ||||
|     queryKey: ['get-challenges', game.playerId, auth.token], | ||||
|     queryFn: () => fetch(`${process.env.EXPO_PUBLIC_TRAINTRAPE_MOI_SERVER}/challenges/?size=10000`, { | ||||
|       headers: { "Authorization": `Bearer ${auth.token}` }} | ||||
|     ).then(resp => resp.json()), | ||||
|     enabled: isAuthValid(auth) && !!game.playerId, | ||||
|     refetchInterval: 5000, | ||||
|   }) | ||||
|   useEffect(() => { | ||||
|     if (challengesQuery.isSuccess && challengesQuery.data) { | ||||
|       downloadChallenges(challengesQuery.data) | ||||
|       const dataWithPlayerActions = challengesQuery.data.data.filter( | ||||
|         (challenge: (Challenge & {action: ChallengeActionPayload | null})) => challenge.action !== null && challenge.action.playerId === game.playerId) | ||||
|       downloadChallengeActions({ data: dataWithPlayerActions }) | ||||
|     } | ||||
|   }, [challengesQuery.status, challengesQuery.dataUpdatedAt]) | ||||
|  | ||||
|   return <> | ||||
|     {children} | ||||
|   </> | ||||
|   | ||||
							
								
								
									
										46
									
								
								client/hooks/mutations/useChallengeMutation.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								client/hooks/mutations/useChallengeMutation.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| import { AuthState } from "@/utils/features/auth/authSlice" | ||||
| import { useMutation } from "@tanstack/react-query" | ||||
|  | ||||
| type ErrorResponse = { | ||||
|   error: string | ||||
|   message: string | ||||
|   statusCode: number | ||||
| } | ||||
|  | ||||
| type onPostSuccessFunc = () => void | ||||
| type ErrorFuncProps = { response?: ErrorResponse, error?: Error } | ||||
| type onErrorFunc = (props: ErrorFuncProps) => void | ||||
|  | ||||
| type ChallengeActionProps = { | ||||
|   auth: AuthState | ||||
|   onPostSuccess?: onPostSuccessFunc | ||||
|   onError?: onErrorFunc | ||||
| } | ||||
|  | ||||
| export const useDrawRandomChallengeMutation = ({ auth, onPostSuccess, onError }: ChallengeActionProps) => { | ||||
|   return useMutation({ | ||||
|     mutationFn: async () => { | ||||
|       return fetch(`${process.env.EXPO_PUBLIC_TRAINTRAPE_MOI_SERVER}/challenges/draw-random/`, { | ||||
|         method: "POST", | ||||
|         headers: { | ||||
|           "Authorization": `Bearer ${auth.token}`, | ||||
|           "Content-Type": "application/json", | ||||
|         }, | ||||
|       }).then(resp => resp.json()) | ||||
|     }, | ||||
|     onSuccess: async (data) => { | ||||
|       if (data.statusCode) { | ||||
|         if (onError) | ||||
|           onError({ response: data }) | ||||
|         return | ||||
|       } | ||||
|       console.log(data) | ||||
|       if (onPostSuccess) | ||||
|         onPostSuccess() | ||||
|     }, | ||||
|     onError: async (error: Error) => { | ||||
|       if (onError) | ||||
|         onError({ error: error }) | ||||
|     } | ||||
|   }) | ||||
| } | ||||
| @@ -1,3 +1,8 @@ | ||||
| import { useAppSelector } from "./useStore" | ||||
| import { ChallengeActionsPayload, downloadChallengeActions } from "@/utils/features/challengeActions/challengeActionsSlice" | ||||
| import { useAppDispatch, useAppSelector } from "./useStore" | ||||
|  | ||||
| export const useChallengeActions = () => useAppSelector((state) => state.challengeActions) | ||||
| export const useDownloadChallengeActions = () => { | ||||
|   const dispath = useAppDispatch() | ||||
|   return (challengesData: ChallengeActionsPayload) => dispath(downloadChallengeActions(challengesData)) | ||||
| } | ||||
|   | ||||
| @@ -1,3 +1,8 @@ | ||||
| import { useAppSelector } from "./useStore" | ||||
| import { ChallengesPayload, downloadChallenges } from "@/utils/features/challenges/challengesSlice" | ||||
| import { useAppDispatch, useAppSelector } from "./useStore" | ||||
|  | ||||
| export const useTrain = () => useAppSelector((state) => state.challenges) | ||||
| export const useChallenges = () => useAppSelector((state) => state.challenges) | ||||
| export const useDownloadChallenges = () => { | ||||
|   const dispath = useAppDispatch() | ||||
|   return (challengesData: ChallengesPayload) => dispath(downloadChallenges(challengesData)) | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import { useAppDispatch, useAppSelector } from "./useStore" | ||||
| import { GamePayload, setPlayerId, updateGameState, updateMoney } from "@/utils/features/game/gameSlice" | ||||
| import { GamePayload, setPlayerId, updateActiveChallengeId, updateGameState, updateMoney } from "@/utils/features/game/gameSlice" | ||||
|  | ||||
| export const useGame = () => useAppSelector((state) => state.game) | ||||
| export const useSetPlayerId = () => { | ||||
| @@ -10,6 +10,10 @@ export const useUpdateMoney = () => { | ||||
|     const dispatch = useAppDispatch() | ||||
|     return (money: number) => dispatch(updateMoney(money)) | ||||
| } | ||||
| export const useUpdateActiveChallengeId = () => { | ||||
|     const dispatch = useAppDispatch() | ||||
|     return (challengeActionId: number) => dispatch(updateActiveChallengeId(challengeActionId)) | ||||
| } | ||||
| export const useUpdateGameState = () => { | ||||
|     const dispatch = useAppDispatch() | ||||
|     return (game: GamePayload) => dispatch(updateGameState(game)) | ||||
|   | ||||
| @@ -19,6 +19,7 @@ | ||||
|     "@expo/vector-icons": "^14.0.2", | ||||
|     "@maplibre/maplibre-react-native": "^10.0.0-alpha.28", | ||||
|     "@pchmn/expo-material3-theme": "github:pchmn/expo-material3-theme", | ||||
|     "@react-native-async-storage/async-storage": "1.23.1", | ||||
|     "@react-navigation/bottom-tabs": "^7.0.0", | ||||
|     "@react-navigation/native": "^7.0.0", | ||||
|     "@reduxjs/toolkit": "^2.4.0", | ||||
| @@ -58,8 +59,7 @@ | ||||
|     "react-native-screens": "~4.1.0", | ||||
|     "react-native-web": "~0.19.13", | ||||
|     "react-native-webview": "13.12.2", | ||||
|     "react-redux": "^9.1.2", | ||||
|     "@react-native-async-storage/async-storage": "1.23.1" | ||||
|     "react-redux": "^9.1.2" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@babel/core": "^7.25.2", | ||||
|   | ||||
| @@ -1,11 +1,9 @@ | ||||
| import { createSlice } from '@reduxjs/toolkit' | ||||
| import { createSlice, PayloadAction } from '@reduxjs/toolkit' | ||||
| import { Challenge } from '../challenges/challengesSlice' | ||||
|  | ||||
| export interface ChallengeAction { | ||||
|   id: number | ||||
|   challengeId: number | ||||
|   title: string, | ||||
|   description: string, | ||||
|   reward: number, | ||||
|   success: boolean, | ||||
|   start: number,  // date | ||||
|   end: number | null,  // date | ||||
| @@ -21,13 +19,45 @@ const initialState: ActionsState = { | ||||
|   challengeActions: [] | ||||
| } | ||||
|  | ||||
| export interface ChallengeActionPayload { | ||||
|   id: number | ||||
|   playerId: number | ||||
|   challengeId: number | ||||
|   success: boolean, | ||||
|   start: string, | ||||
|   end: string | null, | ||||
|   penaltyStart: string | null, | ||||
|   penaltyEnd: string | null, | ||||
| } | ||||
|  | ||||
| export interface ChallengeActionsPayload { | ||||
|   data: (Challenge & { action: ChallengeActionPayload })[] | ||||
| } | ||||
|  | ||||
| export const challengeActionsSlice = createSlice({ | ||||
|   name: 'challengeActions', | ||||
|   initialState: initialState, | ||||
|   reducers: { | ||||
|  | ||||
|     downloadChallengeActions(state, action: PayloadAction<ChallengeActionsPayload>) { | ||||
|       if (state.challengeActions) | ||||
|         state.challengeActions = state.challengeActions.filter(challengeAction => action.payload.data.filter(dlChallenge => dlChallenge.action.id === challengeAction.id) === null) | ||||
|       for (const dlChallenge of action.payload.data) { | ||||
|         state.challengeActions.push({ | ||||
|           id: dlChallenge.action.id, | ||||
|           challengeId: dlChallenge.id, | ||||
|           success: dlChallenge.action.success, | ||||
|           start: new Date(dlChallenge.action.start).getTime(), | ||||
|           end: dlChallenge.action.end ? new Date(dlChallenge.action.end).getTime() : null, | ||||
|           penaltyStart: dlChallenge.action.penaltyStart ? new Date(dlChallenge.action.penaltyStart).getTime() : null, | ||||
|           penaltyEnd: dlChallenge.action.penaltyEnd ? new Date(dlChallenge.action.penaltyEnd).getTime() : null, | ||||
|         }) | ||||
|       } | ||||
|       state.challengeActions.sort((c1, c2) => c2.id - c1.id) | ||||
|     }, | ||||
|   }, | ||||
| }) | ||||
|  | ||||
| export const { } = challengeActionsSlice.actions | ||||
| export const { downloadChallengeActions } = challengeActionsSlice.actions | ||||
|  | ||||
| export default challengeActionsSlice.reducer | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| import { createSlice } from '@reduxjs/toolkit' | ||||
| import { createSlice, PayloadAction } from '@reduxjs/toolkit' | ||||
| import { PaginationMeta } from '../common' | ||||
| import { ChallengeAction } from '../challengeActions/challengeActionsSlice' | ||||
|  | ||||
| export interface Challenge { | ||||
|   id: number | ||||
| @@ -15,13 +17,31 @@ const initialState: ChallengesState = { | ||||
|   challenges: [] | ||||
| } | ||||
|  | ||||
| export interface ChallengesPayload { | ||||
|   data: (Challenge & { action: ChallengeAction | null })[] | ||||
|   meta: PaginationMeta | ||||
| } | ||||
|  | ||||
| export const challengesSlice = createSlice({ | ||||
|   name: 'challenges', | ||||
|   initialState: initialState, | ||||
|   reducers: { | ||||
|     downloadChallenges(state, action: PayloadAction<ChallengesPayload>) { | ||||
|       if (state.challenges) | ||||
|         state.challenges = state.challenges.filter(challenge => action.payload.data.filter(dlChallenge => dlChallenge.id === challenge.id) === null) | ||||
|       for (const dlChallenge of action.payload.data) { | ||||
|         state.challenges.push({ | ||||
|           id: dlChallenge.id, | ||||
|           title: dlChallenge.title, | ||||
|           description: dlChallenge.description, | ||||
|           reward: dlChallenge.reward, | ||||
|         }) | ||||
|       } | ||||
|       state.challenges.sort((c1, c2) => c2.id - c1.id) | ||||
|     }, | ||||
|   }, | ||||
| }) | ||||
|  | ||||
| export const { } = challengesSlice.actions | ||||
| export const { downloadChallenges } = challengesSlice.actions | ||||
|  | ||||
| export default challengesSlice.reducer | ||||
|   | ||||
							
								
								
									
										8
									
								
								client/utils/features/common.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								client/utils/features/common.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| export interface PaginationMeta { | ||||
|   currentPage: number | ||||
|   lastPage: number | ||||
|   nextPage: number | ||||
|   prevPage: number | ||||
|   total: number | ||||
|   totalPerPage: number | ||||
| } | ||||
| @@ -1,4 +1,5 @@ | ||||
| import { createSlice, PayloadAction } from '@reduxjs/toolkit' | ||||
| import { ChallengeAction } from '../challengeActions/challengeActionsSlice' | ||||
|  | ||||
| export interface RunPayload { | ||||
|   id: number | ||||
| @@ -20,9 +21,10 @@ export interface GameState { | ||||
|   gameStarted: boolean | ||||
|   money: number | ||||
|   currentRunner: boolean | ||||
|   activeChallengeId: number | null | ||||
|   chaseFreeTime: number | null  // date | ||||
|   penaltyStart: number | null // date | ||||
|   penaltyEnd: number | null  // date | ||||
|   penaltyEnd: number | null  //date | ||||
| } | ||||
|  | ||||
| const initialState: GameState = { | ||||
| @@ -30,6 +32,7 @@ const initialState: GameState = { | ||||
|   gameStarted: false, | ||||
|   money: 0, | ||||
|   currentRunner: false, | ||||
|   activeChallengeId: null, | ||||
|   chaseFreeTime: null, | ||||
|   penaltyStart: null, | ||||
|   penaltyEnd: null, | ||||
| @@ -45,6 +48,9 @@ export const gameSlice = createSlice({ | ||||
|     updateMoney: (state, action: PayloadAction<number>) => { | ||||
|       state.money = action.payload | ||||
|     }, | ||||
|     updateActiveChallengeId: (state, action: PayloadAction<number | null>) => { | ||||
|       state.activeChallengeId = action.payload | ||||
|     }, | ||||
|     updateGameState: (state, action: PayloadAction<GamePayload>) => { | ||||
|       const game: GamePayload = action.payload | ||||
|       state.gameStarted = game.started | ||||
| @@ -57,6 +63,6 @@ export const gameSlice = createSlice({ | ||||
|   }, | ||||
| }) | ||||
|  | ||||
| export const { setPlayerId, updateMoney, updateGameState } = gameSlice.actions | ||||
| export const { setPlayerId, updateMoney, updateActiveChallengeId, updateGameState } = gameSlice.actions | ||||
|  | ||||
| export default gameSlice.reducer | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import { createSlice, PayloadAction } from '@reduxjs/toolkit' | ||||
| import { PaginationMeta } from '../common' | ||||
|  | ||||
| export interface InterrailTime { | ||||
|   hours: number | ||||
| @@ -73,15 +74,6 @@ const initialState: TrainsState = { | ||||
|   trains: [] | ||||
| } | ||||
|  | ||||
| export interface PaginationMeta { | ||||
|   currentPage: number | ||||
|   lastPage: number | ||||
|   nextPage: number | ||||
|   prevPage: number | ||||
|   total: number | ||||
|   totalPerPage: number | ||||
| } | ||||
|  | ||||
| export interface TrainsPayload { | ||||
|   data: TrainTrip[] | ||||
|   meta: PaginationMeta | ||||
| @@ -105,8 +97,8 @@ export const trainSlice = createSlice({ | ||||
|           arrivalTime: dlTrain.arrivalTime, | ||||
|           info: info, | ||||
|         }) | ||||
|         state.trains.sort((t1, t2) => t1.departureTime > t2.departureTime ? -1 : t1.departureTime == t2.arrivalTime ? 0 : 1) | ||||
|       } | ||||
|       state.trains.sort((t1, t2) => t1.departureTime > t2.departureTime ? -1 : t1.departureTime == t2.arrivalTime ? 0 : 1) | ||||
|     } | ||||
|   }, | ||||
| }) | ||||
|   | ||||
| @@ -0,0 +1,18 @@ | ||||
| /* | ||||
|   Warnings: | ||||
|  | ||||
|   - You are about to drop the column `active` on the `ChallengeAction` table. All the data in the column will be lost. | ||||
|   - A unique constraint covering the columns `[activeChallengeId]` on the table `Player` will be added. If there are existing duplicate values, this will fail. | ||||
|  | ||||
| */ | ||||
| -- AlterTable | ||||
| ALTER TABLE "ChallengeAction" DROP COLUMN "active"; | ||||
|  | ||||
| -- AlterTable | ||||
| ALTER TABLE "Player" ADD COLUMN     "activeChallengeId" INTEGER; | ||||
|  | ||||
| -- CreateIndex | ||||
| CREATE UNIQUE INDEX "Player_activeChallengeId_key" ON "Player"("activeChallengeId"); | ||||
|  | ||||
| -- AddForeignKey | ||||
| ALTER TABLE "Player" ADD CONSTRAINT "Player_activeChallengeId_fkey" FOREIGN KEY ("activeChallengeId") REFERENCES "ChallengeAction"("id") ON DELETE SET NULL ON UPDATE CASCADE; | ||||
| @@ -8,15 +8,17 @@ datasource db { | ||||
| } | ||||
|  | ||||
| model Player { | ||||
|   id            Int               @id @default(autoincrement()) | ||||
|   name          String            @unique | ||||
|   password      String | ||||
|   money         Int               @default(0) | ||||
|   actions       ChallengeAction[] | ||||
|   geolocations  Geolocation[] | ||||
|   moneyUpdates  MoneyUpdate[] | ||||
|   trips         TrainTrip[] | ||||
|   runs          PlayerRun[] | ||||
|   id                Int               @id @default(autoincrement()) | ||||
|   name              String            @unique | ||||
|   password          String | ||||
|   money             Int               @default(0) | ||||
|   activeChallenge   ChallengeAction?  @relation("ActiveChallenge", fields: [activeChallengeId], references: [id]) | ||||
|   activeChallengeId Int?              @unique | ||||
|   actions           ChallengeAction[] | ||||
|   geolocations      Geolocation[] | ||||
|   moneyUpdates      MoneyUpdate[] | ||||
|   trips             TrainTrip[] | ||||
|   runs              PlayerRun[] | ||||
| } | ||||
|  | ||||
| model Game { | ||||
| @@ -68,7 +70,6 @@ model ChallengeAction { | ||||
|   playerId     Int | ||||
|   challenge    Challenge    @relation(fields: [challengeId], references: [id]) | ||||
|   challengeId  Int          @unique | ||||
|   active       Boolean      @default(false) | ||||
|   success      Boolean      @default(false) | ||||
|   start        DateTime     @default(now()) @db.Timestamptz(3) | ||||
|   end          DateTime?    @db.Timestamptz(3) | ||||
| @@ -76,6 +77,7 @@ model ChallengeAction { | ||||
|   penaltyEnd   DateTime?    @db.Timestamptz(3) | ||||
|   run          PlayerRun    @relation(fields: [runId], references: [id]) | ||||
|   runId        Int | ||||
|   activePlayer Player?      @relation("ActiveChallenge") | ||||
|   moneyUpdate  MoneyUpdate? | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -20,11 +20,15 @@ export class ChallengeActionsService { | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   async findAll(queryPagination: QueryPaginationDto, filterChallengeActions: FilterChallengeActionsDto): Promise<[ChallengeAction[], number]> { | ||||
|   async findAll(queryPagination: QueryPaginationDto, { playerId, challengeId, success }: FilterChallengeActionsDto): Promise<[ChallengeAction[], number]> { | ||||
|     return [ | ||||
|       await this.prisma.challengeAction.findMany({ | ||||
|         ...paginate(queryPagination), | ||||
|         where: filterChallengeActions, | ||||
|         where: { | ||||
|           playerId: playerId, | ||||
|           challengeId: challengeId, | ||||
|           success: success, | ||||
|         } | ||||
|       }), | ||||
|       await this.prisma.challengeAction.count(), | ||||
|     ] | ||||
| @@ -54,20 +58,14 @@ export class ChallengeActionsService { | ||||
|   } | ||||
|  | ||||
|   async endCurrentChallenge(player: Player, success: boolean): Promise<ChallengeAction> { | ||||
|     const challengeAction = await this.prisma.challengeAction.findFirst({ | ||||
|       where: { | ||||
|         playerId: player.id, | ||||
|         active: true, | ||||
|       } | ||||
|     }) | ||||
|     if (!challengeAction) | ||||
|     if (!player.activeChallengeId) | ||||
|       throw new BadRequestException("Aucun défi n'est en cours") | ||||
|     const challengeAction = await this.prisma.challengeAction.findUnique({ where: { id: player.activeChallengeId } }) | ||||
|     let data | ||||
|     const now = new Date() | ||||
|     if  (success) { | ||||
|       data = { | ||||
|         success: success, | ||||
|         active: false, | ||||
|         end: now, | ||||
|       } | ||||
|  | ||||
| @@ -85,12 +83,15 @@ export class ChallengeActionsService { | ||||
|     else { | ||||
|       data = { | ||||
|         success: success, | ||||
|         active: false, | ||||
|         end: now, | ||||
|         penaltyStart: now, | ||||
|         penaltyEnd: new Date(now.getTime() + Constants.PENALTY_TIME * 60 * 1000), | ||||
|       } | ||||
|     } | ||||
|     await this.prisma.player.update({ | ||||
|       where: { id: player.id }, | ||||
|       data: { activeChallengeId: null }, | ||||
|     }) | ||||
|     return await this.prisma.challengeAction.update({ | ||||
|       where: { | ||||
|         id: challengeAction.id, | ||||
|   | ||||
| @@ -9,12 +9,6 @@ export class CreateChallengeActionDto { | ||||
|   @ApiProperty({ description: "Identifiant du défi rattaché à l'action" }) | ||||
|   challengeId: number | ||||
|  | ||||
|   @IsOptional() | ||||
|   @IsBoolean() | ||||
|   @BooleanTransform() | ||||
|   @ApiProperty({ description: "Est-ce que le défi est actuellement en train d'être réalisé", default: true }) | ||||
|   active: boolean = true | ||||
|  | ||||
|   @IsOptional() | ||||
|   @IsBoolean() | ||||
|   @BooleanTransform() | ||||
|   | ||||
| @@ -16,12 +16,6 @@ export class FilterChallengeActionsDto { | ||||
|   @ApiProperty({ description: "Identifiant du défi attaché à cette action", required: false }) | ||||
|   challengeId?: number | ||||
|  | ||||
|   @IsOptional() | ||||
|   @IsBoolean() | ||||
|   @BooleanTransform() | ||||
|   @ApiProperty({ description: "Défi en train d'être accompli", required: false }) | ||||
|   active?: boolean | ||||
|  | ||||
|   @IsOptional() | ||||
|   @IsBoolean() | ||||
|   @BooleanTransform() | ||||
|   | ||||
| @@ -22,11 +22,6 @@ export class ChallengeActionEntity implements ChallengeAction { | ||||
|    */ | ||||
|   challengeId: number | ||||
|  | ||||
|   /** | ||||
|    * Est-ce que le défi est actuellement en train d'être réalisé | ||||
|    */ | ||||
|   active: boolean | ||||
|  | ||||
|   /** | ||||
|    * Est-ce que le défi a été réussi | ||||
|    */ | ||||
|   | ||||
| @@ -64,13 +64,7 @@ export class ChallengesService { | ||||
|     const game = await this.prisma.game.findUnique({ where: { id: 1 }, include: { currentRun: true } }) | ||||
|     if (game.currentRun?.runnerId !== player.id) | ||||
|       throw new ConflictException("Vous n'êtes pas en course, ce n'est pas à vous de tirer un défi.") | ||||
|     const currentChallengeAction = await this.prisma.challengeAction.findFirst({ | ||||
|       where: { | ||||
|         playerId: player.id, | ||||
|         active: true, | ||||
|       } | ||||
|     }) | ||||
|     if (currentChallengeAction) | ||||
|     if (player.activeChallengeId) | ||||
|       throw new ConflictException("Un défi est déjà en cours d'accomplissement") | ||||
|     const remaningChallenges = await this.prisma.challenge.count({ | ||||
|       where: { | ||||
| @@ -92,10 +86,13 @@ export class ChallengesService { | ||||
|         playerId: player.id, | ||||
|         challengeId: challenge.id, | ||||
|         runId: game.currentRunId, | ||||
|         active: true, | ||||
|         success: false, | ||||
|       } | ||||
|     }) | ||||
|     await this.prisma.player.update({ | ||||
|       where: { id: player.id }, | ||||
|       data: { activeChallengeId: action.id }, | ||||
|     }) | ||||
|     challengeEntity.action = action | ||||
|     return challengeEntity | ||||
|   } | ||||
|   | ||||
| @@ -58,17 +58,17 @@ export class GameService { | ||||
|       throw new ConflictException("La partie n'a pas encore démarré.") | ||||
|  | ||||
|     // Clôture de l'éventuel défi en cours, qui n'a alors pas été réussi | ||||
|     await this.prisma.challengeAction.updateMany({ | ||||
|       where: { | ||||
|         playerId: game.currentRun.runnerId, | ||||
|         runId: game.currentRunId, | ||||
|         active: true, | ||||
|       }, | ||||
|       data: { | ||||
|         active: false, | ||||
|         success: false, | ||||
|       }, | ||||
|     }) | ||||
|     const currentRunner = await this.prisma.player.findUnique({ where: { id: game.currentRun.runnerId } }) | ||||
|     if (currentRunner.activeChallengeId) { | ||||
|       await this.prisma.challengeAction.update({ | ||||
|         where: { id: currentRunner.activeChallengeId }, | ||||
|         data: { success: false }, | ||||
|       }) | ||||
|       await this.prisma.player.update({ | ||||
|         where: { id: currentRunner.id }, | ||||
|         data: { activeChallengeId: null }, | ||||
|       }) | ||||
|     } | ||||
|  | ||||
|     await this.prisma.playerRun.update({ | ||||
|       where: { id: game.currentRunId }, | ||||
| @@ -173,7 +173,7 @@ export class GameService { | ||||
|  | ||||
|     const orpanChallengeMoneyUpdates = await this.prisma.moneyUpdate.findMany({ where: { reason: MoneyUpdateType.WIN_CHALLENGE, actionId: null } }) | ||||
|     await this.prisma.moneyUpdate.deleteMany({ where: { reason: MoneyUpdateType.WIN_CHALLENGE, actionId: null } }) | ||||
|     deleted.push(...orpanTrainMoneyUpdates) | ||||
|     deleted.push(...orpanChallengeMoneyUpdates) | ||||
|  | ||||
|     return { added: added, deleted: deleted } | ||||
|   } | ||||
|   | ||||
| @@ -1,21 +1,37 @@ | ||||
| import { ApiProperty } from "@nestjs/swagger" | ||||
| import { Player } from "@prisma/client" | ||||
| import { Exclude } from 'class-transformer' | ||||
| import { IsOptional } from "class-validator" | ||||
|  | ||||
| export class PlayerEntity implements Player { | ||||
|   constructor(partial: Partial<PlayerEntity>) { | ||||
|     Object.assign(this, partial) | ||||
|   } | ||||
|  | ||||
|   @ApiProperty({description: "Identifiant unique"}) | ||||
|   /** | ||||
|    * Identifiant unique | ||||
|    */ | ||||
|   id: number | ||||
|  | ||||
|   @ApiProperty({description: "Nom de læ joueur⋅se"}) | ||||
|   /** | ||||
|    * Nom de læ joueur⋅se | ||||
|    */ | ||||
|   name: string | ||||
|  | ||||
|   /** | ||||
|    * Mot de passe hashé | ||||
|    */ | ||||
|   @Exclude() | ||||
|   password: string | ||||
|  | ||||
|   @ApiProperty({description: "Nombre de jetons dont dispose actuellement læ joueur⋅se"}) | ||||
|   /** | ||||
|    * Nombre de jetons dont dispose actuellement læ joueur⋅se | ||||
|    */ | ||||
|   money: number | ||||
|  | ||||
|   /** | ||||
|    * Identifiant du défi en cours d'accomplissement | ||||
|    */ | ||||
|   @IsOptional() | ||||
|   activeChallengeId: number | null | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user