📊 Add scoreboard and played time.

This commit is contained in:
2025-12-10 16:48:53 +01:00
parent 549c504712
commit cfa902a343
11 changed files with 126 additions and 32 deletions

View File

@@ -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 {

View File

@@ -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<Player[]>(Players.GetPlayers());
const [selectedPlayer, setSelectedPlayer] = useState<Player>();
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 (
<>
<frame
@@ -75,6 +82,7 @@ export default function PlayersBoard() {
Position={UDim2.fromScale(0.8, 0.1)}
BackgroundColor3={new Color3(0, 0.72, 1)}
BackgroundTransparency={0.7}
Visible={activated}
>
<frame
BorderColor3={new Color3(1, 1, 1)}

View File

@@ -1,25 +1,47 @@
import React, { useEffect, useRef, useState } from "@rbxts/react";
import { LocalizationService, Players } from "@rbxts/services";
import { LocalizationService, Players, RunService } from "@rbxts/services";
import { ProfileClientEvents } from "client/networking";
function TimePlayed({ targetTime }: { targetTime: number }) {
const [time, setTime] = useState();
const [id, setId] = useState<DateTime>();
function TimePlayed({
targetTime,
reeloffset,
timeOrigin,
}: {
targetTime: number;
timeOrigin: number;
reeloffset: number;
}) {
const [time, setTime] = useState<number>(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 (
<textlabel
Text={
targetTime === -1
? "XX:XX:XX"
: DateTime.fromUnixTimestamp(time).FormatUniversalTime(
"HH:mm:ss",
LocalizationService.RobloxLocaleId,
)
}
Size={new UDim2(1, -reeloffset, 0.3, 0)}
BackgroundTransparency={1}
TextScaled={true}
Position={new UDim2(0, reeloffset, 0.45, 0)}
TextColor3={new Color3(0, 0, 0)}
/>
);
}
export default function Profile({ player }: { player: Player }) {
const [userIcon, setUserIcon] = useState<string>();
const [timePlayed, setTimePlayed] = useState<number>(-1);
const [timeOrigin, setTimeOrigin] = useState<number>(-1);
const ref = useRef<ImageLabel>();
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)}
/>
<textlabel
Text={DateTime.fromUnixTimestamp().FormatLocalTime("LTS", LocalizationService.RobloxLocaleId)}
Size={new UDim2(1, -reeloffset, 0.3, 0)}
BackgroundTransparency={1}
TextScaled={true}
Position={new UDim2(0, reeloffset, 0.45, 0)}
TextColor3={new Color3(0, 0, 0)}
/>
<TimePlayed reeloffset={reeloffset} targetTime={timePlayed} timeOrigin={timeOrigin} />
<textlabel
Text={`Lev : ${0}`}
Size={new UDim2(1, -reeloffset, 0.3, 0)}

View File

@@ -1,3 +1,4 @@
import { GameplayEvents } from "shared/networking";
import { GameplayEvents, ProfileEvents } from "shared/networking";
export const GameplayClientEvents = GameplayEvents.createClient({});
export const ProfileClientEvents = ProfileEvents.createClient({});

View File

@@ -1,3 +1,4 @@
import { GameplayEvents } from "shared/networking";
import { GameplayEvents, ProfileEvents } from "shared/networking";
export const GameplayServerEvents = GameplayEvents.createServer({});
export const ProfileServerEvents = ProfileEvents.createServer({});

View File

@@ -1,23 +1,32 @@
import { Modding, OnStart, Service } from "@flamework/core";
import { Players } from "@rbxts/services";
import { OnPlayerJoined } from "shared/modding/player_events";
import { OnPlayerJoined, OnPlayerQuit } from "shared/modding/player_events";
@Service()
class PlayerJoinService implements OnStart {
onStart() {
const listeners = new Set<OnPlayerJoined>();
const playerJoinListener = new Set<OnPlayerJoined>();
const playerQuitListener = new Set<OnPlayerQuit>();
Modding.onListenerAdded<OnPlayerJoined>((object) => listeners.add(object));
Modding.onListenerRemoved<OnPlayerJoined>((object) => listeners.delete(object));
Modding.onListenerAdded<OnPlayerJoined>((object) => playerJoinListener.add(object));
Modding.onListenerRemoved<OnPlayerJoined>((object) => playerJoinListener.delete(object));
Modding.onListenerAdded<OnPlayerQuit>((object) => playerQuitListener.add(object));
Modding.onListenerRemoved<OnPlayerQuit>((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));
}
}

View File

@@ -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<number, [number, number]>();
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]);
}
}

View File

@@ -1,3 +1,7 @@
export interface OnPlayerJoined {
onPlayerJoined(player: Player): void;
onPlayerJoined(player: Player): void;
}
export interface OnPlayerQuit {
onPlayerQuit(player: Player): void;
}

View File

@@ -6,4 +6,11 @@ interface GameplayClient {
interface GameplayServer {}
interface ProfileClient {
getPlayerStartTime(playerId: number): [number, number];
}
interface ProfileServer {}
export const GameplayEvents = Networking.createEvent<GameplayClient, GameplayServer>();
export const ProfileEvents = Networking.createFunction<ProfileClient, ProfileServer>();