📊 Add scoreboard and played time.
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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)}
|
||||
@@ -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)}
|
||||
@@ -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({});
|
||||
|
||||
@@ -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({});
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
43
src/server/services/stats.ts
Normal file
43
src/server/services/stats.ts
Normal 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]);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
export interface OnPlayerJoined {
|
||||
onPlayerJoined(player: Player): void;
|
||||
}
|
||||
|
||||
export interface OnPlayerQuit {
|
||||
onPlayerQuit(player: Player): void;
|
||||
}
|
||||
|
||||
@@ -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>();
|
||||
|
||||
Reference in New Issue
Block a user