Skip to content

Commit

Permalink
feat: added achievement system
Browse files Browse the repository at this point in the history
  • Loading branch information
Louis3797 committed Jun 19, 2024
1 parent cade518 commit 1ba5ffd
Show file tree
Hide file tree
Showing 7 changed files with 252 additions and 12 deletions.
7 changes: 4 additions & 3 deletions apps/api/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,11 @@ model Notification {
}

model Achievement {
id String @id
id String @id @default(cuid())
type AchievementType
name String
level Int @default(0)
bannerUrl String
condition Int
progress Int // progress to condition 1/10 progress/condition
promotionPoints Int // promotionPoints a user receives for reaching the achievement
userToAchievement UserToAchievement[]
}
Expand All @@ -107,7 +105,9 @@ model UserToAchievement {
userId String
achievement Achievement @relation(fields: [achievementId], references: [id], onDelete: Cascade, onUpdate: Cascade)
achievementId String
progress Int @default(0) // progress to condition 1/10 progress/condition
created_at DateTime @default(now())
updated_at DateTime @updatedAt
@@id([userId, achievementId])
}
Expand Down Expand Up @@ -137,6 +137,7 @@ enum RewardType {

enum AchievementType {
NTH_STREAM
MINUTES_STREAMED
NTH_VIEWERS
}

Expand Down
55 changes: 51 additions & 4 deletions apps/api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,20 @@ import { initRabbitMQ } from "./utils/initRabbitMQ";
import { rewards } from "./utils/rewards";
import { getNotificationsMessage } from "./utils/notificationsMessages";
import isAuth from "./middleware/isAuth";
import { handleAchievements } from "./service/achievments.service";

type Stream = {
id: string;
created_at: string;
streamer: SocketUser;
viewerCount: number;
viewers: Array<string>;
viewers: Array<string>; // viewers that are currently in the livestream
mostViewers: number;
};

// Holds state of active streams
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const streams: Map<string, Stream> = new Map(); // key is the id of the stream
export const streams: Map<string, Stream> = new Map(); // key is the id of the stream
// eslint-disable-next-line prefer-const

const app: Express = express();
Expand Down Expand Up @@ -282,7 +284,8 @@ io.on("connection", (socket) => {
created_at: checkStream.created_at.toString(),
streamer: socket.data.user,
viewerCount: 0,
viewers: []
viewers: [],
mostViewers: 0
});

logger.info(`Created stream: ${streamId}`);
Expand Down Expand Up @@ -375,6 +378,21 @@ io.on("connection", (socket) => {
stream.viewerCount++;
stream.viewers.push(socket.data.user.id);

if (stream.viewerCount > stream.mostViewers) {
stream.mostViewers = stream.viewerCount;
}

await prismaClient.stream.update({
where: {
id: streamId
},
data: {
viewerCount: {
increment: 1
}
}
});

io.to(streamId).emit("user_joined", {
user: socket.data.user
});
Expand Down Expand Up @@ -567,7 +585,8 @@ io.on("connection", (socket) => {
data: {
ended_at,
duration,
active: false
active: false,
mostViewers: stream.mostViewers
}
});

Expand All @@ -583,6 +602,8 @@ io.on("connection", (socket) => {
}
});

await handleAchievements(checkStream.streamerId);

// broadcast stream end to all clients
io.to(streamId).emit("stream_ended", {
duration,
Expand Down Expand Up @@ -629,6 +650,17 @@ io.on("connection", (socket) => {

socket.emit("you-left-stream");

await prismaClient.stream.update({
where: {
id: streamId
},
data: {
viewerCount: {
decrement: 1
}
}
});

socket.to(streamId).emit("user_leaved", {
user: socket.data.user
});
Expand Down Expand Up @@ -898,6 +930,7 @@ io.on("connection", (socket) => {
data: {
ended_at,
duration,
mostViewers: stream.mostViewers,
active: false
}
});
Expand All @@ -914,6 +947,8 @@ io.on("connection", (socket) => {
}
});

await handleAchievements(checkStream.streamerId);

// broadcast stream end to all clients
io.to(streamId).emit("stream_ended", {
duration,
Expand Down Expand Up @@ -957,6 +992,18 @@ io.on("connection", (socket) => {
stream.viewers = stream.viewers.filter(
(id) => id !== socket.data.user.id
);

await prismaClient.stream.update({
where: {
id: streamId
},
data: {
viewerCount: {
decrement: 1
}
}
});

delete socket.data.streamId;

logger.info(
Expand Down
Empty file removed apps/api/src/controller/.gitkeep
Empty file.
1 change: 1 addition & 0 deletions apps/api/src/controller/stream.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export const getStreamInfo = async (

followed = !!followsStreamer;
}

return res.status(httpStatus.OK).json({
success: true,
data: {
Expand Down
34 changes: 29 additions & 5 deletions apps/api/src/controller/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import {
Follows,
Notification,
Stream,
User
User,
UserToAchievement
} from "@prisma/client";
import { createErrorObject } from "../utils/createErrorObject";
import { getNotificationsMessage } from "../utils/notificationsMessages";
Expand All @@ -27,9 +28,10 @@ export const getUserInfo = async (
TypedResponse<{
user: User & {
subscribed: boolean;
userToAchievement: {
achievement: Achievement;
}[];
userToAchievement: (Pick<
UserToAchievement,
"progress" | "created_at" | "updated_at"
> & { achievement: Achievement })[];
streams: Stream[];
};
}>
Expand All @@ -41,6 +43,9 @@ export const getUserInfo = async (
streams: true,
userToAchievement: {
select: {
progress: true,
created_at: true,
updated_at: true,
achievement: true
}
}
Expand Down Expand Up @@ -120,12 +125,31 @@ export const createUser = async (
});
}

// Get all achievements a user can collect
const getAllAchievements = await prismaClient.achievement.findMany({
select: {
id: true
}
});

// create a relationship to all achievements
const userToAchievementsData: { userId: string; achievementId: string }[] =
getAllAchievements.map((v) => ({
userId: id,
achievementId: v.id
}));

const newUser = await prismaClient.user.create({
data: {
id,
username,
dispname: username,
email
email,
userToAchievement: {
createMany: {
data: userToAchievementsData
}
}
},
select: {
id: true,
Expand Down
147 changes: 147 additions & 0 deletions apps/api/src/service/achievments.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import logger from "../middleware/logger";
import prismaClient from "../config/prisma";
import { createAchievementNotification } from "./notification.service";

/**
* Checks and updates the progress of the achievements for a specific user
* @param userId UserID
* @returns Promise<void>
*/
export const handleAchievements = async (userId: string) => {
try {
const userData = await prismaClient.user.findUnique({
where: {
id: userId
},
select: {
id: true,
numStreams: true,
num10minStreams: true,
minStreamed: true,
userToAchievement: {
include: {
achievement: true
}
},
streams: {
select: {
id: true,
mostViewers: true
}
}
}
});

if (!userData || !userData.streams) return;

const mostViewers = Math.max(
...userData.streams.map((stream) => stream.mostViewers)
);

for (const uta of userData.userToAchievement) {
const achievement = uta.achievement;

// if progress equals condition we dont need to update something
if (uta.progress === achievement.condition) continue;

switch (achievement.type) {
case "NTH_STREAM": {
if (
userData.num10minStreams > uta.progress &&
userData.num10minStreams <= achievement.condition
) {
await updateProgress(
uta.userId,
uta.achievementId,
userData.num10minStreams
);
} else if (
userData.num10minStreams > uta.progress &&
userData.num10minStreams > achievement.condition
) {
await updateProgress(
uta.userId,
uta.achievementId,
achievement.condition
);
}

// user has achieved the achievement
if (userData.num10minStreams >= achievement.condition) {
await createAchievementNotification(userId, achievement);
}
break;
}
case "MINUTES_STREAMED": {
if (
userData.minStreamed > uta.progress &&
userData.minStreamed <= achievement.condition
) {
await updateProgress(
uta.userId,
uta.achievementId,
userData.minStreamed
);
} else if (
userData.minStreamed > uta.progress &&
userData.minStreamed > achievement.condition
) {
await updateProgress(
uta.userId,
uta.achievementId,
achievement.condition
);
}

// user has achieved the achievement
if (userData.minStreamed >= achievement.condition) {
await createAchievementNotification(userId, achievement);
}
break;
}
case "NTH_VIEWERS": {
if (
mostViewers > uta.progress &&
mostViewers <= achievement.condition
) {
await updateProgress(uta.userId, uta.achievementId, mostViewers);
} else if (
mostViewers > uta.progress &&
mostViewers > achievement.condition
) {
await updateProgress(
uta.userId,
uta.achievementId,
achievement.condition
);
}

// user has achieved the achievement
if (mostViewers >= achievement.condition) {
await createAchievementNotification(userId, achievement);
}
break;
}
default:
break;
}
}
} catch (error) {
logger.error("Error on handleAchievments: ", error);
}
};

export const updateProgress = async (
userId: string,
achievementId: string,
progress: number
): Promise<void> => {
try {
await prismaClient.userToAchievement.update({
where: { userId_achievementId: { userId, achievementId } },
data: { progress }
});
} catch (error) {
logger.error("Error on updateProgress: ", error);
}
};
Loading

0 comments on commit 1ba5ffd

Please sign in to comment.