Skip to content

Commit a478420

Browse files
committed
✨ Добавлен новый файл для каталога достижений с интерфейсом и списком всех достижений. Обновлены действия для получения респектов, включая расчёт респектов за достижения и сезоны. Добавлены новые типы и функции для обработки данных о респектах, улучшая структуру и читаемость кода.
1 parent 1a7ab49 commit a478420

7 files changed

Lines changed: 159 additions & 6 deletions

File tree

src/features/achievements/get-achievements.action.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { AchievementDal, AchievementDoc } from "@/entities/achievement";
2+
import { ALL_ACHIEVEMENTS } from "@/entities/achievement/lib/catalog";
23
import type { CockDal } from "@/entities/cock";
34
import { getAuthUser } from "@/shared/context";
45
import { di } from "@/shared/injection";
@@ -7,7 +8,6 @@ import { logger } from "@/shared/lib/logger";
78
import { createTicker } from "@/shared/lib/profiling";
89
import type { AchBulkResult } from "./db/pipelines";
910
import { pAchBulk, pAchLightning, pCountSeasons } from "./db/pipelines";
10-
import { ALL_ACHIEVEMENTS } from "./lib/catalog";
1111
import type { CockAchievementsResponse } from "./types";
1212

1313
const val = <T extends Record<string, unknown>>(arr: T[], key: keyof T, fallback = 0): number => (arr[0]?.[key] as number) ?? fallback;
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import type { PipelineStage } from "mongoose";
2+
import type { CockSeason } from "@/shared/lib/seasons";
3+
4+
// ===========================================
5+
// Результаты агрегаций
6+
// ===========================================
7+
8+
interface SeasonRange {
9+
start: Date;
10+
end: Date;
11+
season_num: number;
12+
}
13+
14+
export interface AggSeasonPosition {
15+
season_num: number;
16+
position: number;
17+
}
18+
19+
// ===========================================
20+
// Пайплайны
21+
// ===========================================
22+
23+
/**
24+
* Позиция пользователя в каждом завершённом сезоне (1-based).
25+
* Возвращает массив { season_num, position } только для сезонов, где юзер участвовал.
26+
*/
27+
export const pUserPositionsInSeasons = (userId: number, completedSeasons: SeasonRange[]): PipelineStage[] => {
28+
if (completedSeasons.length === 0) return [{ $limit: 0 }];
29+
30+
const first = completedSeasons[0];
31+
const last = completedSeasons[completedSeasons.length - 1];
32+
if (!first || !last) return [{ $limit: 0 }];
33+
34+
const thenKey = "then";
35+
const branches = completedSeasons.map((s) => ({
36+
case: { $and: [{ $gte: ["$requested_at", s.start] }, { $lt: ["$requested_at", s.end] }] },
37+
[thenKey]: s.season_num,
38+
}));
39+
40+
return [
41+
{ $match: { requested_at: { $gte: first.start, $lt: last.end } } },
42+
{ $addFields: { season_num: { $switch: { branches, default: null } } } },
43+
{ $match: { season_num: { $ne: null } } },
44+
{ $group: { _id: { season_num: "$season_num", user_id: "$user_id" }, total_size: { $sum: "$size" } } },
45+
{ $sort: { "_id.season_num": 1, total_size: -1 } },
46+
{ $group: { _id: "$_id.season_num", users: { $push: { user_id: "$_id.user_id", total_size: "$total_size" } } } },
47+
{
48+
$project: {
49+
_id: 0,
50+
season_num: "$_id",
51+
position: {
52+
$add: [{ $indexOfArray: ["$users.user_id", userId] }, 1],
53+
},
54+
},
55+
},
56+
// position=0 означает юзер не найден (indexOfArray вернул -1, +1 = 0)
57+
{ $match: { position: { $gt: 0 } } },
58+
];
59+
};
60+
61+
export const toSeasonRanges = (seasons: CockSeason[]): SeasonRange[] =>
62+
seasons
63+
.filter((s) => !s.is_active)
64+
.map((s) => ({
65+
start: new Date(s.start_date),
66+
end: new Date(s.end_date),
67+
season_num: s.season_num,
68+
}));
69+
70+
/** Дата первого кока в коллекции. */
71+
export const pFirstCockDate = (): PipelineStage[] => [{ $sort: { requested_at: 1 } }, { $limit: 1 }, { $project: { _id: 0, first_date: "$requested_at" } }];
Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,49 @@
1+
import type { AchievementDal } from "@/entities/achievement";
2+
import { ALL_ACHIEVEMENTS } from "@/entities/achievement/lib/catalog";
3+
import type { CockDal } from "@/entities/cock";
4+
import { getAuthUser } from "@/shared/context";
5+
import { di } from "@/shared/injection";
6+
import { getAllSeasons } from "@/shared/lib/seasons";
7+
import type { AggSeasonPosition } from "./db/pipelines";
8+
import { pFirstCockDate, pUserPositionsInSeasons, toSeasonRanges } from "./db/pipelines";
9+
import { calcSeasonRespect } from "./lib/calc";
110
import type { RespectResponse } from "./types";
211

3-
// todo: добавить кол-во респектов за победы в сезоне и за выполненные ачивки (два отдельных еще поля) ну и + total_respect так и остается
4-
export const createGetRespectAction = () => async (): Promise<RespectResponse> => ({
5-
total_respect: 0,
6-
});
12+
const achievementRespectMap = new Map(ALL_ACHIEVEMENTS.map((a) => [a.id, a.respects]));
713

8-
createGetRespectAction.inject = [] as const;
14+
export const createGetRespectAction = (cockDal: CockDal, achievementDal: AchievementDal) => async (): Promise<RespectResponse> => {
15+
const userId = getAuthUser().id;
16+
17+
const [firstCockResult, userAchievements] = await Promise.all([cockDal.aggregate<{ first_date: Date }>(pFirstCockDate()), achievementDal.findByUserId(userId)]);
18+
19+
// Респект за ачивки
20+
let achievementRespect = 0;
21+
for (const ach of userAchievements) {
22+
if (ach.completed) {
23+
achievementRespect += achievementRespectMap.get(ach.achievement_id) ?? 0;
24+
}
25+
}
26+
27+
// Респект за сезоны
28+
let seasonRespect = 0;
29+
const firstCock = firstCockResult[0];
30+
if (firstCock) {
31+
const seasons = getAllSeasons(firstCock.first_date);
32+
const ranges = toSeasonRanges(seasons);
33+
34+
if (ranges.length > 0) {
35+
const positions = await cockDal.aggregate<AggSeasonPosition>(pUserPositionsInSeasons(userId, ranges));
36+
for (const { position } of positions) {
37+
seasonRespect += calcSeasonRespect(position);
38+
}
39+
}
40+
}
41+
42+
return {
43+
season_respect: seasonRespect,
44+
achievement_respect: achievementRespect,
45+
total_respect: seasonRespect + achievementRespect,
46+
};
47+
};
48+
49+
createGetRespectAction.inject = [di.cockDal, di.achievementDal] as const;
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { describe, expect, it } from "bun:test";
2+
import { calcSeasonRespect } from "./calc";
3+
4+
describe("calcSeasonRespect", () => {
5+
it("возвращает 0 для place <= 0", () => {
6+
expect(calcSeasonRespect(0)).toBe(0);
7+
expect(calcSeasonRespect(-1)).toBe(0);
8+
expect(calcSeasonRespect(-100)).toBe(0);
9+
});
10+
11+
it("возвращает 1488 за первое место", () => {
12+
expect(calcSeasonRespect(1)).toBe(1337);
13+
});
14+
15+
it("возвращает убывающий респект для мест 2..10", () => {
16+
const respects = Array.from({ length: 9 }, (_, i) => calcSeasonRespect(i + 2));
17+
for (let i = 1; i < respects.length; i++) {
18+
expect(respects[i]).toBeLessThanOrEqual(respects[i - 1] ?? -1);
19+
}
20+
});
21+
22+
it("возвращает конкретные значения для известных мест", () => {
23+
expect(calcSeasonRespect(2)).toBe(Math.floor(1337 / 2 ** 1.2));
24+
expect(calcSeasonRespect(3)).toBe(Math.floor(1337 / 3 ** 1.2));
25+
expect(calcSeasonRespect(10)).toBe(Math.floor(1337 / 10 ** 1.2));
26+
});
27+
28+
it("возвращает минимум 1 для очень далёких мест", () => {
29+
expect(calcSeasonRespect(10000)).toBe(1);
30+
expect(calcSeasonRespect(99999)).toBe(1);
31+
});
32+
});

src/features/respects/lib/calc.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/** Респект за место в сезоне (порт из Go-бота). */
2+
export const calcSeasonRespect = (place: number): number => {
3+
if (place <= 0) return 0;
4+
5+
const score = Math.floor(1337 / place ** 1.2);
6+
return score < 1 ? 1 : score;
7+
};

src/features/respects/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { z } from "zod";
22

33
export const RespectResponseSchema = z.object({
4+
season_respect: z.number(),
5+
achievement_respect: z.number(),
46
total_respect: z.number(),
57
});
68

0 commit comments

Comments
 (0)