@four-leaf-studios/rl-overlay 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs.js +10718 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.esm.js +10691 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/types/Overlay.d.ts +19 -0
- package/dist/types/Player.d.ts +7 -0
- package/dist/types/PlayerBoost.d.ts +8 -0
- package/dist/types/Replay.d.ts +3 -0
- package/dist/types/Scoreboard.d.ts +3 -0
- package/dist/types/ScoreboardGameBox.d.ts +7 -0
- package/dist/types/ScoreboardSeriesBox.d.ts +7 -0
- package/dist/types/ScoreboardTeam.d.ts +7 -0
- package/dist/types/StatItem.d.ts +13 -0
- package/dist/types/TargetBoost.d.ts +3 -0
- package/dist/types/TargetPlayer.d.ts +3 -0
- package/dist/types/TargetPlayerLocation.d.ts +7 -0
- package/dist/types/TargetPlayerStats.d.ts +7 -0
- package/dist/types/Team.d.ts +8 -0
- package/dist/types/Teams.d.ts +3 -0
- package/dist/types/Timer.d.ts +3 -0
- package/dist/types/context/BroadcastContext.d.ts +7 -0
- package/dist/types/hooks/index.d.ts +4 -0
- package/dist/types/hooks/useOverlayStyles.d.ts +12 -0
- package/dist/types/hooks/useReplay.d.ts +7 -0
- package/dist/types/hooks/useShowGameComponents.d.ts +2 -0
- package/dist/types/hooks/useTargetPlayer.d.ts +2 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/mockBroadcast.d.ts +18 -0
- package/package.json +50 -0
- package/rollup.config.js +55 -0
- package/src/Overlay.tsx +100 -0
- package/src/Player.tsx +31 -0
- package/src/PlayerBoost.tsx +26 -0
- package/src/Replay.tsx +18 -0
- package/src/Scoreboard.tsx +58 -0
- package/src/ScoreboardGameBox.tsx +25 -0
- package/src/ScoreboardSeriesBox.tsx +44 -0
- package/src/ScoreboardTeam.tsx +39 -0
- package/src/StatItem.tsx +31 -0
- package/src/TargetBoost.tsx +63 -0
- package/src/TargetPlayer.tsx +56 -0
- package/src/TargetPlayerLocation.tsx +27 -0
- package/src/TargetPlayerStats.tsx +25 -0
- package/src/Team.tsx +45 -0
- package/src/Teams.tsx +13 -0
- package/src/Timer.tsx +24 -0
- package/src/assets/overlay-testing-background.png +0 -0
- package/src/context/BroadcastContext.tsx +21 -0
- package/src/css/reset.css +280 -0
- package/src/hooks/index.ts +4 -0
- package/src/hooks/useOverlayStyles.ts +107 -0
- package/src/hooks/useReplay.ts +21 -0
- package/src/hooks/useShowGameComponents.ts +14 -0
- package/src/hooks/useTargetPlayer.ts +21 -0
- package/src/index.ts +3 -0
- package/src/types.d.ts +59 -0
- package/test-overlay/README.md +12 -0
- package/test-overlay/eslint.config.js +33 -0
- package/test-overlay/index.html +13 -0
- package/test-overlay/package.json +27 -0
- package/test-overlay/public/mock-css.css +733 -0
- package/test-overlay/public/vite.svg +1 -0
- package/test-overlay/src/App.jsx +28 -0
- package/test-overlay/src/index.css +0 -0
- package/test-overlay/src/main.jsx +10 -0
- package/test-overlay/src/mockBroadcast.js +33 -0
- package/test-overlay/vite.config.js +7 -0
- package/tsconfig.json +21 -0
package/src/StatItem.tsx
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// StatItem.tsx
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { PlayerState } from "@four-leaf-studios/rl-socket-hook";
|
|
4
|
+
|
|
5
|
+
// 1. Define what “primitive” means for us:
|
|
6
|
+
type Primitive = string | number | boolean;
|
|
7
|
+
|
|
8
|
+
// 2. Extract just the keys whose values are Primitive:
|
|
9
|
+
type PrimitiveKeys<T> = {
|
|
10
|
+
[K in keyof T]: T[K] extends Primitive ? K : never;
|
|
11
|
+
}[keyof T];
|
|
12
|
+
|
|
13
|
+
// 3. Now K can only be one of those “safe” keys:
|
|
14
|
+
type StatItemProps<K extends PrimitiveKeys<PlayerState>> = {
|
|
15
|
+
id: K;
|
|
16
|
+
label: string;
|
|
17
|
+
value: PlayerState[K]; // guaranteed to be string|number|boolean
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const StatItem = <K extends PrimitiveKeys<PlayerState>>({
|
|
21
|
+
id,
|
|
22
|
+
label,
|
|
23
|
+
value,
|
|
24
|
+
}: StatItemProps<K>) => (
|
|
25
|
+
<li className={`stat_box_statistic stat_box_statistic_player_${id}`}>
|
|
26
|
+
<span className="stat_box_statistic_name">{label}</span>
|
|
27
|
+
<span className="stat_box_statistic_value">{value}</span>
|
|
28
|
+
</li>
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
export default StatItem;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { useEventSelector } from "@four-leaf-studios/rl-socket-hook";
|
|
2
|
+
import { AnimatePresence, motion } from "framer-motion";
|
|
3
|
+
import React, { memo } from "react";
|
|
4
|
+
import useTargetPlayer from "./hooks/useTargetPlayer";
|
|
5
|
+
|
|
6
|
+
const TargetBoost = () => {
|
|
7
|
+
const initialized = useEventSelector(
|
|
8
|
+
"game:update_state",
|
|
9
|
+
(state) => state?.hasGame
|
|
10
|
+
);
|
|
11
|
+
const targetPlayer = useTargetPlayer();
|
|
12
|
+
|
|
13
|
+
if (!targetPlayer || !initialized) return null;
|
|
14
|
+
|
|
15
|
+
const modifier = targetPlayer.team === 0 ? "left" : "right";
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<AnimatePresence mode="wait">
|
|
19
|
+
<motion.div
|
|
20
|
+
key={targetPlayer.id}
|
|
21
|
+
className={`target_boost ${modifier}_target_boost`}
|
|
22
|
+
initial={{ x: 400, opacity: 0 }}
|
|
23
|
+
animate={{ x: 0, opacity: 1 }}
|
|
24
|
+
exit={{ x: 400, opacity: 0, scale: 0.9 }}
|
|
25
|
+
transition={{ duration: 1, ease: [0.77, 0, 0.175, 1] }} // Quartic easing equivalent
|
|
26
|
+
>
|
|
27
|
+
<div
|
|
28
|
+
className={`target_boost_container ${modifier}_target_boost_container`}
|
|
29
|
+
>
|
|
30
|
+
<div
|
|
31
|
+
className={`target_boost_meter ${modifier}_target_boost_meter`}
|
|
32
|
+
style={
|
|
33
|
+
{ "--target_boost": targetPlayer.boost } as React.CSSProperties
|
|
34
|
+
}
|
|
35
|
+
>
|
|
36
|
+
<div
|
|
37
|
+
className={`target_boost_meter_inner ${modifier}_target_boost_meter_inner`}
|
|
38
|
+
></div>
|
|
39
|
+
</div>
|
|
40
|
+
<svg className="target_boost_svg">
|
|
41
|
+
<circle
|
|
42
|
+
className={`target_boost_svg_circle ${modifier}_target_boost_svg_circle`}
|
|
43
|
+
cx="50%"
|
|
44
|
+
cy="50%"
|
|
45
|
+
r="50%"
|
|
46
|
+
/>
|
|
47
|
+
</svg>
|
|
48
|
+
</div>
|
|
49
|
+
<motion.div
|
|
50
|
+
className={`target_boost_value ${modifier}_target_boost_value`}
|
|
51
|
+
initial={{ scale: 0 }}
|
|
52
|
+
animate={{ scale: 1 }}
|
|
53
|
+
exit={{ scale: 0 }}
|
|
54
|
+
transition={{ duration: 0.5, ease: "easeOut" }}
|
|
55
|
+
>
|
|
56
|
+
{targetPlayer.boost}
|
|
57
|
+
</motion.div>
|
|
58
|
+
</motion.div>
|
|
59
|
+
</AnimatePresence>
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export default memo(TargetBoost);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { useEventSelector } from "@four-leaf-studios/rl-socket-hook";
|
|
2
|
+
import { AnimatePresence, motion } from "framer-motion";
|
|
3
|
+
import React, { memo } from "react";
|
|
4
|
+
import { useBroadcast } from "./context/BroadcastContext";
|
|
5
|
+
import PlayerBoost from "./PlayerBoost";
|
|
6
|
+
import TargetPlayerStats from "./TargetPlayerStats";
|
|
7
|
+
import useTargetPlayer from "./hooks/useTargetPlayer";
|
|
8
|
+
|
|
9
|
+
const TargetPlayer = () => {
|
|
10
|
+
const broadcast = useBroadcast();
|
|
11
|
+
const targetPlayer = useTargetPlayer();
|
|
12
|
+
|
|
13
|
+
if (!targetPlayer || !broadcast) return null;
|
|
14
|
+
|
|
15
|
+
const teamColor = broadcast.teams[targetPlayer.team].color?.primary_color;
|
|
16
|
+
const modifier = targetPlayer.team === 0 ? "left" : "right";
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<AnimatePresence mode="wait">
|
|
20
|
+
<motion.div
|
|
21
|
+
key={targetPlayer.id}
|
|
22
|
+
className={`stat_box ${modifier}_stat_box`}
|
|
23
|
+
style={{ "--team-color": teamColor } as React.CSSProperties}
|
|
24
|
+
initial={{ x: modifier === "left" ? -300 : 300, opacity: 0 }}
|
|
25
|
+
animate={{ x: 0, opacity: 1 }}
|
|
26
|
+
exit={{ x: modifier === "left" ? -300 : 300, opacity: 0 }}
|
|
27
|
+
transition={{ duration: 0.8, ease: [0.77, 0, 0.175, 1] }} // Quartic easing equivalent
|
|
28
|
+
>
|
|
29
|
+
<motion.div
|
|
30
|
+
className={`stat_box_player ${modifier}_stat_box_player`}
|
|
31
|
+
initial={{ opacity: 0, scale: 0.8 }}
|
|
32
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
33
|
+
transition={{ duration: 0.5, ease: "easeOut" }}
|
|
34
|
+
>
|
|
35
|
+
{targetPlayer.name}
|
|
36
|
+
</motion.div>
|
|
37
|
+
|
|
38
|
+
{/* Player Stats */}
|
|
39
|
+
<TargetPlayerStats targetPlayer={targetPlayer} />
|
|
40
|
+
|
|
41
|
+
{/* Player Boost */}
|
|
42
|
+
<motion.div
|
|
43
|
+
className={`stat_box_player_boost ${modifier}_stat_box_player_boost`}
|
|
44
|
+
initial={{ opacity: 0, y: 10 }}
|
|
45
|
+
animate={{ opacity: 1, y: 0 }}
|
|
46
|
+
exit={{ opacity: 0, y: 10 }}
|
|
47
|
+
transition={{ duration: 0.5, ease: "easeOut" }}
|
|
48
|
+
>
|
|
49
|
+
<PlayerBoost boost={targetPlayer.boost} team={targetPlayer.team} />
|
|
50
|
+
</motion.div>
|
|
51
|
+
</motion.div>
|
|
52
|
+
</AnimatePresence>
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export default memo(TargetPlayer);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React, { memo } from "react";
|
|
2
|
+
import { motion } from "framer-motion";
|
|
3
|
+
import { PlayerState } from "@four-leaf-studios/rl-socket-hook";
|
|
4
|
+
|
|
5
|
+
type Props = {
|
|
6
|
+
location: PlayerState["location"];
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const TargetPlayerLocation = ({ location }: Props) => {
|
|
10
|
+
return (
|
|
11
|
+
<motion.div
|
|
12
|
+
className="stat_box_statistic stat_box_statistic_player_location"
|
|
13
|
+
initial={{ opacity: 0, y: 10 }}
|
|
14
|
+
animate={{ opacity: 1, y: 0 }}
|
|
15
|
+
exit={{ opacity: 0, y: 10 }}
|
|
16
|
+
transition={{ duration: 0.5 }}
|
|
17
|
+
>
|
|
18
|
+
<span className="stat_box_statistic_name">Location</span>{" "}
|
|
19
|
+
<span className="stat_box_statistic_value">
|
|
20
|
+
X {location.X?.toFixed(2) || 0}, Y {location.Y?.toFixed(2) || 0}, Z{" "}
|
|
21
|
+
{location.Z?.toFixed(2) || 0}
|
|
22
|
+
</span>
|
|
23
|
+
</motion.div>
|
|
24
|
+
);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default memo(TargetPlayerLocation);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// TargetPlayerStats.tsx
|
|
2
|
+
import React, { memo } from "react";
|
|
3
|
+
import { PlayerState } from "@four-leaf-studios/rl-socket-hook";
|
|
4
|
+
import StatItem from "./StatItem";
|
|
5
|
+
import TargetPlayerLocation from "./TargetPlayerLocation";
|
|
6
|
+
|
|
7
|
+
type Props = {
|
|
8
|
+
targetPlayer: PlayerState;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const TargetPlayerStats = ({ targetPlayer }: Props) => (
|
|
12
|
+
<ul className="stat_box_statistics">
|
|
13
|
+
<StatItem id="id" label="ID" value={targetPlayer.id} />
|
|
14
|
+
<StatItem id="team" label="Team" value={targetPlayer.team} />
|
|
15
|
+
<StatItem id="score" label="Score" value={targetPlayer.score} />
|
|
16
|
+
<StatItem id="goals" label="Goals" value={targetPlayer.goals} />
|
|
17
|
+
<StatItem id="assists" label="Assists" value={targetPlayer.assists} />
|
|
18
|
+
<StatItem id="shots" label="Shots" value={targetPlayer.shots} />
|
|
19
|
+
<StatItem id="saves" label="Saves" value={targetPlayer.saves} />
|
|
20
|
+
<StatItem id="touches" label="Touches" value={targetPlayer.touches} />
|
|
21
|
+
<TargetPlayerLocation location={targetPlayer.location} />
|
|
22
|
+
</ul>
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
export default memo(TargetPlayerStats);
|
package/src/Team.tsx
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { useEventSelector } from "@four-leaf-studios/rl-socket-hook";
|
|
2
|
+
import { motion, AnimatePresence } from "framer-motion";
|
|
3
|
+
import React, { memo } from "react";
|
|
4
|
+
import { Team } from "./types";
|
|
5
|
+
import Player from "./Player";
|
|
6
|
+
|
|
7
|
+
type Props = {
|
|
8
|
+
id: Team["id"];
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const Team = ({ id }: Props) => {
|
|
12
|
+
const players = useEventSelector("game:update_state", (state) => {
|
|
13
|
+
return (
|
|
14
|
+
state?.players &&
|
|
15
|
+
Object.values(state?.players).filter(
|
|
16
|
+
(player) => player.team.toString() === id
|
|
17
|
+
)
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const isLeft = id === "0";
|
|
22
|
+
const modifier = isLeft ? "left" : "right";
|
|
23
|
+
|
|
24
|
+
if (!players) return null;
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<motion.div
|
|
28
|
+
className={`team_box ${modifier}_team_box`}
|
|
29
|
+
initial={{ x: isLeft ? -300 : 300, opacity: 0 }}
|
|
30
|
+
animate={{ x: 0, opacity: 1 }}
|
|
31
|
+
exit={{ x: isLeft ? -300 : 300, opacity: 0 }}
|
|
32
|
+
transition={{ duration: 1, ease: [0.77, 0, 0.175, 1] }}
|
|
33
|
+
>
|
|
34
|
+
<AnimatePresence>
|
|
35
|
+
{players.map((player) => (
|
|
36
|
+
<Player player={player} key={player.id} />
|
|
37
|
+
))}
|
|
38
|
+
</AnimatePresence>
|
|
39
|
+
</motion.div>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const MemoizedTeam = memo(Team);
|
|
44
|
+
MemoizedTeam.displayName = "Team";
|
|
45
|
+
export default MemoizedTeam;
|
package/src/Teams.tsx
ADDED
package/src/Timer.tsx
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useEventSelector } from "@four-leaf-studios/rl-socket-hook";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { memo } from "react";
|
|
4
|
+
|
|
5
|
+
const TimerComponent: React.FC = () => {
|
|
6
|
+
// Add proper null checks to prevent "Cannot read properties of undefined" error
|
|
7
|
+
const time_seconds = useEventSelector(
|
|
8
|
+
"game:update_state",
|
|
9
|
+
(state) => state?.game?.time_seconds
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
// Return null if time_seconds is not available
|
|
13
|
+
if (typeof time_seconds !== "number") return null;
|
|
14
|
+
|
|
15
|
+
const minutes = Math.floor(time_seconds / 60);
|
|
16
|
+
const seconds = time_seconds % 60;
|
|
17
|
+
const formattedTime = `${minutes}:${seconds < 10 ? "0" : ""}${seconds}`;
|
|
18
|
+
|
|
19
|
+
return <span className="time_box">{formattedTime}</span>;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const Timer = memo(TimerComponent);
|
|
23
|
+
Timer.displayName = "Timer";
|
|
24
|
+
export default Timer;
|
|
Binary file
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React, { createContext, useContext, ReactNode } from "react";
|
|
2
|
+
import { Broadcast } from "../types";
|
|
3
|
+
|
|
4
|
+
const BroadcastContext = createContext<Broadcast | undefined>(undefined);
|
|
5
|
+
|
|
6
|
+
export const BroadcastProvider: React.FC<{
|
|
7
|
+
broadcast: Broadcast;
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
}> = ({ broadcast, children }) => (
|
|
10
|
+
<BroadcastContext.Provider value={broadcast}>
|
|
11
|
+
{children}
|
|
12
|
+
</BroadcastContext.Provider>
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
export function useBroadcast(): Broadcast {
|
|
16
|
+
const ctx = useContext(BroadcastContext);
|
|
17
|
+
if (ctx === undefined) {
|
|
18
|
+
throw new Error("useBroadcast must be used within a BroadcastProvider");
|
|
19
|
+
}
|
|
20
|
+
return ctx;
|
|
21
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/* overlay-reset.css */
|
|
2
|
+
.overlay * {
|
|
3
|
+
all: unset;
|
|
4
|
+
line-height: initial;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.overlay.testing {
|
|
8
|
+
background: url("../assets/overlay-testing-background.png");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
body {
|
|
12
|
+
background-color: transparent !important;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.scoreboard_box {
|
|
16
|
+
display: flex;
|
|
17
|
+
flex-flow: column;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.replay_box {
|
|
21
|
+
position: absolute;
|
|
22
|
+
}
|
|
23
|
+
.top_bar {
|
|
24
|
+
display: flex;
|
|
25
|
+
flex-flow: row;
|
|
26
|
+
justify-content: center;
|
|
27
|
+
text-align: center;
|
|
28
|
+
width: fit-content;
|
|
29
|
+
align-items: center;
|
|
30
|
+
position: relative;
|
|
31
|
+
top: 0;
|
|
32
|
+
left: 50%;
|
|
33
|
+
transform: translate(-50%);
|
|
34
|
+
padding-top: 0;
|
|
35
|
+
color: #fff;
|
|
36
|
+
}
|
|
37
|
+
.main_bar {
|
|
38
|
+
display: flex;
|
|
39
|
+
flex-flow: row;
|
|
40
|
+
justify-content: center;
|
|
41
|
+
text-align: center;
|
|
42
|
+
position: relative;
|
|
43
|
+
top: 0;
|
|
44
|
+
left: 50%;
|
|
45
|
+
transform: translate(-50%);
|
|
46
|
+
padding-top: 0;
|
|
47
|
+
color: #fff;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.bottom_bar {
|
|
51
|
+
display: flex;
|
|
52
|
+
flex-flow: row;
|
|
53
|
+
justify-content: center;
|
|
54
|
+
text-align: center;
|
|
55
|
+
width: fit-content;
|
|
56
|
+
position: relative;
|
|
57
|
+
top: 0;
|
|
58
|
+
left: 50%;
|
|
59
|
+
transform: translate(-50%);
|
|
60
|
+
padding-top: 0;
|
|
61
|
+
color: #fff;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.scoreboard_team_box {
|
|
65
|
+
display: flex;
|
|
66
|
+
}
|
|
67
|
+
.left_scoreboard_team_box {
|
|
68
|
+
order: 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.right_scoreboard_team_box {
|
|
72
|
+
order: 2;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.time_box {
|
|
76
|
+
order: 1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* Left Side of scoreboard */
|
|
80
|
+
.left_score_box {
|
|
81
|
+
order: 3;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.left_name_box {
|
|
85
|
+
order: 2;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.left_logo_box {
|
|
89
|
+
order: 1;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* Righ Side */
|
|
93
|
+
.right_score_box {
|
|
94
|
+
order: 1;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.right_name_box {
|
|
98
|
+
order: 2;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.right_logo_box {
|
|
102
|
+
order: 3;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* Default Styles */
|
|
106
|
+
.logo_box {
|
|
107
|
+
object-fit: scale-down;
|
|
108
|
+
box-sizing: border-box;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.name_box {
|
|
112
|
+
display: flex;
|
|
113
|
+
justify-content: center;
|
|
114
|
+
align-items: center;
|
|
115
|
+
text-align: center;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.series_box {
|
|
119
|
+
height: 100%;
|
|
120
|
+
display: flex;
|
|
121
|
+
justify-content: center;
|
|
122
|
+
align-items: center;
|
|
123
|
+
text-align: center;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.left_series_box {
|
|
127
|
+
order: 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.right_series_box {
|
|
131
|
+
order: 2;
|
|
132
|
+
flex-direction: row-reverse;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.score_box,
|
|
136
|
+
.time_box {
|
|
137
|
+
display: flex;
|
|
138
|
+
justify-content: center;
|
|
139
|
+
align-items: center;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.game_box {
|
|
143
|
+
order: 1;
|
|
144
|
+
height: 100%;
|
|
145
|
+
display: flex;
|
|
146
|
+
justify-content: center;
|
|
147
|
+
align-items: center;
|
|
148
|
+
text-align: center;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.team_box {
|
|
152
|
+
display: flex;
|
|
153
|
+
flex-flow: column;
|
|
154
|
+
color: #fff;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.player_box {
|
|
158
|
+
display: flex;
|
|
159
|
+
position: relative;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.boost_meter_bar {
|
|
163
|
+
height: 100%;
|
|
164
|
+
transition: width 0.35s;
|
|
165
|
+
display: block;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.right_boost_meter_bar {
|
|
169
|
+
margin-left: auto;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.stat_box {
|
|
173
|
+
display: flex;
|
|
174
|
+
position: absolute;
|
|
175
|
+
bottom: 0;
|
|
176
|
+
left: 0;
|
|
177
|
+
color: #fff;
|
|
178
|
+
align-items: center;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.stat_box_statistics {
|
|
182
|
+
display: flex;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.stat_box_statistic_name {
|
|
186
|
+
order: 2;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.stat_box_statistic_value {
|
|
190
|
+
font-weight: bold;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.stat_box_player_boost {
|
|
194
|
+
margin-top: 5px;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* Target Boost */
|
|
198
|
+
.target_boost {
|
|
199
|
+
position: absolute;
|
|
200
|
+
bottom: 0;
|
|
201
|
+
left: 0;
|
|
202
|
+
color: #fff;
|
|
203
|
+
align-items: center;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.target_boost_container {
|
|
207
|
+
display: flex;
|
|
208
|
+
position: relative;
|
|
209
|
+
bottom: 0;
|
|
210
|
+
left: 0;
|
|
211
|
+
color: #fff;
|
|
212
|
+
align-items: center;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.target_boost_meter {
|
|
216
|
+
position: absolute;
|
|
217
|
+
inset: 0;
|
|
218
|
+
border-radius: 50%;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.left_target_boost_meter {
|
|
222
|
+
background: conic-gradient(
|
|
223
|
+
var(--team-left-primary, #0052cc) calc(var(--target_boost) * 1%),
|
|
224
|
+
transparent calc(var(--target_boost) * 1%)
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.right_target_boost_meter {
|
|
229
|
+
background: conic-gradient(
|
|
230
|
+
var(--team-right-primary, #ff6600) calc(var(--target_boost) * 1%),
|
|
231
|
+
transparent calc(var(--target_boost) * 1%)
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.target_boost_meter_inner {
|
|
236
|
+
position: absolute;
|
|
237
|
+
inset: 5px;
|
|
238
|
+
border-radius: 50%;
|
|
239
|
+
background-color: rgba(0, 0, 0, 0.7);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.target_boost_svg {
|
|
243
|
+
position: absolute;
|
|
244
|
+
width: 100%;
|
|
245
|
+
height: 100%;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.target_boost_svg_circle {
|
|
249
|
+
fill: none;
|
|
250
|
+
stroke: rgba(255, 255, 255, 0.2);
|
|
251
|
+
stroke-width: 2;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.target_boost_value {
|
|
255
|
+
position: absolute;
|
|
256
|
+
inset: 0;
|
|
257
|
+
display: flex;
|
|
258
|
+
align-items: center;
|
|
259
|
+
justify-content: center;
|
|
260
|
+
font-size: 24px;
|
|
261
|
+
font-weight: bold;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/* Replay Box */
|
|
265
|
+
.replay_box {
|
|
266
|
+
position: absolute;
|
|
267
|
+
top: 50%;
|
|
268
|
+
left: 50%;
|
|
269
|
+
transform: translate(-50%, -50%);
|
|
270
|
+
background-color: rgba(0, 0, 0, 0.7);
|
|
271
|
+
padding: 10px 20px;
|
|
272
|
+
border-radius: 4px;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.replay_text {
|
|
276
|
+
font-size: 24px;
|
|
277
|
+
font-weight: bold;
|
|
278
|
+
text-transform: uppercase;
|
|
279
|
+
letter-spacing: 2px;
|
|
280
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// src/hooks/useOverlayStyles.ts
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
import jsonToCss from "@rysh/json-to-css";
|
|
4
|
+
import type { Broadcast, Color } from "../types";
|
|
5
|
+
|
|
6
|
+
export type CSSJSON = {
|
|
7
|
+
[selector: string]: {
|
|
8
|
+
[prop: string]: string | number;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Hook that injects both team-color variables (from Broadcast)
|
|
14
|
+
* and optional custom styles (string or JSON) into the document.
|
|
15
|
+
* Uses `@rysh/json-to-css` for JSON → CSS conversion.
|
|
16
|
+
*/
|
|
17
|
+
export function useOverlayStyles(
|
|
18
|
+
broadcast: Broadcast,
|
|
19
|
+
styles?: string | CSSJSON
|
|
20
|
+
) {
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (!broadcast?.teams) return;
|
|
23
|
+
|
|
24
|
+
const shade = (c: string, pct: number): string => {
|
|
25
|
+
const hex = c.replace("#", "");
|
|
26
|
+
const num = parseInt(hex, 16);
|
|
27
|
+
const amt = Math.round(2.55 * pct);
|
|
28
|
+
let R = (num >> 16) + amt;
|
|
29
|
+
let G = ((num >> 8) & 0xff) + amt;
|
|
30
|
+
let B = (num & 0xff) + amt;
|
|
31
|
+
R = Math.min(255, Math.max(0, R));
|
|
32
|
+
G = Math.min(255, Math.max(0, G));
|
|
33
|
+
B = Math.min(255, Math.max(0, B));
|
|
34
|
+
return (
|
|
35
|
+
"#" +
|
|
36
|
+
((1 << 24) + (R << 16) + (G << 8) + B)
|
|
37
|
+
.toString(16)
|
|
38
|
+
.slice(1)
|
|
39
|
+
.toUpperCase()
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const defaultBlue: Color = {
|
|
44
|
+
id: "",
|
|
45
|
+
name: "",
|
|
46
|
+
primary_color: "#0052cc",
|
|
47
|
+
secondary_color: "#ffffff",
|
|
48
|
+
mutual_color: "#00a8ff",
|
|
49
|
+
created_at: "",
|
|
50
|
+
created_by: null,
|
|
51
|
+
};
|
|
52
|
+
const defaultOrange: Color = {
|
|
53
|
+
id: "",
|
|
54
|
+
name: "",
|
|
55
|
+
primary_color: "#ff6600",
|
|
56
|
+
secondary_color: "#ffffff",
|
|
57
|
+
mutual_color: "#ffaa00",
|
|
58
|
+
created_at: "",
|
|
59
|
+
created_by: null,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const blueColor =
|
|
63
|
+
broadcast.teams.find((t) => t.side === "blue")?.color ?? defaultBlue;
|
|
64
|
+
const orangeColor =
|
|
65
|
+
broadcast.teams.find((t) => t.side === "orange")?.color ?? defaultOrange;
|
|
66
|
+
|
|
67
|
+
const teamJSON: CSSJSON = {
|
|
68
|
+
":root": {
|
|
69
|
+
"--team-left-primary": blueColor.primary_color,
|
|
70
|
+
"--team-left-primary-light": shade(blueColor.primary_color, 20),
|
|
71
|
+
"--team-left-primary-dark": shade(blueColor.primary_color, -20),
|
|
72
|
+
"--team-left-secondary": blueColor.secondary_color,
|
|
73
|
+
"--team-left-mutual":
|
|
74
|
+
blueColor.mutual_color ?? defaultBlue.mutual_color!,
|
|
75
|
+
|
|
76
|
+
"--team-right-primary": orangeColor.primary_color,
|
|
77
|
+
"--team-right-primary-light": shade(orangeColor.primary_color, 20),
|
|
78
|
+
"--team-right-primary-dark": shade(orangeColor.primary_color, -20),
|
|
79
|
+
"--team-right-secondary": orangeColor.secondary_color,
|
|
80
|
+
"--team-right-mutual":
|
|
81
|
+
orangeColor.mutual_color ?? defaultOrange.mutual_color!,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// ==== CHANGE HERE: pass a second arg (root selector) ====
|
|
86
|
+
let cssText: string;
|
|
87
|
+
if (styles && typeof styles === "object") {
|
|
88
|
+
const merged: CSSJSON = { ...teamJSON, ...styles };
|
|
89
|
+
cssText = jsonToCss(merged, ""); // <- empty string scopes at top
|
|
90
|
+
} else {
|
|
91
|
+
cssText = jsonToCss(teamJSON, ""); // <- ditto
|
|
92
|
+
if (typeof styles === "string") {
|
|
93
|
+
cssText += "\n\n" + styles;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const styleEl = document.createElement("style");
|
|
98
|
+
styleEl.id = "overlay-styles";
|
|
99
|
+
styleEl.textContent = cssText;
|
|
100
|
+
document.getElementById("overlay-styles")?.remove();
|
|
101
|
+
document.head.appendChild(styleEl);
|
|
102
|
+
|
|
103
|
+
return () => {
|
|
104
|
+
document.getElementById("overlay-styles")?.remove();
|
|
105
|
+
};
|
|
106
|
+
}, [broadcast, styles]);
|
|
107
|
+
}
|