@lagless/create 0.0.33

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 (67) hide show
  1. package/dist/index.d.ts +3 -0
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +123 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/tsconfig.lib.tsbuildinfo +1 -0
  6. package/package.json +59 -0
  7. package/templates/.nxignore +1 -0
  8. package/templates/pixi-react/AGENTS.md +41 -0
  9. package/templates/pixi-react/CLAUDE.md +79 -0
  10. package/templates/pixi-react/README.md +151 -0
  11. package/templates/pixi-react/__packageName__-backend/bunfig.toml +4 -0
  12. package/templates/pixi-react/__packageName__-backend/package.json +19 -0
  13. package/templates/pixi-react/__packageName__-backend/src/game-hooks.ts +47 -0
  14. package/templates/pixi-react/__packageName__-backend/src/main.ts +28 -0
  15. package/templates/pixi-react/__packageName__-backend/tsconfig.json +19 -0
  16. package/templates/pixi-react/__packageName__-frontend/index.html +23 -0
  17. package/templates/pixi-react/__packageName__-frontend/package.json +33 -0
  18. package/templates/pixi-react/__packageName__-frontend/src/app/app.tsx +7 -0
  19. package/templates/pixi-react/__packageName__-frontend/src/app/components/debug-panel.tsx +18 -0
  20. package/templates/pixi-react/__packageName__-frontend/src/app/game-view/game-scene.tsx +14 -0
  21. package/templates/pixi-react/__packageName__-frontend/src/app/game-view/game-view.tsx +46 -0
  22. package/templates/pixi-react/__packageName__-frontend/src/app/game-view/grid-background.tsx +33 -0
  23. package/templates/pixi-react/__packageName__-frontend/src/app/game-view/player-view.tsx +59 -0
  24. package/templates/pixi-react/__packageName__-frontend/src/app/game-view/runner-provider.tsx +180 -0
  25. package/templates/pixi-react/__packageName__-frontend/src/app/hooks/use-start-match.ts +53 -0
  26. package/templates/pixi-react/__packageName__-frontend/src/app/hooks/use-start-multiplayer-match.ts +138 -0
  27. package/templates/pixi-react/__packageName__-frontend/src/app/router.tsx +25 -0
  28. package/templates/pixi-react/__packageName__-frontend/src/app/screens/game.screen.tsx +6 -0
  29. package/templates/pixi-react/__packageName__-frontend/src/app/screens/title.screen.tsx +104 -0
  30. package/templates/pixi-react/__packageName__-frontend/src/main.tsx +7 -0
  31. package/templates/pixi-react/__packageName__-frontend/src/styles.css +24 -0
  32. package/templates/pixi-react/__packageName__-frontend/tsconfig.json +22 -0
  33. package/templates/pixi-react/__packageName__-frontend/vite.config.ts +37 -0
  34. package/templates/pixi-react/__packageName__-simulation/.swcrc +27 -0
  35. package/templates/pixi-react/__packageName__-simulation/package.json +23 -0
  36. package/templates/pixi-react/__packageName__-simulation/src/index.ts +4 -0
  37. package/templates/pixi-react/__packageName__-simulation/src/lib/arena.ts +8 -0
  38. package/templates/pixi-react/__packageName__-simulation/src/lib/schema/code-gen/GameState.ts +29 -0
  39. package/templates/pixi-react/__packageName__-simulation/src/lib/schema/code-gen/MoveInput.ts +23 -0
  40. package/templates/pixi-react/__packageName__-simulation/src/lib/schema/code-gen/MovingFilter.ts +15 -0
  41. package/templates/pixi-react/__packageName__-simulation/src/lib/schema/code-gen/PlayerBody.ts +55 -0
  42. package/templates/pixi-react/__packageName__-simulation/src/lib/schema/code-gen/PlayerFilter.ts +15 -0
  43. package/templates/pixi-react/__packageName__-simulation/src/lib/schema/code-gen/PlayerJoined.ts +24 -0
  44. package/templates/pixi-react/__packageName__-simulation/src/lib/schema/code-gen/PlayerLeft.ts +23 -0
  45. package/templates/pixi-react/__packageName__-simulation/src/lib/schema/code-gen/PlayerResource.ts +83 -0
  46. package/templates/pixi-react/__packageName__-simulation/src/lib/schema/code-gen/ReportHash.ts +23 -0
  47. package/templates/pixi-react/__packageName__-simulation/src/lib/schema/code-gen/Transform2d.ts +65 -0
  48. package/templates/pixi-react/__packageName__-simulation/src/lib/schema/code-gen/Velocity2d.ts +55 -0
  49. package/templates/pixi-react/__packageName__-simulation/src/lib/schema/code-gen/__ProjectName__.core.ts +22 -0
  50. package/templates/pixi-react/__packageName__-simulation/src/lib/schema/code-gen/__ProjectName__.runner.ts +16 -0
  51. package/templates/pixi-react/__packageName__-simulation/src/lib/schema/code-gen/__ProjectName__InputRegistry.ts +8 -0
  52. package/templates/pixi-react/__packageName__-simulation/src/lib/schema/code-gen/index.ts +16 -0
  53. package/templates/pixi-react/__packageName__-simulation/src/lib/schema/ecs.yaml +52 -0
  54. package/templates/pixi-react/__packageName__-simulation/src/lib/signals/index.ts +5 -0
  55. package/templates/pixi-react/__packageName__-simulation/src/lib/systems/apply-move-input.system.ts +23 -0
  56. package/templates/pixi-react/__packageName__-simulation/src/lib/systems/boundary.system.ts +34 -0
  57. package/templates/pixi-react/__packageName__-simulation/src/lib/systems/damping.system.ts +18 -0
  58. package/templates/pixi-react/__packageName__-simulation/src/lib/systems/hash-verification.system.ts +17 -0
  59. package/templates/pixi-react/__packageName__-simulation/src/lib/systems/index.ts +20 -0
  60. package/templates/pixi-react/__packageName__-simulation/src/lib/systems/integrate.system.ts +18 -0
  61. package/templates/pixi-react/__packageName__-simulation/src/lib/systems/player-connection.system.ts +47 -0
  62. package/templates/pixi-react/__packageName__-simulation/src/lib/systems/player-leave.system.ts +21 -0
  63. package/templates/pixi-react/__packageName__-simulation/src/lib/systems/save-prev-transform.system.ts +17 -0
  64. package/templates/pixi-react/__packageName__-simulation/tsconfig.json +22 -0
  65. package/templates/pixi-react/package.json +8 -0
  66. package/templates/pixi-react/pnpm-workspace.yaml +4 -0
  67. package/templates/pixi-react/tsconfig.base.json +21 -0
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "<%= packageName %>-frontend",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "<%= packageName %>-simulation": "workspace:*",
13
+ "@lagless/core": "^<%= laglessVersion %>",
14
+ "@lagless/misc": "^<%= laglessVersion %>",
15
+ "@lagless/relay-client": "^<%= laglessVersion %>",
16
+ "@lagless/net-wire": "^<%= laglessVersion %>",
17
+ "@lagless/react": "^<%= laglessVersion %>",
18
+ "@lagless/pixi-react": "^<%= laglessVersion %>",
19
+ "@abraham/reflection": "^0.12.0",
20
+ "pixi.js": "^8.12.0",
21
+ "@pixi/react": "^8.0.5",
22
+ "react": "^19.1.0",
23
+ "react-dom": "^19.1.0",
24
+ "react-router-dom": "^7.6.1"
25
+ },
26
+ "devDependencies": {
27
+ "@vitejs/plugin-react-swc": "^4.0.0",
28
+ "vite": "^7.0.0",
29
+ "vite-plugin-wasm": "^3.5.0",
30
+ "vite-plugin-top-level-await": "^1.6.0",
31
+ "typescript": "^5.9.0"
32
+ }
33
+ }
@@ -0,0 +1,7 @@
1
+ import { FC } from 'react';
2
+ import { RouterProvider } from 'react-router-dom';
3
+ import { router } from './router';
4
+
5
+ export const App: FC = () => {
6
+ return <RouterProvider router={router} />;
7
+ };
@@ -0,0 +1,18 @@
1
+ import { FC } from 'react';
2
+ import { useRunner } from '../game-view/runner-provider';
3
+ import { DebugPanel as SharedDebugPanel } from '@lagless/react';
4
+ import { PlayerResource, DivergenceSignal } from '<%= packageName %>-simulation';
5
+
6
+ export const DebugPanel: FC = () => {
7
+ const runner = useRunner();
8
+
9
+ return (
10
+ <SharedDebugPanel
11
+ runner={runner}
12
+ hashVerification={{
13
+ playerResourceClass: PlayerResource,
14
+ divergenceSignalClass: DivergenceSignal,
15
+ }}
16
+ />
17
+ );
18
+ };
@@ -0,0 +1,14 @@
1
+ import { FC, useMemo } from 'react';
2
+ import { FilterViews } from '@lagless/pixi-react';
3
+ import { useRunner } from './runner-provider';
4
+ import { PlayerFilter } from '<%= packageName %>-simulation';
5
+ import { PlayerView } from './player-view';
6
+
7
+ export const GameScene: FC = () => {
8
+ const runner = useRunner();
9
+ const playerFilter = useMemo(() => runner.DIContainer.resolve(PlayerFilter), [runner]);
10
+
11
+ return (
12
+ <FilterViews filter={playerFilter} View={PlayerView} />
13
+ );
14
+ };
@@ -0,0 +1,46 @@
1
+ import { FC, useRef } from 'react';
2
+ import { Application, extend } from '@pixi/react';
3
+ import { RunnerProvider, RunnerTicker } from './runner-provider';
4
+ import { Container, Graphics, Text } from 'pixi.js';
5
+ import { GameScene } from './game-scene';
6
+ import { GridBackground } from './grid-background';
7
+ import { DebugPanel } from '../components/debug-panel';
8
+
9
+ extend({
10
+ Container,
11
+ Graphics,
12
+ Text,
13
+ });
14
+
15
+ export const GameView: FC = () => {
16
+ const containerRef = useRef<HTMLDivElement>(null);
17
+
18
+ return (
19
+ <RunnerProvider>
20
+ <div style={styles.wrapper} ref={containerRef}>
21
+ <DebugPanel />
22
+ <Application
23
+ autoDensity
24
+ resolution={devicePixelRatio || 1}
25
+ resizeTo={containerRef}
26
+ background={0x0a0a1a}
27
+ >
28
+ <RunnerTicker>
29
+ <GridBackground />
30
+ <GameScene />
31
+ </RunnerTicker>
32
+ </Application>
33
+ </div>
34
+ </RunnerProvider>
35
+ );
36
+ };
37
+
38
+ const styles: Record<string, React.CSSProperties> = {
39
+ wrapper: {
40
+ top: 0,
41
+ left: 0,
42
+ width: '100%',
43
+ height: '100%',
44
+ position: 'fixed',
45
+ },
46
+ };
@@ -0,0 +1,33 @@
1
+ import { FC, useEffect, useRef } from 'react';
2
+ import { Graphics } from 'pixi.js';
3
+ import { <%= projectName %>Arena } from '<%= packageName %>-simulation';
4
+
5
+ export const GridBackground: FC = () => {
6
+ const graphicsRef = useRef<Graphics>(null);
7
+
8
+ useEffect(() => {
9
+ const g = graphicsRef.current;
10
+ if (!g) return;
11
+
12
+ const w = <%= projectName %>Arena.width;
13
+ const h = <%= projectName %>Arena.height;
14
+ const step = 100;
15
+
16
+ g.clear();
17
+
18
+ for (let x = 0; x <= w; x += step) {
19
+ g.moveTo(x, 0);
20
+ g.lineTo(x, h);
21
+ }
22
+ for (let y = 0; y <= h; y += step) {
23
+ g.moveTo(0, y);
24
+ g.lineTo(w, y);
25
+ }
26
+ g.stroke({ color: 0x333355, width: 1, alpha: 0.4 });
27
+
28
+ g.rect(0, 0, w, h);
29
+ g.stroke({ color: 0x6666aa, width: 3, alpha: 0.8 });
30
+ }, []);
31
+
32
+ return <pixiGraphics ref={graphicsRef} />;
33
+ };
@@ -0,0 +1,59 @@
1
+ import { useImperativeHandle, useRef } from 'react';
2
+ import { Container, Graphics } from 'pixi.js';
3
+ import { filterView, FilterViewRef } from '@lagless/pixi-react';
4
+ import { useRunner } from './runner-provider';
5
+ import { PlayerBody, <%= projectName %>Arena, Transform2d } from '<%= packageName %>-simulation';
6
+ import { VisualSmoother2d } from '@lagless/misc';
7
+
8
+ const PLAYER_COLORS = [0xff4444, 0x4488ff, 0x44ff44, 0xffff44];
9
+
10
+ export const PlayerView = filterView(({ entity }, ref) => {
11
+ const runner = useRunner();
12
+ const containerRef = useRef<Container>(null);
13
+ const graphicsRef = useRef<Graphics>(null);
14
+ const smootherRef = useRef<VisualSmoother2d>(new VisualSmoother2d());
15
+
16
+ const transform2d = runner.DIContainer.resolve(Transform2d);
17
+ const playerBody = runner.DIContainer.resolve(PlayerBody);
18
+
19
+ useImperativeHandle(ref, (): FilterViewRef => ({
20
+ onCreate() {
21
+ const g = graphicsRef.current;
22
+ if (!g) return;
23
+ const slot = playerBody.unsafe.playerSlot[entity];
24
+ const radius = playerBody.unsafe.radius[entity];
25
+ const color = PLAYER_COLORS[slot % PLAYER_COLORS.length];
26
+ g.clear();
27
+ g.circle(0, 0, radius);
28
+ g.fill(color);
29
+ g.circle(0, 0, radius);
30
+ g.stroke({ color: 0xffffff, width: 2, alpha: 0.3 });
31
+ },
32
+ onUpdate() {
33
+ const container = containerRef.current;
34
+ if (!container) return;
35
+ const smoother = smootherRef.current;
36
+ const factor = runner.Simulation.interpolationFactor;
37
+
38
+ smoother.update(
39
+ transform2d.unsafe.prevPositionX[entity],
40
+ transform2d.unsafe.prevPositionY[entity],
41
+ transform2d.unsafe.positionX[entity],
42
+ transform2d.unsafe.positionY[entity],
43
+ factor,
44
+ );
45
+
46
+ container.x = smoother.x;
47
+ container.y = smoother.y;
48
+ },
49
+ onDestroy() {
50
+ // cleanup
51
+ },
52
+ }));
53
+
54
+ return (
55
+ <pixiContainer ref={containerRef}>
56
+ <pixiGraphics ref={graphicsRef} />
57
+ </pixiContainer>
58
+ );
59
+ });
@@ -0,0 +1,180 @@
1
+ import {
2
+ <%= projectName %>Runner,
3
+ <%= projectName %>Systems,
4
+ <%= projectName %>Signals,
5
+ DivergenceSignal,
6
+ MoveInput,
7
+ PlayerJoined,
8
+ ReportHash,
9
+ <%= projectName %>Arena,
10
+ } from '<%= packageName %>-simulation';
11
+ import { createContext, FC, ReactNode, useContext, useEffect, useState } from 'react';
12
+ import { useTick } from '@pixi/react';
13
+ import { useNavigate } from 'react-router-dom';
14
+ import { ProviderStore } from '../hooks/use-start-match';
15
+ import { ECSConfig, LocalInputProvider, RPC, createHashReporter } from '@lagless/core';
16
+ import { RelayInputProvider, RelayConnection } from '@lagless/relay-client';
17
+ import { getMatchInfo } from '../hooks/use-start-multiplayer-match';
18
+ import { UUID } from '@lagless/misc';
19
+
20
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
21
+ const RunnerContext = createContext<<%= projectName %>Runner>(null!);
22
+
23
+ export const useRunner = () => {
24
+ return useContext(RunnerContext);
25
+ };
26
+
27
+ interface RunnerProviderProps {
28
+ children: ReactNode;
29
+ }
30
+
31
+ const SQRT2_INV = 1 / Math.sqrt(2);
32
+
33
+ export const RunnerProvider: FC<RunnerProviderProps> = ({ children }) => {
34
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
35
+ const [runner, setRunner] = useState<<%= projectName %>Runner>(null!);
36
+ const [v, setV] = useState(0);
37
+ const navigate = useNavigate();
38
+
39
+ useEffect(() => {
40
+ return ProviderStore.onProvider(() => {
41
+ setV((v) => v + 1);
42
+ });
43
+ }, []);
44
+
45
+ useEffect(() => {
46
+ let disposed = false;
47
+ let _runner: <%= projectName %>Runner;
48
+ let _connection: RelayConnection | undefined;
49
+ const inputProvider = ProviderStore.getInvalidate();
50
+
51
+ if (!inputProvider) {
52
+ navigate('/');
53
+ return;
54
+ }
55
+
56
+ // Keyboard state
57
+ const keys = new Set<string>();
58
+ const onKeyDown = (e: KeyboardEvent) => keys.add(e.key.toLowerCase());
59
+ const onKeyUp = (e: KeyboardEvent) => keys.delete(e.key.toLowerCase());
60
+ window.addEventListener('keydown', onKeyDown);
61
+ window.addEventListener('keyup', onKeyUp);
62
+
63
+ (async () => {
64
+ if (disposed) {
65
+ inputProvider.dispose();
66
+ return;
67
+ }
68
+
69
+ if (inputProvider instanceof RelayInputProvider) {
70
+ const matchInfo = getMatchInfo(inputProvider);
71
+ if (matchInfo) {
72
+ _connection = new RelayConnection(
73
+ {
74
+ serverUrl: matchInfo.serverUrl,
75
+ matchId: matchInfo.matchId,
76
+ token: matchInfo.token,
77
+ },
78
+ {
79
+ onServerHello: (data) => inputProvider.handleServerHello(data),
80
+ onTickInputFanout: (data) => inputProvider.handleTickInputFanout(data),
81
+ onCancelInput: (data) => inputProvider.handleCancelInput(data),
82
+ onPong: (data) => inputProvider.handlePong(data),
83
+ onStateRequest: (requestId) => inputProvider.handleStateRequest(requestId),
84
+ onStateResponse: (data) => inputProvider.handleStateResponse(data),
85
+ onConnected: () => console.log('[Relay] Connected'),
86
+ onDisconnected: () => console.log('[Relay] Disconnected'),
87
+ },
88
+ );
89
+
90
+ inputProvider.setConnection(_connection);
91
+ _connection.connect();
92
+
93
+ const serverHello = await inputProvider.serverHello;
94
+ if (disposed) { inputProvider.dispose(); return; }
95
+
96
+ const seededConfig = new ECSConfig({ ...inputProvider.ecsConfig, seed: serverHello.seed });
97
+ _runner = new <%= projectName %>Runner(seededConfig, inputProvider, <%= projectName %>Systems, <%= projectName %>Signals);
98
+ } else {
99
+ _runner = new <%= projectName %>Runner(inputProvider.ecsConfig, inputProvider, <%= projectName %>Systems, <%= projectName %>Signals);
100
+ }
101
+ } else {
102
+ _runner = new <%= projectName %>Runner(inputProvider.ecsConfig, inputProvider, <%= projectName %>Systems, <%= projectName %>Signals);
103
+ }
104
+
105
+ // Set up keyboard input drainer with hash reporting
106
+ const reportHash = createHashReporter(_runner, {
107
+ reportInterval: <%= projectName %>Arena.hashReportInterval,
108
+ reportHashRpc: ReportHash,
109
+ });
110
+
111
+ inputProvider.drainInputs((addRPC) => {
112
+ let dx = 0;
113
+ let dy = 0;
114
+ if (keys.has('a') || keys.has('arrowleft')) dx -= 1;
115
+ if (keys.has('d') || keys.has('arrowright')) dx += 1;
116
+ if (keys.has('w') || keys.has('arrowup')) dy -= 1;
117
+ if (keys.has('s') || keys.has('arrowdown')) dy += 1;
118
+
119
+ if (dx !== 0 || dy !== 0) {
120
+ if (dx !== 0 && dy !== 0) {
121
+ dx *= SQRT2_INV;
122
+ dy *= SQRT2_INV;
123
+ }
124
+ addRPC(MoveInput, { directionX: dx, directionY: dy });
125
+ }
126
+
127
+ reportHash(addRPC);
128
+ });
129
+
130
+ _runner.start();
131
+
132
+ if (inputProvider instanceof RelayInputProvider) {
133
+ const serverHello = await inputProvider.serverHello;
134
+ if (serverHello.serverTick > 0) {
135
+ _runner.Simulation.clock.setAccumulatedTime(serverHello.serverTick * _runner.Config.frameLength);
136
+ }
137
+ }
138
+
139
+ if (inputProvider instanceof LocalInputProvider) {
140
+ const playerId = UUID.generate().asUint8();
141
+ const joinRpc = new RPC(PlayerJoined.id, {
142
+ tick: 1,
143
+ seq: 0,
144
+ ordinal: 0,
145
+ playerSlot: 255,
146
+ }, {
147
+ slot: 0,
148
+ playerId,
149
+ });
150
+ inputProvider.addRemoteRpc(joinRpc);
151
+ }
152
+
153
+ const divergenceSignal = _runner.DIContainer.resolve(DivergenceSignal);
154
+ divergenceSignal.Predicted.subscribe((e) => {
155
+ console.warn(`[DIVERGENCE] Players ${e.data.slotA} vs ${e.data.slotB}: hash ${e.data.hashA} != ${e.data.hashB} at tick ${e.data.atTick}`);
156
+ });
157
+
158
+ setRunner(_runner);
159
+ })();
160
+
161
+ return () => {
162
+ disposed = true;
163
+ window.removeEventListener('keydown', onKeyDown);
164
+ window.removeEventListener('keyup', onKeyUp);
165
+ _connection?.disconnect();
166
+ _runner?.dispose();
167
+ };
168
+ }, [v, navigate]);
169
+
170
+ return !runner ? null : <RunnerContext.Provider value={runner}>{children}</RunnerContext.Provider>;
171
+ };
172
+
173
+ export const RunnerTicker: FC<{ children: ReactNode }> = ({ children }) => {
174
+ const runner = useRunner();
175
+ useTick((ticker) => {
176
+ runner.update(ticker.deltaMS);
177
+ });
178
+
179
+ return children;
180
+ };
@@ -0,0 +1,53 @@
1
+ import { <%= projectName %>InputRegistry } from '<%= packageName %>-simulation';
2
+ import { AbstractInputProvider, ECSConfig, LocalInputProvider } from '@lagless/core';
3
+ import { useCallback, useState } from 'react';
4
+ import { useNavigate } from 'react-router-dom';
5
+
6
+ export class ProviderStore {
7
+ private static readonly _listeners = new Set<() => void>();
8
+ private static _provider: AbstractInputProvider | undefined;
9
+
10
+ public static onProvider(listener: () => void): () => void {
11
+ this._listeners.add(listener);
12
+ return () => {
13
+ this._listeners.delete(listener);
14
+ };
15
+ }
16
+
17
+ public static set(provider: AbstractInputProvider) {
18
+ this._provider = provider;
19
+ for (const listener of this._listeners) {
20
+ listener();
21
+ }
22
+ }
23
+
24
+ public static getInvalidate(): AbstractInputProvider | undefined {
25
+ const provider = this._provider;
26
+ this._provider = undefined;
27
+ return provider;
28
+ }
29
+ }
30
+
31
+ export const useStartMatch = () => {
32
+ const [isBusy, setIsBusy] = useState(false);
33
+ const navigate = useNavigate();
34
+
35
+ const startMatch = useCallback(async () => {
36
+ if (isBusy) return;
37
+ setIsBusy(true);
38
+ try {
39
+ const ecsConfig = new ECSConfig({ fps: 60 });
40
+ const inputProvider = new LocalInputProvider(ecsConfig, <%= projectName %>InputRegistry);
41
+
42
+ ProviderStore.set(inputProvider);
43
+ navigate('/game');
44
+ } finally {
45
+ setIsBusy(false);
46
+ }
47
+ }, [isBusy, navigate]);
48
+
49
+ return {
50
+ isBusy,
51
+ startMatch,
52
+ };
53
+ };
@@ -0,0 +1,138 @@
1
+ import { <%= projectName %>InputRegistry } from '<%= packageName %>-simulation';
2
+ import { ECSConfig } from '@lagless/core';
3
+ import { RelayInputProvider } from '@lagless/relay-client';
4
+ import { useCallback, useRef, useState } from 'react';
5
+ import { useNavigate } from 'react-router-dom';
6
+ import { ProviderStore } from './use-start-match';
7
+
8
+ const SERVER_URL = import.meta.env.VITE_RELAY_URL || 'ws://localhost:<%= serverPort %>';
9
+
10
+ export type MatchmakingState = 'idle' | 'queuing' | 'connecting' | 'error';
11
+
12
+ interface MatchFoundData {
13
+ type: 'match_found';
14
+ matchId: string;
15
+ playerSlot: number;
16
+ token: string;
17
+ serverUrl: string;
18
+ }
19
+
20
+ export const useStartMultiplayerMatch = () => {
21
+ const [state, setState] = useState<MatchmakingState>('idle');
22
+ const [queuePosition, setQueuePosition] = useState<number | null>(null);
23
+ const [error, setError] = useState<string | null>(null);
24
+ const wsRef = useRef<WebSocket | null>(null);
25
+ const navigate = useNavigate();
26
+
27
+ const cancel = useCallback(() => {
28
+ if (wsRef.current) {
29
+ wsRef.current.close();
30
+ wsRef.current = null;
31
+ }
32
+ setState('idle');
33
+ setQueuePosition(null);
34
+ setError(null);
35
+ }, []);
36
+
37
+ const startMatch = useCallback(() => {
38
+ if (state !== 'idle') return;
39
+
40
+ setState('queuing');
41
+ setError(null);
42
+
43
+ const playerId = crypto.randomUUID();
44
+ const ws = new WebSocket(`${SERVER_URL}/matchmaking?playerId=${playerId}`);
45
+ wsRef.current = ws;
46
+
47
+ ws.onopen = () => {
48
+ ws.send(
49
+ JSON.stringify({
50
+ type: 'join',
51
+ scope: '<%= packageName %>',
52
+ }),
53
+ );
54
+ };
55
+
56
+ ws.onmessage = (event) => {
57
+ const msg = JSON.parse(event.data);
58
+
59
+ switch (msg.type) {
60
+ case 'queued':
61
+ setQueuePosition(msg.position);
62
+ break;
63
+
64
+ case 'match_found': {
65
+ ws.close();
66
+ wsRef.current = null;
67
+ setState('connecting');
68
+ handleMatchFound(msg as MatchFoundData, playerId);
69
+ break;
70
+ }
71
+
72
+ case 'error':
73
+ setState('error');
74
+ setError(msg.message);
75
+ ws.close();
76
+ wsRef.current = null;
77
+ break;
78
+ }
79
+ };
80
+
81
+ ws.onerror = () => {
82
+ setState('error');
83
+ setError('Connection failed');
84
+ wsRef.current = null;
85
+ };
86
+
87
+ ws.onclose = () => {
88
+ if (state === 'queuing') {
89
+ setState('idle');
90
+ }
91
+ };
92
+ }, [state]);
93
+
94
+ const handleMatchFound = useCallback(
95
+ (data: MatchFoundData, playerId: string) => {
96
+ const ecsConfig = new ECSConfig({
97
+ fps: 60,
98
+ maxPlayers: 4,
99
+ });
100
+
101
+ const inputProvider = new RelayInputProvider(data.playerSlot, ecsConfig, <%= projectName %>InputRegistry);
102
+
103
+ (inputProvider as RelayInputProviderWithMatchInfo)._matchInfo = {
104
+ matchId: data.matchId,
105
+ token: data.token,
106
+ serverUrl: data.serverUrl,
107
+ playerId,
108
+ };
109
+
110
+ ProviderStore.set(inputProvider);
111
+ navigate('/game');
112
+ },
113
+ [navigate],
114
+ );
115
+
116
+ return {
117
+ state,
118
+ queuePosition,
119
+ error,
120
+ startMatch,
121
+ cancel,
122
+ };
123
+ };
124
+
125
+ export interface MatchInfo {
126
+ matchId: string;
127
+ token: string;
128
+ serverUrl: string;
129
+ playerId: string;
130
+ }
131
+
132
+ interface RelayInputProviderWithMatchInfo extends RelayInputProvider {
133
+ _matchInfo?: MatchInfo;
134
+ }
135
+
136
+ export function getMatchInfo(provider: RelayInputProvider): MatchInfo | undefined {
137
+ return (provider as RelayInputProviderWithMatchInfo)._matchInfo;
138
+ }
@@ -0,0 +1,25 @@
1
+ import { createBrowserRouter, useOutlet } from 'react-router-dom';
2
+ import { FC } from 'react';
3
+ import { TitleScreen } from './screens/title.screen';
4
+ import { GameScreen } from './screens/game.screen';
5
+
6
+ const Root: FC = () => {
7
+ return useOutlet();
8
+ };
9
+
10
+ export const router: ReturnType<typeof createBrowserRouter> = createBrowserRouter([
11
+ {
12
+ path: '/',
13
+ Component: Root,
14
+ children: [
15
+ {
16
+ index: true,
17
+ Component: TitleScreen,
18
+ },
19
+ {
20
+ path: 'game',
21
+ Component: GameScreen,
22
+ },
23
+ ],
24
+ },
25
+ ]);
@@ -0,0 +1,6 @@
1
+ import { FC } from 'react';
2
+ import { GameView } from '../game-view/game-view';
3
+
4
+ export const GameScreen: FC = () => {
5
+ return <GameView />;
6
+ };