mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-08-01 08:36:37 -05:00
* Tables * Clocks * Maplist preference selector * Fix SSR * Nav icon * RankedOrScrim * Map pool * Create group * Redirect logic * Persist map pool * Advance from preparing page * Rename query * Fix merge * Fix migration order * Seed groups * Find looking groups SQL * Renders something * More UI work * Back to 30min * Likes/dislikes * Always return own group * Fix like order * 3 tc/rm/cb -> 2 * Show only 3 weapons * Pass group size * Handle both liked and liked by same group * Fix SQL * Group preference frontend work * Morphing * Styling * Don't show group controls if not manager * Give/remove manager * Leave group * Leave with confirm * Delete likes when morphing groups * Clocks consistency * Remove bad invariant * Persist settings to local storage * Fix initial value flashing * Fix never resolving loading indicator * REFRESH_GROUP * Flip animations * Tweaks * Auto refresh logic * Groups of 4 seed * Reduce throwing * Load full groups initial * Create match * Match UI initial * Score reporter initial * Push footer down on match page * Score reporter knows when set ended * Score reporting untested * Show score after report * Align better * Look again with same group functionality * More migrations * Team on match page * Show confirmer before reporting score * Report weapons * Report weapos again by admin + skill changing * Handle no tiebreaker given to MapPool * Remove unranked * Remove support for "team id skill" * no-wrap -> nowrap * Preparing page work * Use common GroupCard component * Add some metas * MemberAdder in looking page * Fix GroupCard actions * Fix SZ only map list including other modes * Add season info * Prompt login * Joining team * Manage group on preparing page * Manage group on preparing page * Seed past matches * Add to seed * No map list preference when full group + fix expiry * Fix skill matchesCount calculation * Tiers initial work * Some progress on tiers * Tiering logic * MMR in group cards * Name to challenge * Team MMR * Big team rank icons * Adjust todos * Match score report with confirm * Allow regular members to report score * Handle reporting weapons edge cases * Add tier images * Improve GroupCard spacing * Refactor looking page * Looking mobile UI * Calculate skill only for current season * Divide groups visually when reporting weapons * Fix match page weapons sorting * Add cache to user skills+tier calculation * Admin report match score * Initial leaderboard * Cached leaderboard * Weapon category lb's * Populate SkillTeamUser in SendouQ * Team leaderboard filtered down * Add TODOs * Seasons initlal * Season weapons initial * Weapons stylized * Show rest weapons as + * Hide peak if same as current * Load matches SQL initial * Season matches UI initial * Take user id in account * Add weapons * Paginated matches * Fix pages count logic * Scroll top on data change * Day headers for matches * Link from user page to user seasons page * Summarize maps + ui initial * Map stats * Player info tabs * MMR chart * Chart adjustments * Handle basing team MMR on player MMR * Set initial MMR * Add info about discord to match page * Season support to tournaments * Get tournament skills as well for the graph * WIP * New team rating logic + misc other * tiered -> tiered.server * Update season starting time * TODOs * Add rules page * Hide elements correctly when off-season * Fix crash when only one player with skill * How-to video * Fix StartRank showing when not logged in * Make user leaderboard the default * Make Skill season non-nullable * Add suggested pass to match * Add rule * identifierToUserIds helper * Fix tiers not showing
203 lines
5.0 KiB
TypeScript
203 lines
5.0 KiB
TypeScript
import invariant from "tiny-invariant";
|
|
import type { Group, GroupLike } from "~/db/types";
|
|
import { databaseTimestampToDate } from "~/utils/dates";
|
|
import { FULL_GROUP_SIZE } from "../q-constants";
|
|
import type {
|
|
DividedGroups,
|
|
DividedGroupsUncensored,
|
|
LookingGroup,
|
|
LookingGroupWithInviteCode,
|
|
} from "../q-types";
|
|
import type {
|
|
SkillTierInterval,
|
|
TieredSkill,
|
|
} from "~/features/mmr/tiered.server";
|
|
|
|
export function divideGroups({
|
|
groups,
|
|
ownGroupId,
|
|
likes,
|
|
}: {
|
|
groups: LookingGroupWithInviteCode[];
|
|
ownGroupId: number;
|
|
likes: Pick<GroupLike, "likerGroupId" | "targetGroupId">[];
|
|
}): DividedGroupsUncensored {
|
|
let own: LookingGroupWithInviteCode | null = null;
|
|
let neutral: LookingGroupWithInviteCode[] = [];
|
|
const likesReceived: LookingGroupWithInviteCode[] = [];
|
|
const likesGiven: LookingGroupWithInviteCode[] = [];
|
|
|
|
const unneutralGroupIds = new Set<number>();
|
|
for (const like of likes) {
|
|
for (const group of groups) {
|
|
if (group.id === ownGroupId) continue;
|
|
|
|
// handles edge case where they liked each other
|
|
// right after each other so the group didn't morph
|
|
// so instead it will look so that the group liked us
|
|
// and there is the option to morph
|
|
if (unneutralGroupIds.has(group.id)) continue;
|
|
|
|
if (like.likerGroupId === group.id) {
|
|
likesReceived.push(group);
|
|
unneutralGroupIds.add(group.id);
|
|
break;
|
|
}
|
|
if (like.targetGroupId === group.id) {
|
|
likesGiven.push(group);
|
|
unneutralGroupIds.add(group.id);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const group of groups) {
|
|
if (group.id === ownGroupId) {
|
|
own = group;
|
|
continue;
|
|
}
|
|
|
|
if (unneutralGroupIds.has(group.id)) continue;
|
|
|
|
neutral.push(group);
|
|
}
|
|
|
|
invariant(own && own.members, "own group not found");
|
|
|
|
return {
|
|
own,
|
|
neutral,
|
|
likesGiven,
|
|
likesReceived,
|
|
};
|
|
}
|
|
|
|
export function filterOutGroupsWithIncompatibleMapListPreference(
|
|
groups: DividedGroupsUncensored
|
|
): DividedGroupsUncensored {
|
|
if (
|
|
groups.own.mapListPreference !== "SZ_ONLY" &&
|
|
groups.own.mapListPreference !== "ALL_MODES_ONLY"
|
|
) {
|
|
return groups;
|
|
}
|
|
|
|
return {
|
|
...groups,
|
|
neutral: groups.neutral.filter((group) => {
|
|
if (
|
|
group.mapListPreference !== "SZ_ONLY" &&
|
|
group.mapListPreference !== "ALL_MODES_ONLY"
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
return group.mapListPreference === groups.own.mapListPreference;
|
|
}),
|
|
};
|
|
}
|
|
|
|
const censorGroupFully = ({
|
|
inviteCode: _inviteCode,
|
|
...group
|
|
}: LookingGroupWithInviteCode): LookingGroup => ({
|
|
...group,
|
|
members: undefined,
|
|
mapListPreference: undefined,
|
|
});
|
|
const censorGroupPartly = ({
|
|
inviteCode: _inviteCode,
|
|
...group
|
|
}: LookingGroupWithInviteCode): LookingGroup => group;
|
|
export function censorGroups({
|
|
groups,
|
|
showMembers,
|
|
showInviteCode,
|
|
}: {
|
|
groups: DividedGroupsUncensored;
|
|
showMembers: boolean;
|
|
showInviteCode: boolean;
|
|
}): DividedGroups {
|
|
return {
|
|
own: showInviteCode ? groups.own : censorGroupPartly(groups.own),
|
|
neutral: groups.neutral.map(
|
|
showMembers ? censorGroupPartly : censorGroupFully
|
|
),
|
|
likesGiven: groups.likesGiven.map(
|
|
showMembers ? censorGroupPartly : censorGroupFully
|
|
),
|
|
likesReceived: groups.likesReceived.map(
|
|
showMembers ? censorGroupPartly : censorGroupFully
|
|
),
|
|
};
|
|
}
|
|
|
|
export function addSkillsToGroups({
|
|
groups,
|
|
userSkills,
|
|
intervals,
|
|
}: {
|
|
groups: DividedGroupsUncensored;
|
|
userSkills: Record<string, TieredSkill>;
|
|
intervals: SkillTierInterval[];
|
|
}): DividedGroupsUncensored {
|
|
const resolveGroupSkill = (
|
|
group: LookingGroupWithInviteCode
|
|
): TieredSkill["tier"] | undefined => {
|
|
if (group.members.length < FULL_GROUP_SIZE) return;
|
|
|
|
const skills = group.members
|
|
.map((m) => userSkills[String(m.id)])
|
|
.filter(Boolean);
|
|
const averageOrdinal =
|
|
skills.reduce((acc, s) => acc + s.ordinal, 0) / skills.length;
|
|
|
|
return (
|
|
intervals.find(
|
|
(i) => i.neededOrdinal && averageOrdinal > i.neededOrdinal
|
|
) ?? { isPlus: false, name: "IRON" }
|
|
);
|
|
};
|
|
const addSkill = (group: LookingGroupWithInviteCode) => ({
|
|
...group,
|
|
members: group.members?.map((m) => ({
|
|
...m,
|
|
skill: userSkills[String(m.id)],
|
|
})),
|
|
tier: resolveGroupSkill(group),
|
|
});
|
|
|
|
return {
|
|
own: addSkill(groups.own),
|
|
neutral: groups.neutral.map(addSkill),
|
|
likesGiven: groups.likesGiven.map(addSkill),
|
|
likesReceived: groups.likesReceived.map(addSkill),
|
|
};
|
|
}
|
|
|
|
export function membersNeededForFull(currentSize: number) {
|
|
return FULL_GROUP_SIZE - currentSize;
|
|
}
|
|
|
|
export function groupExpiryStatus(
|
|
group: Pick<Group, "latestActionAt">
|
|
): null | "EXPIRING_SOON" | "EXPIRED" {
|
|
// group expires in 30min without actions performed
|
|
const groupExpiresAt =
|
|
databaseTimestampToDate(group.latestActionAt).getTime() + 30 * 60 * 1000;
|
|
|
|
const now = new Date().getTime();
|
|
|
|
if (now > groupExpiresAt) {
|
|
return "EXPIRED";
|
|
}
|
|
|
|
const tenMinutesFromNow = now + 10 * 60 * 1000;
|
|
|
|
if (tenMinutesFromNow > groupExpiresAt) {
|
|
return "EXPIRING_SOON";
|
|
}
|
|
|
|
return null;
|
|
}
|