@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
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { program } from 'commander';
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import * as ejs from 'ejs';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
function toPascalCase(kebab) {
|
|
10
|
+
return kebab.split('-').map((s)=>s.charAt(0).toUpperCase() + s.slice(1)).join('');
|
|
11
|
+
}
|
|
12
|
+
function getTemplatesDir() {
|
|
13
|
+
// In development (source), templates are sibling to src/
|
|
14
|
+
// In dist, templates are at package root (copied by files field)
|
|
15
|
+
const devPath = path.resolve(__dirname, '..', 'templates');
|
|
16
|
+
if (fs.existsSync(devPath)) return devPath;
|
|
17
|
+
// Fallback for npm install
|
|
18
|
+
const distPath = path.resolve(__dirname, '..', '..', 'templates');
|
|
19
|
+
if (fs.existsSync(distPath)) return distPath;
|
|
20
|
+
throw new Error('Templates directory not found');
|
|
21
|
+
}
|
|
22
|
+
function walkDir(dir) {
|
|
23
|
+
const results = [];
|
|
24
|
+
for (const entry of fs.readdirSync(dir, {
|
|
25
|
+
withFileTypes: true
|
|
26
|
+
})){
|
|
27
|
+
const full = path.join(dir, entry.name);
|
|
28
|
+
if (entry.isDirectory()) {
|
|
29
|
+
results.push(...walkDir(full));
|
|
30
|
+
} else {
|
|
31
|
+
results.push(full);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return results;
|
|
35
|
+
}
|
|
36
|
+
function processPath(filePath, vars) {
|
|
37
|
+
let result = filePath;
|
|
38
|
+
result = result.replace(/__packageName__/g, vars.packageName);
|
|
39
|
+
result = result.replace(/__ProjectName__/g, vars.projectName);
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
program.name('create-lagless').description('Scaffold a new Lagless multiplayer game project').version('0.0.30').argument('<project-name>', 'Project name in kebab-case (e.g., my-game)').option('--preset <preset>', 'Project preset', 'pixi-react').option('--port <port>', 'Frontend dev server port', '4200').option('--server-port <port>', 'Backend server port', '3333').action(async (projectArg, options)=>{
|
|
43
|
+
const targetDir = path.resolve(process.cwd(), projectArg);
|
|
44
|
+
const packageName = path.basename(targetDir).toLowerCase();
|
|
45
|
+
const pascalName = toPascalCase(packageName);
|
|
46
|
+
if (fs.existsSync(targetDir)) {
|
|
47
|
+
console.error(`Error: Directory "${targetDir}" already exists.`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
const templatesDir = getTemplatesDir();
|
|
51
|
+
const presetDir = path.join(templatesDir, options.preset);
|
|
52
|
+
if (!fs.existsSync(presetDir)) {
|
|
53
|
+
console.error(`Error: Preset "${options.preset}" not found. Available: ${fs.readdirSync(templatesDir).join(', ')}`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
// Read package.json from this package to get current lagless version
|
|
57
|
+
const pkgJsonPath = path.resolve(__dirname, '..', 'package.json');
|
|
58
|
+
let laglessVersion = '0.0.30';
|
|
59
|
+
try {
|
|
60
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
61
|
+
laglessVersion = pkg.version || laglessVersion;
|
|
62
|
+
} catch (e) {
|
|
63
|
+
// fallback
|
|
64
|
+
}
|
|
65
|
+
const vars = {
|
|
66
|
+
projectName: pascalName,
|
|
67
|
+
packageName,
|
|
68
|
+
frontendPort: options.port,
|
|
69
|
+
serverPort: options.serverPort,
|
|
70
|
+
laglessVersion
|
|
71
|
+
};
|
|
72
|
+
console.log(`\nCreating Lagless project "${packageName}"...`);
|
|
73
|
+
console.log(` Preset: ${options.preset}`);
|
|
74
|
+
console.log(` Frontend port: ${options.port}`);
|
|
75
|
+
console.log(` Server port: ${options.serverPort}`);
|
|
76
|
+
console.log(` Target: ${targetDir}\n`);
|
|
77
|
+
const templateFiles = walkDir(presetDir);
|
|
78
|
+
for (const templateFile of templateFiles){
|
|
79
|
+
const relativePath = path.relative(presetDir, templateFile);
|
|
80
|
+
const outputRelative = processPath(relativePath, vars);
|
|
81
|
+
const outputPath = path.join(targetDir, outputRelative);
|
|
82
|
+
const outputDir = path.dirname(outputPath);
|
|
83
|
+
if (!fs.existsSync(outputDir)) {
|
|
84
|
+
fs.mkdirSync(outputDir, {
|
|
85
|
+
recursive: true
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
const content = fs.readFileSync(templateFile, 'utf-8');
|
|
89
|
+
// Only process .ejs files or text files that might contain EJS tags
|
|
90
|
+
const ext = path.extname(templateFile);
|
|
91
|
+
const textExts = [
|
|
92
|
+
'.ts',
|
|
93
|
+
'.tsx',
|
|
94
|
+
'.json',
|
|
95
|
+
'.yaml',
|
|
96
|
+
'.yml',
|
|
97
|
+
'.html',
|
|
98
|
+
'.css',
|
|
99
|
+
'.md',
|
|
100
|
+
'.toml',
|
|
101
|
+
'.gitignore'
|
|
102
|
+
];
|
|
103
|
+
if (textExts.includes(ext) || ext === '.ejs') {
|
|
104
|
+
const rendered = ejs.render(content, vars, {
|
|
105
|
+
filename: templateFile
|
|
106
|
+
});
|
|
107
|
+
const finalPath = ext === '.ejs' ? outputPath.replace(/\.ejs$/, '') : outputPath;
|
|
108
|
+
fs.writeFileSync(finalPath, rendered, 'utf-8');
|
|
109
|
+
} else {
|
|
110
|
+
// Binary or unknown — copy as-is
|
|
111
|
+
fs.copyFileSync(templateFile, outputPath);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
console.log('Project created successfully!\n');
|
|
115
|
+
console.log('Next steps:');
|
|
116
|
+
console.log(` cd ${packageName}`);
|
|
117
|
+
console.log(' pnpm install');
|
|
118
|
+
console.log(' pnpm dev:backend # Start game server');
|
|
119
|
+
console.log(' pnpm dev:frontend # Start frontend dev server\n');
|
|
120
|
+
});
|
|
121
|
+
program.parse();
|
|
122
|
+
|
|
123
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { program } from 'commander';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport * as ejs from 'ejs';\nimport { fileURLToPath } from 'node:url';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\ninterface CreateOptions {\n preset: string;\n port: string;\n serverPort: string;\n}\n\nfunction toPascalCase(kebab: string): string {\n return kebab.split('-').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join('');\n}\n\nfunction getTemplatesDir(): string {\n // In development (source), templates are sibling to src/\n // In dist, templates are at package root (copied by files field)\n const devPath = path.resolve(__dirname, '..', 'templates');\n if (fs.existsSync(devPath)) return devPath;\n // Fallback for npm install\n const distPath = path.resolve(__dirname, '..', '..', 'templates');\n if (fs.existsSync(distPath)) return distPath;\n throw new Error('Templates directory not found');\n}\n\nfunction walkDir(dir: string): string[] {\n const results: string[] = [];\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n const full = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n results.push(...walkDir(full));\n } else {\n results.push(full);\n }\n }\n return results;\n}\n\nfunction processPath(filePath: string, vars: Record<string, string>): string {\n let result = filePath;\n result = result.replace(/__packageName__/g, vars.packageName);\n result = result.replace(/__ProjectName__/g, vars.projectName);\n return result;\n}\n\nprogram\n .name('create-lagless')\n .description('Scaffold a new Lagless multiplayer game project')\n .version('0.0.30')\n .argument('<project-name>', 'Project name in kebab-case (e.g., my-game)')\n .option('--preset <preset>', 'Project preset', 'pixi-react')\n .option('--port <port>', 'Frontend dev server port', '4200')\n .option('--server-port <port>', 'Backend server port', '3333')\n .action(async (projectArg: string, options: CreateOptions) => {\n const targetDir = path.resolve(process.cwd(), projectArg);\n const packageName = path.basename(targetDir).toLowerCase();\n const pascalName = toPascalCase(packageName);\n\n if (fs.existsSync(targetDir)) {\n console.error(`Error: Directory \"${targetDir}\" already exists.`);\n process.exit(1);\n }\n\n const templatesDir = getTemplatesDir();\n const presetDir = path.join(templatesDir, options.preset);\n\n if (!fs.existsSync(presetDir)) {\n console.error(`Error: Preset \"${options.preset}\" not found. Available: ${fs.readdirSync(templatesDir).join(', ')}`);\n process.exit(1);\n }\n\n // Read package.json from this package to get current lagless version\n const pkgJsonPath = path.resolve(__dirname, '..', 'package.json');\n let laglessVersion = '0.0.30';\n try {\n const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));\n laglessVersion = pkg.version || laglessVersion;\n } catch {\n // fallback\n }\n\n const vars = {\n projectName: pascalName,\n packageName,\n frontendPort: options.port,\n serverPort: options.serverPort,\n laglessVersion,\n };\n\n console.log(`\\nCreating Lagless project \"${packageName}\"...`);\n console.log(` Preset: ${options.preset}`);\n console.log(` Frontend port: ${options.port}`);\n console.log(` Server port: ${options.serverPort}`);\n console.log(` Target: ${targetDir}\\n`);\n\n const templateFiles = walkDir(presetDir);\n\n for (const templateFile of templateFiles) {\n const relativePath = path.relative(presetDir, templateFile);\n const outputRelative = processPath(relativePath, vars);\n const outputPath = path.join(targetDir, outputRelative);\n const outputDir = path.dirname(outputPath);\n\n if (!fs.existsSync(outputDir)) {\n fs.mkdirSync(outputDir, { recursive: true });\n }\n\n const content = fs.readFileSync(templateFile, 'utf-8');\n\n // Only process .ejs files or text files that might contain EJS tags\n const ext = path.extname(templateFile);\n const textExts = ['.ts', '.tsx', '.json', '.yaml', '.yml', '.html', '.css', '.md', '.toml', '.gitignore'];\n\n if (textExts.includes(ext) || ext === '.ejs') {\n const rendered = ejs.render(content, vars, { filename: templateFile });\n const finalPath = ext === '.ejs' ? outputPath.replace(/\\.ejs$/, '') : outputPath;\n fs.writeFileSync(finalPath, rendered, 'utf-8');\n } else {\n // Binary or unknown — copy as-is\n fs.copyFileSync(templateFile, outputPath);\n }\n }\n\n console.log('Project created successfully!\\n');\n console.log('Next steps:');\n console.log(` cd ${packageName}`);\n console.log(' pnpm install');\n console.log(' pnpm dev:backend # Start game server');\n console.log(' pnpm dev:frontend # Start frontend dev server\\n');\n });\n\nprogram.parse();\n"],"names":["program","fs","path","ejs","fileURLToPath","__filename","url","__dirname","dirname","toPascalCase","kebab","split","map","s","charAt","toUpperCase","slice","join","getTemplatesDir","devPath","resolve","existsSync","distPath","Error","walkDir","dir","results","entry","readdirSync","withFileTypes","full","name","isDirectory","push","processPath","filePath","vars","result","replace","packageName","projectName","description","version","argument","option","action","projectArg","options","targetDir","process","cwd","basename","toLowerCase","pascalName","console","error","exit","templatesDir","presetDir","preset","pkgJsonPath","laglessVersion","pkg","JSON","parse","readFileSync","frontendPort","port","serverPort","log","templateFiles","templateFile","relativePath","relative","outputRelative","outputPath","outputDir","mkdirSync","recursive","content","ext","extname","textExts","includes","rendered","render","filename","finalPath","writeFileSync","copyFileSync"],"rangeMappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":"5.9.3"}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lagless/create",
|
|
3
|
+
"version": "0.0.33",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/ppauel/lagless",
|
|
8
|
+
"directory": "tools/create"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"main": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"bin": {
|
|
14
|
+
"create-lagless": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"exports": {
|
|
17
|
+
"./package.json": "./package.json",
|
|
18
|
+
".": {
|
|
19
|
+
"@lagless/source": "./src/index.ts",
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js",
|
|
22
|
+
"default": "./dist/index.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"templates",
|
|
28
|
+
"README.md"
|
|
29
|
+
],
|
|
30
|
+
"nx": {
|
|
31
|
+
"sourceRoot": "tools/create/src",
|
|
32
|
+
"targets": {
|
|
33
|
+
"build": {
|
|
34
|
+
"executor": "@nx/js:swc",
|
|
35
|
+
"outputs": [
|
|
36
|
+
"{options.outputPath}"
|
|
37
|
+
],
|
|
38
|
+
"options": {
|
|
39
|
+
"outputPath": "tools/create/dist",
|
|
40
|
+
"main": "tools/create/src/index.ts",
|
|
41
|
+
"tsConfig": "tools/create/tsconfig.lib.json",
|
|
42
|
+
"skipTypeCheck": true,
|
|
43
|
+
"stripLeadingPaths": true
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"commander": "^14.0.1",
|
|
50
|
+
"ejs": "^3.1.10",
|
|
51
|
+
"@swc/helpers": "~0.5.11"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/ejs": "^3.1.5"
|
|
55
|
+
},
|
|
56
|
+
"publishConfig": {
|
|
57
|
+
"access": "public"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# AGENTS.md — Multi-Agent Development Guide
|
|
2
|
+
|
|
3
|
+
## Project Overview
|
|
4
|
+
|
|
5
|
+
This is a Lagless multiplayer game with three workspace packages:
|
|
6
|
+
- `<%= packageName %>-simulation` — deterministic ECS logic
|
|
7
|
+
- `<%= packageName %>-frontend` — React + Pixi.js client
|
|
8
|
+
- `<%= packageName %>-backend` — Bun relay server
|
|
9
|
+
|
|
10
|
+
## Task Decomposition
|
|
11
|
+
|
|
12
|
+
### Adding a New Game Feature
|
|
13
|
+
|
|
14
|
+
1. **Schema**: Add components/inputs/filters to `ecs.yaml`, run `pnpm codegen`
|
|
15
|
+
2. **Systems**: Implement game logic as ECS systems in the simulation package
|
|
16
|
+
3. **Rendering**: Add Pixi.js views in the frontend using `filterView` + `FilterViews`
|
|
17
|
+
4. **Server hooks**: Update `game-hooks.ts` if the feature needs server-side events
|
|
18
|
+
|
|
19
|
+
### Debugging Determinism Issues
|
|
20
|
+
|
|
21
|
+
- Open two browser tabs, both "Play Online"
|
|
22
|
+
- Press F3 to show debug panel — check hash verification table
|
|
23
|
+
- If hashes diverge: the system running between the two reported hashes has non-deterministic code
|
|
24
|
+
- Common causes: using `Math.random()`, `Date.now()`, `Math.sin()` instead of `MathOps`, uninitialized memory
|
|
25
|
+
|
|
26
|
+
### Testing Strategy
|
|
27
|
+
|
|
28
|
+
- **Simulation tests**: Write vitest tests that create a runner, inject RPCs, advance ticks, assert state
|
|
29
|
+
- **Determinism tests**: Run same inputs twice, compare final ArrayBuffer hashes
|
|
30
|
+
- **Visual tests**: Use local play to verify rendering
|
|
31
|
+
|
|
32
|
+
## Key Files
|
|
33
|
+
|
|
34
|
+
| File | Purpose |
|
|
35
|
+
|------|---------|
|
|
36
|
+
| `*-simulation/src/lib/schema/ecs.yaml` | ECS data model definition |
|
|
37
|
+
| `*-simulation/src/lib/systems/index.ts` | System execution order |
|
|
38
|
+
| `*-simulation/src/lib/arena.ts` | Game constants |
|
|
39
|
+
| `*-frontend/src/app/game-view/runner-provider.tsx` | Simulation initialization + input draining |
|
|
40
|
+
| `*-frontend/src/app/game-view/game-scene.tsx` | Main Pixi.js scene composition |
|
|
41
|
+
| `*-backend/src/game-hooks.ts` | Server room lifecycle hooks |
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
## What This Is
|
|
4
|
+
|
|
5
|
+
<%= projectName %> is a multiplayer browser game built with **Lagless**, a deterministic ECS framework. TypeScript, simulate/rollback netcode, all simulation state in a single ArrayBuffer.
|
|
6
|
+
|
|
7
|
+
## Commands
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Install
|
|
11
|
+
pnpm install
|
|
12
|
+
|
|
13
|
+
# Start game server (Bun)
|
|
14
|
+
pnpm dev:backend
|
|
15
|
+
|
|
16
|
+
# Start frontend dev server (Vite)
|
|
17
|
+
pnpm dev:frontend
|
|
18
|
+
|
|
19
|
+
# Regenerate ECS code after schema changes
|
|
20
|
+
pnpm codegen
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Architecture
|
|
24
|
+
|
|
25
|
+
### Three Packages
|
|
26
|
+
|
|
27
|
+
- **<%= packageName %>-simulation** — Shared deterministic game logic (ECS systems, components, signals)
|
|
28
|
+
- **<%= packageName %>-frontend** — React + Pixi.js game client
|
|
29
|
+
- **<%= packageName %>-backend** — Bun game server (relay model, no simulation)
|
|
30
|
+
|
|
31
|
+
### ECS Memory Model
|
|
32
|
+
|
|
33
|
+
All state lives in one contiguous ArrayBuffer with Structure-of-Arrays layout. Snapshot = `ArrayBuffer.slice(0)`. Rollback = `Uint8Array.set()` from snapshot.
|
|
34
|
+
|
|
35
|
+
### Simulation Loop
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
1. clock.update(dt)
|
|
39
|
+
2. targetTick = floor(accTime / frameLength)
|
|
40
|
+
3. checkAndRollback() — if remote inputs arrived
|
|
41
|
+
4. simulationTicks: for each tick → run systems in order → process signals
|
|
42
|
+
5. inputProvider.update() — drain inputs, send to server
|
|
43
|
+
6. interpolationFactor = leftover / frameLength
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Input System
|
|
47
|
+
|
|
48
|
+
RPCs are deterministically ordered by `(playerSlot, ordinal, seq)`. Local inputs are scheduled `currentTick + inputDelay` ticks ahead. Server relays inputs to all clients.
|
|
49
|
+
|
|
50
|
+
### Key Conventions
|
|
51
|
+
|
|
52
|
+
- **Determinism is paramount**: Same inputs + same seed = identical state on every client
|
|
53
|
+
- All deterministic math must use `MathOps` (WASM-backed), never `Math.*` trig functions
|
|
54
|
+
- `MathOps.init()` must be called before simulation starts
|
|
55
|
+
- When spawning entities: always set `prevPositionX/Y = positionX/Y` to avoid interpolation jumps
|
|
56
|
+
- `@abraham/reflection` must be imported before any decorated class
|
|
57
|
+
- Generated code in `code-gen/` directories — never edit manually, regenerate from YAML
|
|
58
|
+
- Systems array order = execution order (deterministic)
|
|
59
|
+
|
|
60
|
+
### Schema & Codegen
|
|
61
|
+
|
|
62
|
+
ECS schema is defined in `<%= packageName %>-simulation/src/lib/schema/ecs.yaml`. Run `pnpm codegen` after changes.
|
|
63
|
+
|
|
64
|
+
Supported field types: `uint8`, `uint16`, `uint32`, `int8`, `int16`, `int32`, `float32`, `float64`, `uint8[N]` (fixed arrays).
|
|
65
|
+
|
|
66
|
+
### Adding Components/Systems
|
|
67
|
+
|
|
68
|
+
1. Add to `ecs.yaml`, run `pnpm codegen`
|
|
69
|
+
2. Create system file in `systems/` with `@ECSSystem()` decorator
|
|
70
|
+
3. Add to systems array in `systems/index.ts` (order matters)
|
|
71
|
+
4. Systems get dependencies via constructor injection (DI container)
|
|
72
|
+
|
|
73
|
+
### Multiplayer
|
|
74
|
+
|
|
75
|
+
- Server never runs simulation — relay model
|
|
76
|
+
- `RelayInputProvider` handles prediction + rollback
|
|
77
|
+
- `RelayConnection` manages WebSocket to relay server
|
|
78
|
+
- State transfer for late-join via `StateRequest`/`StateResponse`
|
|
79
|
+
- `RoomHooks` in backend define game lifecycle (join, leave, reconnect)
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# <%= projectName %>
|
|
2
|
+
|
|
3
|
+
A multiplayer game built with [Lagless](https://github.com/ppauel/lagless) — a deterministic ECS framework for real-time multiplayer browser games.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- [Node.js](https://nodejs.org/) 20+
|
|
8
|
+
- [pnpm](https://pnpm.io/) 9+
|
|
9
|
+
- [Bun](https://bun.sh/) (for the game server)
|
|
10
|
+
|
|
11
|
+
## Getting Started
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm install
|
|
15
|
+
|
|
16
|
+
# Terminal 1 — Start the game server
|
|
17
|
+
pnpm dev:backend
|
|
18
|
+
|
|
19
|
+
# Terminal 2 — Start the frontend dev server
|
|
20
|
+
pnpm dev:frontend
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Open http://localhost:<%= frontendPort %> in your browser. Click "Play Local" for single-player or "Play Online" for multiplayer.
|
|
24
|
+
|
|
25
|
+
Press **F3** to toggle the debug panel (shows network stats, tick info, hash verification).
|
|
26
|
+
|
|
27
|
+
## Project Structure
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
<%= packageName %>/
|
|
31
|
+
├── <%= packageName %>-simulation/ # Shared deterministic game logic (ECS)
|
|
32
|
+
│ └── src/lib/
|
|
33
|
+
│ ├── schema/ecs.yaml # ECS schema definition
|
|
34
|
+
│ ├── schema/code-gen/ # Generated code (don't edit manually)
|
|
35
|
+
│ ├── systems/ # ECS systems (game logic)
|
|
36
|
+
│ ├── signals/ # Rollback-aware events
|
|
37
|
+
│ └── arena.ts # Game constants
|
|
38
|
+
├── <%= packageName %>-frontend/ # React + Pixi.js game client
|
|
39
|
+
│ └── src/app/
|
|
40
|
+
│ ├── screens/ # Title screen, game screen
|
|
41
|
+
│ ├── hooks/ # Match start hooks
|
|
42
|
+
│ └── game-view/ # Pixi.js rendering
|
|
43
|
+
└── <%= packageName %>-backend/ # Bun game server
|
|
44
|
+
└── src/
|
|
45
|
+
├── main.ts # Server entry point
|
|
46
|
+
└── game-hooks.ts # Room lifecycle hooks
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## ECS Schema
|
|
50
|
+
|
|
51
|
+
The game's data model is defined in `<%= packageName %>-simulation/src/lib/schema/ecs.yaml`. After modifying the schema, regenerate code:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pnpm codegen
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Schema Reference
|
|
58
|
+
|
|
59
|
+
**Components** — Per-entity data stored in Structure-of-Arrays layout:
|
|
60
|
+
```yaml
|
|
61
|
+
components:
|
|
62
|
+
MyComponent:
|
|
63
|
+
fieldName: float32 # Supported: uint8, uint16, uint32, int8, int16, int32, float32, float64
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Singletons** — Global game state (single instance):
|
|
67
|
+
```yaml
|
|
68
|
+
singletons:
|
|
69
|
+
GameState:
|
|
70
|
+
gamePhase: uint8
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Player Resources** — Per-player data (indexed by player slot):
|
|
74
|
+
```yaml
|
|
75
|
+
playerResources:
|
|
76
|
+
PlayerResource:
|
|
77
|
+
score: uint32
|
|
78
|
+
id: uint8[16] # Fixed-size arrays supported
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Inputs** — RPCs sent by clients and server:
|
|
82
|
+
```yaml
|
|
83
|
+
inputs:
|
|
84
|
+
MoveInput:
|
|
85
|
+
directionX: float32
|
|
86
|
+
directionY: float32
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Filters** — Cached entity queries:
|
|
90
|
+
```yaml
|
|
91
|
+
filters:
|
|
92
|
+
PlayerFilter:
|
|
93
|
+
include: [Transform2d, PlayerBody]
|
|
94
|
+
# exclude: [Dead] # Optional
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Adding a New System
|
|
98
|
+
|
|
99
|
+
1. Create `<%= packageName %>-simulation/src/lib/systems/my-system.system.ts`:
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
import { ECSSystem, IECSSystem } from '@lagless/core';
|
|
103
|
+
import { Transform2d, PlayerFilter } from '../schema/code-gen/index.js';
|
|
104
|
+
|
|
105
|
+
@ECSSystem()
|
|
106
|
+
export class MySystem implements IECSSystem {
|
|
107
|
+
constructor(
|
|
108
|
+
private readonly _PlayerFilter: PlayerFilter,
|
|
109
|
+
private readonly _Transform2d: Transform2d,
|
|
110
|
+
) {}
|
|
111
|
+
|
|
112
|
+
public update(tick: number): void {
|
|
113
|
+
for (const entity of this._PlayerFilter) {
|
|
114
|
+
// Your logic here
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
2. Add it to the systems array in `systems/index.ts` (order matters — systems run sequentially).
|
|
121
|
+
|
|
122
|
+
## Key Conventions
|
|
123
|
+
|
|
124
|
+
- **Determinism**: All simulation math must use `MathOps` for trig functions (WASM-backed). Never use `Math.sin/cos/atan2/sqrt` in systems.
|
|
125
|
+
- **Interpolation**: Always set `prevPositionX/Y` equal to `positionX/Y` when spawning entities to avoid one-frame jumps.
|
|
126
|
+
- **Vector math**: Use `Vector2.addToRef()`/`.addInPlace()` in hot paths to avoid allocations.
|
|
127
|
+
- **Decorators**: `@abraham/reflection` must be imported before any decorated class (done in `main.tsx`).
|
|
128
|
+
|
|
129
|
+
## Architecture
|
|
130
|
+
|
|
131
|
+
- **Server never runs simulation** — it relays inputs between clients
|
|
132
|
+
- **Clients are authoritative on determinism** — same inputs + same seed = identical state
|
|
133
|
+
- **Rollback netcode** — local inputs are predicted, remote inputs trigger rollback when they arrive
|
|
134
|
+
- **State transfer** — late-joining players receive a state snapshot from existing clients
|
|
135
|
+
|
|
136
|
+
## @lagless Packages
|
|
137
|
+
|
|
138
|
+
| Package | Description |
|
|
139
|
+
|---------|-------------|
|
|
140
|
+
| `@lagless/core` | ECS engine, simulation loop, input system, signals |
|
|
141
|
+
| `@lagless/binary` | Memory tracking, typed array utilities |
|
|
142
|
+
| `@lagless/math` | Deterministic math (WASM), Vector2 |
|
|
143
|
+
| `@lagless/misc` | Logging, UUID, VisualSmoother2d |
|
|
144
|
+
| `@lagless/net-wire` | Binary network protocol messages |
|
|
145
|
+
| `@lagless/relay-client` | Client-side relay connection, RelayInputProvider |
|
|
146
|
+
| `@lagless/relay-server` | Server-side relay room, RoomHooks |
|
|
147
|
+
| `@lagless/relay-game-server` | Full game server with matchmaking |
|
|
148
|
+
| `@lagless/matchmaking` | Matchmaking queue service |
|
|
149
|
+
| `@lagless/react` | React debug panel, auth utilities |
|
|
150
|
+
| `@lagless/pixi-react` | FilterViews, VFX container, virtual joystick |
|
|
151
|
+
| `@lagless/codegen` | ECS code generation from YAML |
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "<%= packageName %>-backend",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "bun --watch run src/main.ts",
|
|
8
|
+
"start": "bun run src/main.ts"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"<%= packageName %>-simulation": "workspace:*",
|
|
12
|
+
"@lagless/relay-game-server": "^<%= laglessVersion %>",
|
|
13
|
+
"@lagless/misc": "^<%= laglessVersion %>",
|
|
14
|
+
"reflect-metadata": "^0.2.0"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/bun": "latest"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { RoomHooks, RoomContext, PlayerInfo } from '@lagless/relay-server';
|
|
2
|
+
import { LeaveReason } from '@lagless/relay-server';
|
|
3
|
+
import { createLogger, UUID } from '@lagless/misc';
|
|
4
|
+
import { PlayerJoined, PlayerLeft } from '<%= packageName %>-simulation';
|
|
5
|
+
|
|
6
|
+
const log = createLogger('<%= projectName %>Hooks');
|
|
7
|
+
|
|
8
|
+
export const gameHooks: RoomHooks = {
|
|
9
|
+
onRoomCreated(ctx: RoomContext) {
|
|
10
|
+
log.info(`[${ctx.matchId}] Room created, ${ctx.getPlayers().length} players`);
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
onPlayerJoin(ctx: RoomContext, player: PlayerInfo) {
|
|
14
|
+
log.info(`[${ctx.matchId}] Player joined: slot=${player.slot} bot=${player.isBot} id=${player.playerId.slice(0, 8)}`);
|
|
15
|
+
|
|
16
|
+
ctx.emitServerEvent(PlayerJoined.id, {
|
|
17
|
+
playerId: player.isBot
|
|
18
|
+
? UUID.generateMasked().asUint8()
|
|
19
|
+
: UUID.fromString(player.playerId).asUint8(),
|
|
20
|
+
slot: player.slot,
|
|
21
|
+
} satisfies PlayerJoined['schema']);
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
onPlayerReconnect(ctx: RoomContext, player: PlayerInfo) {
|
|
25
|
+
log.info(`[${ctx.matchId}] Player reconnected: slot=${player.slot} id=${player.playerId.slice(0, 8)}`);
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
onPlayerLeave(ctx: RoomContext, player: PlayerInfo, reason: LeaveReason) {
|
|
29
|
+
log.info(`[${ctx.matchId}] Player left: slot=${player.slot} reason=${LeaveReason[reason]}`);
|
|
30
|
+
ctx.emitServerEvent(PlayerLeft.id, {
|
|
31
|
+
slot: player.slot,
|
|
32
|
+
reason,
|
|
33
|
+
} satisfies PlayerLeft['schema']);
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
shouldAcceptLateJoin() {
|
|
37
|
+
return true;
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
shouldAcceptReconnect() {
|
|
41
|
+
return true;
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
onRoomDisposed(ctx: RoomContext) {
|
|
45
|
+
log.info(`[${ctx.matchId}] Room disposed`);
|
|
46
|
+
},
|
|
47
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
import { RelayGameServer } from '@lagless/relay-game-server';
|
|
3
|
+
import { <%= projectName %>InputRegistry } from '<%= packageName %>-simulation';
|
|
4
|
+
import { gameHooks } from './game-hooks.js';
|
|
5
|
+
|
|
6
|
+
const server = new RelayGameServer({
|
|
7
|
+
port: Number(process.env.PORT ?? <%= serverPort %>),
|
|
8
|
+
loggerName: '<%= projectName %>Server',
|
|
9
|
+
roomType: {
|
|
10
|
+
name: '<%= packageName %>',
|
|
11
|
+
config: {
|
|
12
|
+
maxPlayers: 4,
|
|
13
|
+
tickRateHz: 60,
|
|
14
|
+
maxFutureTicks: 20,
|
|
15
|
+
lateJoinEnabled: true,
|
|
16
|
+
reconnectTimeoutMs: 30_000,
|
|
17
|
+
stateTransferTimeoutMs: 5_000,
|
|
18
|
+
},
|
|
19
|
+
hooks: gameHooks,
|
|
20
|
+
inputRegistry: <%= projectName %>InputRegistry,
|
|
21
|
+
},
|
|
22
|
+
matchmaking: {
|
|
23
|
+
scope: '<%= packageName %>',
|
|
24
|
+
config: { minPlayersToStart: 1, maxPlayers: 4, waitTimeoutMs: 2_000 },
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
server.start();
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"experimentalDecorators": true,
|
|
12
|
+
"emitDecoratorMetadata": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"isolatedModules": true,
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
"types": ["bun-types"]
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*.ts"]
|
|
19
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<title><%= projectName %></title>
|
|
6
|
+
<base href="/" />
|
|
7
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
8
|
+
<link rel="stylesheet" href="/src/styles.css" />
|
|
9
|
+
<style>
|
|
10
|
+
html, body {
|
|
11
|
+
margin: 0;
|
|
12
|
+
padding: 0;
|
|
13
|
+
background: #0a0a1a;
|
|
14
|
+
font-family: 'Courier New', Courier, monospace;
|
|
15
|
+
color: #e0e0e0;
|
|
16
|
+
}
|
|
17
|
+
</style>
|
|
18
|
+
</head>
|
|
19
|
+
<body>
|
|
20
|
+
<div id="root"></div>
|
|
21
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
22
|
+
</body>
|
|
23
|
+
</html>
|