@lagless/create 0.0.44 → 0.0.48

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,
@@ -169,6 +170,10 @@ program.name('create-lagless').description('Scaffold a new Lagless multiplayer g
169
170
  const f = path.join(docsDir, '08-physics3d.md');
170
171
  if (fs.existsSync(f)) fs.rmSync(f);
171
172
  }
173
+ if (simulationType !== 'physics2d') {
174
+ const f = path.join(docsDir, '11-2d-map-generation.md');
175
+ if (fs.existsSync(f)) fs.rmSync(f);
176
+ }
172
177
  // Clone lagless framework source for AI reference
173
178
  const sourcesDir = path.join(docsDir, 'sources');
174
179
  fs.mkdirSync(sourcesDir, {
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 if (simulationType !== 'physics2d') {\n const f = path.join(docsDir, '11-2d-map-generation.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.48",
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
@@ -240,6 +240,9 @@ Systems run in array order every tick. Canonical ordering:
240
240
  <% } else if (simulationType === 'physics3d') { -%>
241
241
  | [docs/08-physics3d.md](docs/08-physics3d.md) | Rapier 3D integration, character controller, animation |
242
242
  <% } -%>
243
+ <% if (simulationType === 'physics2d') { -%>
244
+ | [docs/11-2d-map-generation.md](docs/11-2d-map-generation.md) | Procedural 2D map generation, terrain rendering, object placement |
245
+ <% } -%>
243
246
  | [docs/09-recipes.md](docs/09-recipes.md) | Step-by-step cookbook for common game features |
244
247
  | [docs/10-common-mistakes.md](docs/10-common-mistakes.md) | "Never do X" reference + error solutions |
245
248
  | [docs/api-quick-reference.md](docs/api-quick-reference.md) | One-page API cheat sheet |
@@ -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
+ "@lagless/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",
@@ -1,7 +1,6 @@
1
1
  import { FC } from 'react';
2
2
  import { useRunner } from '../game-view/runner-provider';
3
3
  import { DebugPanel as SharedDebugPanel } from '@lagless/react';
4
- import { PlayerResource, DivergenceSignal } from '<%= packageName %>-simulation';
5
4
 
6
5
  export const DebugPanel: FC = () => {
7
6
  const runner = useRunner();
@@ -9,10 +8,6 @@ export const DebugPanel: FC = () => {
9
8
  return (
10
9
  <SharedDebugPanel
11
10
  runner={runner}
12
- hashVerification={{
13
- playerResourceClass: PlayerResource,
14
- divergenceSignalClass: DivergenceSignal,
15
- }}
16
11
  />
17
12
  );
18
13
  };
@@ -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} />;
@@ -2,18 +2,11 @@ import {
2
2
  <%= projectName %>Runner,
3
3
  <%= projectName %>Systems,
4
4
  <%= projectName %>Signals,
5
- DivergenceSignal,
6
5
  MoveInput,
7
6
  PlayerJoined,
8
- ReportHash,
9
7
  <%= projectName %>Arena,
10
- <% if (simulationType !== 'raw') { -%>
11
- PhysicsRefs,
12
- PhysicsRefsFilter,
13
- PlayerFilter,
14
- <% } -%>
15
8
  } from '<%= packageName %>-simulation';
16
- import { createContext, FC, ReactNode, useContext, useEffect, useState } from 'react';
9
+ import { createContext, FC, ReactNode, useContext, useEffect, useRef, useState } from 'react';
17
10
  import { useTick } from '@pixi/react';
18
11
  import { useNavigate } from 'react-router-dom';
19
12
  import { ProviderStore } from '../hooks/use-start-match';
@@ -21,7 +14,7 @@ import { ECSConfig, LocalInputProvider, RPC, createHashReporter } from '@lagless
21
14
  import { RelayInputProvider, RelayConnection } from '@lagless/relay-client';
22
15
  import { getMatchInfo } from '../hooks/use-start-multiplayer-match';
23
16
  import { UUID } from '@lagless/misc';
24
- import { useDevBridge } from '@lagless/react';
17
+ import { useDevBridge, useDiagnosticsControl } from '@lagless/react';
25
18
  <% if (simulationType === 'physics2d') { -%>
26
19
  import { PhysicsWorldManager2d, type RapierModule2d } from '@lagless/physics2d';
27
20
  <% } else if (simulationType === 'physics3d') { -%>
@@ -46,6 +39,9 @@ export const RunnerProvider: FC<RunnerProviderProps> = ({ children }) => {
46
39
  const [runner, setRunner] = useState<<%= projectName %>Runner>(null!);
47
40
  const [v, setV] = useState(0);
48
41
  const navigate = useNavigate();
42
+ const diagnosticsEnabled = useDiagnosticsControl();
43
+ const hashReporterRef = useRef<ReturnType<typeof createHashReporter> | null>(null);
44
+ const connectionRef = useRef<RelayConnection | null>(null);
49
45
 
50
46
  useEffect(() => {
51
47
  return ProviderStore.onProvider(() => {
@@ -80,7 +76,7 @@ export const RunnerProvider: FC<RunnerProviderProps> = ({ children }) => {
80
76
  <% if (simulationType === 'physics2d') { -%>
81
77
  // Load Rapier 2D WASM
82
78
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
83
- const RAPIER = (await import('@dimforge/rapier2d-compat')).default as any;
79
+ const RAPIER = (await import('@lagless/rapier2d-deterministic-compat')).default as any;
84
80
  await RAPIER.init();
85
81
  const rapier = RAPIER as unknown as RapierModule2d;
86
82
  if (disposed) { inputProvider.dispose(); return; }
@@ -88,7 +84,7 @@ export const RunnerProvider: FC<RunnerProviderProps> = ({ children }) => {
88
84
  <% } else if (simulationType === 'physics3d') { -%>
89
85
  // Load Rapier 3D WASM
90
86
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
91
- const RAPIER = (await import('@dimforge/rapier3d-compat')).default as any;
87
+ const RAPIER = (await import('@dimforge/rapier3d-deterministic-compat')).default as any;
92
88
  await RAPIER.init();
93
89
  const rapier = RAPIER as unknown as RapierModule3d;
94
90
  if (disposed) { inputProvider.dispose(); return; }
@@ -110,11 +106,13 @@ export const RunnerProvider: FC<RunnerProviderProps> = ({ children }) => {
110
106
  onPong: (data) => inputProvider.handlePong(data),
111
107
  onStateRequest: (requestId) => inputProvider.handleStateRequest(requestId),
112
108
  onStateResponse: (data) => inputProvider.handleStateResponse(data),
109
+ onHashMismatch: (data) => hashReporterRef.current?.reportMismatch(data),
113
110
  onConnected: () => console.log('[Relay] Connected'),
114
111
  onDisconnected: () => console.log('[Relay] Disconnected'),
115
112
  },
116
113
  );
117
114
 
115
+ connectionRef.current = _connection;
118
116
  inputProvider.setConnection(_connection);
119
117
  _connection.connect();
120
118
 
@@ -142,26 +140,6 @@ export const RunnerProvider: FC<RunnerProviderProps> = ({ children }) => {
142
140
  <% } -%>
143
141
  }
144
142
 
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
- // Set up keyboard input drainer with hash reporting
160
- const reportHash = createHashReporter(_runner, {
161
- reportInterval: <%= projectName %>Arena.hashReportInterval,
162
- reportHashRpc: ReportHash,
163
- });
164
-
165
143
  inputProvider.drainInputs((addRPC) => {
166
144
  let dx = 0;
167
145
  let dy = 0;
@@ -177,11 +155,8 @@ export const RunnerProvider: FC<RunnerProviderProps> = ({ children }) => {
177
155
  }
178
156
  addRPC(MoveInput, { directionX: dx, directionY: dy });
179
157
  }
180
-
181
- reportHash(addRPC);
182
158
  });
183
159
 
184
- _runner.Simulation.enableHashTracking(<%= projectName %>Arena.hashReportInterval);
185
160
  _runner.start();
186
161
 
187
162
  if (inputProvider instanceof RelayInputProvider) {
@@ -205,11 +180,6 @@ export const RunnerProvider: FC<RunnerProviderProps> = ({ children }) => {
205
180
  inputProvider.addRemoteRpc(joinRpc);
206
181
  }
207
182
 
208
- const divergenceSignal = _runner.DIContainer.resolve(DivergenceSignal);
209
- divergenceSignal.Predicted.subscribe((e) => {
210
- console.warn(`[DIVERGENCE] Players ${e.data.slotA} vs ${e.data.slotB}: hash ${e.data.hashA} != ${e.data.hashB} at tick ${e.data.atTick}`);
211
- });
212
-
213
183
  setRunner(_runner);
214
184
  })();
215
185
 
@@ -217,12 +187,36 @@ export const RunnerProvider: FC<RunnerProviderProps> = ({ children }) => {
217
187
  disposed = true;
218
188
  window.removeEventListener('keydown', onKeyDown);
219
189
  window.removeEventListener('keyup', onKeyUp);
190
+ connectionRef.current = null;
220
191
  _connection?.disconnect();
221
192
  _runner?.dispose();
222
193
  };
223
194
  }, [v, navigate]);
224
195
 
225
- useDevBridge(runner, { hashTrackingInterval: <%= projectName %>Arena.hashReportInterval });
196
+ // Diagnostics lifecycle: enable/disable hash tracking and hash reporter based on toggle
197
+ useEffect(() => {
198
+ if (!runner || !diagnosticsEnabled) {
199
+ runner?.Simulation.disableHashTracking();
200
+ hashReporterRef.current?.dispose();
201
+ hashReporterRef.current = null;
202
+ return;
203
+ }
204
+ runner.Simulation.enableHashTracking(<%= projectName %>Arena.hashReportInterval);
205
+ const reporter = createHashReporter(runner, {
206
+ reportInterval: <%= projectName %>Arena.hashReportInterval,
207
+ send: (data) => connectionRef.current?.sendHashReport(data),
208
+ });
209
+ reporter.subscribeDivergence((data) => {
210
+ console.warn(`[DIVERGENCE] Players ${data.slotA} vs ${data.slotB}: hash ${data.hashA} != ${data.hashB} at tick ${data.atTick}`);
211
+ });
212
+ hashReporterRef.current = reporter;
213
+ return () => {
214
+ reporter.dispose();
215
+ hashReporterRef.current = null;
216
+ };
217
+ }, [runner, diagnosticsEnabled]);
218
+
219
+ useDevBridge(runner, { hashTrackingInterval: <%= projectName %>Arena.hashReportInterval, diagnosticsEnabled });
226
220
 
227
221
  return !runner ? null : <RunnerContext.Provider value={runner}>{children}</RunnerContext.Provider>;
228
222
  };
@@ -24,9 +24,6 @@ playerResources:
24
24
  id: uint8[16]
25
25
  entity: uint32
26
26
  connected: uint8
27
- lastReportedHash: uint32
28
- lastReportedHashTick: uint32
29
- hashMismatchCount: uint16
30
27
 
31
28
  inputs:
32
29
  PlayerJoined:
@@ -38,9 +35,6 @@ inputs:
38
35
  MoveInput:
39
36
  directionX: float32
40
37
  directionY: float32
41
- ReportHash:
42
- hash: uint32
43
- atTick: uint32
44
38
 
45
39
  filters:
46
40
  PlayerFilter:
@@ -69,9 +63,6 @@ playerResources:
69
63
  id: uint8[16]
70
64
  entity: uint32
71
65
  connected: uint8
72
- lastReportedHash: uint32
73
- lastReportedHashTick: uint32
74
- hashMismatchCount: uint16
75
66
 
76
67
  inputs:
77
68
  PlayerJoined:
@@ -83,9 +74,6 @@ inputs:
83
74
  MoveInput:
84
75
  directionX: float32
85
76
  directionY: float32
86
- ReportHash:
87
- hash: uint32
88
- atTick: uint32
89
77
 
90
78
  filters:
91
79
  PlayerFilter:
@@ -112,9 +100,6 @@ playerResources:
112
100
  id: uint8[16]
113
101
  entity: uint32
114
102
  connected: uint8
115
- lastReportedHash: uint32
116
- lastReportedHashTick: uint32
117
- hashMismatchCount: uint16
118
103
 
119
104
  inputs:
120
105
  PlayerJoined:
@@ -126,9 +111,6 @@ inputs:
126
111
  MoveInput:
127
112
  directionX: float32
128
113
  directionY: float32
129
- ReportHash:
130
- hash: uint32
131
- atTick: uint32
132
114
 
133
115
  filters:
134
116
  PlayerFilter:
@@ -1,5 +1,3 @@
1
- import { ISignalConstructor, DivergenceSignal } from '@lagless/core';
1
+ import { ISignalConstructor } from '@lagless/core';
2
2
 
3
- export { DivergenceSignal, type DivergenceData } from '@lagless/core';
4
-
5
- export const <%= projectName %>Signals: ISignalConstructor[] = [DivergenceSignal];
3
+ export const <%= projectName %>Signals: ISignalConstructor[] = [];
@@ -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
  }
@@ -10,7 +10,6 @@ import { BoundarySystem } from './boundary.system.js';
10
10
  <% } else { -%>
11
11
  import { PhysicsStepSystem } from './physics-step.system.js';
12
12
  <% } -%>
13
- import { HashVerificationSystem } from './hash-verification.system.js';
14
13
 
15
14
  export const <%= projectName %>Systems: IECSSystemConstructor[] = [
16
15
  SavePrevTransformSystem,
@@ -24,5 +23,4 @@ export const <%= projectName %>Systems: IECSSystemConstructor[] = [
24
23
  <% } else { -%>
25
24
  PhysicsStepSystem,
26
25
  <% } -%>
27
- HashVerificationSystem,
28
26
  ];