Skip to content

Commit

Permalink
SendouQ match page show & save powers + plus tier status (#1519)
Browse files Browse the repository at this point in the history
* Initial

* New group cards initial

* Remove unused prop

* Vc

* Styling for after the match is locked

* Team

* Impersonate always in dev

* Diff

* Fix crash if match has no memento when inserting skill
  • Loading branch information
Sendouc authored Oct 3, 2023
1 parent 4c6254d commit 218c854
Show file tree
Hide file tree
Showing 22 changed files with 587 additions and 229 deletions.
12 changes: 10 additions & 2 deletions app/db/seed/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1732,6 +1732,7 @@ function playedMatches() {

invariant(groupAlpha !== 0 && groupBravo !== 0, "groups not created");

// @ts-expect-error creating without memento on purpose
const match = createMatch({
alphaGroupId: groupAlpha,
bravoGroupId: groupBravo,
Expand Down Expand Up @@ -1772,10 +1773,12 @@ function playedMatches() {
const winner = winnersArrayToWinner(winners);
const finishedMatch = findMatchById(match.id)!;

const newSkills = calculateMatchSkills({
const { newSkills, differences } = calculateMatchSkills({
groupMatchId: match.id,
winner: winner === "ALPHA" ? groupAlphaMembers : groupBravoMembers,
loser: winner === "ALPHA" ? groupBravoMembers : groupAlphaMembers,
loserGroupId: winner === "ALPHA" ? groupBravo : groupAlpha,
winnerGroupId: winner === "ALPHA" ? groupAlpha : groupBravo,
});
const members = [
...groupForMatch(match.alphaGroupId)!.members.map((m) => ({
Expand All @@ -1794,7 +1797,12 @@ function playedMatches() {
Math.random() > 0.5 ? groupAlphaMembers[0] : groupBravoMembers[0],
winners,
});
addSkills(newSkills);
addSkills({
skills: newSkills,
differences,
groupMatchId: match.id,
oldMatchMemento: { users: {}, groups: {} },
});
setGroupAsInactive(groupAlpha);
setGroupAsInactive(groupBravo);
addMapResults(summarizeMaps({ match: finishedMatch, members, winners }));
Expand Down
39 changes: 39 additions & 0 deletions app/db/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
StageId,
} from "~/modules/in-game-lists";
import type allTags from "../routes/calendar/tags.json";
import type { TieredSkill } from "~/features/mmr/tiered.server";

export interface User {
id: number;
Expand Down Expand Up @@ -543,6 +544,43 @@ export interface GroupLike {
createdAt: number;
}

type CalculatingSkill = {
calculated: false;
matchesCount: number;
matchesCountNeeded: number;
/** Freshly calculated skill */
newSp?: number;
};
export type UserSkillDifference =
| {
calculated: true;
spDiff: number;
}
| CalculatingSkill;
export type GroupSkillDifference =
| {
calculated: true;
oldSp: number;
newSp: number;
}
| CalculatingSkill;
export type ParsedMemento = {
users: Record<
User["id"],
{
plusTier?: PlusTier["tier"];
skill?: TieredSkill;
skillDifference?: UserSkillDifference;
}
>;
groups: Record<
Group["id"],
{
tier?: TieredSkill["tier"];
skillDifference?: GroupSkillDifference;
}
>;
};
export interface GroupMatch {
id: number;
alphaGroupId: number;
Expand All @@ -551,6 +589,7 @@ export interface GroupMatch {
reportedAt: number | null;
reportedByUserId: number | null;
chatCode: string | null;
memento: string | null;
}

export interface GroupMatchMap {
Expand Down
12 changes: 6 additions & 6 deletions app/features/mmr/mmr-utils.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ export function queryCurrentUserRating({
const skill = findCurrentSkillByUserId({ userId, season: season ?? null });

if (!skill) {
return rating();
return { rating: rating(), matchesCount: 0 };
}

return rating(skill);
return { rating: rating(skill), matchesCount: skill.matchesCount };
}

export function queryCurrentTeamRating({
Expand All @@ -31,9 +31,9 @@ export function queryCurrentTeamRating({
season,
});

if (!skill) return rating();
if (!skill) return { rating: rating(), matchesCount: 0 };

return rating(skill);
return { rating: rating(skill), matchesCount: skill.matchesCount };
}

export function queryTeamPlayerRatingAverage({
Expand All @@ -43,8 +43,8 @@ export function queryTeamPlayerRatingAverage({
identifier: string;
season: number;
}) {
const playerRatings = identifierToUserIds(identifier).map((userId) =>
queryCurrentUserRating({ userId, season }),
const playerRatings = identifierToUserIds(identifier).map(
(userId) => queryCurrentUserRating({ userId, season }).rating,
);

if (playerRatings.length === 0) return rating();
Expand Down
8 changes: 6 additions & 2 deletions app/features/mmr/queries/findCurrentSkillByUserId.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import type { Skill } from "~/db/types";
const stm = sql.prepare(/* sql */ `
select
"mu",
"sigma"
"sigma",
"matchesCount"
from
"Skill"
where
Expand All @@ -24,5 +25,8 @@ export function findCurrentSkillByUserId({
userId: number;
season: number;
}) {
return stm.get({ userId, season }) as Pick<Skill, "mu" | "sigma"> | null;
return stm.get({ userId, season }) as Pick<
Skill,
"mu" | "sigma" | "matchesCount"
> | null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import type { Skill } from "~/db/types";
const stm = sql.prepare(/* sql */ `
select
"mu",
"sigma"
"sigma",
"matchesCount"
from
"Skill"
where
Expand All @@ -24,5 +25,8 @@ export function findCurrentTeamSkillByIdentifier({
identifier: string;
season: number;
}) {
return stm.get({ identifier, season }) as Pick<Skill, "mu" | "sigma"> | null;
return stm.get({ identifier, season }) as Pick<
Skill,
"mu" | "sigma" | "matchesCount"
> | null;
}
123 changes: 115 additions & 8 deletions app/features/sendouq/components/GroupCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { SubmitButton } from "~/components/SubmitButton";
import { MicrophoneIcon } from "~/components/icons/Microphone";
import { SpeakerIcon } from "~/components/icons/Speaker";
import { SpeakerXIcon } from "~/components/icons/SpeakerX";
import type { Group, GroupMember as GroupMemberType } from "~/db/types";
import type { GroupMember as GroupMemberType, ParsedMemento } from "~/db/types";
import { ordinalToRoundedSp } from "~/features/mmr/mmr-utils";
import type { TieredSkill } from "~/features/mmr/tiered.server";
import { useTranslation } from "~/hooks/useTranslation";
Expand All @@ -27,18 +27,25 @@ export function GroupCard({
ownRole,
ownGroup = false,
isExpired = false,
displayOnly = false,
hideVc = false,
hideWeapons = false,
}: {
group: LookingGroup;
action?: "LIKE" | "UNLIKE" | "GROUP_UP" | "MATCH_UP";
mapListPreference?: Group["mapListPreference"];
ownRole?: GroupMemberType["role"];
ownGroup?: boolean;
isExpired?: boolean;
displayOnly?: boolean;
hideVc?: boolean;
hideWeapons?: boolean;
}) {
const fetcher = useFetcher();

return (
<section className="q__group">
<section
className={clsx("q__group", { "q__group__display-only": displayOnly })}
>
<div
className={clsx("stack md", {
"horizontal justify-center": !group.members,
Expand All @@ -50,6 +57,9 @@ export function GroupCard({
member={member}
showActions={ownGroup && ownRole === "OWNER"}
key={member.discordId}
displayOnly={displayOnly}
hideVc={hideVc}
hideWeapons={hideWeapons}
/>
);
})}
Expand All @@ -63,7 +73,7 @@ export function GroupCard({
})
: null}
</div>
{group.tier ? (
{group.tier && !displayOnly ? (
<div className="stack xs text-lighter font-bold items-center justify-center text-xs">
<TierImage tier={group.tier} width={100} />
<div>
Expand All @@ -77,6 +87,16 @@ export function GroupCard({
</div>
</div>
) : null}
{group.tier && displayOnly ? (
<div className="q__group__display-group-tier">
<TierImage tier={group.tier} width={38} />
{group.tier.name}
{group.tier.isPlus ? "+" : ""}
</div>
) : null}
{group.skillDifference ? (
<GroupSkillDifference skillDifference={group.skillDifference} />
) : null}
{action &&
(ownRole === "OWNER" || ownRole === "MANAGER") &&
!isExpired ? (
Expand Down Expand Up @@ -119,9 +139,15 @@ export function GroupCard({
function GroupMember({
member,
showActions,
displayOnly,
hideVc,
hideWeapons,
}: {
member: NonNullable<LookingGroup["members"]>[number];
showActions: boolean;
displayOnly?: boolean;
hideVc?: boolean;
hideWeapons?: boolean;
}) {
return (
<div className="stack xxs">
Expand All @@ -135,13 +161,15 @@ function GroupMember({
<span className="q__group-member__name">{member.discordName}</span>
</Link>
<div className="ml-auto stack horizontal sm items-center">
{showActions ? <MemberRoleManager member={member} /> : null}
{showActions || displayOnly ? (
<MemberRoleManager member={member} displayOnly={displayOnly} />
) : null}
{member.skill ? <TierInfo skill={member.skill} /> : null}
</div>
</div>
<div className="stack horizontal justify-between">
<div className="stack horizontal xxs">
{member.vc ? (
{member.vc && !hideVc ? (
<div className="q__group-member__extra-info">
<VoiceChatInfo member={member} />
</div>
Expand All @@ -153,7 +181,7 @@ function GroupMember({
</div>
) : null}
</div>
{member.weapons ? (
{member.weapons && member.weapons.length > 0 && !hideWeapons ? (
<div className="q__group-member__extra-info">
{member.weapons?.map((weapon) => {
return (
Expand All @@ -167,20 +195,99 @@ function GroupMember({
})}
</div>
) : null}
{member.skillDifference ? (
<MemberSkillDifference skillDifference={member.skillDifference} />
) : null}
</div>
</div>
);
}

function GroupSkillDifference({
skillDifference,
}: {
skillDifference: NonNullable<
ParsedMemento["groups"][number]["skillDifference"]
>;
}) {
if (skillDifference.calculated) {
return (
<div className="text-center font-semi-bold">
Team SP {skillDifference.oldSp}{skillDifference.newSp}
</div>
);
}

if (skillDifference.newSp) {
return (
<div className="text-center font-semi-bold">
Team SP calculated: {skillDifference.newSp}
</div>
);
}

return (
<div className="text-center font-semi-bold">
Team SP calculating... ({skillDifference.matchesCount}/
{skillDifference.matchesCountNeeded})
</div>
);
}

function MemberSkillDifference({
skillDifference,
}: {
skillDifference: NonNullable<
ParsedMemento["users"][number]["skillDifference"]
>;
}) {
if (skillDifference.calculated) {
if (skillDifference.spDiff === 0) return null;

const symbol =
skillDifference.spDiff > 0 ? (
<span className="text-success"></span>
) : (
<span className="text-warning"></span>
);
return (
<div className="q__group-member__extra-info">
{symbol}
{Math.abs(skillDifference.spDiff)}SP
</div>
);
}

if (skillDifference.matchesCount === skillDifference.matchesCountNeeded) {
return (
<div className="q__group-member__extra-info">
<span className="text-lighter">Calculated:</span>{" "}
{skillDifference.newSp ? <>{skillDifference.newSp}SP</> : null}
</div>
);
}

return (
<div className="q__group-member__extra-info">
<span className="text-lighter">Calculating...</span> (
{skillDifference.matchesCount}/{skillDifference.matchesCountNeeded})
</div>
);
}

function MemberRoleManager({
member,
displayOnly,
}: {
member: NonNullable<LookingGroup["members"]>[number];
displayOnly?: boolean;
}) {
const fetcher = useFetcher();
const { t } = useTranslation(["q"]);
const Icon = member.role === "OWNER" ? StarFilledIcon : StarIcon;

if (displayOnly && member.role !== "OWNER") return null;

return (
<Popover
buttonChildren={
Expand All @@ -193,7 +300,7 @@ function MemberRoleManager({
>
<div className="stack md items-center">
<div>{t(`q:roles.${member.role}`)}</div>
{member.role !== "OWNER" ? (
{member.role !== "OWNER" && !displayOnly ? (
<fetcher.Form method="post" action={SENDOUQ_LOOKING_PAGE}>
<input type="hidden" name="userId" value={member.id} />
{member.role === "REGULAR" ? (
Expand Down
Loading

0 comments on commit 218c854

Please sign in to comment.