@lagless/create 0.0.44 → 0.0.46

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.js CHANGED
@@ -8,6 +8,15 @@ import { fileURLToPath } from 'node:url';
8
8
  import { select } from '@inquirer/prompts';
9
9
  const __filename = fileURLToPath(import.meta.url);
10
10
  const __dirname = path.dirname(__filename);
11
+ // Read version from package.json once (single source of truth)
12
+ const _pkgJsonPath = path.resolve(__dirname, '..', 'package.json');
13
+ let _packageVersion = '0.0.0';
14
+ try {
15
+ const _pkg = JSON.parse(fs.readFileSync(_pkgJsonPath, 'utf-8'));
16
+ _packageVersion = _pkg.version || _packageVersion;
17
+ } catch (e) {
18
+ // fallback
19
+ }
11
20
  function toPascalCase(kebab) {
12
21
  return kebab.split('-').map((s)=>s.charAt(0).toUpperCase() + s.slice(1)).join('');
13
22
  }
@@ -60,7 +69,7 @@ async function promptSimulationType() {
60
69
  ]
61
70
  });
62
71
  }
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', '4203').option('--server-port <port>', 'Backend server port', '3400').option('--simulation-type <type>', 'Simulation type: raw, physics2d, or physics3d').action(async (projectArg, options)=>{
72
+ program.name('create-lagless').description('Scaffold a new Lagless multiplayer game project').version(_packageVersion).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', '4203').option('--server-port <port>', 'Backend server port', '3400').option('--simulation-type <type>', 'Simulation type: raw, physics2d, or physics3d').action(async (projectArg, options)=>{
64
73
  const targetDir = path.resolve(process.cwd(), projectArg);
65
74
  const packageName = path.basename(targetDir).toLowerCase();
66
75
  const pascalName = toPascalCase(packageName);
@@ -90,15 +99,7 @@ program.name('create-lagless').description('Scaffold a new Lagless multiplayer g
90
99
  } else {
91
100
  simulationType = await promptSimulationType();
92
101
  }
93
- // Read package.json from this package to get current lagless version
94
- const pkgJsonPath = path.resolve(__dirname, '..', 'package.json');
95
- let laglessVersion = '0.0.38';
96
- try {
97
- const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
98
- laglessVersion = pkg.version || laglessVersion;
99
- } catch (e) {
100
- // fallback
101
- }
102
+ const laglessVersion = _packageVersion;
102
103
  const vars = {
103
104
  projectName: pascalName,
104
105
  packageName,
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 { 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', '4203')\n .option('--server-port <port>', 'Backend server port', '3400')\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 // Install dependencies\n console.log('\\nInstalling dependencies...');\n try {\n execSync('pnpm install', { cwd: targetDir, stdio: 'inherit' });\n } catch {\n console.error('Warning: pnpm install failed. Run it manually after creation.');\n }\n\n // Run ECS codegen to generate code from schema\n console.log('\\nGenerating ECS code from schema...');\n try {\n execSync('pnpm codegen', { cwd: targetDir, stdio: 'inherit' });\n } catch {\n console.error('Warning: codegen failed. Run \"pnpm codegen\" manually after creation.');\n }\n\n console.log('\\nProject created successfully!\\n');\n console.log('To start developing:');\n console.log(` cd ${packageName}`);\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 console.log('To regenerate ECS code after schema changes:');\n console.log(' pnpm codegen\\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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"}
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\n// Read version from package.json once (single source of truth)\nconst _pkgJsonPath = path.resolve(__dirname, '..', 'package.json');\nlet _packageVersion = '0.0.0';\ntry {\n const _pkg = JSON.parse(fs.readFileSync(_pkgJsonPath, 'utf-8'));\n _packageVersion = _pkg.version || _packageVersion;\n} catch {\n // fallback\n}\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(_packageVersion)\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', '4203')\n .option('--server-port <port>', 'Backend server port', '3400')\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 const laglessVersion = _packageVersion;\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 // Install dependencies\n console.log('\\nInstalling dependencies...');\n try {\n execSync('pnpm install', { cwd: targetDir, stdio: 'inherit' });\n } catch {\n console.error('Warning: pnpm install failed. Run it manually after creation.');\n }\n\n // Run ECS codegen to generate code from schema\n console.log('\\nGenerating ECS code from schema...');\n try {\n execSync('pnpm codegen', { cwd: targetDir, stdio: 'inherit' });\n } catch {\n console.error('Warning: codegen failed. Run \"pnpm codegen\" manually after creation.');\n }\n\n console.log('\\nProject created successfully!\\n');\n console.log('To start developing:');\n console.log(` cd ${packageName}`);\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 console.log('To regenerate ECS code after schema changes:');\n console.log(' pnpm codegen\\n');\n });\n\nprogram.parse();\n"],"names":["program","fs","path","ejs","execSync","fileURLToPath","select","__filename","url","__dirname","dirname","_pkgJsonPath","resolve","_packageVersion","_pkg","JSON","parse","readFileSync","version","toPascalCase","kebab","split","map","s","charAt","toUpperCase","slice","join","getTemplatesDir","devPath","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","argument","option","action","projectArg","options","targetDir","process","cwd","basename","toLowerCase","pascalName","console","error","exit","templatesDir","presetDir","preset","simulationType","valid","includes","laglessVersion","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,6 +1,6 @@
1
1
  {
2
2
  "name": "@lagless/create",
3
- "version": "0.0.44",
3
+ "version": "0.0.46",
4
4
  "license": "CC-BY-NC-4.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -53,8 +53,7 @@ This is a Lagless multiplayer game with three packages:
53
53
 
54
54
  ### For Physics Changes (<%= simulationType !== 'raw' ? 'APPLIES TO THIS PROJECT' : 'if applicable' %>)
55
55
  - [ ] Bodies created with correct BodyType
56
- - [ ] `updateSceneQueries()` called after snapshot restore
57
- - [ ] ColliderEntityMap rebuilt after state transfer
56
+ - [ ] ColliderEntityMap rebuild is automatic (verify no manual rebuild code)
58
57
  - [ ] Collision layers configured correctly
59
58
 
60
59
  ## Key Constraints
@@ -19,11 +19,12 @@
19
19
  "@lagless/pixi-react": "^<%= laglessVersion %>",
20
20
  <% if (simulationType === 'physics2d') { -%>
21
21
  "@lagless/physics2d": "^<%= laglessVersion %>",
22
- "@dimforge/rapier2d-compat": "^0.14.0",
22
+ "@lagless/physics-shared": "^<%= laglessVersion %>",
23
+ "@dimforge/rapier2d-deterministic-compat": "^0.19.0",
23
24
  <% } else if (simulationType === 'physics3d') { -%>
24
25
  "@lagless/physics3d": "^<%= laglessVersion %>",
25
26
  "@lagless/physics-shared": "^<%= laglessVersion %>",
26
- "@dimforge/rapier3d-compat": "^0.14.0",
27
+ "@dimforge/rapier3d-deterministic-compat": "^0.19.0",
27
28
  <% } -%>
28
29
  "@abraham/reflection": "^0.12.0",
29
30
  "pixi.js": "^8.12.0",
@@ -9,12 +9,36 @@ export const GridBackground: FC = () => {
9
9
  const g = graphicsRef.current;
10
10
  if (!g) return;
11
11
 
12
- const w = <%= projectName %>Arena.width;
13
12
  <% if (simulationType === 'physics3d') { -%>
14
- const h = <%= projectName %>Arena.depth;
13
+ // physics3d: world units screen pixels (top-down view)
14
+ const SCALE = 20;
15
+ const OFFSET_X = 400;
16
+ const OFFSET_Y = 300;
17
+ const arenaW = <%= projectName %>Arena.width;
18
+ const arenaH = <%= projectName %>Arena.depth;
19
+ const screenW = arenaW * SCALE;
20
+ const screenH = arenaH * SCALE;
21
+ const startX = OFFSET_X - screenW / 2;
22
+ const startY = OFFSET_Y - screenH / 2;
23
+ const step = 100;
24
+
25
+ g.clear();
26
+
27
+ for (let x = startX; x <= startX + screenW; x += step) {
28
+ g.moveTo(x, startY);
29
+ g.lineTo(x, startY + screenH);
30
+ }
31
+ for (let y = startY; y <= startY + screenH; y += step) {
32
+ g.moveTo(startX, y);
33
+ g.lineTo(startX + screenW, y);
34
+ }
35
+ g.stroke({ color: 0x333355, width: 1, alpha: 0.4 });
36
+
37
+ g.rect(startX, startY, screenW, screenH);
38
+ g.stroke({ color: 0x6666aa, width: 3, alpha: 0.8 });
15
39
  <% } else { -%>
40
+ const w = <%= projectName %>Arena.width;
16
41
  const h = <%= projectName %>Arena.height;
17
- <% } -%>
18
42
  const step = 100;
19
43
 
20
44
  g.clear();
@@ -31,6 +55,7 @@ export const GridBackground: FC = () => {
31
55
 
32
56
  g.rect(0, 0, w, h);
33
57
  g.stroke({ color: 0x6666aa, width: 3, alpha: 0.8 });
58
+ <% } -%>
34
59
  }, []);
35
60
 
36
61
  return <pixiGraphics ref={graphicsRef} />;
@@ -7,11 +7,6 @@ import {
7
7
  PlayerJoined,
8
8
  ReportHash,
9
9
  <%= projectName %>Arena,
10
- <% if (simulationType !== 'raw') { -%>
11
- PhysicsRefs,
12
- PhysicsRefsFilter,
13
- PlayerFilter,
14
- <% } -%>
15
10
  } from '<%= packageName %>-simulation';
16
11
  import { createContext, FC, ReactNode, useContext, useEffect, useState } from 'react';
17
12
  import { useTick } from '@pixi/react';
@@ -80,7 +75,7 @@ export const RunnerProvider: FC<RunnerProviderProps> = ({ children }) => {
80
75
  <% if (simulationType === 'physics2d') { -%>
81
76
  // Load Rapier 2D WASM
82
77
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
83
- const RAPIER = (await import('@dimforge/rapier2d-compat')).default as any;
78
+ const RAPIER = (await import('@dimforge/rapier2d-deterministic-compat')).default as any;
84
79
  await RAPIER.init();
85
80
  const rapier = RAPIER as unknown as RapierModule2d;
86
81
  if (disposed) { inputProvider.dispose(); return; }
@@ -88,7 +83,7 @@ export const RunnerProvider: FC<RunnerProviderProps> = ({ children }) => {
88
83
  <% } else if (simulationType === 'physics3d') { -%>
89
84
  // Load Rapier 3D WASM
90
85
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
91
- const RAPIER = (await import('@dimforge/rapier3d-compat')).default as any;
86
+ const RAPIER = (await import('@dimforge/rapier3d-deterministic-compat')).default as any;
92
87
  await RAPIER.init();
93
88
  const rapier = RAPIER as unknown as RapierModule3d;
94
89
  if (disposed) { inputProvider.dispose(); return; }
@@ -142,20 +137,6 @@ export const RunnerProvider: FC<RunnerProviderProps> = ({ children }) => {
142
137
  <% } -%>
143
138
  }
144
139
 
145
- <% if (simulationType !== 'raw') { -%>
146
- // Hook state transfer to rebuild ColliderEntityMap after receiving external state
147
- const worldManager = _runner.PhysicsWorldManager;
148
- _runner.Simulation.addStateTransferHandler(() => {
149
- worldManager.colliderEntityMap.clear();
150
- const physicsFilter = _runner.DIContainer.resolve(PhysicsRefsFilter);
151
- const refs = _runner.DIContainer.resolve(PhysicsRefs);
152
- const refsUnsafe = refs.unsafe;
153
- for (const e of physicsFilter) {
154
- worldManager.registerCollider(refsUnsafe.colliderHandle[e], e);
155
- }
156
- });
157
-
158
- <% } -%>
159
140
  // Set up keyboard input drainer with hash reporting
160
141
  const reportHash = createHashReporter(_runner, {
161
142
  reportInterval: <%= projectName %>Arena.hashReportInterval,
@@ -74,6 +74,8 @@ import { ECSSystem, IECSSystem, InputProvider, PlayerResources } from '@lagless/
74
74
  import { MoveInput, PlayerResource, Velocity2d } from '../schema/code-gen/index.js';
75
75
  import { <%= projectName %>Arena } from '../arena.js';
76
76
 
77
+ const finite = (v: number): number => Number.isFinite(v) ? v : 0;
78
+
77
79
  @ECSSystem()
78
80
  export class ApplyMoveInputSystem implements IECSSystem {
79
81
  constructor(
@@ -88,8 +90,13 @@ export class ApplyMoveInputSystem implements IECSSystem {
88
90
  for (const rpc of rpcs) {
89
91
  const playerResource = this._PlayerResources.get(PlayerResource, rpc.meta.playerSlot);
90
92
  const entity = playerResource.safe.entity;
91
- this._Velocity2d.unsafe.velocityX[entity] = rpc.data.directionX * <%= projectName %>Arena.moveSpeed;
92
- this._Velocity2d.unsafe.velocityY[entity] = rpc.data.directionY * <%= projectName %>Arena.moveSpeed;
93
+
94
+ // Sanitize input
95
+ const dirX = finite(rpc.data.directionX);
96
+ const dirY = finite(rpc.data.directionY);
97
+
98
+ this._Velocity2d.unsafe.velocityX[entity] = dirX * <%= projectName %>Arena.moveSpeed;
99
+ this._Velocity2d.unsafe.velocityY[entity] = dirY * <%= projectName %>Arena.moveSpeed;
93
100
  }
94
101
  }
95
102
  }
@@ -35,12 +35,13 @@ BodyType.KINEMATIC_VELOCITY // 3 — moved by setting velocity directly
35
35
 
36
36
  ## Creating Bodies and Colliders
37
37
 
38
- Use `PhysicsWorldManager2d` to create physics bodies:
38
+ Use `PhysicsWorldManager2d` to create physics bodies. The manager provides factory methods that return Rapier body/collider objects. You configure them via Rapier's native API, then store handles in the ECS `PhysicsRefs` component and register the collider for entity lookup.
39
39
 
40
40
  ```typescript
41
- import { ECSSystem, IECSSystem } from '@lagless/core';
41
+ import { ECSSystem, IECSSystem, EntitiesManager } from '@lagless/core';
42
42
  import { PhysicsWorldManager2d } from '@lagless/physics2d';
43
43
  import { BodyType, CollisionLayers } from '@lagless/physics-shared';
44
+ import { Transform2d, PhysicsRefs } from '../code-gen/core.js';
44
45
 
45
46
  @ECSSystem()
46
47
  export class SpawnSystem implements IECSSystem {
@@ -48,6 +49,7 @@ export class SpawnSystem implements IECSSystem {
48
49
  private readonly _physics: PhysicsWorldManager2d,
49
50
  private readonly _entities: EntitiesManager,
50
51
  private readonly _transform: Transform2d,
52
+ private readonly _physicsRefs: PhysicsRefs,
51
53
  ) {}
52
54
 
53
55
  update(tick: number): void {
@@ -56,45 +58,60 @@ export class SpawnSystem implements IECSSystem {
56
58
  this._entities.addComponent(entity, Transform2d);
57
59
  this._entities.addComponent(entity, PhysicsRefs);
58
60
 
59
- // Set initial position
60
- this._transform.set(entity, {
61
- positionX: 100, positionY: 200,
62
- prevPositionX: 100, prevPositionY: 200,
63
- rotation: 0, prevRotation: 0,
64
- });
65
-
66
- // Create physics body + collider
67
- this._physics.createBody(entity, {
68
- bodyType: BodyType.DYNAMIC,
69
- position: { x: 100, y: 200 },
70
- rotation: 0,
71
- });
72
-
73
- this._physics.createCollider(entity, {
74
- shape: { type: 'ball', radius: 20 },
75
- density: 1.0,
76
- friction: 0.5,
77
- restitution: 0.3,
78
- collisionLayer: CollisionLayers.get('player'),
79
- });
61
+ // Set initial position in ECS (including prev for interpolation)
62
+ const t = this._transform.unsafe;
63
+ t.positionX[entity] = 100;
64
+ t.positionY[entity] = 200;
65
+ t.prevPositionX[entity] = 100;
66
+ t.prevPositionY[entity] = 200;
67
+ t.rotation[entity] = 0;
68
+ t.prevRotation[entity] = 0;
69
+
70
+ // Create a dynamic Rapier body and configure it
71
+ const body = this._physics.createDynamicBody();
72
+ body.setTranslation({ x: 100, y: 200 }, true);
73
+ body.setLinearDamping(5.0);
74
+
75
+ // Create a ball collider attached to the body
76
+ const groups = CollisionLayers.get('player');
77
+ const collider = this._physics.createBallCollider(20, body, groups);
78
+
79
+ // Store handles in ECS for later lookup
80
+ const pr = this._physicsRefs.unsafe;
81
+ pr.bodyHandle[entity] = body.handle;
82
+ pr.colliderHandle[entity] = collider.handle;
83
+ pr.bodyType[entity] = BodyType.DYNAMIC;
84
+ pr.collisionLayer[entity] = groups;
85
+
86
+ // Register collider→entity mapping (used by collision events)
87
+ this._physics.registerCollider(collider.handle, entity);
80
88
  }
81
89
  }
82
90
  ```
83
91
 
84
92
  ### Collider Shapes
85
93
 
94
+ All collider factories take an optional `parent` body, `groups` (collision groups), and `activeEvents` bitmask:
95
+
86
96
  ```typescript
87
97
  // Circle
88
- { type: 'ball', radius: 20 }
98
+ this._physics.createBallCollider(radius, parent, groups, activeEvents);
89
99
 
90
- // Rectangle
91
- { type: 'cuboid', hx: 50, hy: 25 } // half-extents
100
+ // Rectangle (half-extents)
101
+ this._physics.createCuboidCollider(hx, hy, parent, groups, activeEvents);
92
102
 
93
103
  // Capsule
94
- { type: 'capsule', halfHeight: 30, radius: 10 }
104
+ this._physics.createCapsuleCollider(halfHeight, radius, parent, groups, activeEvents);
105
+
106
+ // Convex polygon (returns null if hull computation fails)
107
+ this._physics.createConvexHullCollider(new Float32Array([x1,y1, x2,y2, ...]), parent, groups, activeEvents);
95
108
 
96
- // Convex polygon
97
- { type: 'convexHull', points: [x1,y1, x2,y2, ...] }
109
+ // Triangle mesh (static geometry only)
110
+ this._physics.createTrimeshCollider(vertices, indices, parent, groups, activeEvents);
111
+
112
+ // Custom collider from a Rapier ColliderDesc
113
+ const desc = this._physics.rapier.ColliderDesc.ball(10).setDensity(2.0).setFriction(0.5);
114
+ this._physics.createColliderFromDesc(desc, parent);
98
115
  ```
99
116
 
100
117
  ## Collision Layers
@@ -199,9 +216,7 @@ You generally don't interact with this directly — it's managed by `PhysicsWorl
199
216
  On rollback:
200
217
  1. ArrayBuffer is restored → ECS state reverts
201
218
  2. Rapier world snapshot is restored → physics state reverts
202
- 3. `updateSceneQueries()` is called → QueryPipeline is rebuilt
203
-
204
- **Critical fix applied:** `World.restoreSnapshot()` creates a world with an **empty** QueryPipeline (not serialized). The framework calls `updateSceneQueries()` after restore to fix this. Without it, ray casts and shape casts fail on the first tick after rollback.
219
+ 3. `ColliderEntityMap` is rebuilt automatically
205
220
 
206
221
  ## State Transfer
207
222
 
@@ -217,7 +232,7 @@ This is handled automatically by the physics runner. You don't need to do anythi
217
232
  ```typescript
218
233
  import { ECSSystem, IECSSystem, AbstractInputProvider, ECSConfig, EntitiesManager } from '@lagless/core';
219
234
  import { MathOps } from '@lagless/math';
220
- import { PhysicsWorldManager2d, CollisionEvents2d } from '@lagless/physics2d';
235
+ import { PhysicsWorldManager2d } from '@lagless/physics2d';
221
236
  import { BodyType } from '@lagless/physics-shared';
222
237
  import { Transform2d, PhysicsRefs, PlayerBody, PlayerFilter, MoveInput } from '../code-gen/core.js';
223
238
 
@@ -228,9 +243,9 @@ export class ApplyMoveInputSystem implements IECSSystem {
228
243
  constructor(
229
244
  private readonly _input: AbstractInputProvider,
230
245
  private readonly _physics: PhysicsWorldManager2d,
246
+ private readonly _physicsRefs: PhysicsRefs,
231
247
  private readonly _playerBody: PlayerBody,
232
248
  private readonly _filter: PlayerFilter,
233
- private readonly _config: ECSConfig,
234
249
  ) {}
235
250
 
236
251
  update(tick: number): void {
@@ -244,11 +259,10 @@ export class ApplyMoveInputSystem implements IECSSystem {
244
259
  if (this._playerBody.unsafe.playerSlot[entity] !== slot) continue;
245
260
 
246
261
  const speed = 300;
247
- // Apply velocity to Rapier body
248
- this._physics.setLinearVelocity(entity, {
249
- x: dirX * speed,
250
- y: dirY * speed,
251
- });
262
+ // Get the Rapier body via its handle stored in PhysicsRefs
263
+ const body = this._physics.getBody(this._physicsRefs.unsafe.bodyHandle[entity]);
264
+ // Apply velocity directly on the Rapier body
265
+ body.setLinvel({ x: dirX * speed, y: dirY * speed }, true);
252
266
  break;
253
267
  }
254
268
  }
@@ -37,9 +37,13 @@ BodyType.KINEMATIC_VELOCITY // 3 — moved by setting velocity
37
37
 
38
38
  ## Creating Bodies and Colliders
39
39
 
40
+ The manager provides factory methods that return Rapier body/collider objects. You configure them via Rapier's native API, then store handles in the ECS `PhysicsRefs` component and register the collider for entity lookup.
41
+
40
42
  ```typescript
43
+ import { ECSSystem, IECSSystem, EntitiesManager } from '@lagless/core';
41
44
  import { PhysicsWorldManager3d } from '@lagless/physics3d';
42
45
  import { BodyType, CollisionLayers } from '@lagless/physics-shared';
46
+ import { Transform3d, PhysicsRefs } from '../code-gen/core.js';
43
47
 
44
48
  @ECSSystem()
45
49
  export class SpawnSystem implements IECSSystem {
@@ -47,6 +51,7 @@ export class SpawnSystem implements IECSSystem {
47
51
  private readonly _physics: PhysicsWorldManager3d,
48
52
  private readonly _entities: EntitiesManager,
49
53
  private readonly _transform: Transform3d,
54
+ private readonly _physicsRefs: PhysicsRefs,
50
55
  ) {}
51
56
 
52
57
  update(tick: number): void {
@@ -54,50 +59,74 @@ export class SpawnSystem implements IECSSystem {
54
59
  this._entities.addComponent(entity, Transform3d);
55
60
  this._entities.addComponent(entity, PhysicsRefs);
56
61
 
57
- this._transform.set(entity, {
58
- positionX: 0, positionY: 5, positionZ: 0,
59
- rotationX: 0, rotationY: 0, rotationZ: 0, rotationW: 1,
60
- prevPositionX: 0, prevPositionY: 5, prevPositionZ: 0,
61
- prevRotationX: 0, prevRotationY: 0, prevRotationZ: 0, prevRotationW: 1,
62
- });
63
-
64
- this._physics.createBody(entity, {
65
- bodyType: BodyType.DYNAMIC,
66
- position: { x: 0, y: 5, z: 0 },
67
- rotation: { x: 0, y: 0, z: 0, w: 1 },
68
- });
69
-
70
- this._physics.createCollider(entity, {
71
- shape: { type: 'ball', radius: 0.5 },
72
- density: 1.0,
73
- friction: 0.5,
74
- restitution: 0.3,
75
- collisionLayer: CollisionLayers.get('player'),
76
- });
62
+ // Set initial position in ECS (including prev for interpolation)
63
+ const t = this._transform.unsafe;
64
+ t.positionX[entity] = 0;
65
+ t.positionY[entity] = 5;
66
+ t.positionZ[entity] = 0;
67
+ t.rotationX[entity] = 0;
68
+ t.rotationY[entity] = 0;
69
+ t.rotationZ[entity] = 0;
70
+ t.rotationW[entity] = 1;
71
+ t.prevPositionX[entity] = 0;
72
+ t.prevPositionY[entity] = 5;
73
+ t.prevPositionZ[entity] = 0;
74
+ t.prevRotationX[entity] = 0;
75
+ t.prevRotationY[entity] = 0;
76
+ t.prevRotationZ[entity] = 0;
77
+ t.prevRotationW[entity] = 1;
78
+
79
+ // Create a dynamic Rapier body and configure it
80
+ const body = this._physics.createDynamicBody();
81
+ body.setTranslation({ x: 0, y: 5, z: 0 }, true);
82
+ body.setRotation({ x: 0, y: 0, z: 0, w: 1 }, true);
83
+
84
+ // Create a ball collider attached to the body
85
+ const groups = CollisionLayers.get('player');
86
+ const collider = this._physics.createBallCollider(0.5, body, groups);
87
+
88
+ // Store handles in ECS for later lookup
89
+ const pr = this._physicsRefs.unsafe;
90
+ pr.bodyHandle[entity] = body.handle;
91
+ pr.colliderHandle[entity] = collider.handle;
92
+ pr.bodyType[entity] = BodyType.DYNAMIC;
93
+ pr.collisionLayer[entity] = groups;
94
+
95
+ // Register collider→entity mapping (used by collision events)
96
+ this._physics.registerCollider(collider.handle, entity);
77
97
  }
78
98
  }
79
99
  ```
80
100
 
81
101
  ### Collider Shapes (3D)
82
102
 
103
+ All collider factories take an optional `parent` body, `groups` (collision groups), and `activeEvents` bitmask:
104
+
83
105
  ```typescript
84
106
  // Sphere
85
- { type: 'ball', radius: 0.5 }
107
+ this._physics.createBallCollider(radius, parent, groups, activeEvents);
86
108
 
87
- // Box
88
- { type: 'cuboid', hx: 1, hy: 0.5, hz: 1 } // half-extents
109
+ // Box (half-extents, 3 dimensions)
110
+ this._physics.createCuboidCollider(hx, hy, hz, parent, groups, activeEvents);
89
111
 
90
112
  // Capsule
91
- { type: 'capsule', halfHeight: 0.5, radius: 0.3 }
113
+ this._physics.createCapsuleCollider(halfHeight, radius, parent, groups, activeEvents);
114
+
115
+ // Cylinder (3D only)
116
+ this._physics.createCylinderCollider(halfHeight, radius, parent, groups, activeEvents);
92
117
 
93
- // Cylinder
94
- { type: 'cylinder', halfHeight: 1.0, radius: 0.5 }
118
+ // Cone (3D only)
119
+ this._physics.createConeCollider(halfHeight, radius, parent, groups, activeEvents);
95
120
 
96
- // Convex hull
97
- { type: 'convexHull', points: Float32Array }
121
+ // Convex hull (returns null if hull computation fails)
122
+ this._physics.createConvexHullCollider(new Float32Array([...]), parent, groups, activeEvents);
98
123
 
99
- // Triangle mesh (static only)
100
- { type: 'trimesh', vertices: Float32Array, indices: Uint32Array }
124
+ // Triangle mesh (static geometry only)
125
+ this._physics.createTrimeshCollider(vertices, indices, parent, groups, activeEvents);
126
+
127
+ // Custom collider from a Rapier ColliderDesc
128
+ const desc = this._physics.rapier.ColliderDesc.ball(0.5).setDensity(2.0).setFriction(0.5);
129
+ this._physics.createColliderFromDesc(desc, parent);
101
130
  ```
102
131
 
103
132
  ## Collision Layers and Events
@@ -281,11 +310,9 @@ export const systems = [
281
310
  On rollback:
282
311
  1. ArrayBuffer is restored → ECS state reverts
283
312
  2. Rapier 3D world snapshot is restored → physics state reverts
284
- 3. `updateSceneQueries()` is called → QueryPipeline is rebuilt
313
+ 3. `ColliderEntityMap` is rebuilt automatically
285
314
  4. KCC controllers are recreated via `recreateAll()`
286
315
 
287
- **Critical fix:** `World.restoreSnapshot()` creates a world with an empty QueryPipeline. The framework calls `updateSceneQueries()` after restore. Without this, `computeColliderMovement()` queries fail on the first tick after rollback.
288
-
289
316
  ## State Transfer
290
317
 
291
318
  After `applyExternalState()`:
@@ -178,17 +178,12 @@ this._transform.unsafe.positionX[entity] = newX; // Physics will overwrite!
178
178
  ```
179
179
  **Correct:**
180
180
  ```typescript
181
- this._physics.setLinearVelocity(entity, { x: vx, y: vy });
181
+ const body = this._physics.getBody(this._physicsRefs.unsafe.bodyHandle[entity]);
182
+ body.setLinvel({ x: vx, y: vy }, true);
182
183
  // or
183
- this._physics.applyImpulse(entity, { x: fx, y: fy });
184
+ body.applyImpulse({ x: fx, y: fy }, true);
184
185
  ```
185
- **Why:** PhysicsStep syncs Rapier→ECS, overwriting manual position changes. Move dynamic bodies via forces/velocity.
186
-
187
- ### Forgetting updateSceneQueries After Restore
188
-
189
- The framework handles this automatically, but if you're doing custom Rapier operations:
190
- **Always** call `world.updateSceneQueries()` after `World.restoreSnapshot()`.
191
- **Why:** Restored worlds have empty QueryPipeline — ray casts and shape casts will miss all colliders.
186
+ **Why:** PhysicsStep syncs Rapier→ECS, overwriting manual position changes. Move dynamic bodies via forces/velocity on the Rapier body.
192
187
 
193
188
  ## Multiplayer
194
189