Compare commits

..

4 Commits

22 changed files with 353 additions and 116 deletions

View File

@ -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'
export default function ChallengesScreen() { function ChallengeScreenHeader() {
return ( return <>
<Surface style={{ flex: 1 }}>
<Appbar.Header> <Appbar.Header>
<Appbar.Content title={"Défis"} /> <Appbar.Content title={"Défis"} />
<Appbar.Action icon='format-list-bulleted' /> <Appbar.Action icon='format-list-bulleted' />
</Appbar.Header> </Appbar.Header>
<PenaltyBanner /> <PenaltyBanner />
<Surface elevation={2} style={{ flex: 1, margin: 20, borderRadius: 20 }}> </>
<View style={{ padding: 10 }}> }
<Text variant='headlineMedium' style={{ textAlign: 'center' }}>Titre</Text>
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> </View>
<View style={{ flexGrow: 1 }}> </>}
<Surface elevation={5} mode='flat' style={{ flexGrow: 1, padding: 15 }}> <Banner
<Text variant='bodyLarge' style={{ flexGrow: 1 }}>Description</Text> visible={game.gameStarted && !game.currentRunner}
<Text variant='titleMedium'> icon={({ size }) => <FontAwesome6 name='cat' size={size} color={'pink'} />}
Récompense : 500 <FontAwesome6 name='coins' /> style={{ backgroundColor: MD3Colors.secondary30 }}>
</Text> Vous êtes poursuiveuse, et n'avez donc pas de défi à accomplir.
</Surface> </Banner>
</View> </>
<View style={{ flexWrap: 'wrap', flexDirection: 'row', justifyContent: 'space-around', padding: 15 }}> }
<Button mode='outlined' icon='cancel'>
Passer export default function ChallengesScreen() {
</Button> return (
<Button mode='contained' icon='check'> <Surface style={{ flex: 1 }}>
Terminer <ChallengeScreenHeader />
</Button> <ChallengeScreenBody />
</View>
</Surface>
</Surface> </Surface>
) )
} }

View 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>
)
}

View File

@ -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}
</> </>

View 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 })
}
})
}

View File

@ -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))
}

View File

@ -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))
}

View File

@ -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))

View File

@ -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",

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,8 @@
export interface PaginationMeta {
currentPage: number
lastPage: number
nextPage: number
prevPage: number
total: number
totalPerPage: number
}

View File

@ -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,6 +21,7 @@ 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
@ -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

View File

@ -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)
} }
}, },
}) })

View File

@ -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;

View File

@ -12,6 +12,8 @@ model Player {
name String @unique name String @unique
password String password String
money Int @default(0) money Int @default(0)
activeChallenge ChallengeAction? @relation("ActiveChallenge", fields: [activeChallengeId], references: [id])
activeChallengeId Int? @unique
actions ChallengeAction[] actions ChallengeAction[]
geolocations Geolocation[] geolocations Geolocation[]
moneyUpdates MoneyUpdate[] moneyUpdates MoneyUpdate[]
@ -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?
} }

View File

@ -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,

View File

@ -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()

View File

@ -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()

View File

@ -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 é réussi * Est-ce que le défi a é réussi
*/ */

View File

@ -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
} }

View File

@ -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: {
active: false,
success: false,
},
}) })
await this.prisma.player.update({
where: { id: currentRunner.id },
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 }
} }

View File

@ -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 joueurse
*/
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 joueurse
*/
money: number money: number
/**
* Identifiant du défi en cours d'accomplissement
*/
@IsOptional()
activeChallengeId: number | null
} }