@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.
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +123 -0
- package/dist/index.js.map +1 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -0
- package/package.json +59 -0
- package/templates/.nxignore +1 -0
- package/templates/pixi-react/AGENTS.md +41 -0
- package/templates/pixi-react/CLAUDE.md +79 -0
- package/templates/pixi-react/README.md +151 -0
- package/templates/pixi-react/__packageName__-backend/bunfig.toml +4 -0
- package/templates/pixi-react/__packageName__-backend/package.json +19 -0
- package/templates/pixi-react/__packageName__-backend/src/game-hooks.ts +47 -0
- package/templates/pixi-react/__packageName__-backend/src/main.ts +28 -0
- package/templates/pixi-react/__packageName__-backend/tsconfig.json +19 -0
- package/templates/pixi-react/__packageName__-frontend/index.html +23 -0
- package/templates/pixi-react/__packageName__-frontend/package.json +33 -0
- package/templates/pixi-react/__packageName__-frontend/src/app/app.tsx +7 -0
- package/templates/pixi-react/__packageName__-frontend/src/app/components/debug-panel.tsx +18 -0
- package/templates/pixi-react/__packageName__-frontend/src/app/game-view/game-scene.tsx +14 -0
- package/templates/pixi-react/__packageName__-frontend/src/app/game-view/game-view.tsx +46 -0
- package/templates/pixi-react/__packageName__-frontend/src/app/game-view/grid-background.tsx +33 -0
- package/templates/pixi-react/__packageName__-frontend/src/app/game-view/player-view.tsx +59 -0
- package/templates/pixi-react/__packageName__-frontend/src/app/game-view/runner-provider.tsx +180 -0
- package/templates/pixi-react/__packageName__-frontend/src/app/hooks/use-start-match.ts +53 -0
- package/templates/pixi-react/__packageName__-frontend/src/app/hooks/use-start-multiplayer-match.ts +138 -0
- package/templates/pixi-react/__packageName__-frontend/src/app/router.tsx +25 -0
- package/templates/pixi-react/__packageName__-frontend/src/app/screens/game.screen.tsx +6 -0
- package/templates/pixi-react/__packageName__-frontend/src/app/screens/title.screen.tsx +104 -0
- package/templates/pixi-react/__packageName__-frontend/src/main.tsx +7 -0
- package/templates/pixi-react/__packageName__-frontend/src/styles.css +24 -0
- package/templates/pixi-react/__packageName__-frontend/tsconfig.json +22 -0
- package/templates/pixi-react/__packageName__-frontend/vite.config.ts +37 -0
- package/templates/pixi-react/__packageName__-simulation/.swcrc +27 -0
- package/templates/pixi-react/__packageName__-simulation/package.json +23 -0
- package/templates/pixi-react/__packageName__-simulation/src/index.ts +4 -0
- package/templates/pixi-react/__packageName__-simulation/src/lib/arena.ts +8 -0
- package/templates/pixi-react/__packageName__-simulation/src/lib/schema/code-gen/GameState.ts +29 -0
- package/templates/pixi-react/__packageName__-simulation/src/lib/schema/code-gen/MoveInput.ts +23 -0
- package/templates/pixi-react/__packageName__-simulation/src/lib/schema/code-gen/MovingFilter.ts +15 -0
- package/templates/pixi-react/__packageName__-simulation/src/lib/schema/code-gen/PlayerBody.ts +55 -0
- package/templates/pixi-react/__packageName__-simulation/src/lib/schema/code-gen/PlayerFilter.ts +15 -0
- package/templates/pixi-react/__packageName__-simulation/src/lib/schema/code-gen/PlayerJoined.ts +24 -0
- package/templates/pixi-react/__packageName__-simulation/src/lib/schema/code-gen/PlayerLeft.ts +23 -0
- package/templates/pixi-react/__packageName__-simulation/src/lib/schema/code-gen/PlayerResource.ts +83 -0
- package/templates/pixi-react/__packageName__-simulation/src/lib/schema/code-gen/ReportHash.ts +23 -0
- package/templates/pixi-react/__packageName__-simulation/src/lib/schema/code-gen/Transform2d.ts +65 -0
- package/templates/pixi-react/__packageName__-simulation/src/lib/schema/code-gen/Velocity2d.ts +55 -0
- package/templates/pixi-react/__packageName__-simulation/src/lib/schema/code-gen/__ProjectName__.core.ts +22 -0
- package/templates/pixi-react/__packageName__-simulation/src/lib/schema/code-gen/__ProjectName__.runner.ts +16 -0
- package/templates/pixi-react/__packageName__-simulation/src/lib/schema/code-gen/__ProjectName__InputRegistry.ts +8 -0
- package/templates/pixi-react/__packageName__-simulation/src/lib/schema/code-gen/index.ts +16 -0
- package/templates/pixi-react/__packageName__-simulation/src/lib/schema/ecs.yaml +52 -0
- package/templates/pixi-react/__packageName__-simulation/src/lib/signals/index.ts +5 -0
- package/templates/pixi-react/__packageName__-simulation/src/lib/systems/apply-move-input.system.ts +23 -0
- package/templates/pixi-react/__packageName__-simulation/src/lib/systems/boundary.system.ts +34 -0
- package/templates/pixi-react/__packageName__-simulation/src/lib/systems/damping.system.ts +18 -0
- package/templates/pixi-react/__packageName__-simulation/src/lib/systems/hash-verification.system.ts +17 -0
- package/templates/pixi-react/__packageName__-simulation/src/lib/systems/index.ts +20 -0
- package/templates/pixi-react/__packageName__-simulation/src/lib/systems/integrate.system.ts +18 -0
- package/templates/pixi-react/__packageName__-simulation/src/lib/systems/player-connection.system.ts +47 -0
- package/templates/pixi-react/__packageName__-simulation/src/lib/systems/player-leave.system.ts +21 -0
- package/templates/pixi-react/__packageName__-simulation/src/lib/systems/save-prev-transform.system.ts +17 -0
- package/templates/pixi-react/__packageName__-simulation/tsconfig.json +22 -0
- package/templates/pixi-react/package.json +8 -0
- package/templates/pixi-react/pnpm-workspace.yaml +4 -0
- 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,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
|
+
};
|
package/templates/pixi-react/__packageName__-frontend/src/app/hooks/use-start-multiplayer-match.ts
ADDED
|
@@ -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
|
+
]);
|