mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-08-27 21:55:15 -05:00
Show "Replay" indicator in GroupCard
This commit is contained in:
@@ -74,7 +74,10 @@ export function GroupCard({
|
||||
{group.teamMMR.value}
|
||||
</div>
|
||||
)}
|
||||
{group.MMRRelation && <MMRRelation relation={group.MMRRelation} />}
|
||||
{group.MMRRelation && !group.replay && (
|
||||
<MMRRelation relation={group.MMRRelation} />
|
||||
)}
|
||||
{group.replay && <div className="play__card__replay">Replay</div>}
|
||||
<input type="hidden" name="targetGroupId" value={group.id} />
|
||||
{action === "UNITE_GROUPS" && (
|
||||
<input
|
||||
|
||||
@@ -4,6 +4,7 @@ import { BIT_HIGHER_MMR_LIMIT } from "~/constants";
|
||||
import {
|
||||
calculateDifference,
|
||||
groupsToWinningAndLosingPlayerIds,
|
||||
isMatchReplay,
|
||||
scoresAreIdentical,
|
||||
uniteGroupInfo,
|
||||
UniteGroupInfoArg,
|
||||
@@ -15,6 +16,7 @@ const GroupsToWinningAndLosingPlayerIds = suite(
|
||||
"groupsToWinningAndLosingPlayerIds()"
|
||||
);
|
||||
const CalculateDifference = suite("calculateDifference()");
|
||||
const IsMatchReplay = suite("isMatchReplay()");
|
||||
|
||||
const SMALL_GROUP: UniteGroupInfoArg = { id: "small", memberCount: 1 };
|
||||
const BIG_GROUP: UniteGroupInfoArg = { id: "big", memberCount: 3 };
|
||||
@@ -110,7 +112,7 @@ CalculateDifference("Higher/lower", () => {
|
||||
assert.equal(calculateDifference({ ourMMR: 10_000, theirMMR: 0 }), "LOWER");
|
||||
});
|
||||
|
||||
CalculateDifference.only("A bit higher/lower", () => {
|
||||
CalculateDifference("A bit higher/lower", () => {
|
||||
assert.equal(
|
||||
calculateDifference({ ourMMR: 0, theirMMR: BIT_HIGHER_MMR_LIMIT }),
|
||||
"BIT_HIGHER"
|
||||
@@ -121,7 +123,67 @@ CalculateDifference.only("A bit higher/lower", () => {
|
||||
);
|
||||
});
|
||||
|
||||
const user = { id: "a" };
|
||||
const createMembers = (input: string[]) => input.map((v) => ({ memberId: v }));
|
||||
|
||||
IsMatchReplay("Detects replays", () => {
|
||||
assert.ok(
|
||||
isMatchReplay({
|
||||
user,
|
||||
recentMatch: {
|
||||
groups: [
|
||||
{ members: createMembers(["a", "b", "c", "d"]) },
|
||||
{ members: createMembers(["e", "f", "g", "h"]) },
|
||||
],
|
||||
},
|
||||
group: { members: createMembers(["e", "f", "g", "h"]) },
|
||||
})
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
isMatchReplay({
|
||||
user,
|
||||
recentMatch: {
|
||||
groups: [
|
||||
{ members: createMembers(["e", "f", "g", "h"]) },
|
||||
{ members: createMembers(["a", "b", "c", "d"]) },
|
||||
],
|
||||
},
|
||||
group: { members: createMembers(["e", "f", "g", "1"]) },
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
IsMatchReplay("Detects not replays", () => {
|
||||
assert.not.ok(
|
||||
isMatchReplay({
|
||||
user,
|
||||
recentMatch: {
|
||||
groups: [
|
||||
{ members: createMembers(["a", "b", "c", "d"]) },
|
||||
{ members: createMembers(["e", "f", "g", "h"]) },
|
||||
],
|
||||
},
|
||||
group: { members: createMembers(["1", "2", "3", "4"]) },
|
||||
})
|
||||
);
|
||||
|
||||
assert.not.ok(
|
||||
isMatchReplay({
|
||||
user,
|
||||
recentMatch: {
|
||||
groups: [
|
||||
{ members: createMembers(["a", "b", "c", "d"]) },
|
||||
{ members: createMembers(["e", "f", "g", "h"]) },
|
||||
],
|
||||
},
|
||||
group: { members: createMembers(["e", "f", "3", "4"]) },
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
UniteGroupInfo.run();
|
||||
ScoresAreIdentical.run();
|
||||
GroupsToWinningAndLosingPlayerIds.run();
|
||||
CalculateDifference.run();
|
||||
IsMatchReplay.run();
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
LFG_GROUP_INACTIVE_MINUTES,
|
||||
} from "~/constants";
|
||||
import * as LFGGroup from "~/models/LFGGroup.server";
|
||||
import * as LFGMatch from "~/models/LFGMatch.server";
|
||||
import { PlayFrontPageLoader } from "~/routes/play/index";
|
||||
import {
|
||||
LookingLoaderData,
|
||||
@@ -132,6 +133,8 @@ export function otherGroupsForResponse({
|
||||
lookingForMatch,
|
||||
ownGroup,
|
||||
useRelativeSkillLevel,
|
||||
recentMatch,
|
||||
user,
|
||||
}: {
|
||||
groups: LFGGroup.FindLookingAndOwnActive;
|
||||
likes: {
|
||||
@@ -141,6 +144,8 @@ export function otherGroupsForResponse({
|
||||
lookingForMatch: boolean;
|
||||
ownGroup: Unpacked<LFGGroup.FindLookingAndOwnActive>;
|
||||
useRelativeSkillLevel: boolean;
|
||||
recentMatch: LFGMatch.RecentOfUser;
|
||||
user: { id: string };
|
||||
}) {
|
||||
return (
|
||||
groups
|
||||
@@ -179,6 +184,7 @@ export function otherGroupsForResponse({
|
||||
};
|
||||
}),
|
||||
ranked: ranked(),
|
||||
replay: isMatchReplay({ user, group, recentMatch }),
|
||||
MMRRelation:
|
||||
ownGroup.ranked &&
|
||||
group.ranked &&
|
||||
@@ -220,6 +226,34 @@ export function otherGroupsForResponse({
|
||||
);
|
||||
}
|
||||
|
||||
export function isMatchReplay({
|
||||
recentMatch,
|
||||
user,
|
||||
group,
|
||||
}: {
|
||||
recentMatch: { groups: { members: { memberId: string }[] }[] } | null;
|
||||
user: { id: string };
|
||||
group: { members: { memberId: string }[] };
|
||||
}): boolean {
|
||||
if (!recentMatch) return false;
|
||||
|
||||
const opponentGroupOfRecent = recentMatch.groups.find((g) =>
|
||||
g.members.every((m) => m.memberId !== user.id)
|
||||
);
|
||||
invariant(
|
||||
opponentGroupOfRecent,
|
||||
"Unexpected opponentGroupOfRecent undefined"
|
||||
);
|
||||
|
||||
const memberIdsOfGroup = new Set(group.members.map((m) => m.memberId));
|
||||
let sameCount = 0;
|
||||
for (const { memberId } of opponentGroupOfRecent.members) {
|
||||
if (memberIdsOfGroup.has(memberId)) sameCount++;
|
||||
}
|
||||
|
||||
return sameCount > 2;
|
||||
}
|
||||
|
||||
export function filterExpiredGroups(group: { lastActionAt: Date }) {
|
||||
const { EXPIRED: expiredDate } = groupExpiredDates();
|
||||
|
||||
|
||||
@@ -64,6 +64,42 @@ export function findByUserId({ userId }: { userId: string }) {
|
||||
});
|
||||
}
|
||||
|
||||
export type RecentOfUser = Prisma.PromiseReturnType<typeof recentOfUser>;
|
||||
export function recentOfUser(userId: string) {
|
||||
const twoHoursAgo = () => {
|
||||
const result = new Date();
|
||||
result.setHours(result.getHours() - 2);
|
||||
|
||||
return result;
|
||||
};
|
||||
return db.lfgGroupMatch.findFirst({
|
||||
where: {
|
||||
groups: {
|
||||
some: {
|
||||
members: {
|
||||
some: {
|
||||
memberId: userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
createdAt: {
|
||||
gte: twoHoursAgo(),
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
include: {
|
||||
groups: {
|
||||
include: {
|
||||
members: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function reportScore({
|
||||
UNSAFE_matchId,
|
||||
UNSAFE_winnerGroupIds,
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
import { canUniteWithGroup, isGroupAdmin } from "~/core/play/validators";
|
||||
import { usePolling } from "~/hooks/common";
|
||||
import * as LFGGroup from "~/models/LFGGroup.server";
|
||||
import * as LFGMatch from "~/models/LFGMatch.server";
|
||||
import styles from "~/styles/play-looking.css";
|
||||
import {
|
||||
makeTitle,
|
||||
@@ -219,6 +220,7 @@ export type LookingLoaderDataGroup = {
|
||||
};
|
||||
MMRRelation?: "LOWER" | "BIT_LOWER" | "CLOSE" | "BIT_HIGHER" | "HIGHER";
|
||||
ranked?: boolean;
|
||||
replay?: boolean;
|
||||
};
|
||||
|
||||
export interface LookingLoaderData {
|
||||
@@ -233,7 +235,10 @@ export interface LookingLoaderData {
|
||||
|
||||
export const loader: LoaderFunction = async ({ context }) => {
|
||||
const user = requireUser(context);
|
||||
const { groups, ownGroup } = await LFGGroup.findLookingAndOwnActive(user.id);
|
||||
const [{ groups, ownGroup }, recentMatch] = await Promise.all([
|
||||
LFGGroup.findLookingAndOwnActive(user.id),
|
||||
LFGMatch.recentOfUser(user.id),
|
||||
]);
|
||||
|
||||
if (!ownGroup) return redirect("/play");
|
||||
if (ownGroup.status === "MATCH") {
|
||||
@@ -283,6 +288,8 @@ export const loader: LoaderFunction = async ({ context }) => {
|
||||
isCaptain: isGroupAdmin({ group: ownGroup, user }),
|
||||
lastActionAtTimestamp: ownGroup.lastActionAt.getTime(),
|
||||
...otherGroupsForResponse({
|
||||
recentMatch,
|
||||
user,
|
||||
useRelativeSkillLevel: USE_RELATIVE_SKILL_LEVEL,
|
||||
groups: groupsOfType,
|
||||
lookingForMatch,
|
||||
|
||||
@@ -116,6 +116,12 @@
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
.play__card__replay {
|
||||
color: var(--theme-info);
|
||||
font-size: var(--fonts-sm);
|
||||
font-weight: var(--semi-bold);
|
||||
}
|
||||
|
||||
.play__card__member-power {
|
||||
display: flex;
|
||||
margin-top: -4px;
|
||||
|
||||
@@ -80,6 +80,7 @@ export async function seed(variation?: SeedVariations) {
|
||||
}
|
||||
if (variation === "looking" || variation === "looking-match") {
|
||||
await ownGroup(variation === "looking-match", remainingUserIdsForGroups);
|
||||
await lfgMatches();
|
||||
}
|
||||
|
||||
async function adminUser() {
|
||||
@@ -405,19 +406,32 @@ export async function seed(variation?: SeedVariations) {
|
||||
}
|
||||
|
||||
for (let i = 0; i < 24; i++) {
|
||||
const members = {
|
||||
createMany: {
|
||||
data: new Array(4).fill(null).map((_, i) => ({
|
||||
memberId: userIdsStack.shift()!,
|
||||
captain: i === 0,
|
||||
})),
|
||||
},
|
||||
};
|
||||
if (i === 0) {
|
||||
await prisma.lfgGroup.create({
|
||||
data: {
|
||||
id: `quad-${i + 1}-past`,
|
||||
status: "INACTIVE",
|
||||
type: "VERSUS",
|
||||
ranked: true,
|
||||
members,
|
||||
},
|
||||
});
|
||||
}
|
||||
await prisma.lfgGroup.create({
|
||||
data: {
|
||||
id: `quad-${i + 1}`,
|
||||
status: "LOOKING",
|
||||
type: "VERSUS",
|
||||
ranked: i < 12,
|
||||
members: {
|
||||
createMany: {
|
||||
data: new Array(4).fill(null).map((_, i) => ({
|
||||
memberId: userIdsStack.shift()!,
|
||||
captain: i === 0,
|
||||
})),
|
||||
},
|
||||
},
|
||||
members,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -442,6 +456,20 @@ export async function seed(variation?: SeedVariations) {
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.lfgGroup.create({
|
||||
data: {
|
||||
id: "own-group-past",
|
||||
status: "INACTIVE",
|
||||
type: "VERSUS",
|
||||
ranked: true,
|
||||
members: {
|
||||
createMany: {
|
||||
data: members,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return prisma.lfgGroup.create({
|
||||
data: {
|
||||
status: "LOOKING",
|
||||
@@ -463,6 +491,26 @@ export async function seed(variation?: SeedVariations) {
|
||||
});
|
||||
}
|
||||
|
||||
async function lfgMatches() {
|
||||
return prisma.lfgGroupMatch.create({
|
||||
data: {
|
||||
groups: {
|
||||
connect: [{ id: "quad-1-past" }, { id: "own-group-past" }],
|
||||
},
|
||||
stages: {
|
||||
createMany: {
|
||||
data: [
|
||||
{ order: 1, stageId: 1, winnerGroupId: "own-group-past" },
|
||||
{ order: 2, stageId: 40, winnerGroupId: "own-group-past" },
|
||||
{ order: 3, stageId: 60, winnerGroupId: "own-group-past" },
|
||||
{ order: 4, stageId: 101, winnerGroupId: "own-group-past" },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function tournamentRoundsCreate() {
|
||||
const stages = await prisma.stage.findMany({});
|
||||
await createTournamentRounds({
|
||||
|
||||
Reference in New Issue
Block a user