From cfa902a3432b8c3060a8ee505c08d2d909e7bce9 Mon Sep 17 00:00:00 2001 From: azur Date: Wed, 10 Dec 2025 16:48:53 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=8A=20Add=20scoreboard=20and=20played?= =?UTF-8?q?=20time.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/client/controllers/gui.tsx | 2 +- .../gui/players_board/index.story.ts | 0 .../gui/players_board/index.tsx | 10 ++- .../gui/players_board/profile/index.story.tsx | 0 .../gui/players_board/profile/index.tsx | 63 ++++++++++++------- src/client/networking.ts | 3 +- src/server/networking.ts | 3 +- src/server/services/modding/player_events.ts | 21 +++++-- src/server/services/stats.ts | 43 +++++++++++++ src/shared/modding/player_events.ts | 6 +- src/shared/networking.ts | 7 +++ 11 files changed, 126 insertions(+), 32 deletions(-) rename src/{shared => client}/gui/players_board/index.story.ts (100%) rename src/{shared => client}/gui/players_board/index.tsx (91%) rename src/{shared => client}/gui/players_board/profile/index.story.tsx (100%) rename src/{shared => client}/gui/players_board/profile/index.tsx (64%) create mode 100644 src/server/services/stats.ts diff --git a/src/client/controllers/gui.tsx b/src/client/controllers/gui.tsx index 79acb6a..19d8a05 100644 --- a/src/client/controllers/gui.tsx +++ b/src/client/controllers/gui.tsx @@ -3,7 +3,7 @@ import React from "@rbxts/react"; import { StrictMode } from "@rbxts/react"; import { createPortal, createRoot } from "@rbxts/react-roblox"; import { Players, StarterGui } from "@rbxts/services"; -import PlayersBoard from "shared/gui/players_board"; +import PlayersBoard from "client/gui/players_board"; @Controller() class GuiController implements OnStart { diff --git a/src/shared/gui/players_board/index.story.ts b/src/client/gui/players_board/index.story.ts similarity index 100% rename from src/shared/gui/players_board/index.story.ts rename to src/client/gui/players_board/index.story.ts diff --git a/src/shared/gui/players_board/index.tsx b/src/client/gui/players_board/index.tsx similarity index 91% rename from src/shared/gui/players_board/index.tsx rename to src/client/gui/players_board/index.tsx index f0e2bb9..c18d631 100644 --- a/src/shared/gui/players_board/index.tsx +++ b/src/client/gui/players_board/index.tsx @@ -1,4 +1,4 @@ -import { lerp, useMotion } from "@rbxts/pretty-react-hooks"; +import { lerp, useKeyPress, useMotion } from "@rbxts/pretty-react-hooks"; import React, { Component, ReactNode, StrictMode, useEffect, useState } from "@rbxts/react"; import { Players } from "@rbxts/services"; import Profile from "./profile"; @@ -55,6 +55,9 @@ function PlayerEntry({ export default function PlayersBoard() { const [players, setPlayers] = useState(Players.GetPlayers()); const [selectedPlayer, setSelectedPlayer] = useState(); + const [activated, setActivated] = useState(true); + const tabPressed = useKeyPress(["Tab"]); + useEffect(() => { const playerAdded = Players.PlayerAdded.Connect(() => { setPlayers(Players.GetPlayers()); @@ -68,6 +71,10 @@ export default function PlayersBoard() { }; }, []); + useEffect(() => { + if (tabPressed) setActivated(!activated); + }, [tabPressed]); + return ( <> (); +function TimePlayed({ + targetTime, + reeloffset, + timeOrigin, +}: { + targetTime: number; + timeOrigin: number; + reeloffset: number; +}) { + const [time, setTime] = useState(0); useEffect(() => { - let killed = false; - setId(DateTime.fromUnixTimestamp(targetTime)); - task.spawn(() => { - while (true) { - if (killed) return; - } + const connection = RunService.PreRender.Connect(() => { + setTime(targetTime + os.time() - timeOrigin); }); - return () => { - killed = true; - }; + return () => connection.Disconnect(); }, [targetTime]); + + return ( + + ); } export default function Profile({ player }: { player: Player }) { const [userIcon, setUserIcon] = useState(); + const [timePlayed, setTimePlayed] = useState(-1); + const [timeOrigin, setTimeOrigin] = useState(-1); const ref = useRef(); useEffect(() => { const [image] = Players.GetUserThumbnailAsync( @@ -28,6 +50,12 @@ export default function Profile({ player }: { player: Player }) { Enum.ThumbnailSize.Size180x180, ); setUserIcon(image); + if (RunService.IsRunning()) { + ProfileClientEvents.getPlayerStartTime(player.UserId).then((value) => { + setTimePlayed(value[0]); + setTimeOrigin(value[1]); + }); + } }, [player]); const reeloffset = ref.current?.AbsoluteSize.X ?? 0; return ( @@ -72,14 +100,7 @@ export default function Profile({ player }: { player: Player }) { Position={new UDim2(0, reeloffset, 0.25, 0)} TextColor3={new Color3(0.13, 0.13, 0.13)} /> - + (); + const playerJoinListener = new Set(); + const playerQuitListener = new Set(); - Modding.onListenerAdded((object) => listeners.add(object)); - Modding.onListenerRemoved((object) => listeners.delete(object)); + Modding.onListenerAdded((object) => playerJoinListener.add(object)); + Modding.onListenerRemoved((object) => playerJoinListener.delete(object)); + Modding.onListenerAdded((object) => playerQuitListener.add(object)); + Modding.onListenerRemoved((object) => playerQuitListener.delete(object)); Players.PlayerAdded.Connect((player) => { - for (const listener of listeners) { + for (const listener of playerJoinListener) { task.spawn(() => listener.onPlayerJoined(player)); } }); + Players.PlayerRemoving.Connect((player) => { + for (const listener of playerQuitListener) { + task.spawn(() => listener.onPlayerQuit(player)); + } + }); + for (const player of Players.GetPlayers()) { - for (const listener of listeners) { + for (const listener of playerJoinListener) { task.spawn(() => listener.onPlayerJoined(player)); } } diff --git a/src/server/services/stats.ts b/src/server/services/stats.ts new file mode 100644 index 0000000..04c5954 --- /dev/null +++ b/src/server/services/stats.ts @@ -0,0 +1,43 @@ +import { OnStart, Service } from "@flamework/core"; +import { DataStoreService } from "@rbxts/services"; +import { t } from "@rbxts/t"; +import { ProfileServerEvents } from "server/networking"; +import { OnPlayerJoined, OnPlayerQuit } from "shared/modding/player_events"; + +@Service() +class StatsService implements OnPlayerJoined, OnStart, OnPlayerQuit { + firstJoinStoreCache = new Map(); + firstJoinStore!: DataStore; + + onStart(): void { + this.firstJoinStore = DataStoreService.GetDataStore("Stats_Players_Played_Time"); + + ProfileServerEvents.getPlayerStartTime.setCallback((_, playerId) => { + const playedtime = this.getPlayerPlayedTime(playerId); + return playedtime!!; + }); + } + + onPlayerJoined(player: Player): void { + const playerId = tostring(player.UserId); + const [value] = this.firstJoinStore.GetAsync(playerId); + + if (!t.optional(t.number)(value)) throw `Bad Data in DataBase for ${player.UserId}`; + + if (!value) { + this.firstJoinStore.SetAsync(playerId, 0); + } + + this.firstJoinStoreCache.set(player.UserId, [value ?? 0, os.time()]); + } + + getPlayerPlayedTime(playerId: number) { + return this.firstJoinStoreCache.get(playerId); + } + + onPlayerQuit(player: Player): void { + const [value, time] = this.firstJoinStoreCache.get(player.UserId)!!; + const t = value + os.time() - time; + this.firstJoinStore.SetAsync(tostring(player.UserId), t, [player.UserId]); + } +} diff --git a/src/shared/modding/player_events.ts b/src/shared/modding/player_events.ts index 2faf0f5..8c68e51 100644 --- a/src/shared/modding/player_events.ts +++ b/src/shared/modding/player_events.ts @@ -1,3 +1,7 @@ export interface OnPlayerJoined { - onPlayerJoined(player: Player): void; + onPlayerJoined(player: Player): void; +} + +export interface OnPlayerQuit { + onPlayerQuit(player: Player): void; } diff --git a/src/shared/networking.ts b/src/shared/networking.ts index 6d33123..8d0f377 100644 --- a/src/shared/networking.ts +++ b/src/shared/networking.ts @@ -6,4 +6,11 @@ interface GameplayClient { interface GameplayServer {} +interface ProfileClient { + getPlayerStartTime(playerId: number): [number, number]; +} + +interface ProfileServer {} + export const GameplayEvents = Networking.createEvent(); +export const ProfileEvents = Networking.createFunction();