mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-08-27 13:45:27 -05:00
Reported weapons while reporting score + report only own team or own (#1527)
* Initial * Move code * More events implemented * Auto refresh take in account recent revalidates * Add sound effects * Add creds * Settings * Add error handling * Reported weapons merging logic * TODOs * Add envs to example * Reported weapons new input format * Report own/own team/all wpn * Report own weapon when reporting score * Don't revalidate * Gridified * Remove irrelevant comment * Don't wipe weapons if different
This commit is contained in:
@@ -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
|
||||
|
||||
110
app/features/sendouq/core/reported-weapons.server.test.ts
Normal file
110
app/features/sendouq/core/reported-weapons.server.test.ts
Normal file
@@ -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<typeof mergeReportedWeapons>[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();
|
||||
93
app/features/sendouq/core/reported-weapons.server.ts
Normal file
93
app/features/sendouq/core/reported-weapons.server.ts
Normal file
@@ -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<typeof reportedWeaponsByMatchId>;
|
||||
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;
|
||||
}
|
||||
@@ -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,
|
||||
}),
|
||||
]);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<ReportedWeapon>;
|
||||
const rows = stm.all({ matchId }) as Array<
|
||||
ReportedWeapon & {
|
||||
mapIndex: GroupMatchMap["index"];
|
||||
groupMatchMapId: number;
|
||||
}
|
||||
>;
|
||||
|
||||
if (rows.length === 0) return null;
|
||||
|
||||
|
||||
@@ -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<ChatMessage["type"]> => {
|
||||
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}
|
||||
</div>
|
||||
@@ -686,38 +707,8 @@ function AfterMatchActions({
|
||||
showWeaponsForm: boolean;
|
||||
setShowWeaponsForm: (show: boolean) => void;
|
||||
}) {
|
||||
const { t } = useTranslation(["game-misc"]);
|
||||
const data = useLoaderData<typeof loader>();
|
||||
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 (
|
||||
<div className="stack lg">
|
||||
<lookAgainFetcher.Form
|
||||
@@ -761,160 +748,233 @@ function AfterMatchActions({
|
||||
</Button>
|
||||
) : null}
|
||||
</lookAgainFetcher.Form>
|
||||
{showWeaponsForm ? (
|
||||
<weaponsFetcher.Form method="post" className="stack lg">
|
||||
<input
|
||||
type="hidden"
|
||||
name="weapons"
|
||||
value={JSON.stringify(weaponsUsage)}
|
||||
/>
|
||||
<div className="stack md mx-auto">
|
||||
{playedMaps.map((map, i) => {
|
||||
return (
|
||||
<div key={map.stageId} className="stack md">
|
||||
<MapListMap
|
||||
canReportScore={false}
|
||||
i={i}
|
||||
map={map}
|
||||
winners={winners}
|
||||
/>
|
||||
{i !== 0 ? (
|
||||
<Button
|
||||
size="tiny"
|
||||
variant="outlined"
|
||||
className="self-center"
|
||||
onClick={() => {
|
||||
setWeaponsUsage((val) => {
|
||||
const newVal = [...val];
|
||||
newVal[i] = [...newVal[i - 1]];
|
||||
return newVal;
|
||||
});
|
||||
}}
|
||||
>
|
||||
Copy weapons from above map
|
||||
</Button>
|
||||
) : null}
|
||||
<div className="stack sm">
|
||||
{[
|
||||
...data.groupAlpha.members,
|
||||
...data.groupBravo.members,
|
||||
].map((member, j) => {
|
||||
return (
|
||||
<React.Fragment key={member.id}>
|
||||
{j === 0 ? (
|
||||
<Divider className="text-sm">Alpha</Divider>
|
||||
) : null}
|
||||
{j === FULL_GROUP_SIZE ? (
|
||||
<Divider className="text-sm">Bravo</Divider>
|
||||
) : null}
|
||||
<div
|
||||
key={member.id}
|
||||
className="stack horizontal sm justify-between items-center flex-wrap"
|
||||
>
|
||||
<div className="q-match__report__user-name-container">
|
||||
<Avatar user={member} size="xxs" />{" "}
|
||||
{member.inGameName ? (
|
||||
<>
|
||||
<span className="text-lighter font-semi-bold">
|
||||
IGN:
|
||||
</span>{" "}
|
||||
{inGameNameWithoutDiscriminator(
|
||||
member.inGameName,
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
member.discordName
|
||||
)}
|
||||
</div>
|
||||
<div className="stack horizontal sm items-center">
|
||||
<WeaponImage
|
||||
weaponSplId={weaponsUsage[i][j] ?? 0}
|
||||
variant="badge"
|
||||
width={32}
|
||||
className={clsx("ml-auto", {
|
||||
invisible:
|
||||
typeof weaponsUsage[i][j] !== "number",
|
||||
})}
|
||||
/>
|
||||
<WeaponCombobox
|
||||
inputName="weapon"
|
||||
value={weaponsUsage[i][j]}
|
||||
onChange={(weapon) => {
|
||||
if (!weapon) return;
|
||||
|
||||
setWeaponsUsage((val) => {
|
||||
const newVal = [...val];
|
||||
newVal[i] = [...newVal[i]];
|
||||
newVal[i][j] = Number(
|
||||
weapon.value,
|
||||
) as MainWeaponId;
|
||||
return newVal;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="stack sm">
|
||||
{weaponsUsage.map((match, i) => {
|
||||
return (
|
||||
<div key={i} className="stack xs">
|
||||
<div className="text-sm font-semi-bold text-center">
|
||||
{t(`game-misc:MODE_SHORT_${data.match.mapList[i].mode}`)}{" "}
|
||||
{t(`game-misc:STAGE_${data.match.mapList[i].stageId}`)}
|
||||
</div>
|
||||
<div className="stack sm horizontal justify-center items-center">
|
||||
{match.map((weapon, j) => {
|
||||
return (
|
||||
<React.Fragment key={j}>
|
||||
{typeof weapon === "number" ? (
|
||||
<WeaponImage
|
||||
key={j}
|
||||
weaponSplId={weapon}
|
||||
variant="badge"
|
||||
size={32}
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
className="text-lg font-bold text-center q-match__weapon-grid-item"
|
||||
key={j}
|
||||
>
|
||||
?
|
||||
</span>
|
||||
)}
|
||||
{j === 3 ? <div className="w-4" /> : null}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{weaponsUsage.flat().some((val) => val === null) ? (
|
||||
<div className="text-sm text-center text-warning font-semi-bold">
|
||||
Report all weapons to submit
|
||||
</div>
|
||||
) : (
|
||||
<div className="stack items-center">
|
||||
<SubmitButton _action="REPORT_WEAPONS">
|
||||
Report weapons
|
||||
</SubmitButton>
|
||||
</div>
|
||||
)}
|
||||
</weaponsFetcher.Form>
|
||||
) : null}
|
||||
{showWeaponsForm ? <ReportWeaponsForm /> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ReportWeaponsForm() {
|
||||
const user = useUser();
|
||||
const data = useLoaderData<typeof loader>();
|
||||
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 (
|
||||
<weaponsFetcher.Form method="post" className="stack lg">
|
||||
<input
|
||||
type="hidden"
|
||||
name="weapons"
|
||||
value={JSON.stringify(weaponsUsage)}
|
||||
/>
|
||||
<div className="stack horizontal sm justify-between w-max mx-auto">
|
||||
<h3 className="text-md">Who to report?</h3>
|
||||
<label className="stack horizontal xs items-center mb-0">
|
||||
Me
|
||||
<input
|
||||
type="radio"
|
||||
checked={reportingMode === "MYSELF"}
|
||||
onChange={() => setReportingMode("MYSELF")}
|
||||
/>
|
||||
</label>
|
||||
<label className="stack horizontal xs items-center mb-0">
|
||||
My team
|
||||
<input
|
||||
type="radio"
|
||||
checked={reportingMode === "MY_TEAM"}
|
||||
onChange={() => setReportingMode("MY_TEAM")}
|
||||
/>
|
||||
</label>
|
||||
<label className="stack horizontal xs items-center mb-0">
|
||||
Everyone
|
||||
<input
|
||||
type="radio"
|
||||
checked={reportingMode === "ALL"}
|
||||
onChange={() => setReportingMode("ALL")}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="stack md mx-auto">
|
||||
{playedMaps.map((map, i) => {
|
||||
const groupMatchMapId = map.id;
|
||||
|
||||
return (
|
||||
<div key={map.stageId} className="stack md">
|
||||
<MapListMap
|
||||
canReportScore={false}
|
||||
i={i}
|
||||
map={map}
|
||||
winners={winners}
|
||||
showReportedOwnWeapon={false}
|
||||
/>
|
||||
{i !== 0 ? (
|
||||
<Button
|
||||
size="tiny"
|
||||
variant="outlined"
|
||||
className="self-center"
|
||||
onClick={handleCopyWeaponsFromPreviousMap({
|
||||
groupMatchMapId,
|
||||
mapIndex: i,
|
||||
})}
|
||||
>
|
||||
Copy weapons from above map
|
||||
</Button>
|
||||
) : null}
|
||||
<div className="stack sm">
|
||||
{playersToReport().map((member, j) => {
|
||||
const weaponSplId =
|
||||
weaponsUsage.find(
|
||||
(w) =>
|
||||
w.groupMatchMapId === groupMatchMapId &&
|
||||
w.userId === member.id,
|
||||
)?.weaponSplId ?? null;
|
||||
|
||||
return (
|
||||
<React.Fragment key={member.id}>
|
||||
{j === 0 && reportingMode === "ALL" ? (
|
||||
<Divider className="text-sm">Alpha</Divider>
|
||||
) : null}
|
||||
{j === FULL_GROUP_SIZE && reportingMode === "ALL" ? (
|
||||
<Divider className="text-sm">Bravo</Divider>
|
||||
) : null}
|
||||
<div
|
||||
key={member.id}
|
||||
className="stack horizontal sm justify-between items-center flex-wrap"
|
||||
>
|
||||
<div className="q-match__report__user-name-container">
|
||||
<Avatar user={member} size="xxs" />{" "}
|
||||
{member.inGameName ? (
|
||||
<>
|
||||
<span className="text-lighter font-semi-bold">
|
||||
IGN:
|
||||
</span>{" "}
|
||||
{inGameNameWithoutDiscriminator(
|
||||
member.inGameName,
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
member.discordName
|
||||
)}
|
||||
</div>
|
||||
<div className="stack horizontal sm items-center">
|
||||
<WeaponImage
|
||||
weaponSplId={weaponSplId ?? 0}
|
||||
variant="badge"
|
||||
width={32}
|
||||
className={clsx("ml-auto", {
|
||||
invisible: typeof weaponSplId !== "number",
|
||||
})}
|
||||
/>
|
||||
<WeaponCombobox
|
||||
inputName="weapon"
|
||||
value={weaponSplId}
|
||||
onChange={(weapon) => {
|
||||
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;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{weaponsUsage.flat().some((val) => val === null) ? (
|
||||
<div className="text-sm text-center text-warning font-semi-bold">
|
||||
Report all weapons to submit
|
||||
</div>
|
||||
) : (
|
||||
<div className="stack items-center">
|
||||
<SubmitButton _action="REPORT_WEAPONS">Report weapons</SubmitButton>
|
||||
</div>
|
||||
)}
|
||||
</weaponsFetcher.Form>
|
||||
);
|
||||
}
|
||||
|
||||
function MapList({
|
||||
canReportScore,
|
||||
isResubmission,
|
||||
@@ -927,6 +987,9 @@ function MapList({
|
||||
const user = useUser();
|
||||
const data = useLoaderData<typeof loader>();
|
||||
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 (
|
||||
<fetcher.Form method="post">
|
||||
<input type="hidden" name="winners" value={JSON.stringify(winners)} />
|
||||
<input
|
||||
type="hidden"
|
||||
name="weapons"
|
||||
value={JSON.stringify(ownWeaponsUsage)}
|
||||
/>
|
||||
<Flipper flipKey={winners.join("")}>
|
||||
<div className="stack md w-max mx-auto">
|
||||
{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<SerializeFrom<typeof loader>["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<typeof loader>();
|
||||
const { t } = useTranslation(["game-misc", "tournament"]);
|
||||
|
||||
@@ -1099,16 +1181,22 @@ function MapListMap({
|
||||
</div>
|
||||
</div>
|
||||
</Flipped>
|
||||
{weapons ? (
|
||||
{weapons && map.winnerGroupId && !showReportedOwnWeapon ? (
|
||||
<div className="stack sm horizontal">
|
||||
{weapons.map((w, i) => {
|
||||
{weapons.map((weaponSplId, i) => {
|
||||
return (
|
||||
<React.Fragment key={w.userId}>
|
||||
<WeaponImage
|
||||
weaponSplId={w.weaponSplId}
|
||||
size={30}
|
||||
variant="badge"
|
||||
/>
|
||||
<React.Fragment key={i}>
|
||||
{typeof weaponSplId === "number" ? (
|
||||
<WeaponImage
|
||||
weaponSplId={weaponSplId}
|
||||
size={30}
|
||||
variant="badge"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-4 font-semi-bold stack items-center justify-center">
|
||||
?
|
||||
</div>
|
||||
)}
|
||||
{i === 3 ? <div className="w-4" /> : null}
|
||||
</React.Fragment>
|
||||
);
|
||||
@@ -1126,34 +1214,60 @@ function MapListMap({
|
||||
el.style.opacity = "1";
|
||||
}}
|
||||
>
|
||||
<div className="stack horizontal sm text-xs">
|
||||
<div className="q-match__report-section">
|
||||
<label className="mb-0 text-theme-secondary">Winner</label>
|
||||
<div className="stack sm horizontal items-center font-semi-bold">
|
||||
<input
|
||||
type="radio"
|
||||
name={`winner-${i}`}
|
||||
value="alpha"
|
||||
id={`alpha-${i}`}
|
||||
checked={winners[i] === "ALPHA"}
|
||||
onChange={handleReportScore(i, "ALPHA")}
|
||||
/>
|
||||
<label className="mb-0" htmlFor={`alpha-${i}`}>
|
||||
{`Alpha${relativeSideText("ALPHA")}`}
|
||||
</label>
|
||||
</div>
|
||||
<div className="stack sm horizontal items-center font-semi-bold">
|
||||
<input
|
||||
type="radio"
|
||||
name={`winner-${i}`}
|
||||
value="bravo"
|
||||
id={`bravo-${i}`}
|
||||
checked={winners[i] === "BRAVO"}
|
||||
onChange={handleReportScore(i, "BRAVO")}
|
||||
/>
|
||||
<label className="mb-0" htmlFor={`bravo-${i}`}>
|
||||
{`Bravo${relativeSideText("BRAVO")}`}
|
||||
</label>
|
||||
<div className="stack sm horizontal items-center">
|
||||
<div className="stack sm horizontal items-center font-semi-bold">
|
||||
<input
|
||||
type="radio"
|
||||
name={`winner-${i}`}
|
||||
value="alpha"
|
||||
id={`alpha-${i}`}
|
||||
checked={winners[i] === "ALPHA"}
|
||||
onChange={handleReportScore(i, "ALPHA")}
|
||||
/>
|
||||
<label className="mb-0" htmlFor={`alpha-${i}`}>
|
||||
{`Alpha${relativeSideText("ALPHA")}`}
|
||||
</label>
|
||||
</div>
|
||||
<div className="stack sm horizontal items-center font-semi-bold">
|
||||
<input
|
||||
type="radio"
|
||||
name={`winner-${i}`}
|
||||
value="bravo"
|
||||
id={`bravo-${i}`}
|
||||
checked={winners[i] === "BRAVO"}
|
||||
onChange={handleReportScore(i, "BRAVO")}
|
||||
/>
|
||||
<label className="mb-0" htmlFor={`bravo-${i}`}>
|
||||
{`Bravo${relativeSideText("BRAVO")}`}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showReportedOwnWeapon && onOwnWeaponSelected ? (
|
||||
<>
|
||||
<label className="mb-0 text-theme-secondary">Your weapon</label>
|
||||
<WeaponCombobox
|
||||
inputName="weapon"
|
||||
onChange={(weapon) => {
|
||||
const userId = user!.id;
|
||||
const groupMatchMapId = map.id;
|
||||
|
||||
onOwnWeaponSelected(
|
||||
weapon
|
||||
? {
|
||||
weaponSplId: Number(weapon.value) as MainWeaponId,
|
||||
mapIndex: i,
|
||||
groupMatchMapId,
|
||||
userId,
|
||||
}
|
||||
: null,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</Flipped>
|
||||
) : null}
|
||||
|
||||
@@ -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}
|
||||
/>,
|
||||
<MatchMembersRow
|
||||
key="bravo"
|
||||
members={match.groupBravoMembers}
|
||||
score={specialScoreMarking() ?? score[1]}
|
||||
reserveWeaponSpace={reserveWeaponSpace}
|
||||
/>,
|
||||
]
|
||||
: [
|
||||
@@ -710,11 +716,13 @@ function Match({
|
||||
key="bravo"
|
||||
members={match.groupBravoMembers}
|
||||
score={specialScoreMarking() ?? score[1]}
|
||||
reserveWeaponSpace={reserveWeaponSpace}
|
||||
/>,
|
||||
<MatchMembersRow
|
||||
key="alpha"
|
||||
members={match.groupAlphaMembers}
|
||||
score={specialScoreMarking() ?? score[0]}
|
||||
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 (
|
||||
<div className="stack horizontal xs items-center">
|
||||
@@ -755,6 +765,13 @@ function MatchMembersRow({
|
||||
variant="badge"
|
||||
size={28}
|
||||
/>
|
||||
) : reserveWeaponSpace ? (
|
||||
<WeaponImage
|
||||
weaponSplId={0}
|
||||
variant="badge"
|
||||
size={28}
|
||||
className="invisible"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user