@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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/dist/GameAuth.d.ts +10 -0
  3. package/dist/GameAuth.d.ts.map +1 -0
  4. package/dist/GameAuth.js +108 -0
  5. package/dist/GameAuth.js.map +1 -0
  6. package/dist/GameButton.d.ts +21 -0
  7. package/dist/GameButton.d.ts.map +1 -0
  8. package/dist/GameButton.js +72 -0
  9. package/dist/GameButton.js.map +1 -0
  10. package/dist/GameShell.d.ts +26 -0
  11. package/dist/GameShell.d.ts.map +1 -0
  12. package/dist/GameShell.js +61 -0
  13. package/dist/GameShell.js.map +1 -0
  14. package/dist/GameTopbar.d.ts +62 -0
  15. package/dist/GameTopbar.d.ts.map +1 -0
  16. package/dist/GameTopbar.js +184 -0
  17. package/dist/GameTopbar.js.map +1 -0
  18. package/dist/Leaderboard.d.ts +8 -0
  19. package/dist/Leaderboard.d.ts.map +1 -0
  20. package/dist/Leaderboard.js +14 -0
  21. package/dist/Leaderboard.js.map +1 -0
  22. package/dist/SoundContext.d.ts +16 -0
  23. package/dist/SoundContext.d.ts.map +1 -0
  24. package/dist/SoundContext.js +16 -0
  25. package/dist/SoundContext.js.map +1 -0
  26. package/dist/index.d.ts +25 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +25 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/useAuth.d.ts +12 -0
  31. package/dist/useAuth.d.ts.map +1 -0
  32. package/dist/useAuth.js +46 -0
  33. package/dist/useAuth.js.map +1 -0
  34. package/dist/useGameSounds.d.ts +16 -0
  35. package/dist/useGameSounds.d.ts.map +1 -0
  36. package/dist/useGameSounds.js +93 -0
  37. package/dist/useGameSounds.js.map +1 -0
  38. package/dist/useLeaderboard.d.ts +18 -0
  39. package/dist/useLeaderboard.d.ts.map +1 -0
  40. package/dist/useLeaderboard.js +49 -0
  41. package/dist/useLeaderboard.js.map +1 -0
  42. package/dist/useRooms.d.ts +78 -0
  43. package/dist/useRooms.d.ts.map +1 -0
  44. package/dist/useRooms.js +104 -0
  45. package/dist/useRooms.js.map +1 -0
  46. 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"}
@@ -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,12 @@
1
+ export interface User {
2
+ id: string;
3
+ name: string;
4
+ avatar: string;
5
+ }
6
+ export declare function useAuth(): {
7
+ user: User | null;
8
+ loading: boolean;
9
+ signIn: () => void;
10
+ signOut: () => void;
11
+ };
12
+ //# sourceMappingURL=useAuth.d.ts.map
@@ -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"}
@@ -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"}
@@ -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":"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"}