@lagless/create 0.0.38 → 0.0.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +26 -0
- package/dist/index.js +96 -16
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
- package/templates/pixi-react/AGENTS.md +57 -27
- package/templates/pixi-react/CLAUDE.md +225 -49
- package/templates/pixi-react/README.md +16 -6
- package/templates/pixi-react/__packageName__-backend/package.json +1 -0
- package/templates/pixi-react/__packageName__-backend/src/main.ts +2 -0
- package/templates/pixi-react/__packageName__-frontend/package.json +8 -0
- package/templates/pixi-react/__packageName__-frontend/src/app/game-view/grid-background.tsx +4 -0
- package/templates/pixi-react/__packageName__-frontend/src/app/game-view/player-view.tsx +68 -0
- package/templates/pixi-react/__packageName__-frontend/src/app/game-view/runner-provider.tsx +57 -0
- package/templates/pixi-react/__packageName__-frontend/src/app/hooks/use-start-multiplayer-match.ts +5 -5
- package/templates/pixi-react/__packageName__-frontend/src/app/screens/title.screen.tsx +18 -1
- package/templates/pixi-react/__packageName__-simulation/package.json +7 -0
- package/templates/pixi-react/__packageName__-simulation/src/lib/arena.ts +12 -0
- package/templates/pixi-react/__packageName__-simulation/src/lib/schema/ecs.yaml +90 -6
- package/templates/pixi-react/__packageName__-simulation/src/lib/systems/apply-move-input.system.ts +73 -0
- package/templates/pixi-react/__packageName__-simulation/src/lib/systems/boundary.system.ts +2 -0
- package/templates/pixi-react/__packageName__-simulation/src/lib/systems/damping.system.ts +2 -0
- package/templates/pixi-react/__packageName__-simulation/src/lib/systems/index.ts +8 -0
- package/templates/pixi-react/__packageName__-simulation/src/lib/systems/integrate.system.ts +2 -0
- package/templates/pixi-react/__packageName__-simulation/src/lib/systems/physics-step.system.ts +65 -0
- package/templates/pixi-react/__packageName__-simulation/src/lib/systems/player-connection.system.ts +158 -0
- package/templates/pixi-react/__packageName__-simulation/src/lib/systems/player-leave.system.ts +70 -0
- package/templates/pixi-react/__packageName__-simulation/src/lib/systems/save-prev-transform.system.ts +46 -0
- package/templates/pixi-react/docs/01-schema-and-codegen.md +244 -0
- package/templates/pixi-react/docs/02-ecs-systems.md +293 -0
- package/templates/pixi-react/docs/03-determinism.md +204 -0
- package/templates/pixi-react/docs/04-input-system.md +255 -0
- package/templates/pixi-react/docs/05-signals.md +175 -0
- package/templates/pixi-react/docs/06-rendering.md +256 -0
- package/templates/pixi-react/docs/07-multiplayer.md +277 -0
- package/templates/pixi-react/docs/08-physics2d.md +266 -0
- package/templates/pixi-react/docs/08-physics3d.md +312 -0
- package/templates/pixi-react/docs/09-recipes.md +362 -0
- package/templates/pixi-react/docs/10-common-mistakes.md +224 -0
- package/templates/pixi-react/docs/api-quick-reference.md +254 -0
- package/templates/pixi-react/package.json +6 -0
- /package/templates/pixi-react/__packageName__-backend/{tsconfig.json → tsconfig.json.ejs} +0 -0
- /package/templates/pixi-react/__packageName__-frontend/{tsconfig.json → tsconfig.json.ejs} +0 -0
- /package/templates/pixi-react/__packageName__-frontend/{vite.config.ts → vite.config.ts.ejs} +0 -0
- /package/templates/pixi-react/__packageName__-simulation/{.swcrc → .swcrc.ejs} +0 -0
- /package/templates/pixi-react/__packageName__-simulation/{tsconfig.json → tsconfig.json.ejs} +0 -0
- /package/templates/pixi-react/{tsconfig.base.json → tsconfig.base.json.ejs} +0 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Creative Commons Attribution-NonCommercial 4.0 International
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Lagless
|
|
4
|
+
|
|
5
|
+
This work is licensed under the Creative Commons
|
|
6
|
+
Attribution-NonCommercial 4.0 International License.
|
|
7
|
+
|
|
8
|
+
You are free to:
|
|
9
|
+
|
|
10
|
+
Share — copy and redistribute the material in any medium or format
|
|
11
|
+
Adapt — remix, transform, and build upon the material
|
|
12
|
+
|
|
13
|
+
Under the following terms:
|
|
14
|
+
|
|
15
|
+
Attribution — You must give appropriate credit, provide a link to
|
|
16
|
+
the license, and indicate if changes were made. You may do so in
|
|
17
|
+
any reasonable manner, but not in any way that suggests the licensor
|
|
18
|
+
endorses you or your use.
|
|
19
|
+
|
|
20
|
+
NonCommercial — You may not use the material for commercial purposes.
|
|
21
|
+
|
|
22
|
+
No additional restrictions — You may not apply legal terms or
|
|
23
|
+
technological measures that legally restrict others from doing
|
|
24
|
+
anything the license permits.
|
|
25
|
+
|
|
26
|
+
Full license text: https://creativecommons.org/licenses/by-nc/4.0/legalcode
|
package/dist/index.js
CHANGED
|
@@ -3,18 +3,17 @@ import { program } from 'commander';
|
|
|
3
3
|
import * as fs from 'node:fs';
|
|
4
4
|
import * as path from 'node:path';
|
|
5
5
|
import * as ejs from 'ejs';
|
|
6
|
+
import { execSync } from 'node:child_process';
|
|
6
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { select } from '@inquirer/prompts';
|
|
7
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
8
10
|
const __dirname = path.dirname(__filename);
|
|
9
11
|
function toPascalCase(kebab) {
|
|
10
12
|
return kebab.split('-').map((s)=>s.charAt(0).toUpperCase() + s.slice(1)).join('');
|
|
11
13
|
}
|
|
12
14
|
function getTemplatesDir() {
|
|
13
|
-
// In development (source), templates are sibling to src/
|
|
14
|
-
// In dist, templates are at package root (copied by files field)
|
|
15
15
|
const devPath = path.resolve(__dirname, '..', 'templates');
|
|
16
16
|
if (fs.existsSync(devPath)) return devPath;
|
|
17
|
-
// Fallback for npm install
|
|
18
17
|
const distPath = path.resolve(__dirname, '..', '..', 'templates');
|
|
19
18
|
if (fs.existsSync(distPath)) return distPath;
|
|
20
19
|
throw new Error('Templates directory not found');
|
|
@@ -39,7 +38,29 @@ function processPath(filePath, vars) {
|
|
|
39
38
|
result = result.replace(/__ProjectName__/g, vars.projectName);
|
|
40
39
|
return result;
|
|
41
40
|
}
|
|
42
|
-
|
|
41
|
+
async function promptSimulationType() {
|
|
42
|
+
return select({
|
|
43
|
+
message: 'Select simulation type:',
|
|
44
|
+
choices: [
|
|
45
|
+
{
|
|
46
|
+
value: 'raw',
|
|
47
|
+
name: 'Raw ECS',
|
|
48
|
+
description: 'Manual velocity/position management, no physics engine'
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
value: 'physics2d',
|
|
52
|
+
name: 'Physics 2D (Rapier)',
|
|
53
|
+
description: 'Rapier 2D rigid body physics with auto-managed transforms'
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
value: 'physics3d',
|
|
57
|
+
name: 'Physics 3D (Rapier)',
|
|
58
|
+
description: 'Rapier 3D rigid body physics with top-down 2D rendering'
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
program.name('create-lagless').description('Scaffold a new Lagless multiplayer game project').version('0.0.38').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').option('--simulation-type <type>', 'Simulation type: raw, physics2d, or physics3d').action(async (projectArg, options)=>{
|
|
43
64
|
const targetDir = path.resolve(process.cwd(), projectArg);
|
|
44
65
|
const packageName = path.basename(targetDir).toLowerCase();
|
|
45
66
|
const pascalName = toPascalCase(packageName);
|
|
@@ -53,9 +74,25 @@ program.name('create-lagless').description('Scaffold a new Lagless multiplayer g
|
|
|
53
74
|
console.error(`Error: Preset "${options.preset}" not found. Available: ${fs.readdirSync(templatesDir).join(', ')}`);
|
|
54
75
|
process.exit(1);
|
|
55
76
|
}
|
|
77
|
+
// Determine simulation type — from CLI flag or interactive prompt
|
|
78
|
+
let simulationType;
|
|
79
|
+
if (options.simulationType) {
|
|
80
|
+
const valid = [
|
|
81
|
+
'raw',
|
|
82
|
+
'physics2d',
|
|
83
|
+
'physics3d'
|
|
84
|
+
];
|
|
85
|
+
if (!valid.includes(options.simulationType)) {
|
|
86
|
+
console.error(`Error: Invalid simulation type "${options.simulationType}". Valid: ${valid.join(', ')}`);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
simulationType = options.simulationType;
|
|
90
|
+
} else {
|
|
91
|
+
simulationType = await promptSimulationType();
|
|
92
|
+
}
|
|
56
93
|
// Read package.json from this package to get current lagless version
|
|
57
94
|
const pkgJsonPath = path.resolve(__dirname, '..', 'package.json');
|
|
58
|
-
let laglessVersion = '0.0.
|
|
95
|
+
let laglessVersion = '0.0.38';
|
|
59
96
|
try {
|
|
60
97
|
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
61
98
|
laglessVersion = pkg.version || laglessVersion;
|
|
@@ -67,10 +104,12 @@ program.name('create-lagless').description('Scaffold a new Lagless multiplayer g
|
|
|
67
104
|
packageName,
|
|
68
105
|
frontendPort: options.port,
|
|
69
106
|
serverPort: options.serverPort,
|
|
70
|
-
laglessVersion
|
|
107
|
+
laglessVersion,
|
|
108
|
+
simulationType
|
|
71
109
|
};
|
|
72
110
|
console.log(`\nCreating Lagless project "${packageName}"...`);
|
|
73
111
|
console.log(` Preset: ${options.preset}`);
|
|
112
|
+
console.log(` Simulation: ${simulationType}`);
|
|
74
113
|
console.log(` Frontend port: ${options.port}`);
|
|
75
114
|
console.log(` Server port: ${options.serverPort}`);
|
|
76
115
|
console.log(` Target: ${targetDir}\n`);
|
|
@@ -80,13 +119,8 @@ program.name('create-lagless').description('Scaffold a new Lagless multiplayer g
|
|
|
80
119
|
const outputRelative = processPath(relativePath, vars);
|
|
81
120
|
const outputPath = path.join(targetDir, outputRelative);
|
|
82
121
|
const outputDir = path.dirname(outputPath);
|
|
83
|
-
if (!fs.existsSync(outputDir)) {
|
|
84
|
-
fs.mkdirSync(outputDir, {
|
|
85
|
-
recursive: true
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
122
|
const content = fs.readFileSync(templateFile, 'utf-8');
|
|
89
|
-
// Only process
|
|
123
|
+
// Only process text files that might contain EJS tags
|
|
90
124
|
const ext = path.extname(templateFile);
|
|
91
125
|
const textExts = [
|
|
92
126
|
'.ts',
|
|
@@ -105,19 +139,65 @@ program.name('create-lagless').description('Scaffold a new Lagless multiplayer g
|
|
|
105
139
|
filename: templateFile
|
|
106
140
|
});
|
|
107
141
|
const finalPath = ext === '.ejs' ? outputPath.replace(/\.ejs$/, '') : outputPath;
|
|
142
|
+
// Skip writing empty/whitespace-only files (conditional template exclusion)
|
|
143
|
+
if (rendered.trim().length === 0) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (!fs.existsSync(outputDir)) {
|
|
147
|
+
fs.mkdirSync(outputDir, {
|
|
148
|
+
recursive: true
|
|
149
|
+
});
|
|
150
|
+
}
|
|
108
151
|
fs.writeFileSync(finalPath, rendered, 'utf-8');
|
|
109
152
|
} else {
|
|
110
153
|
// Binary or unknown — copy as-is
|
|
154
|
+
if (!fs.existsSync(outputDir)) {
|
|
155
|
+
fs.mkdirSync(outputDir, {
|
|
156
|
+
recursive: true
|
|
157
|
+
});
|
|
158
|
+
}
|
|
111
159
|
fs.copyFileSync(templateFile, outputPath);
|
|
112
160
|
}
|
|
113
161
|
}
|
|
114
|
-
|
|
162
|
+
// Remove physics docs that don't match the selected simulationType
|
|
163
|
+
const docsDir = path.join(targetDir, 'docs');
|
|
164
|
+
if (simulationType !== 'physics2d') {
|
|
165
|
+
const f = path.join(docsDir, '08-physics2d.md');
|
|
166
|
+
if (fs.existsSync(f)) fs.rmSync(f);
|
|
167
|
+
}
|
|
168
|
+
if (simulationType !== 'physics3d') {
|
|
169
|
+
const f = path.join(docsDir, '08-physics3d.md');
|
|
170
|
+
if (fs.existsSync(f)) fs.rmSync(f);
|
|
171
|
+
}
|
|
172
|
+
// Clone lagless framework source for AI reference
|
|
173
|
+
const sourcesDir = path.join(docsDir, 'sources');
|
|
174
|
+
fs.mkdirSync(sourcesDir, {
|
|
175
|
+
recursive: true
|
|
176
|
+
});
|
|
177
|
+
console.log('Cloning lagless framework source for AI reference...');
|
|
178
|
+
try {
|
|
179
|
+
execSync(`git clone --depth 1 https://github.com/GbGr/lagless.git "${path.join(sourcesDir, 'lagless')}"`, {
|
|
180
|
+
stdio: 'inherit'
|
|
181
|
+
});
|
|
182
|
+
// Remove .git to save space
|
|
183
|
+
fs.rmSync(path.join(sourcesDir, 'lagless', '.git'), {
|
|
184
|
+
recursive: true,
|
|
185
|
+
force: true
|
|
186
|
+
});
|
|
187
|
+
} catch (e) {
|
|
188
|
+
console.warn('Warning: Could not clone lagless source. AI reference will be unavailable.');
|
|
189
|
+
console.warn('You can manually clone later: git clone --depth 1 https://github.com/GbGr/lagless.git docs/sources/lagless');
|
|
190
|
+
}
|
|
191
|
+
console.log('\nProject created successfully!\n');
|
|
115
192
|
console.log('Next steps:');
|
|
116
193
|
console.log(` cd ${packageName}`);
|
|
117
194
|
console.log(' pnpm install');
|
|
118
|
-
console.log(' pnpm codegen
|
|
119
|
-
console.log(' pnpm dev
|
|
120
|
-
console.log('
|
|
195
|
+
console.log(' pnpm codegen # Generate ECS code from schema');
|
|
196
|
+
console.log(' pnpm dev # Start backend + frontend + dev-player\n');
|
|
197
|
+
console.log('Or run individually:');
|
|
198
|
+
console.log(' pnpm dev:backend # Game server (Bun, watches for changes)');
|
|
199
|
+
console.log(' pnpm dev:frontend # Frontend (Vite HMR)');
|
|
200
|
+
console.log(' pnpm dev:player # Dev-player (multiplayer testing, port 4210)\n');
|
|
121
201
|
});
|
|
122
202
|
program.parse();
|
|
123
203
|
|
package/dist/index.js.map
CHANGED
|
@@ -1 +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 codegen # Generate ECS code from schema');\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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"}
|
|
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 { execSync } from 'node:child_process';\nimport { fileURLToPath } from 'node:url';\nimport { select } from '@inquirer/prompts';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\ntype SimulationType = 'raw' | 'physics2d' | 'physics3d';\n\ninterface CreateOptions {\n preset: string;\n port: string;\n serverPort: string;\n simulationType?: SimulationType;\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 const devPath = path.resolve(__dirname, '..', 'templates');\n if (fs.existsSync(devPath)) return devPath;\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\nasync function promptSimulationType(): Promise<SimulationType> {\n return select<SimulationType>({\n message: 'Select simulation type:',\n choices: [\n { value: 'raw', name: 'Raw ECS', description: 'Manual velocity/position management, no physics engine' },\n { value: 'physics2d', name: 'Physics 2D (Rapier)', description: 'Rapier 2D rigid body physics with auto-managed transforms' },\n { value: 'physics3d', name: 'Physics 3D (Rapier)', description: 'Rapier 3D rigid body physics with top-down 2D rendering' },\n ],\n });\n}\n\nprogram\n .name('create-lagless')\n .description('Scaffold a new Lagless multiplayer game project')\n .version('0.0.38')\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 .option('--simulation-type <type>', 'Simulation type: raw, physics2d, or physics3d')\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 // Determine simulation type — from CLI flag or interactive prompt\n let simulationType: SimulationType;\n if (options.simulationType) {\n const valid: SimulationType[] = ['raw', 'physics2d', 'physics3d'];\n if (!valid.includes(options.simulationType as SimulationType)) {\n console.error(`Error: Invalid simulation type \"${options.simulationType}\". Valid: ${valid.join(', ')}`);\n process.exit(1);\n }\n simulationType = options.simulationType as SimulationType;\n } else {\n simulationType = await promptSimulationType();\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.38';\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: Record<string, string> = {\n projectName: pascalName,\n packageName,\n frontendPort: options.port,\n serverPort: options.serverPort,\n laglessVersion,\n simulationType,\n };\n\n console.log(`\\nCreating Lagless project \"${packageName}\"...`);\n console.log(` Preset: ${options.preset}`);\n console.log(` Simulation: ${simulationType}`);\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 const content = fs.readFileSync(templateFile, 'utf-8');\n\n // Only process 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\n // Skip writing empty/whitespace-only files (conditional template exclusion)\n if (rendered.trim().length === 0) {\n continue;\n }\n\n if (!fs.existsSync(outputDir)) {\n fs.mkdirSync(outputDir, { recursive: true });\n }\n fs.writeFileSync(finalPath, rendered, 'utf-8');\n } else {\n // Binary or unknown — copy as-is\n if (!fs.existsSync(outputDir)) {\n fs.mkdirSync(outputDir, { recursive: true });\n }\n fs.copyFileSync(templateFile, outputPath);\n }\n }\n\n // Remove physics docs that don't match the selected simulationType\n const docsDir = path.join(targetDir, 'docs');\n if (simulationType !== 'physics2d') {\n const f = path.join(docsDir, '08-physics2d.md');\n if (fs.existsSync(f)) fs.rmSync(f);\n }\n if (simulationType !== 'physics3d') {\n const f = path.join(docsDir, '08-physics3d.md');\n if (fs.existsSync(f)) fs.rmSync(f);\n }\n\n // Clone lagless framework source for AI reference\n const sourcesDir = path.join(docsDir, 'sources');\n fs.mkdirSync(sourcesDir, { recursive: true });\n console.log('Cloning lagless framework source for AI reference...');\n try {\n execSync(`git clone --depth 1 https://github.com/GbGr/lagless.git \"${path.join(sourcesDir, 'lagless')}\"`, {\n stdio: 'inherit',\n });\n // Remove .git to save space\n fs.rmSync(path.join(sourcesDir, 'lagless', '.git'), { recursive: true, force: true });\n } catch {\n console.warn('Warning: Could not clone lagless source. AI reference will be unavailable.');\n console.warn('You can manually clone later: git clone --depth 1 https://github.com/GbGr/lagless.git docs/sources/lagless');\n }\n\n console.log('\\nProject created successfully!\\n');\n console.log('Next steps:');\n console.log(` cd ${packageName}`);\n console.log(' pnpm install');\n console.log(' pnpm codegen # Generate ECS code from schema');\n console.log(' pnpm dev # Start backend + frontend + dev-player\\n');\n console.log('Or run individually:');\n console.log(' pnpm dev:backend # Game server (Bun, watches for changes)');\n console.log(' pnpm dev:frontend # Frontend (Vite HMR)');\n console.log(' pnpm dev:player # Dev-player (multiplayer testing, port 4210)\\n');\n });\n\nprogram.parse();\n"],"names":["program","fs","path","ejs","execSync","fileURLToPath","select","__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","promptSimulationType","message","choices","value","description","version","argument","option","action","projectArg","options","targetDir","process","cwd","basename","toLowerCase","pascalName","console","error","exit","templatesDir","presetDir","preset","simulationType","valid","includes","pkgJsonPath","laglessVersion","pkg","JSON","parse","readFileSync","frontendPort","port","serverPort","log","templateFiles","templateFile","relativePath","relative","outputRelative","outputPath","outputDir","content","ext","extname","textExts","rendered","render","filename","finalPath","trim","length","mkdirSync","recursive","writeFileSync","copyFileSync","docsDir","f","rmSync","sourcesDir","stdio","force","warn"],"rangeMappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lagless/create",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"license": "
|
|
3
|
+
"version": "0.0.39",
|
|
4
|
+
"license": "CC-BY-NC-4.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/GbGr/lagless",
|
|
@@ -45,9 +45,10 @@
|
|
|
45
45
|
}
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
+
"@inquirer/prompts": "^8.3.0",
|
|
49
|
+
"@swc/helpers": "~0.5.11",
|
|
48
50
|
"commander": "^14.0.1",
|
|
49
|
-
"ejs": "^3.1.10"
|
|
50
|
-
"@swc/helpers": "~0.5.11"
|
|
51
|
+
"ejs": "^3.1.10"
|
|
51
52
|
},
|
|
52
53
|
"devDependencies": {
|
|
53
54
|
"@types/ejs": "^3.1.5"
|
|
@@ -1,41 +1,71 @@
|
|
|
1
|
-
# AGENTS.md — Multi-Agent
|
|
1
|
+
# AGENTS.md — Multi-Agent Guide for <%= projectName %>
|
|
2
2
|
|
|
3
3
|
## Project Overview
|
|
4
4
|
|
|
5
|
-
This is a Lagless multiplayer game with three
|
|
6
|
-
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
5
|
+
This is a Lagless multiplayer game with three packages:
|
|
6
|
+
- **<%= packageName %>-simulation** — Deterministic ECS game logic (shared between all clients)
|
|
7
|
+
- **<%= packageName %>-frontend** — React + Pixi.js rendering client
|
|
8
|
+
- **<%= packageName %>-backend** — Bun relay server (no simulation)
|
|
9
9
|
|
|
10
10
|
## Task Decomposition
|
|
11
11
|
|
|
12
12
|
### Adding a New Game Feature
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
1. **Schema & Codegen** — Edit `ecs.yaml`, run `pnpm codegen`
|
|
14
|
+
2. **System** — Write `@ECSSystem()` in simulation, add to systems array
|
|
15
|
+
3. **View** — Add Pixi.js component in frontend `game-view/`
|
|
16
|
+
4. **Input** — Wire drainInputs in `runner-provider.tsx` if feature needs player input
|
|
17
|
+
5. **Test** — Verify determinism: open two tabs, play, check F3 hash table matches
|
|
18
18
|
|
|
19
19
|
### Debugging Determinism Issues
|
|
20
|
+
1. **Reproduce** — Open dev-player (`pnpm dev:player`), run 2+ instances
|
|
21
|
+
2. **Identify tick** — F3 debug panel shows hash divergence tick
|
|
22
|
+
3. **Binary search** — Add hash checks between systems to narrow down which system diverges
|
|
23
|
+
4. **Check rules** — Review `docs/03-determinism.md` for common violations
|
|
24
|
+
5. **Common causes** — `Math.sin` instead of `MathOps.sin`, unsorted iteration, missing `prevPosition` init
|
|
25
|
+
|
|
26
|
+
### Adding Multiplayer Features
|
|
27
|
+
1. **Server events** — Add to `RoomHooks` in `game-hooks.ts`
|
|
28
|
+
2. **Server RPCs** — Use `ctx.emitServerEvent()` for server-originated inputs
|
|
29
|
+
3. **State transfer** — Handled automatically, but test with late-join scenario
|
|
30
|
+
4. **Reconnect** — Test disconnect/reconnect via F3 debug panel buttons
|
|
31
|
+
|
|
32
|
+
## File Ownership by Task Type
|
|
33
|
+
|
|
34
|
+
| Task | Primary Files |
|
|
35
|
+
|------|--------------|
|
|
36
|
+
| New component/input | `ecs.yaml` → `pnpm codegen` |
|
|
37
|
+
| Game logic | `*-simulation/src/lib/systems/*.system.ts` |
|
|
38
|
+
| Entity rendering | `*-frontend/src/app/game-view/*.tsx` |
|
|
39
|
+
| UI/screens | `*-frontend/src/app/screens/*.tsx` |
|
|
40
|
+
| Player input | `*-frontend/src/app/game-view/runner-provider.tsx` |
|
|
41
|
+
| Server hooks | `*-backend/src/game-hooks.ts` |
|
|
42
|
+
| Server config | `*-backend/src/main.ts` |
|
|
43
|
+
| Signals | `*-simulation/src/lib/signals/index.ts` |
|
|
20
44
|
|
|
21
|
-
|
|
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
|
|
45
|
+
## Verification Checklist
|
|
25
46
|
|
|
26
|
-
###
|
|
47
|
+
### Before Submitting Any Change
|
|
48
|
+
- [ ] `pnpm codegen` runs without errors (if schema changed)
|
|
49
|
+
- [ ] Game starts: `pnpm dev` → no console errors
|
|
50
|
+
- [ ] Single-player works: title screen → game → entities move correctly
|
|
51
|
+
- [ ] Multiplayer works: two browser tabs, both see each other
|
|
52
|
+
- [ ] Determinism holds: F3 panel → hash table → no red (divergence) entries
|
|
27
53
|
|
|
28
|
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
54
|
+
### For Physics Changes (<%= simulationType !== 'raw' ? 'APPLIES TO THIS PROJECT' : 'if applicable' %>)
|
|
55
|
+
- [ ] Bodies created with correct BodyType
|
|
56
|
+
- [ ] `updateSceneQueries()` called after snapshot restore
|
|
57
|
+
- [ ] ColliderEntityMap rebuilt after state transfer
|
|
58
|
+
- [ ] Collision layers configured correctly
|
|
31
59
|
|
|
32
|
-
## Key
|
|
60
|
+
## Key Constraints
|
|
61
|
+
- **Never edit `code-gen/` files** — always edit `ecs.yaml` and run `pnpm codegen`
|
|
62
|
+
- **Never use `Math.sin/cos/atan2/sqrt`** — use `MathOps.*` equivalents
|
|
63
|
+
- **Never use `Math.random()`** — use `PRNG.getFloat()` or `PRNG.getRandomInt()`
|
|
64
|
+
- **Always sanitize RPC inputs** — `Number.isFinite()` before `MathOps.clamp()`
|
|
65
|
+
- **Always set prevPosition = position** when spawning entities with Transform2d
|
|
66
|
+
- **Systems array order = execution order** — it's deterministic and matters
|
|
33
67
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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 |
|
|
68
|
+
## Documentation Reference
|
|
69
|
+
- `CLAUDE.md` — Primary instruction file with code patterns
|
|
70
|
+
- `docs/` — Detailed documentation on all framework topics
|
|
71
|
+
- `docs/sources/lagless/` — Full framework source code for deep reference
|