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 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 { FontAwesome6 } from '@expo/vector-icons'
|
||||||
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { View } from 'react-native'
|
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() {
|
export default function ChallengesScreen() {
|
||||||
return (
|
return (
|
||||||
<Surface style={{ flex: 1 }}>
|
<Surface style={{ flex: 1 }}>
|
||||||
<Appbar.Header>
|
<ChallengeScreenHeader />
|
||||||
<Appbar.Content title={"Défis"} />
|
<ChallengeScreenBody />
|
||||||
<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>
|
|
||||||
</Surface>
|
</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 { 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 { useDownloadTrains } from '@/hooks/useTrain'
|
||||||
import { isAuthValid } from '@/utils/features/auth/authSlice'
|
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 { useQuery } from '@tanstack/react-query'
|
||||||
import { ReactNode, useEffect } from 'react'
|
import { ReactNode, useEffect } from 'react'
|
||||||
|
|
||||||
@ -10,7 +14,10 @@ export default function GameProvider({ children }: { children: ReactNode }) {
|
|||||||
const game = useGame()
|
const game = useGame()
|
||||||
const updateGameState = useUpdateGameState()
|
const updateGameState = useUpdateGameState()
|
||||||
const updateMoney = useUpdateMoney()
|
const updateMoney = useUpdateMoney()
|
||||||
|
const updateActiveChallengeId = useUpdateActiveChallengeId()
|
||||||
const downloadTrains = useDownloadTrains()
|
const downloadTrains = useDownloadTrains()
|
||||||
|
const downloadChallenges = useDownloadChallenges()
|
||||||
|
const downloadChallengeActions = useDownloadChallengeActions()
|
||||||
|
|
||||||
const gameQuery = useQuery({
|
const gameQuery = useQuery({
|
||||||
queryKey: ['get-game', auth.token],
|
queryKey: ['get-game', auth.token],
|
||||||
@ -34,8 +41,10 @@ export default function GameProvider({ children }: { children: ReactNode }) {
|
|||||||
refetchInterval: 5000,
|
refetchInterval: 5000,
|
||||||
})
|
})
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (playerQuery.isSuccess && playerQuery.data)
|
if (playerQuery.isSuccess && playerQuery.data) {
|
||||||
updateMoney(playerQuery.data.money)
|
updateMoney(playerQuery.data.money)
|
||||||
|
updateActiveChallengeId(playerQuery.data.activeChallengeId)
|
||||||
|
}
|
||||||
}, [playerQuery.status, playerQuery.dataUpdatedAt])
|
}, [playerQuery.status, playerQuery.dataUpdatedAt])
|
||||||
|
|
||||||
const trainsQuery = useQuery({
|
const trainsQuery = useQuery({
|
||||||
@ -47,10 +56,27 @@ export default function GameProvider({ children }: { children: ReactNode }) {
|
|||||||
refetchInterval: 5000,
|
refetchInterval: 5000,
|
||||||
})
|
})
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (trainsQuery.isSuccess && trainsQuery.data && trainsQuery)
|
if (trainsQuery.isSuccess && trainsQuery.data)
|
||||||
downloadTrains(trainsQuery.data)
|
downloadTrains(trainsQuery.data)
|
||||||
}, [trainsQuery.status, trainsQuery.dataUpdatedAt])
|
}, [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 <>
|
return <>
|
||||||
{children}
|
{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 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 { 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 useGame = () => useAppSelector((state) => state.game)
|
||||||
export const useSetPlayerId = () => {
|
export const useSetPlayerId = () => {
|
||||||
@ -10,6 +10,10 @@ export const useUpdateMoney = () => {
|
|||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
return (money: number) => dispatch(updateMoney(money))
|
return (money: number) => dispatch(updateMoney(money))
|
||||||
}
|
}
|
||||||
|
export const useUpdateActiveChallengeId = () => {
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
return (challengeActionId: number) => dispatch(updateActiveChallengeId(challengeActionId))
|
||||||
|
}
|
||||||
export const useUpdateGameState = () => {
|
export const useUpdateGameState = () => {
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
return (game: GamePayload) => dispatch(updateGameState(game))
|
return (game: GamePayload) => dispatch(updateGameState(game))
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
"@expo/vector-icons": "^14.0.2",
|
"@expo/vector-icons": "^14.0.2",
|
||||||
"@maplibre/maplibre-react-native": "^10.0.0-alpha.28",
|
"@maplibre/maplibre-react-native": "^10.0.0-alpha.28",
|
||||||
"@pchmn/expo-material3-theme": "github:pchmn/expo-material3-theme",
|
"@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/bottom-tabs": "^7.0.0",
|
||||||
"@react-navigation/native": "^7.0.0",
|
"@react-navigation/native": "^7.0.0",
|
||||||
"@reduxjs/toolkit": "^2.4.0",
|
"@reduxjs/toolkit": "^2.4.0",
|
||||||
@ -58,8 +59,7 @@
|
|||||||
"react-native-screens": "~4.1.0",
|
"react-native-screens": "~4.1.0",
|
||||||
"react-native-web": "~0.19.13",
|
"react-native-web": "~0.19.13",
|
||||||
"react-native-webview": "13.12.2",
|
"react-native-webview": "13.12.2",
|
||||||
"react-redux": "^9.1.2",
|
"react-redux": "^9.1.2"
|
||||||
"@react-native-async-storage/async-storage": "1.23.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.25.2",
|
"@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 {
|
export interface ChallengeAction {
|
||||||
id: number
|
id: number
|
||||||
challengeId: number
|
challengeId: number
|
||||||
title: string,
|
|
||||||
description: string,
|
|
||||||
reward: number,
|
|
||||||
success: boolean,
|
success: boolean,
|
||||||
start: number, // date
|
start: number, // date
|
||||||
end: number | null, // date
|
end: number | null, // date
|
||||||
@ -21,13 +19,45 @@ const initialState: ActionsState = {
|
|||||||
challengeActions: []
|
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({
|
export const challengeActionsSlice = createSlice({
|
||||||
name: 'challengeActions',
|
name: 'challengeActions',
|
||||||
initialState: initialState,
|
initialState: initialState,
|
||||||
reducers: {
|
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
|
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 {
|
export interface Challenge {
|
||||||
id: number
|
id: number
|
||||||
@ -15,13 +17,31 @@ const initialState: ChallengesState = {
|
|||||||
challenges: []
|
challenges: []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ChallengesPayload {
|
||||||
|
data: (Challenge & { action: ChallengeAction | null })[]
|
||||||
|
meta: PaginationMeta
|
||||||
|
}
|
||||||
|
|
||||||
export const challengesSlice = createSlice({
|
export const challengesSlice = createSlice({
|
||||||
name: 'challenges',
|
name: 'challenges',
|
||||||
initialState: initialState,
|
initialState: initialState,
|
||||||
reducers: {
|
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
|
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 { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||||
|
import { ChallengeAction } from '../challengeActions/challengeActionsSlice'
|
||||||
|
|
||||||
export interface RunPayload {
|
export interface RunPayload {
|
||||||
id: number
|
id: number
|
||||||
@ -20,9 +21,10 @@ export interface GameState {
|
|||||||
gameStarted: boolean
|
gameStarted: boolean
|
||||||
money: number
|
money: number
|
||||||
currentRunner: boolean
|
currentRunner: boolean
|
||||||
|
activeChallengeId: number | null
|
||||||
chaseFreeTime: number | null // date
|
chaseFreeTime: number | null // date
|
||||||
penaltyStart: number | null // date
|
penaltyStart: number | null // date
|
||||||
penaltyEnd: number | null // date
|
penaltyEnd: number | null //date
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: GameState = {
|
const initialState: GameState = {
|
||||||
@ -30,6 +32,7 @@ const initialState: GameState = {
|
|||||||
gameStarted: false,
|
gameStarted: false,
|
||||||
money: 0,
|
money: 0,
|
||||||
currentRunner: false,
|
currentRunner: false,
|
||||||
|
activeChallengeId: null,
|
||||||
chaseFreeTime: null,
|
chaseFreeTime: null,
|
||||||
penaltyStart: null,
|
penaltyStart: null,
|
||||||
penaltyEnd: null,
|
penaltyEnd: null,
|
||||||
@ -45,6 +48,9 @@ export const gameSlice = createSlice({
|
|||||||
updateMoney: (state, action: PayloadAction<number>) => {
|
updateMoney: (state, action: PayloadAction<number>) => {
|
||||||
state.money = action.payload
|
state.money = action.payload
|
||||||
},
|
},
|
||||||
|
updateActiveChallengeId: (state, action: PayloadAction<number | null>) => {
|
||||||
|
state.activeChallengeId = action.payload
|
||||||
|
},
|
||||||
updateGameState: (state, action: PayloadAction<GamePayload>) => {
|
updateGameState: (state, action: PayloadAction<GamePayload>) => {
|
||||||
const game: GamePayload = action.payload
|
const game: GamePayload = action.payload
|
||||||
state.gameStarted = game.started
|
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
|
export default gameSlice.reducer
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||||
|
import { PaginationMeta } from '../common'
|
||||||
|
|
||||||
export interface InterrailTime {
|
export interface InterrailTime {
|
||||||
hours: number
|
hours: number
|
||||||
@ -73,15 +74,6 @@ const initialState: TrainsState = {
|
|||||||
trains: []
|
trains: []
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PaginationMeta {
|
|
||||||
currentPage: number
|
|
||||||
lastPage: number
|
|
||||||
nextPage: number
|
|
||||||
prevPage: number
|
|
||||||
total: number
|
|
||||||
totalPerPage: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TrainsPayload {
|
export interface TrainsPayload {
|
||||||
data: TrainTrip[]
|
data: TrainTrip[]
|
||||||
meta: PaginationMeta
|
meta: PaginationMeta
|
||||||
@ -105,8 +97,8 @@ export const trainSlice = createSlice({
|
|||||||
arrivalTime: dlTrain.arrivalTime,
|
arrivalTime: dlTrain.arrivalTime,
|
||||||
info: info,
|
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 {
|
model Player {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
name String @unique
|
name String @unique
|
||||||
password String
|
password String
|
||||||
money Int @default(0)
|
money Int @default(0)
|
||||||
actions ChallengeAction[]
|
activeChallenge ChallengeAction? @relation("ActiveChallenge", fields: [activeChallengeId], references: [id])
|
||||||
geolocations Geolocation[]
|
activeChallengeId Int? @unique
|
||||||
moneyUpdates MoneyUpdate[]
|
actions ChallengeAction[]
|
||||||
trips TrainTrip[]
|
geolocations Geolocation[]
|
||||||
runs PlayerRun[]
|
moneyUpdates MoneyUpdate[]
|
||||||
|
trips TrainTrip[]
|
||||||
|
runs PlayerRun[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Game {
|
model Game {
|
||||||
@ -68,7 +70,6 @@ model ChallengeAction {
|
|||||||
playerId Int
|
playerId Int
|
||||||
challenge Challenge @relation(fields: [challengeId], references: [id])
|
challenge Challenge @relation(fields: [challengeId], references: [id])
|
||||||
challengeId Int @unique
|
challengeId Int @unique
|
||||||
active Boolean @default(false)
|
|
||||||
success Boolean @default(false)
|
success Boolean @default(false)
|
||||||
start DateTime @default(now()) @db.Timestamptz(3)
|
start DateTime @default(now()) @db.Timestamptz(3)
|
||||||
end DateTime? @db.Timestamptz(3)
|
end DateTime? @db.Timestamptz(3)
|
||||||
@ -76,6 +77,7 @@ model ChallengeAction {
|
|||||||
penaltyEnd DateTime? @db.Timestamptz(3)
|
penaltyEnd DateTime? @db.Timestamptz(3)
|
||||||
run PlayerRun @relation(fields: [runId], references: [id])
|
run PlayerRun @relation(fields: [runId], references: [id])
|
||||||
runId Int
|
runId Int
|
||||||
|
activePlayer Player? @relation("ActiveChallenge")
|
||||||
moneyUpdate MoneyUpdate?
|
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 [
|
return [
|
||||||
await this.prisma.challengeAction.findMany({
|
await this.prisma.challengeAction.findMany({
|
||||||
...paginate(queryPagination),
|
...paginate(queryPagination),
|
||||||
where: filterChallengeActions,
|
where: {
|
||||||
|
playerId: playerId,
|
||||||
|
challengeId: challengeId,
|
||||||
|
success: success,
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
await this.prisma.challengeAction.count(),
|
await this.prisma.challengeAction.count(),
|
||||||
]
|
]
|
||||||
@ -54,20 +58,14 @@ export class ChallengeActionsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async endCurrentChallenge(player: Player, success: boolean): Promise<ChallengeAction> {
|
async endCurrentChallenge(player: Player, success: boolean): Promise<ChallengeAction> {
|
||||||
const challengeAction = await this.prisma.challengeAction.findFirst({
|
if (!player.activeChallengeId)
|
||||||
where: {
|
|
||||||
playerId: player.id,
|
|
||||||
active: true,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (!challengeAction)
|
|
||||||
throw new BadRequestException("Aucun défi n'est en cours")
|
throw new BadRequestException("Aucun défi n'est en cours")
|
||||||
|
const challengeAction = await this.prisma.challengeAction.findUnique({ where: { id: player.activeChallengeId } })
|
||||||
let data
|
let data
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
if (success) {
|
if (success) {
|
||||||
data = {
|
data = {
|
||||||
success: success,
|
success: success,
|
||||||
active: false,
|
|
||||||
end: now,
|
end: now,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,12 +83,15 @@ export class ChallengeActionsService {
|
|||||||
else {
|
else {
|
||||||
data = {
|
data = {
|
||||||
success: success,
|
success: success,
|
||||||
active: false,
|
|
||||||
end: now,
|
end: now,
|
||||||
penaltyStart: now,
|
penaltyStart: now,
|
||||||
penaltyEnd: new Date(now.getTime() + Constants.PENALTY_TIME * 60 * 1000),
|
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({
|
return await this.prisma.challengeAction.update({
|
||||||
where: {
|
where: {
|
||||||
id: challengeAction.id,
|
id: challengeAction.id,
|
||||||
|
@ -9,12 +9,6 @@ export class CreateChallengeActionDto {
|
|||||||
@ApiProperty({ description: "Identifiant du défi rattaché à l'action" })
|
@ApiProperty({ description: "Identifiant du défi rattaché à l'action" })
|
||||||
challengeId: number
|
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()
|
@IsOptional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
@BooleanTransform()
|
@BooleanTransform()
|
||||||
|
@ -16,12 +16,6 @@ export class FilterChallengeActionsDto {
|
|||||||
@ApiProperty({ description: "Identifiant du défi attaché à cette action", required: false })
|
@ApiProperty({ description: "Identifiant du défi attaché à cette action", required: false })
|
||||||
challengeId?: number
|
challengeId?: number
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
@BooleanTransform()
|
|
||||||
@ApiProperty({ description: "Défi en train d'être accompli", required: false })
|
|
||||||
active?: boolean
|
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
@BooleanTransform()
|
@BooleanTransform()
|
||||||
|
@ -22,11 +22,6 @@ export class ChallengeActionEntity implements ChallengeAction {
|
|||||||
*/
|
*/
|
||||||
challengeId: number
|
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
|
* 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 } })
|
const game = await this.prisma.game.findUnique({ where: { id: 1 }, include: { currentRun: true } })
|
||||||
if (game.currentRun?.runnerId !== player.id)
|
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.")
|
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({
|
if (player.activeChallengeId)
|
||||||
where: {
|
|
||||||
playerId: player.id,
|
|
||||||
active: true,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (currentChallengeAction)
|
|
||||||
throw new ConflictException("Un défi est déjà en cours d'accomplissement")
|
throw new ConflictException("Un défi est déjà en cours d'accomplissement")
|
||||||
const remaningChallenges = await this.prisma.challenge.count({
|
const remaningChallenges = await this.prisma.challenge.count({
|
||||||
where: {
|
where: {
|
||||||
@ -92,10 +86,13 @@ export class ChallengesService {
|
|||||||
playerId: player.id,
|
playerId: player.id,
|
||||||
challengeId: challenge.id,
|
challengeId: challenge.id,
|
||||||
runId: game.currentRunId,
|
runId: game.currentRunId,
|
||||||
active: true,
|
|
||||||
success: false,
|
success: false,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
await this.prisma.player.update({
|
||||||
|
where: { id: player.id },
|
||||||
|
data: { activeChallengeId: action.id },
|
||||||
|
})
|
||||||
challengeEntity.action = action
|
challengeEntity.action = action
|
||||||
return challengeEntity
|
return challengeEntity
|
||||||
}
|
}
|
||||||
|
@ -58,17 +58,17 @@ export class GameService {
|
|||||||
throw new ConflictException("La partie n'a pas encore démarré.")
|
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
|
// Clôture de l'éventuel défi en cours, qui n'a alors pas été réussi
|
||||||
await this.prisma.challengeAction.updateMany({
|
const currentRunner = await this.prisma.player.findUnique({ where: { id: game.currentRun.runnerId } })
|
||||||
where: {
|
if (currentRunner.activeChallengeId) {
|
||||||
playerId: game.currentRun.runnerId,
|
await this.prisma.challengeAction.update({
|
||||||
runId: game.currentRunId,
|
where: { id: currentRunner.activeChallengeId },
|
||||||
active: true,
|
data: { success: false },
|
||||||
},
|
})
|
||||||
data: {
|
await this.prisma.player.update({
|
||||||
active: false,
|
where: { id: currentRunner.id },
|
||||||
success: false,
|
data: { activeChallengeId: null },
|
||||||
},
|
})
|
||||||
})
|
}
|
||||||
|
|
||||||
await this.prisma.playerRun.update({
|
await this.prisma.playerRun.update({
|
||||||
where: { id: game.currentRunId },
|
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 } })
|
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 } })
|
await this.prisma.moneyUpdate.deleteMany({ where: { reason: MoneyUpdateType.WIN_CHALLENGE, actionId: null } })
|
||||||
deleted.push(...orpanTrainMoneyUpdates)
|
deleted.push(...orpanChallengeMoneyUpdates)
|
||||||
|
|
||||||
return { added: added, deleted: deleted }
|
return { added: added, deleted: deleted }
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,37 @@
|
|||||||
import { ApiProperty } from "@nestjs/swagger"
|
import { ApiProperty } from "@nestjs/swagger"
|
||||||
import { Player } from "@prisma/client"
|
import { Player } from "@prisma/client"
|
||||||
import { Exclude } from 'class-transformer'
|
import { Exclude } from 'class-transformer'
|
||||||
|
import { IsOptional } from "class-validator"
|
||||||
|
|
||||||
export class PlayerEntity implements Player {
|
export class PlayerEntity implements Player {
|
||||||
constructor(partial: Partial<PlayerEntity>) {
|
constructor(partial: Partial<PlayerEntity>) {
|
||||||
Object.assign(this, partial)
|
Object.assign(this, partial)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiProperty({description: "Identifiant unique"})
|
/**
|
||||||
|
* Identifiant unique
|
||||||
|
*/
|
||||||
id: number
|
id: number
|
||||||
|
|
||||||
@ApiProperty({description: "Nom de læ joueur⋅se"})
|
/**
|
||||||
|
* Nom de læ joueur⋅se
|
||||||
|
*/
|
||||||
name: string
|
name: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mot de passe hashé
|
||||||
|
*/
|
||||||
@Exclude()
|
@Exclude()
|
||||||
password: string
|
password: string
|
||||||
|
|
||||||
@ApiProperty({description: "Nombre de jetons dont dispose actuellement læ joueur⋅se"})
|
/**
|
||||||
|
* Nombre de jetons dont dispose actuellement læ joueur⋅se
|
||||||
|
*/
|
||||||
money: number
|
money: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Identifiant du défi en cours d'accomplissement
|
||||||
|
*/
|
||||||
|
@IsOptional()
|
||||||
|
activeChallengeId: number | null
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user