@progamestore/games 0.2.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/LICENSE +21 -0
- package/dist/GameAuth.d.ts +10 -0
- package/dist/GameAuth.d.ts.map +1 -0
- package/dist/GameAuth.js +108 -0
- package/dist/GameAuth.js.map +1 -0
- package/dist/GameButton.d.ts +21 -0
- package/dist/GameButton.d.ts.map +1 -0
- package/dist/GameButton.js +72 -0
- package/dist/GameButton.js.map +1 -0
- package/dist/GameShell.d.ts +26 -0
- package/dist/GameShell.d.ts.map +1 -0
- package/dist/GameShell.js +61 -0
- package/dist/GameShell.js.map +1 -0
- package/dist/GameTopbar.d.ts +62 -0
- package/dist/GameTopbar.d.ts.map +1 -0
- package/dist/GameTopbar.js +184 -0
- package/dist/GameTopbar.js.map +1 -0
- package/dist/Leaderboard.d.ts +8 -0
- package/dist/Leaderboard.d.ts.map +1 -0
- package/dist/Leaderboard.js +14 -0
- package/dist/Leaderboard.js.map +1 -0
- package/dist/SoundContext.d.ts +16 -0
- package/dist/SoundContext.d.ts.map +1 -0
- package/dist/SoundContext.js +16 -0
- package/dist/SoundContext.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/useAuth.d.ts +12 -0
- package/dist/useAuth.d.ts.map +1 -0
- package/dist/useAuth.js +46 -0
- package/dist/useAuth.js.map +1 -0
- package/dist/useGameSounds.d.ts +16 -0
- package/dist/useGameSounds.d.ts.map +1 -0
- package/dist/useGameSounds.js +93 -0
- package/dist/useGameSounds.js.map +1 -0
- package/dist/useLeaderboard.d.ts +18 -0
- package/dist/useLeaderboard.d.ts.map +1 -0
- package/dist/useLeaderboard.js +49 -0
- package/dist/useLeaderboard.js.map +1 -0
- package/dist/useRooms.d.ts +78 -0
- package/dist/useRooms.d.ts.map +1 -0
- package/dist/useRooms.js +104 -0
- package/dist/useRooms.js.map +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Leaderboard.d.ts","sourceRoot":"","sources":["../src/Leaderboard.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAC9B,YAAY,EAAE,gBAAgB,EAAE,CAAC;IACjC,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,wBAAgB,WAAW,CAAC,EAC1B,SAAS,EACT,YAAY,EACZ,OAAO,GACR,EAAE,gBAAgB,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CA2DtC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
export function Leaderboard({ topScores, recentScores, loading, }) {
|
|
4
|
+
const [tab, setTab] = useState('top');
|
|
5
|
+
const scores = tab === 'top' ? topScores : recentScores;
|
|
6
|
+
if (loading) {
|
|
7
|
+
return (_jsx("div", { className: "px-4 py-3 text-xs", style: { color: 'var(--muted)' }, children: "Loading scores..." }));
|
|
8
|
+
}
|
|
9
|
+
return (_jsxs("div", { className: "flex flex-col gap-2 px-4 py-3", children: [_jsx("div", { className: "flex gap-1", children: ['top', 'recent'].map((t) => (_jsx("button", { onClick: () => setTab(t), className: "px-3 py-1 text-xs font-semibold rounded-lg", style: {
|
|
10
|
+
background: tab === t ? 'var(--accent)' : 'transparent',
|
|
11
|
+
color: tab === t ? '#fff' : 'var(--muted)',
|
|
12
|
+
}, children: t === 'top' ? 'Top' : 'Recent' }, t))) }), scores.length === 0 ? (_jsx("div", { className: "text-xs", style: { color: 'var(--muted)' }, children: "No scores yet. Be the first!" })) : (_jsx("div", { className: "flex flex-col gap-1", children: scores.map((entry, i) => (_jsxs("div", { className: "flex items-center justify-between text-xs py-1", style: { borderBottom: '1px solid var(--line)' }, children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "w-5 text-right font-semibold", style: { color: i < 3 ? 'var(--accent)' : 'var(--muted)' }, children: i + 1 }), _jsx("span", { className: "truncate max-w-[8rem]", children: entry.player_name })] }), _jsx("span", { className: "font-bold", style: { fontFamily: 'Fraunces, serif' }, children: entry.score.toLocaleString() })] }, `${entry.player_name}-${entry.score}-${i}`))) }))] }));
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=Leaderboard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Leaderboard.js","sourceRoot":"","sources":["../src/Leaderboard.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AASjC,MAAM,UAAU,WAAW,CAAC,EAC1B,SAAS,EACT,YAAY,EACZ,OAAO,GACU;IACjB,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,QAAQ,CAAmB,KAAK,CAAC,CAAC;IACxD,MAAM,MAAM,GAAG,GAAG,KAAK,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,YAAY,CAAC;IAExD,IAAI,OAAO,EAAE,CAAC;QACZ,OAAO,CACL,cAAK,SAAS,EAAC,mBAAmB,EAAC,KAAK,EAAE,EAAE,KAAK,EAAE,cAAc,EAAE,kCAE7D,CACP,CAAC;IACJ,CAAC;IAED,OAAO,CACL,eAAK,SAAS,EAAC,+BAA+B,aAC5C,cAAK,SAAS,EAAC,YAAY,YACvB,CAAC,KAAK,EAAE,QAAQ,CAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CACvC,iBAEE,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EACxB,SAAS,EAAC,4CAA4C,EACtD,KAAK,EAAE;wBACL,UAAU,EAAE,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,aAAa;wBACvD,KAAK,EAAE,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,cAAc;qBAC3C,YAEA,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ,IAR1B,CAAC,CASC,CACV,CAAC,GACE,EACL,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CACrB,cAAK,SAAS,EAAC,SAAS,EAAC,KAAK,EAAE,EAAE,KAAK,EAAE,cAAc,EAAE,6CAEnD,CACP,CAAC,CAAC,CAAC,CACF,cAAK,SAAS,EAAC,qBAAqB,YACjC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,CACxB,eAEE,SAAS,EAAC,gDAAgD,EAC1D,KAAK,EAAE,EAAE,YAAY,EAAE,uBAAuB,EAAE,aAEhD,eAAK,SAAS,EAAC,yBAAyB,aACtC,eACE,SAAS,EAAC,8BAA8B,EACxC,KAAK,EAAE,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,cAAc,EAAE,YAEzD,CAAC,GAAG,CAAC,GACD,EACP,eAAM,SAAS,EAAC,uBAAuB,YAAE,KAAK,CAAC,WAAW,GAAQ,IAC9D,EACN,eAAM,SAAS,EAAC,WAAW,EAAC,KAAK,EAAE,EAAE,UAAU,EAAE,iBAAiB,EAAE,YACjE,KAAK,CAAC,KAAK,CAAC,cAAc,EAAE,GACxB,KAfF,GAAG,KAAK,CAAC,WAAW,IAAI,KAAK,CAAC,KAAK,IAAI,CAAC,EAAE,CAgB3C,CACP,CAAC,GACE,CACP,IACG,CACP,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type * as React from 'react';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
interface SoundState {
|
|
4
|
+
muted: boolean;
|
|
5
|
+
toggle: () => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function SoundProvider({ children }: {
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
}): React.JSX.Element;
|
|
10
|
+
/**
|
|
11
|
+
* Read the platform sound state. Muted by default.
|
|
12
|
+
* Games MUST check `muted` before playing any audio.
|
|
13
|
+
*/
|
|
14
|
+
export declare function useSound(): SoundState;
|
|
15
|
+
export {};
|
|
16
|
+
//# sourceMappingURL=SoundContext.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SoundContext.d.ts","sourceRoot":"","sources":["../src/SoundContext.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAC;AACpC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAGvC,UAAU,UAAU;IAClB,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB;AAID,wBAAgB,aAAa,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAItF;AAED;;;GAGG;AACH,wBAAgB,QAAQ,IAAI,UAAU,CAErC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useCallback, useContext, useState } from 'react';
|
|
3
|
+
const SoundContext = createContext({ muted: true, toggle: () => { } });
|
|
4
|
+
export function SoundProvider({ children }) {
|
|
5
|
+
const [muted, setMuted] = useState(true);
|
|
6
|
+
const toggle = useCallback(() => setMuted((m) => !m), []);
|
|
7
|
+
return _jsx(SoundContext.Provider, { value: { muted, toggle }, children: children });
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Read the platform sound state. Muted by default.
|
|
11
|
+
* Games MUST check `muted` before playing any audio.
|
|
12
|
+
*/
|
|
13
|
+
export function useSound() {
|
|
14
|
+
return useContext(SoundContext);
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=SoundContext.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SoundContext.js","sourceRoot":"","sources":["../src/SoundContext.tsx"],"names":[],"mappings":";AAEA,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAOzE,MAAM,YAAY,GAAG,aAAa,CAAa,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,GAAE,CAAC,EAAE,CAAC,CAAC;AAElF,MAAM,UAAU,aAAa,CAAC,EAAE,QAAQ,EAA2B;IACjE,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;IACzC,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAC1D,OAAO,KAAC,YAAY,CAAC,QAAQ,IAAC,KAAK,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,YAAG,QAAQ,GAAyB,CAAC;AAC7F,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,QAAQ;IACtB,OAAO,UAAU,CAAC,YAAY,CAAC,CAAC;AAClC,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @freeappstore/games — shared React UI primitives for ProGameStore games.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists:
|
|
5
|
+
* - Games on the platform must be **brand-consistent** (no per-game custom
|
|
6
|
+
* topbars). The compliance suite enforces brand fonts and CSS tokens; the
|
|
7
|
+
* topbar is the next leak.
|
|
8
|
+
* - Games must **fit the viewport** (no scrolling). GameShell hard-locks
|
|
9
|
+
* layout to 100svh and prevents overflow on the wrapper, so a game can't
|
|
10
|
+
* accidentally introduce vertical / horizontal scroll.
|
|
11
|
+
*
|
|
12
|
+
* What you get:
|
|
13
|
+
* <GameShell topbar={<GameTopbar score={42} />}>{your game}</GameShell>
|
|
14
|
+
*/
|
|
15
|
+
export { GameAuth } from './GameAuth.js';
|
|
16
|
+
export { GameButton, type GameButtonProps, type GameButtonSize, type GameButtonVariant, } from './GameButton.js';
|
|
17
|
+
export { GameShell, type GameShellProps } from './GameShell.js';
|
|
18
|
+
export { GameTopbar, type GameTopbarProps, type GameTopbarStat, } from './GameTopbar.js';
|
|
19
|
+
export { Leaderboard, type LeaderboardProps } from './Leaderboard.js';
|
|
20
|
+
export { useSound } from './SoundContext.js';
|
|
21
|
+
export { type User, useAuth } from './useAuth.js';
|
|
22
|
+
export { useGameSounds } from './useGameSounds.js';
|
|
23
|
+
export { type LeaderboardEntry, useLeaderboard, } from './useLeaderboard.js';
|
|
24
|
+
export { type RoomMessage, type RoomStatus, type UseRoomsOptions, type UseRoomsResult, useRooms, } from './useRooms.js';
|
|
25
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EACL,UAAU,EACV,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,iBAAiB,GACvB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChE,OAAO,EACL,UAAU,EACV,KAAK,eAAe,EACpB,KAAK,cAAc,GACpB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACtE,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAAE,KAAK,IAAI,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EACL,KAAK,gBAAgB,EACrB,cAAc,GACf,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,QAAQ,GACT,MAAM,eAAe,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @freeappstore/games — shared React UI primitives for ProGameStore games.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists:
|
|
5
|
+
* - Games on the platform must be **brand-consistent** (no per-game custom
|
|
6
|
+
* topbars). The compliance suite enforces brand fonts and CSS tokens; the
|
|
7
|
+
* topbar is the next leak.
|
|
8
|
+
* - Games must **fit the viewport** (no scrolling). GameShell hard-locks
|
|
9
|
+
* layout to 100svh and prevents overflow on the wrapper, so a game can't
|
|
10
|
+
* accidentally introduce vertical / horizontal scroll.
|
|
11
|
+
*
|
|
12
|
+
* What you get:
|
|
13
|
+
* <GameShell topbar={<GameTopbar score={42} />}>{your game}</GameShell>
|
|
14
|
+
*/
|
|
15
|
+
export { GameAuth } from './GameAuth.js';
|
|
16
|
+
export { GameButton, } from './GameButton.js';
|
|
17
|
+
export { GameShell } from './GameShell.js';
|
|
18
|
+
export { GameTopbar, } from './GameTopbar.js';
|
|
19
|
+
export { Leaderboard } from './Leaderboard.js';
|
|
20
|
+
export { useSound } from './SoundContext.js';
|
|
21
|
+
export { useAuth } from './useAuth.js';
|
|
22
|
+
export { useGameSounds } from './useGameSounds.js';
|
|
23
|
+
export { useLeaderboard, } from './useLeaderboard.js';
|
|
24
|
+
export { useRooms, } from './useRooms.js';
|
|
25
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EACL,UAAU,GAIX,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,SAAS,EAAuB,MAAM,gBAAgB,CAAC;AAChE,OAAO,EACL,UAAU,GAGX,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,WAAW,EAAyB,MAAM,kBAAkB,CAAC;AACtE,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAAa,OAAO,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAEL,cAAc,GACf,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAKL,QAAQ,GACT,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useAuth.d.ts","sourceRoot":"","sources":["../src/useAuth.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,OAAO,IAAI;IACzB,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CA4CA"}
|
package/dist/useAuth.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
export function useAuth() {
|
|
3
|
+
const [user, setUser] = useState(null);
|
|
4
|
+
const [loading, setLoading] = useState(true);
|
|
5
|
+
useEffect(() => {
|
|
6
|
+
let cancelled = false;
|
|
7
|
+
fetch('https://auth.progamestore.online/me', { credentials: 'include' })
|
|
8
|
+
.then((res) => {
|
|
9
|
+
if (!cancelled && res.ok)
|
|
10
|
+
return res.json();
|
|
11
|
+
return null;
|
|
12
|
+
})
|
|
13
|
+
.then((data) => {
|
|
14
|
+
if (!cancelled)
|
|
15
|
+
setUser(data);
|
|
16
|
+
})
|
|
17
|
+
.catch(() => {
|
|
18
|
+
// 401 or network error — user is not signed in
|
|
19
|
+
})
|
|
20
|
+
.finally(() => {
|
|
21
|
+
if (!cancelled)
|
|
22
|
+
setLoading(false);
|
|
23
|
+
});
|
|
24
|
+
return () => {
|
|
25
|
+
cancelled = true;
|
|
26
|
+
};
|
|
27
|
+
}, []);
|
|
28
|
+
const signIn = useCallback(() => {
|
|
29
|
+
window.location.href = `https://auth.progamestore.online/login?redirect=${encodeURIComponent(window.location.href)}`;
|
|
30
|
+
}, []);
|
|
31
|
+
const signOut = useCallback(() => {
|
|
32
|
+
fetch('https://auth.progamestore.online/logout', {
|
|
33
|
+
method: 'POST',
|
|
34
|
+
credentials: 'include',
|
|
35
|
+
})
|
|
36
|
+
.catch(() => {
|
|
37
|
+
// best-effort
|
|
38
|
+
})
|
|
39
|
+
.finally(() => {
|
|
40
|
+
setUser(null);
|
|
41
|
+
window.location.reload();
|
|
42
|
+
});
|
|
43
|
+
}, []);
|
|
44
|
+
return { user, loading, signIn, signOut };
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=useAuth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useAuth.js","sourceRoot":"","sources":["../src/useAuth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAQzD,MAAM,UAAU,OAAO;IAMrB,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAc,IAAI,CAAC,CAAC;IACpD,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;IAE7C,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,SAAS,GAAG,KAAK,CAAC;QACtB,KAAK,CAAC,qCAAqC,EAAE,EAAE,WAAW,EAAE,SAAS,EAAE,CAAC;aACrE,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE;YACZ,IAAI,CAAC,SAAS,IAAI,GAAG,CAAC,EAAE;gBAAE,OAAO,GAAG,CAAC,IAAI,EAAE,CAAC;YAC5C,OAAO,IAAI,CAAC;QACd,CAAC,CAAC;aACD,IAAI,CAAC,CAAC,IAAiB,EAAE,EAAE;YAC1B,IAAI,CAAC,SAAS;gBAAE,OAAO,CAAC,IAAI,CAAC,CAAC;QAChC,CAAC,CAAC;aACD,KAAK,CAAC,GAAG,EAAE;YACV,+CAA+C;QACjD,CAAC,CAAC;aACD,OAAO,CAAC,GAAG,EAAE;YACZ,IAAI,CAAC,SAAS;gBAAE,UAAU,CAAC,KAAK,CAAC,CAAC;QACpC,CAAC,CAAC,CAAC;QACL,OAAO,GAAG,EAAE;YACV,SAAS,GAAG,IAAI,CAAC;QACnB,CAAC,CAAC;IACJ,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,EAAE;QAC9B,MAAM,CAAC,QAAQ,CAAC,IAAI,GAAG,mDAAmD,kBAAkB,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;IACvH,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,EAAE;QAC/B,KAAK,CAAC,yCAAyC,EAAE;YAC/C,MAAM,EAAE,MAAM;YACd,WAAW,EAAE,SAAS;SACvB,CAAC;aACC,KAAK,CAAC,GAAG,EAAE;YACV,cAAc;QAChB,CAAC,CAAC;aACD,OAAO,CAAC,GAAG,EAAE;YACZ,OAAO,CAAC,IAAI,CAAC,CAAC;YACd,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;QAC3B,CAAC,CAAC,CAAC;IACP,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;AAC5C,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synthesized game sound effects via Web Audio API.
|
|
3
|
+
* Zero audio files — works offline, no downloads.
|
|
4
|
+
* All sounds respect the SDK mute toggle automatically.
|
|
5
|
+
*/
|
|
6
|
+
export declare function useGameSounds(): {
|
|
7
|
+
playMove: () => void;
|
|
8
|
+
playScore: () => void;
|
|
9
|
+
playError: () => void;
|
|
10
|
+
playGameOver: () => void;
|
|
11
|
+
playLevelUp: () => void;
|
|
12
|
+
playDrop: () => void;
|
|
13
|
+
playClear: () => void;
|
|
14
|
+
playTick: () => void;
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=useGameSounds.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useGameSounds.d.ts","sourceRoot":"","sources":["../src/useGameSounds.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,wBAAgB,aAAa;;;;;;;;;EA+F5B"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { useCallback, useRef } from 'react';
|
|
2
|
+
import { useSound } from './SoundContext.js';
|
|
3
|
+
/**
|
|
4
|
+
* Synthesized game sound effects via Web Audio API.
|
|
5
|
+
* Zero audio files — works offline, no downloads.
|
|
6
|
+
* All sounds respect the SDK mute toggle automatically.
|
|
7
|
+
*/
|
|
8
|
+
export function useGameSounds() {
|
|
9
|
+
const { muted } = useSound();
|
|
10
|
+
const ctxRef = useRef(null);
|
|
11
|
+
const getCtx = useCallback(() => {
|
|
12
|
+
if (muted)
|
|
13
|
+
return null;
|
|
14
|
+
if (!ctxRef.current) {
|
|
15
|
+
try {
|
|
16
|
+
ctxRef.current = new AudioContext();
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (ctxRef.current.state === 'suspended') {
|
|
23
|
+
ctxRef.current.resume();
|
|
24
|
+
}
|
|
25
|
+
return ctxRef.current;
|
|
26
|
+
}, [muted]);
|
|
27
|
+
const tone = useCallback((freq, duration, type = 'sine', volume = 0.15) => {
|
|
28
|
+
const ctx = getCtx();
|
|
29
|
+
if (!ctx)
|
|
30
|
+
return;
|
|
31
|
+
const osc = ctx.createOscillator();
|
|
32
|
+
const gain = ctx.createGain();
|
|
33
|
+
osc.type = type;
|
|
34
|
+
osc.frequency.value = freq;
|
|
35
|
+
gain.gain.setValueAtTime(volume, ctx.currentTime);
|
|
36
|
+
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + duration);
|
|
37
|
+
osc.connect(gain);
|
|
38
|
+
gain.connect(ctx.destination);
|
|
39
|
+
osc.start(ctx.currentTime);
|
|
40
|
+
osc.stop(ctx.currentTime + duration);
|
|
41
|
+
}, [getCtx]);
|
|
42
|
+
/** Short click/tap — piece moved, card flipped, button pressed */
|
|
43
|
+
const playMove = useCallback(() => {
|
|
44
|
+
tone(600, 0.06, 'square', 0.08);
|
|
45
|
+
}, [tone]);
|
|
46
|
+
/** Positive ding — scored a point, matched, correct answer */
|
|
47
|
+
const playScore = useCallback(() => {
|
|
48
|
+
tone(880, 0.12, 'sine', 0.15);
|
|
49
|
+
setTimeout(() => tone(1100, 0.15, 'sine', 0.12), 60);
|
|
50
|
+
}, [tone]);
|
|
51
|
+
/** Negative buzz — wrong answer, hit obstacle, lost life */
|
|
52
|
+
const playError = useCallback(() => {
|
|
53
|
+
tone(200, 0.2, 'sawtooth', 0.1);
|
|
54
|
+
}, [tone]);
|
|
55
|
+
/** Game over — descending tones */
|
|
56
|
+
const playGameOver = useCallback(() => {
|
|
57
|
+
tone(440, 0.15, 'sine', 0.12);
|
|
58
|
+
setTimeout(() => tone(350, 0.15, 'sine', 0.1), 100);
|
|
59
|
+
setTimeout(() => tone(260, 0.3, 'sine', 0.08), 200);
|
|
60
|
+
}, [tone]);
|
|
61
|
+
/** Level up / achievement — ascending arpeggio */
|
|
62
|
+
const playLevelUp = useCallback(() => {
|
|
63
|
+
tone(523, 0.1, 'sine', 0.12);
|
|
64
|
+
setTimeout(() => tone(659, 0.1, 'sine', 0.12), 80);
|
|
65
|
+
setTimeout(() => tone(784, 0.1, 'sine', 0.12), 160);
|
|
66
|
+
setTimeout(() => tone(1047, 0.2, 'sine', 0.15), 240);
|
|
67
|
+
}, [tone]);
|
|
68
|
+
/** Hard drop / thud — Tetris block landing, bowling throw */
|
|
69
|
+
const playDrop = useCallback(() => {
|
|
70
|
+
tone(150, 0.12, 'triangle', 0.2);
|
|
71
|
+
}, [tone]);
|
|
72
|
+
/** Line clear / combo — satisfying sweep */
|
|
73
|
+
const playClear = useCallback(() => {
|
|
74
|
+
tone(700, 0.08, 'sine', 0.1);
|
|
75
|
+
setTimeout(() => tone(900, 0.08, 'sine', 0.1), 50);
|
|
76
|
+
setTimeout(() => tone(1200, 0.12, 'sine', 0.12), 100);
|
|
77
|
+
}, [tone]);
|
|
78
|
+
/** Countdown tick — timer warning */
|
|
79
|
+
const playTick = useCallback(() => {
|
|
80
|
+
tone(1000, 0.03, 'square', 0.06);
|
|
81
|
+
}, [tone]);
|
|
82
|
+
return {
|
|
83
|
+
playMove,
|
|
84
|
+
playScore,
|
|
85
|
+
playError,
|
|
86
|
+
playGameOver,
|
|
87
|
+
playLevelUp,
|
|
88
|
+
playDrop,
|
|
89
|
+
playClear,
|
|
90
|
+
playTick,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=useGameSounds.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useGameSounds.js","sourceRoot":"","sources":["../src/useGameSounds.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface LeaderboardEntry {
|
|
2
|
+
player_name: string;
|
|
3
|
+
score: number;
|
|
4
|
+
user_id?: string;
|
|
5
|
+
avatar_url?: string;
|
|
6
|
+
created_at: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function useLeaderboard(gameId: string): {
|
|
9
|
+
topScores: LeaderboardEntry[];
|
|
10
|
+
recentScores: LeaderboardEntry[];
|
|
11
|
+
submitScore: (score: number) => Promise<{
|
|
12
|
+
ok: boolean;
|
|
13
|
+
rank?: number;
|
|
14
|
+
}>;
|
|
15
|
+
loading: boolean;
|
|
16
|
+
refresh: () => void;
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=useLeaderboard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useLeaderboard.d.ts","sourceRoot":"","sources":["../src/useLeaderboard.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG;IAC9C,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAC9B,YAAY,EAAE,gBAAgB,EAAE,CAAC;IACjC,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACxE,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CAiDA"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
const API_BASE = 'https://progamestore-leaderboard.serge-the-dev.workers.dev';
|
|
3
|
+
export function useLeaderboard(gameId) {
|
|
4
|
+
const [topScores, setTopScores] = useState([]);
|
|
5
|
+
const [recentScores, setRecentScores] = useState([]);
|
|
6
|
+
const [loading, setLoading] = useState(true);
|
|
7
|
+
const load = useCallback(() => {
|
|
8
|
+
setLoading(true);
|
|
9
|
+
Promise.all([
|
|
10
|
+
fetch(`${API_BASE}/scores/${gameId}?sort=top`, { credentials: 'include' })
|
|
11
|
+
.then((r) => (r.ok ? r.json() : []))
|
|
12
|
+
.catch(() => []),
|
|
13
|
+
fetch(`${API_BASE}/scores/${gameId}?sort=recent`, { credentials: 'include' })
|
|
14
|
+
.then((r) => (r.ok ? r.json() : []))
|
|
15
|
+
.catch(() => []),
|
|
16
|
+
]).then(([top, recent]) => {
|
|
17
|
+
setTopScores(top);
|
|
18
|
+
setRecentScores(recent);
|
|
19
|
+
setLoading(false);
|
|
20
|
+
});
|
|
21
|
+
}, [gameId]);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
load();
|
|
24
|
+
}, [load]);
|
|
25
|
+
const submitScore = useCallback(async (score) => {
|
|
26
|
+
try {
|
|
27
|
+
const res = await fetch(`${API_BASE}/scores/${gameId}`, {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
credentials: 'include',
|
|
30
|
+
headers: { 'Content-Type': 'application/json' },
|
|
31
|
+
body: JSON.stringify({ score }),
|
|
32
|
+
});
|
|
33
|
+
if (!res.ok)
|
|
34
|
+
return { ok: false };
|
|
35
|
+
const data = (await res.json());
|
|
36
|
+
// Refresh scores after submission
|
|
37
|
+
load();
|
|
38
|
+
const result = { ok: true };
|
|
39
|
+
if (data.rank !== undefined)
|
|
40
|
+
result.rank = data.rank;
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return { ok: false };
|
|
45
|
+
}
|
|
46
|
+
}, [gameId, load]);
|
|
47
|
+
return { topScores, recentScores, submitScore, loading, refresh: load };
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=useLeaderboard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useLeaderboard.js","sourceRoot":"","sources":["../src/useLeaderboard.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shape of a message handled by a room. Games narrow this with their
|
|
3
|
+
* own message types via the generic parameter:
|
|
4
|
+
*
|
|
5
|
+
* type ChessMsg = { type: 'move'; uci: string } | { type: 'state'; fen: string };
|
|
6
|
+
* const room = useRooms<ChessMsg>({ ... });
|
|
7
|
+
*
|
|
8
|
+
* The only contract is `type: string` — payload fields are entirely up to
|
|
9
|
+
* the game. Discriminated unions across `type` are the recommended shape.
|
|
10
|
+
*/
|
|
11
|
+
export type RoomMessage = {
|
|
12
|
+
type: string;
|
|
13
|
+
};
|
|
14
|
+
export type RoomStatus = 'idle' | 'connecting' | 'connected' | 'closed' | 'error';
|
|
15
|
+
export interface UseRoomsOptions<TServer extends RoomMessage> {
|
|
16
|
+
/**
|
|
17
|
+
* A namespace identifying the game. Today this is mostly a label for
|
|
18
|
+
* the URL; the multiplayer Worker is per-game so a single Worker only
|
|
19
|
+
* serves one `gameId`. Reserved as a hook input so games written
|
|
20
|
+
* against this API don't break if the platform ever moves to a shared
|
|
21
|
+
* rooms Worker that namespaces by game.
|
|
22
|
+
*/
|
|
23
|
+
gameId: string;
|
|
24
|
+
/**
|
|
25
|
+
* The room to connect to. `null` keeps the hook idle — useful before
|
|
26
|
+
* the user has either created a new room or joined an existing one.
|
|
27
|
+
*/
|
|
28
|
+
roomId: string | null;
|
|
29
|
+
/**
|
|
30
|
+
* Base URL of the multiplayer Worker. Defaults to same-origin, which
|
|
31
|
+
* is the layout the templates ship: one Worker serves the static SPA
|
|
32
|
+
* AND owns the WebSocket route. Override only if you split the two.
|
|
33
|
+
*/
|
|
34
|
+
baseUrl?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Called for every JSON-parsed message received from the room.
|
|
37
|
+
* Use the `TServer` type parameter to narrow.
|
|
38
|
+
*/
|
|
39
|
+
onMessage?: (msg: TServer) => void;
|
|
40
|
+
/**
|
|
41
|
+
* Called on transitions of the connection status. Useful for UI
|
|
42
|
+
* (showing a "reconnecting…" badge, disabling Send while not
|
|
43
|
+
* connected, etc.).
|
|
44
|
+
*/
|
|
45
|
+
onStatusChange?: (status: RoomStatus) => void;
|
|
46
|
+
}
|
|
47
|
+
export interface UseRoomsResult<TClient extends RoomMessage> {
|
|
48
|
+
/** Current connection status. */
|
|
49
|
+
status: RoomStatus;
|
|
50
|
+
/**
|
|
51
|
+
* Send a message to the room. JSON-encoded and pushed to the server.
|
|
52
|
+
* Messages sent while the socket isn't open are dropped — the hook
|
|
53
|
+
* does not queue. Most game protocols are stateful enough that
|
|
54
|
+
* silent retries on a stale connection cause more bugs than they fix.
|
|
55
|
+
*/
|
|
56
|
+
send: (msg: TClient) => void;
|
|
57
|
+
/**
|
|
58
|
+
* POST `/api/rooms/new` against the Worker and return the new room id.
|
|
59
|
+
* Throws on non-2xx. The hook does not auto-connect to the returned
|
|
60
|
+
* id — call sites should navigate / set state and let the hook
|
|
61
|
+
* pick up the new `roomId` prop.
|
|
62
|
+
*/
|
|
63
|
+
create: () => Promise<string>;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Connect to a server-authoritative multiplayer room.
|
|
67
|
+
*
|
|
68
|
+
* The platform's multiplayer model is "WebSocket to a Durable Object owned
|
|
69
|
+
* by the game's own Worker". This hook handles the connection lifecycle
|
|
70
|
+
* (open / message / close) and the `POST /api/rooms/new` call to mint
|
|
71
|
+
* room ids. Per-game protocol — what messages mean, what state looks like,
|
|
72
|
+
* how moves get validated — lives in the game's Worker code, not here.
|
|
73
|
+
*
|
|
74
|
+
* Why this is a Pro-only API: the Worker + DO + per-GB-second billing
|
|
75
|
+
* pricing model means hosting rooms isn't free for the platform.
|
|
76
|
+
*/
|
|
77
|
+
export declare function useRooms<TServer extends RoomMessage = RoomMessage, TClient extends RoomMessage = RoomMessage>(opts: UseRoomsOptions<TServer>): UseRoomsResult<TClient>;
|
|
78
|
+
//# sourceMappingURL=useRooms.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useRooms.d.ts","sourceRoot":"","sources":["../src/useRooms.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AACH,MAAM,MAAM,WAAW,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3C,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,YAAY,GAAG,WAAW,GAAG,QAAQ,GAAG,OAAO,CAAC;AAElF,MAAM,WAAW,eAAe,CAAC,OAAO,SAAS,WAAW;IAC1D;;;;;;OAMG;IACH,MAAM,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAEtB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IAEnC;;;;OAIG;IACH,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,CAAC;CAC/C;AAED,MAAM,WAAW,cAAc,CAAC,OAAO,SAAS,WAAW;IACzD,iCAAiC;IACjC,MAAM,EAAE,UAAU,CAAC;IAEnB;;;;;OAKG;IACH,IAAI,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IAE7B;;;;;OAKG;IACH,MAAM,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;CAC/B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,QAAQ,CACtB,OAAO,SAAS,WAAW,GAAG,WAAW,EACzC,OAAO,SAAS,WAAW,GAAG,WAAW,EACzC,IAAI,EAAE,eAAe,CAAC,OAAO,CAAC,GAAG,cAAc,CAAC,OAAO,CAAC,CA+FzD"}
|
package/dist/useRooms.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Connect to a server-authoritative multiplayer room.
|
|
4
|
+
*
|
|
5
|
+
* The platform's multiplayer model is "WebSocket to a Durable Object owned
|
|
6
|
+
* by the game's own Worker". This hook handles the connection lifecycle
|
|
7
|
+
* (open / message / close) and the `POST /api/rooms/new` call to mint
|
|
8
|
+
* room ids. Per-game protocol — what messages mean, what state looks like,
|
|
9
|
+
* how moves get validated — lives in the game's Worker code, not here.
|
|
10
|
+
*
|
|
11
|
+
* Why this is a Pro-only API: the Worker + DO + per-GB-second billing
|
|
12
|
+
* pricing model means hosting rooms isn't free for the platform.
|
|
13
|
+
*/
|
|
14
|
+
export function useRooms(opts) {
|
|
15
|
+
const { gameId, roomId, baseUrl, onMessage, onStatusChange } = opts;
|
|
16
|
+
const [status, setStatus] = useState('idle');
|
|
17
|
+
const wsRef = useRef(null);
|
|
18
|
+
// Refs so the connect-effect doesn't re-fire when these change.
|
|
19
|
+
const onMessageRef = useRef(onMessage);
|
|
20
|
+
const onStatusChangeRef = useRef(onStatusChange);
|
|
21
|
+
onMessageRef.current = onMessage;
|
|
22
|
+
onStatusChangeRef.current = onStatusChange;
|
|
23
|
+
const setStatusAndNotify = useCallback((s) => {
|
|
24
|
+
setStatus(s);
|
|
25
|
+
onStatusChangeRef.current?.(s);
|
|
26
|
+
}, []);
|
|
27
|
+
// (Re)connect when roomId changes.
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (roomId === null) {
|
|
30
|
+
setStatusAndNotify('idle');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
setStatusAndNotify('connecting');
|
|
34
|
+
// Build wss:// URL from the page's origin (or the override baseUrl).
|
|
35
|
+
const base = baseUrl ?? (typeof window !== 'undefined' ? window.location.origin : '');
|
|
36
|
+
const wsUrl = base.replace(/^http/, 'ws').replace(/\/$/, '') +
|
|
37
|
+
`/api/rooms/${encodeURIComponent(roomId)}/ws`;
|
|
38
|
+
const ws = new WebSocket(wsUrl);
|
|
39
|
+
wsRef.current = ws;
|
|
40
|
+
let closedCleanly = false;
|
|
41
|
+
ws.addEventListener('open', () => {
|
|
42
|
+
setStatusAndNotify('connected');
|
|
43
|
+
});
|
|
44
|
+
ws.addEventListener('message', (e) => {
|
|
45
|
+
let parsed;
|
|
46
|
+
try {
|
|
47
|
+
parsed = JSON.parse(e.data);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// Non-JSON message — ignore. The protocol contract is JSON.
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
onMessageRef.current?.(parsed);
|
|
54
|
+
});
|
|
55
|
+
ws.addEventListener('close', () => {
|
|
56
|
+
if (!closedCleanly)
|
|
57
|
+
setStatusAndNotify('closed');
|
|
58
|
+
});
|
|
59
|
+
ws.addEventListener('error', () => {
|
|
60
|
+
setStatusAndNotify('error');
|
|
61
|
+
});
|
|
62
|
+
return () => {
|
|
63
|
+
closedCleanly = true;
|
|
64
|
+
wsRef.current = null;
|
|
65
|
+
// 1000 = normal closure. No reconnect — the next mount or roomId
|
|
66
|
+
// change will create a fresh socket.
|
|
67
|
+
try {
|
|
68
|
+
ws.close(1000);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// Worth a console hint? No — socket may already be closed.
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
// gameId is intentionally not in deps — the URL doesn't reference
|
|
75
|
+
// it today (per-game Worker = the gameId is implicit in the
|
|
76
|
+
// baseUrl). If/when the platform switches to a shared rooms Worker
|
|
77
|
+
// that namespaces by gameId in the path, add gameId to deps then.
|
|
78
|
+
}, [roomId, baseUrl, setStatusAndNotify]);
|
|
79
|
+
const send = useCallback((msg) => {
|
|
80
|
+
const ws = wsRef.current;
|
|
81
|
+
if (!ws || ws.readyState !== WebSocket.OPEN)
|
|
82
|
+
return;
|
|
83
|
+
ws.send(JSON.stringify(msg));
|
|
84
|
+
}, []);
|
|
85
|
+
const create = useCallback(async () => {
|
|
86
|
+
const base = baseUrl ?? (typeof window !== 'undefined' ? window.location.origin : '');
|
|
87
|
+
const res = await fetch(`${base.replace(/\/$/, '')}/api/rooms/new`, {
|
|
88
|
+
method: 'POST',
|
|
89
|
+
});
|
|
90
|
+
if (!res.ok) {
|
|
91
|
+
throw new Error(`POST /api/rooms/new → ${res.status}`);
|
|
92
|
+
}
|
|
93
|
+
const data = (await res.json());
|
|
94
|
+
// Tolerate either { roomId } or { id } from the Worker — the docs
|
|
95
|
+
// settle on `roomId`, but some early templates returned `id`.
|
|
96
|
+
const id = data.roomId ?? data.id;
|
|
97
|
+
if (!id || typeof id !== 'string') {
|
|
98
|
+
throw new Error('Worker returned no roomId');
|
|
99
|
+
}
|
|
100
|
+
return id;
|
|
101
|
+
}, [baseUrl]);
|
|
102
|
+
return { status, send, create };
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=useRooms.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useRooms.js","sourceRoot":"","sources":["../src/useRooms.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AA0EjE;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,QAAQ,CAGtB,IAA8B;IAC9B,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,GAAG,IAAI,CAAC;IACpE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAa,MAAM,CAAC,CAAC;IACzD,MAAM,KAAK,GAAG,MAAM,CAAmB,IAAI,CAAC,CAAC;IAC7C,gEAAgE;IAChE,MAAM,YAAY,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC;IACvC,MAAM,iBAAiB,GAAG,MAAM,CAAC,cAAc,CAAC,CAAC;IACjD,YAAY,CAAC,OAAO,GAAG,SAAS,CAAC;IACjC,iBAAiB,CAAC,OAAO,GAAG,cAAc,CAAC;IAE3C,MAAM,kBAAkB,GAAG,WAAW,CAAC,CAAC,CAAa,EAAE,EAAE;QACvD,SAAS,CAAC,CAAC,CAAC,CAAC;QACb,iBAAiB,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;IACjC,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,mCAAmC;IACnC,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;YACpB,kBAAkB,CAAC,MAAM,CAAC,CAAC;YAC3B,OAAO;QACT,CAAC;QAED,kBAAkB,CAAC,YAAY,CAAC,CAAC;QACjC,qEAAqE;QACrE,MAAM,IAAI,GAAG,OAAO,IAAI,CAAC,OAAO,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QACtF,MAAM,KAAK,GACT,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC;YAC9C,cAAc,kBAAkB,CAAC,MAAM,CAAC,KAAK,CAAC;QAEhD,MAAM,EAAE,GAAG,IAAI,SAAS,CAAC,KAAK,CAAC,CAAC;QAChC,KAAK,CAAC,OAAO,GAAG,EAAE,CAAC;QACnB,IAAI,aAAa,GAAG,KAAK,CAAC;QAE1B,EAAE,CAAC,gBAAgB,CAAC,MAAM,EAAE,GAAG,EAAE;YAC/B,kBAAkB,CAAC,WAAW,CAAC,CAAC;QAClC,CAAC,CAAC,CAAC;QACH,EAAE,CAAC,gBAAgB,CAAC,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE;YACnC,IAAI,MAAe,CAAC;YACpB,IAAI,CAAC;gBACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAY,CAAC;YACzC,CAAC;YAAC,MAAM,CAAC;gBACP,4DAA4D;gBAC5D,OAAO;YACT,CAAC;YACD,YAAY,CAAC,OAAO,EAAE,CAAC,MAAM,CAAC,CAAC;QACjC,CAAC,CAAC,CAAC;QACH,EAAE,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE;YAChC,IAAI,CAAC,aAAa;gBAAE,kBAAkB,CAAC,QAAQ,CAAC,CAAC;QACnD,CAAC,CAAC,CAAC;QACH,EAAE,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE;YAChC,kBAAkB,CAAC,OAAO,CAAC,CAAC;QAC9B,CAAC,CAAC,CAAC;QAEH,OAAO,GAAG,EAAE;YACV,aAAa,GAAG,IAAI,CAAC;YACrB,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC;YACrB,iEAAiE;YACjE,qCAAqC;YACrC,IAAI,CAAC;gBACH,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YACjB,CAAC;YAAC,MAAM,CAAC;gBACP,2DAA2D;YAC7D,CAAC;QACH,CAAC,CAAC;QACF,kEAAkE;QAClE,4DAA4D;QAC5D,mEAAmE;QACnE,kEAAkE;IACpE,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,kBAAkB,CAAC,CAAC,CAAC;IAE1C,MAAM,IAAI,GAAG,WAAW,CAAC,CAAC,GAAY,EAAE,EAAE;QACxC,MAAM,EAAE,GAAG,KAAK,CAAC,OAAO,CAAC;QACzB,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,UAAU,KAAK,SAAS,CAAC,IAAI;YAAE,OAAO;QACpD,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC;IAC/B,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,MAAM,GAAG,WAAW,CAAC,KAAK,IAAqB,EAAE;QACrD,MAAM,IAAI,GAAG,OAAO,IAAI,CAAC,OAAO,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QACtF,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,gBAAgB,EAAE;YAClE,MAAM,EAAE,MAAM;SACf,CAAC,CAAC;QACH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,MAAM,IAAI,KAAK,CAAC,yBAAyB,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;QACzD,CAAC;QACD,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAqC,CAAC;QACpE,kEAAkE;QAClE,8DAA8D;QAC9D,MAAM,EAAE,GAAG,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,EAAE,CAAC;QAClC,IAAI,CAAC,EAAE,IAAI,OAAO,EAAE,KAAK,QAAQ,EAAE,CAAC;YAClC,MAAM,IAAI,KAAK,CAAC,2BAA2B,CAAC,CAAC;QAC/C,CAAC;QACD,OAAO,EAAE,CAAC;IACZ,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;IAEd,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AAClC,CAAC"}
|