diff --git a/app/features/chat/components/Chat.tsx b/app/features/chat/components/Chat.tsx index a59f2632e..da59c5199 100644 --- a/app/features/chat/components/Chat.tsx +++ b/app/features/chat/components/Chat.tsx @@ -30,6 +30,7 @@ export interface ChatProps { onUnmount?: () => void; disabled?: boolean; missingUserName?: string; + revalidates?: boolean; } const systemMessageText = (msg: ChatMessage) => { @@ -278,9 +279,11 @@ function SystemMessage({ export function useChat({ rooms, onNewMessage, + revalidates = true, }: { rooms: ChatProps["rooms"]; onNewMessage?: (message: ChatMessage) => void; + revalidates?: boolean; }) { const { revalidate } = useRevalidator(); const rootLoaderData = useRootLoaderData(); @@ -321,7 +324,7 @@ export function useChat({ // something interesting happened // -> let's run data loaders so they can see it sooner const isSystemMessage = Boolean(messageArr[0].type); - if (isSystemMessage) { + if (isSystemMessage && revalidates) { revalidate(); } @@ -356,7 +359,7 @@ export function useChat({ wsCurrent?.close(); setMessages([]); }; - }, [rooms, onNewMessage, rootLoaderData.skalopUrl, revalidate]); + }, [rooms, onNewMessage, rootLoaderData.skalopUrl, revalidate, revalidates]); React.useEffect(() => { // ping every minute to keep connection alive diff --git a/app/features/sendouq/core/reported-weapons.server.test.ts b/app/features/sendouq/core/reported-weapons.server.test.ts new file mode 100644 index 000000000..67ea9ab4f --- /dev/null +++ b/app/features/sendouq/core/reported-weapons.server.test.ts @@ -0,0 +1,110 @@ +import { suite } from "uvu"; +import * as assert from "uvu/assert"; +import { mergeReportedWeapons } from "./reported-weapons.server"; +import type { MainWeaponId } from "~/modules/in-game-lists"; + +const MergeReportedWeapons = suite("mergeReportedWeapons()"); + +const newWeapons: Parameters[0]["newWeapons"] = [ + { + groupMatchMapId: 1, + mapIndex: 0, + userId: 1, + weaponSplId: 0 as MainWeaponId, + }, +]; + +MergeReportedWeapons("handles no old weapons", () => { + const result = mergeReportedWeapons({ newWeapons, oldWeapons: [] }); + + assert.equal(result, newWeapons); +}); + +MergeReportedWeapons("replaces a weapon", () => { + const result = mergeReportedWeapons({ + newWeapons, + oldWeapons: [ + { + groupMatchMapId: 1, + mapIndex: 0, + userId: 1, + weaponSplId: 1 as MainWeaponId, + }, + ], + }); + + assert.equal(result, newWeapons); +}); + +MergeReportedWeapons("merges two completely separate lists", () => { + const result = mergeReportedWeapons({ + newWeapons, + oldWeapons: [ + { + groupMatchMapId: 1, + mapIndex: 0, + userId: 2, + weaponSplId: 0 as MainWeaponId, + }, + ], + }); + + assert.equal(result, [ + { + groupMatchMapId: 1, + mapIndex: 0, + userId: 2, + weaponSplId: 0 as MainWeaponId, + }, + ...newWeapons, + ]); +}); + +MergeReportedWeapons("handles merging partially same list", () => { + const result = mergeReportedWeapons({ + newWeapons, + oldWeapons: [ + { + groupMatchMapId: 1, + mapIndex: 0, + userId: 1, + weaponSplId: 1 as MainWeaponId, + }, + { + groupMatchMapId: 1, + mapIndex: 0, + userId: 2, + weaponSplId: 0 as MainWeaponId, + }, + ], + }); + + assert.equal(result, [ + ...newWeapons, + { + groupMatchMapId: 1, + mapIndex: 0, + userId: 2, + weaponSplId: 0 as MainWeaponId, + }, + ]); +}); + +MergeReportedWeapons("slices unplayed maps", () => { + const result = mergeReportedWeapons({ + newWeapons, + oldWeapons: [ + { + groupMatchMapId: 1, + mapIndex: 1, + userId: 1, + weaponSplId: 0 as MainWeaponId, + }, + ], + newReportedMapsCount: 1, + }); + + assert.equal(result, newWeapons); +}); + +MergeReportedWeapons.run(); diff --git a/app/features/sendouq/core/reported-weapons.server.ts b/app/features/sendouq/core/reported-weapons.server.ts new file mode 100644 index 000000000..c39fae36a --- /dev/null +++ b/app/features/sendouq/core/reported-weapons.server.ts @@ -0,0 +1,93 @@ +import type { MainWeaponId } from "~/modules/in-game-lists"; +import type { reportedWeaponsByMatchId } from "../queries/reportedWeaponsByMatchId.server"; +import type { MatchById } from "../queries/findMatchById.server"; +import type { GroupForMatch } from "../queries/groupForMatch.server"; + +export type ReportedWeaponForMerging = { + weaponSplId: MainWeaponId; + mapIndex: number; + groupMatchMapId: number; + userId: number; +}; +export function mergeReportedWeapons({ + newWeapons, + oldWeapons, + newReportedMapsCount, +}: { + newWeapons: ReportedWeaponForMerging[]; + oldWeapons: ReportedWeaponForMerging[]; + newReportedMapsCount?: number; +}) { + let result: ReportedWeaponForMerging[] = []; + + // make corrections to the old weapons + for (const oldWeapon of oldWeapons) { + const replacement = newWeapons.find( + (newWeapon) => + newWeapon.groupMatchMapId === oldWeapon.groupMatchMapId && + newWeapon.userId === oldWeapon.userId, + ); + + if (replacement) { + result.push(replacement); + } else { + result.push(oldWeapon); + } + } + + // add new weapons that were not reported in the old list + for (const newWeapon of newWeapons) { + if ( + !result.some( + (oldWeapon) => + newWeapon.groupMatchMapId === oldWeapon.groupMatchMapId && + newWeapon.userId === oldWeapon.userId, + ) + ) { + result.push(newWeapon); + } + } + + // if the score got adjusted we need to get rid of the extra reported weapons + if (newReportedMapsCount) { + result = result.filter((wpn) => wpn.mapIndex < newReportedMapsCount); + } + + return result; +} + +export function reportedWeaponsToArrayOfArrays({ + reportedWeapons, + mapList, + groupAlpha, + groupBravo, +}: { + reportedWeapons: ReturnType; + mapList: MatchById["mapList"]; + groupAlpha: GroupForMatch; + groupBravo: GroupForMatch; +}) { + if (!reportedWeapons) return null; + + const result: (MainWeaponId | null)[][] = []; + + const allMembers = [...groupAlpha.members, ...groupBravo.members].map( + (m) => m.id, + ); + + for (const map of mapList) { + const mapWeapons: (MainWeaponId | null)[] = []; + + for (const userId of allMembers) { + const reportedWeapon = reportedWeapons.find( + (wpn) => wpn.groupMatchMapId === map.id && wpn.userId === userId, + ); + + mapWeapons.push(reportedWeapon ? reportedWeapon.weaponSplId : null); + } + + result.push(mapWeapons); + } + + return result; +} diff --git a/app/features/sendouq/q-schemas.server.ts b/app/features/sendouq/q-schemas.server.ts index 9adf16d3b..536f8e1d4 100644 --- a/app/features/sendouq/q-schemas.server.ts +++ b/app/features/sendouq/q-schemas.server.ts @@ -1,24 +1,23 @@ import { z } from "zod"; -import { - FULL_GROUP_SIZE, - MAP_LIST_PREFERENCE_OPTIONS, - SENDOUQ, - SENDOUQ_BEST_OF, -} from "./q-constants"; +import { languagesUnified } from "~/modules/i18n/config"; import { _action, checkboxValueToBoolean, deduplicate, + falsyToNull, id, + modeShort, noDuplicates, safeJSONParse, - weaponSplId, stageId, - modeShort, - falsyToNull, + weaponSplId, } from "~/utils/zod"; import { matchEndedAtIndex } from "./core/match"; -import { languagesUnified } from "~/modules/i18n/config"; +import { + MAP_LIST_PREFERENCE_OPTIONS, + SENDOUQ, + SENDOUQ_BEST_OF, +} from "./q-constants"; export const frontPageSchema = z.union([ z.object({ @@ -120,10 +119,23 @@ const winners = z.preprocess( return val.length === matchEndedAt + 1; }), ); + +const weapons = z.preprocess( + safeJSONParse, + z.array( + z.object({ + weaponSplId, + userId: id, + mapIndex: z.number().int().nonnegative(), + groupMatchMapId: id, + }), + ), +); export const matchSchema = z.union([ z.object({ _action: _action("REPORT_SCORE"), winners, + weapons, adminReport: z.preprocess( checkboxValueToBoolean, z.boolean().nullish().default(false), @@ -135,10 +147,7 @@ export const matchSchema = z.union([ }), z.object({ _action: _action("REPORT_WEAPONS"), - weapons: z.preprocess( - safeJSONParse, - z.array(z.array(weaponSplId).length(FULL_GROUP_SIZE * 2)), - ), + weapons, }), ]); diff --git a/app/features/sendouq/q.css b/app/features/sendouq/q.css index 329030482..6b31b794a 100644 --- a/app/features/sendouq/q.css +++ b/app/features/sendouq/q.css @@ -282,6 +282,15 @@ text-overflow: ellipsis; } +.q-match__report-section { + display: grid; + grid-template-columns: max-content 1fr; + row-gap: var(--s-2); + column-gap: var(--s-4); + align-items: center; + font-size: var(--fonts-xs); +} + @media screen and (min-width: 640px) { .q-match__teams-container.with-chat { grid-template-columns: 1fr 1fr 1fr; diff --git a/app/features/sendouq/queries/reportedWeaponsByMatchId.server.ts b/app/features/sendouq/queries/reportedWeaponsByMatchId.server.ts index 4688c3bb9..8fb655bd3 100644 --- a/app/features/sendouq/queries/reportedWeaponsByMatchId.server.ts +++ b/app/features/sendouq/queries/reportedWeaponsByMatchId.server.ts @@ -1,11 +1,12 @@ import { sql } from "~/db/sql"; -import type { ReportedWeapon } from "~/db/types"; +import type { GroupMatchMap, ReportedWeapon } from "~/db/types"; const stm = sql.prepare(/* sql */ ` select "ReportedWeapon"."groupMatchMapId", "ReportedWeapon"."weaponSplId", - "ReportedWeapon"."userId" + "ReportedWeapon"."userId", + "GroupMatchMap"."index" as "mapIndex" from "ReportedWeapon" left join "GroupMatchMap" on "GroupMatchMap"."id" = "ReportedWeapon"."groupMatchMapId" @@ -13,7 +14,12 @@ const stm = sql.prepare(/* sql */ ` `); export function reportedWeaponsByMatchId(matchId: number) { - const rows = stm.all({ matchId }) as Array; + const rows = stm.all({ matchId }) as Array< + ReportedWeapon & { + mapIndex: GroupMatchMap["index"]; + groupMatchMapId: number; + } + >; if (rows.length === 0) return null; diff --git a/app/features/sendouq/routes/q.match.$id.tsx b/app/features/sendouq/routes/q.match.$id.tsx index 0bbf19c4b..65790a454 100644 --- a/app/features/sendouq/routes/q.match.$id.tsx +++ b/app/features/sendouq/routes/q.match.$id.tsx @@ -13,7 +13,6 @@ import { Flipped, Flipper } from "react-flip-toolkit"; import invariant from "tiny-invariant"; import { Avatar } from "~/components/Avatar"; import { Button } from "~/components/Button"; -import { ConnectedChat, type ChatProps } from "~/features/chat/components/Chat"; import { WeaponCombobox } from "~/components/Combobox"; import { Divider } from "~/components/Divider"; import { FormWithConfirm } from "~/components/FormWithConfirm"; @@ -26,6 +25,9 @@ import { ArchiveBoxIcon } from "~/components/icons/ArchiveBox"; import { RefreshArrowsIcon } from "~/components/icons/RefreshArrows"; import { sql } from "~/db/sql"; import type { GroupMember, ReportedWeapon } from "~/db/types"; +import * as NotificationService from "~/features/chat/NotificationService.server"; +import type { ChatMessage } from "~/features/chat/chat-types"; +import { ConnectedChat, type ChatProps } from "~/features/chat/components/Chat"; import { currentSeason } from "~/features/mmr"; import { resolveRoomPass } from "~/features/tournament-bracket/tournament-bracket-utils"; import { useIsMounted } from "~/hooks/useIsMounted"; @@ -38,12 +40,7 @@ import { cache } from "~/utils/cache.server"; import { databaseTimestampToDate } from "~/utils/dates"; import { animate } from "~/utils/flip"; import type { SendouRouteHandle } from "~/utils/remix"; -import { - badRequestIfFalsy, - notFoundIfFalsy, - parseRequestFormData, - validate, -} from "~/utils/remix"; +import { notFoundIfFalsy, parseRequestFormData, validate } from "~/utils/remix"; import { inGameNameWithoutDiscriminator } from "~/utils/strings"; import type { Unpacked } from "~/utils/types"; import { assertUnreachable } from "~/utils/types"; @@ -59,6 +56,11 @@ import { import { GroupCard } from "../components/GroupCard"; import { matchEndedAtIndex } from "../core/match"; import { compareMatchToReportedScores } from "../core/match.server"; +import type { ReportedWeaponForMerging } from "../core/reported-weapons.server"; +import { + mergeReportedWeapons, + reportedWeaponsToArrayOfArrays, +} from "../core/reported-weapons.server"; import { calculateMatchSkills } from "../core/skills.server"; import { summarizeMaps, @@ -81,8 +83,6 @@ import { groupForMatch } from "../queries/groupForMatch.server"; import { reportScore } from "../queries/reportScore.server"; import { reportedWeaponsByMatchId } from "../queries/reportedWeaponsByMatchId.server"; import { setGroupAsInactive } from "../queries/setGroupAsInactive.server"; -import * as NotificationService from "~/features/chat/NotificationService.server"; -import type { ChatMessage } from "~/features/chat/chat-types"; export const links: LinksFunction = () => { return [{ rel: "stylesheet", href: styles }]; @@ -107,8 +107,27 @@ export const action = async ({ request, params }: ActionArgs) => { switch (data._action) { case "REPORT_SCORE": { + const reportWeapons = () => { + const oldReportedWeapons = reportedWeaponsByMatchId(matchId) ?? []; + + const mergedWeapons = mergeReportedWeapons({ + oldWeapons: oldReportedWeapons, + newWeapons: data.weapons as (ReportedWeapon & { + mapIndex: number; + groupMatchMapId: number; + })[], + newReportedMapsCount: data.winners.length, + }); + + sql.transaction(() => { + deleteReporterWeaponsByMatchId(matchId); + addReportedWeapons(mergedWeapons); + })(); + }; + const match = notFoundIfFalsy(findMatchById(matchId)); if (match.isLocked) { + reportWeapons(); return null; } @@ -153,6 +172,7 @@ export const action = async ({ request, params }: ActionArgs) => { // same group reporting same score, probably by mistake if (compared === "DUPLICATE") { + reportWeapons(); return null; } @@ -224,6 +244,9 @@ export const action = async ({ request, params }: ActionArgs) => { }; } + // in a different transaction but it's okay + reportWeapons(); + if (match.chatCode) { const type = (): NonNullable => { if (compared === "SAME") { @@ -275,34 +298,19 @@ export const action = async ({ request, params }: ActionArgs) => { const match = notFoundIfFalsy(findMatchById(matchId)); validate(match.reportedAt, "Match has not been reported yet"); - const reportedMaps = match.mapList.reduce( - (acc, cur) => acc + (cur.winnerGroupId ? 1 : 0), - 0, - ); - validate( - reportedMaps === data.weapons.length, - "Not reporting weapons for all maps", - ); + const oldReportedWeapons = reportedWeaponsByMatchId(matchId) ?? []; + + const mergedWeapons = mergeReportedWeapons({ + oldWeapons: oldReportedWeapons, + newWeapons: data.weapons as (ReportedWeapon & { + mapIndex: number; + groupMatchMapId: number; + })[], + }); - const groupAlpha = badRequestIfFalsy(groupForMatch(match.alphaGroupId)); - const groupBravo = badRequestIfFalsy(groupForMatch(match.bravoGroupId)); - const users = [ - ...groupAlpha.members.map((m) => m.id), - ...groupBravo.members.map((m) => m.id), - ]; sql.transaction(() => { deleteReporterWeaponsByMatchId(matchId); - addReportedWeapons( - match.mapList - .filter((m) => m.winnerGroupId) - .flatMap((matchMap, i) => - data.weapons[i].map((weaponSplId, j) => ({ - groupMatchMapId: matchMap.id, - weaponSplId: weaponSplId as MainWeaponId, - userId: users[j], - })), - ), - ); + addReportedWeapons(mergedWeapons); })(); break; @@ -342,6 +350,10 @@ export const loader = async ({ params, request }: LoaderArgs) => { return null; }; + const rawReportedWeapons = match.reportedAt + ? reportedWeaponsByMatchId(matchId) + : null; + return { match: censoredMatch, matchChatCode: canAccessMatchChat ? match.chatCode : null, @@ -355,8 +367,14 @@ export const loader = async ({ params, request }: LoaderArgs) => { ? ("BRAVO" as const) : null, reportedWeapons: match.reportedAt - ? reportedWeaponsByMatchId(matchId) - : undefined, + ? reportedWeaponsToArrayOfArrays({ + groupAlpha, + groupBravo, + mapList: match.mapList, + reportedWeapons: rawReportedWeapons, + }) + : null, + rawReportedWeapons, }; }; @@ -453,7 +471,7 @@ export default function QMatchPage() { reportedAt={data.match.reportedAt} showWeaponsForm={showWeaponsForm} setShowWeaponsForm={setShowWeaponsForm} - key={data.reportedWeapons?.map((w) => w.weaponSplId).join("")} + key={data.reportedWeapons?.join("")} /> ) : null} @@ -501,6 +519,9 @@ export default function QMatchPage() { users={chatUsers} rooms={chatRooms} disabled={!data.canPostChatMessages} + // we don't want the user to lose the weapons they are reporting + // when the match gets suddenly locked + revalidates={false} /> ) : null} @@ -686,38 +707,8 @@ function AfterMatchActions({ showWeaponsForm: boolean; setShowWeaponsForm: (show: boolean) => void; }) { - const { t } = useTranslation(["game-misc"]); const data = useLoaderData(); const lookAgainFetcher = useFetcher(); - const weaponsFetcher = useFetcher(); - - const playedMaps = data.match.mapList.filter((m) => m.winnerGroupId); - - const weaponsUsageInitialValue = () => { - if (!data.reportedWeapons) - return playedMaps.map(() => new Array(FULL_GROUP_SIZE * 2).fill(null)); - - const result: MainWeaponId[][] = []; - - const players = [...data.groupAlpha.members, ...data.groupBravo.members]; - for (const matchMap of data.match.mapList.filter((m) => m.winnerGroupId)) { - result.push( - players.map((u) => { - const weaponSplId = data.reportedWeapons?.find( - (rw) => rw.groupMatchMapId === matchMap.id && rw.userId === u.id, - )?.weaponSplId; - - invariant(typeof weaponSplId === "number", "weaponSplId is null"); - return weaponSplId; - }), - ); - } - - return result; - }; - const [weaponsUsage, setWeaponsUsage] = React.useState< - (null | MainWeaponId)[][] - >(weaponsUsageInitialValue()); const wasReportedInTheLastHour = databaseTimestampToDate(reportedAt).getTime() > Date.now() - 3600 * 1000; @@ -731,10 +722,6 @@ function AfterMatchActions({ const showWeaponsFormButton = wasReportedInTheLastWeek && data.match.mapList[0].winnerGroupId; - const winners = playedMaps.map((m) => - m.winnerGroupId === data.match.alphaGroupId ? "ALPHA" : "BRAVO", - ); - return (
) : null} - {showWeaponsForm ? ( - - -
- {playedMaps.map((map, i) => { - return ( -
- - {i !== 0 ? ( - - ) : null} -
- {[ - ...data.groupAlpha.members, - ...data.groupBravo.members, - ].map((member, j) => { - return ( - - {j === 0 ? ( - Alpha - ) : null} - {j === FULL_GROUP_SIZE ? ( - Bravo - ) : null} -
-
- {" "} - {member.inGameName ? ( - <> - - IGN: - {" "} - {inGameNameWithoutDiscriminator( - member.inGameName, - )} - - ) : ( - member.discordName - )} -
-
- - { - if (!weapon) return; - - setWeaponsUsage((val) => { - const newVal = [...val]; - newVal[i] = [...newVal[i]]; - newVal[i][j] = Number( - weapon.value, - ) as MainWeaponId; - return newVal; - }); - }} - /> -
-
-
- ); - })} -
-
- ); - })} -
-
- {weaponsUsage.map((match, i) => { - return ( -
-
- {t(`game-misc:MODE_SHORT_${data.match.mapList[i].mode}`)}{" "} - {t(`game-misc:STAGE_${data.match.mapList[i].stageId}`)} -
-
- {match.map((weapon, j) => { - return ( - - {typeof weapon === "number" ? ( - - ) : ( - - ? - - )} - {j === 3 ?
: null} - - ); - })} -
-
- ); - })} -
- {weaponsUsage.flat().some((val) => val === null) ? ( -
- Report all weapons to submit -
- ) : ( -
- - Report weapons - -
- )} - - ) : null} + {showWeaponsForm ? : null}
); } +function ReportWeaponsForm() { + const user = useUser(); + const data = useLoaderData(); + const weaponsFetcher = useFetcher(); + + const [weaponsUsage, setWeaponsUsage] = React.useState< + ReportedWeaponForMerging[] + >(data.rawReportedWeapons ?? []); + const [reportingMode, setReportingMode] = React.useState< + "ALL" | "MYSELF" | "MY_TEAM" + >("MYSELF"); + + const playedMaps = data.match.mapList.filter((m) => m.winnerGroupId); + const winners = playedMaps.map((m) => + m.winnerGroupId === data.match.alphaGroupId ? "ALPHA" : "BRAVO", + ); + + const handleCopyWeaponsFromPreviousMap = + ({ + mapIndex, + groupMatchMapId, + }: { + mapIndex: number; + groupMatchMapId: number; + }) => + () => { + setWeaponsUsage((val) => { + const previousWeapons = val.filter( + (reportedWeapon) => reportedWeapon.mapIndex === mapIndex - 1, + ); + + return [ + ...val.filter( + (reportedWeapon) => reportedWeapon.mapIndex !== mapIndex, + ), + ...previousWeapons.map((reportedWeapon) => ({ + ...reportedWeapon, + mapIndex, + groupMatchMapId, + })), + ]; + }); + }; + + const playersToReport = () => { + const allPlayers = [...data.groupAlpha.members, ...data.groupBravo.members]; + + switch (reportingMode) { + case "ALL": { + return allPlayers; + } + case "MYSELF": { + const me = allPlayers.find((m) => m.id === user?.id); + invariant(me, "User not found"); + + return [me]; + } + case "MY_TEAM": { + return data.groupMemberOf === "ALPHA" + ? data.groupAlpha.members + : data.groupBravo.members; + } + default: + assertUnreachable(reportingMode); + } + }; + + return ( + + +
+

Who to report?

+ + + +
+
+ {playedMaps.map((map, i) => { + const groupMatchMapId = map.id; + + return ( +
+ + {i !== 0 ? ( + + ) : null} +
+ {playersToReport().map((member, j) => { + const weaponSplId = + weaponsUsage.find( + (w) => + w.groupMatchMapId === groupMatchMapId && + w.userId === member.id, + )?.weaponSplId ?? null; + + return ( + + {j === 0 && reportingMode === "ALL" ? ( + Alpha + ) : null} + {j === FULL_GROUP_SIZE && reportingMode === "ALL" ? ( + Bravo + ) : null} +
+
+ {" "} + {member.inGameName ? ( + <> + + IGN: + {" "} + {inGameNameWithoutDiscriminator( + member.inGameName, + )} + + ) : ( + member.discordName + )} +
+
+ + { + if (!weapon) return; + + setWeaponsUsage((val) => { + const result = val.filter( + (reportedWeapon) => + reportedWeapon.groupMatchMapId !== + groupMatchMapId || + reportedWeapon.userId !== member.id, + ); + + result.push({ + weaponSplId: Number( + weapon.value, + ) as MainWeaponId, + mapIndex: i, + groupMatchMapId, + userId: member.id, + }); + + return result; + }); + }} + /> +
+
+
+ ); + })} +
+
+ ); + })} +
+ {weaponsUsage.flat().some((val) => val === null) ? ( +
+ Report all weapons to submit +
+ ) : ( +
+ Report weapons +
+ )} +
+ ); +} + function MapList({ canReportScore, isResubmission, @@ -927,6 +987,9 @@ function MapList({ const user = useUser(); const data = useLoaderData(); const [adminToggleChecked, setAdminToggleChecked] = React.useState(false); + const [ownWeaponsUsage, setOwnWeaponsUsage] = React.useState< + ReportedWeaponForMerging[] + >([]); const previouslyReportedWinners = isResubmission ? data.match.mapList @@ -947,14 +1010,18 @@ function MapList({ Boolean(matchEndedAtIndex(winners)) && !data.match.isLocked && newScoresAreDifferent; + const ownWeaponReported = data.rawReportedWeapons?.some( + (reportedWeapon) => reportedWeapon.userId === user?.id, + ); - const allMembers = [ - ...data.groupAlpha.members, - ...data.groupBravo.members, - ].map((m) => m.id); return ( +
{data.match.mapList.map((map, i) => { @@ -966,13 +1033,23 @@ function MapList({ map={map} winners={winners} setWinners={setWinners} - weapons={data.reportedWeapons - ?.filter((w) => w.groupMatchMapId === map.id) - .sort( - (a, b) => - allMembers.indexOf(a.userId) - - allMembers.indexOf(b.userId), - )} + weapons={data.reportedWeapons?.[i]} + showReportedOwnWeapon={!ownWeaponReported} + onOwnWeaponSelected={(newReportedWeapon) => { + if (!newReportedWeapon) return; + + setOwnWeaponsUsage((val) => { + const result = val.filter( + (reportedWeapon) => + reportedWeapon.groupMatchMapId !== + newReportedWeapon.groupMatchMapId, + ); + + result.push(newReportedWeapon); + + return result; + }); + }} /> ); })} @@ -1007,14 +1084,19 @@ function MapListMap({ setWinners, canReportScore, weapons, + onOwnWeaponSelected, + showReportedOwnWeapon, }: { i: number; map: Unpacked["match"]["mapList"]>; winners: ("ALPHA" | "BRAVO")[]; setWinners?: (winners: ("ALPHA" | "BRAVO")[]) => void; canReportScore: boolean; - weapons?: ReportedWeapon[]; + weapons?: (MainWeaponId | null)[] | null; + onOwnWeaponSelected?: (weapon: ReportedWeaponForMerging | null) => void; + showReportedOwnWeapon: boolean; }) { + const user = useUser(); const data = useLoaderData(); const { t } = useTranslation(["game-misc", "tournament"]); @@ -1099,16 +1181,22 @@ function MapListMap({
- {weapons ? ( + {weapons && map.winnerGroupId && !showReportedOwnWeapon ? (
- {weapons.map((w, i) => { + {weapons.map((weaponSplId, i) => { return ( - - + + {typeof weaponSplId === "number" ? ( + + ) : ( +
+ ? +
+ )} {i === 3 ?
: null} ); @@ -1126,34 +1214,60 @@ function MapListMap({ el.style.opacity = "1"; }} > -
+
-
- - -
-
- - +
+
+ + +
+
+ + +
+ + {showReportedOwnWeapon && onOwnWeaponSelected ? ( + <> + + { + const userId = user!.id; + const groupMatchMapId = map.id; + + onOwnWeaponSelected( + weapon + ? { + weaponSplId: Number(weapon.value) as MainWeaponId, + mapIndex: i, + groupMatchMapId, + userId, + } + : null, + ); + }} + /> + + ) : null}
) : null} diff --git a/app/routes/u.$identifier/seasons.tsx b/app/routes/u.$identifier/seasons.tsx index 431851e8a..dbb0f1848 100644 --- a/app/routes/u.$identifier/seasons.tsx +++ b/app/routes/u.$identifier/seasons.tsx @@ -691,6 +691,10 @@ function Match({ return null; }; + const reserveWeaponSpace = + match.groupAlphaMembers.some((m) => m.weaponSplId) || + match.groupBravoMembers.some((m) => m.weaponSplId); + // make sure user's team is always on the top const rows = match.groupAlphaMembers.some((m) => m.id === userId) ? [ @@ -698,11 +702,13 @@ function Match({ key="alpha" members={match.groupAlphaMembers} score={specialScoreMarking() ?? score[0]} + reserveWeaponSpace={reserveWeaponSpace} />, , ] : [ @@ -710,11 +716,13 @@ function Match({ key="bravo" members={match.groupBravoMembers} score={specialScoreMarking() ?? score[1]} + reserveWeaponSpace={reserveWeaponSpace} />, , ]; @@ -734,11 +742,13 @@ function Match({ function MatchMembersRow({ score, members, + reserveWeaponSpace, }: { score: React.ReactNode; members: SerializeFrom< typeof loader >["matches"]["value"][0]["groupAlphaMembers"]; + reserveWeaponSpace: boolean; }) { return (
@@ -755,6 +765,13 @@ function MatchMembersRow({ variant="badge" size={28} /> + ) : reserveWeaponSpace ? ( + ) : null}
);