@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 +15 -10
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/pixi-react/AGENTS.md +1 -2
- package/templates/pixi-react/CLAUDE.md +3 -0
- package/templates/pixi-react/__packageName__-frontend/package.json +3 -2
- package/templates/pixi-react/__packageName__-frontend/src/app/components/debug-panel.tsx +0 -5
- package/templates/pixi-react/__packageName__-frontend/src/app/game-view/grid-background.tsx +28 -3
- package/templates/pixi-react/__packageName__-frontend/src/app/game-view/runner-provider.tsx +34 -40
- package/templates/pixi-react/__packageName__-simulation/src/lib/schema/ecs.yaml +0 -18
- package/templates/pixi-react/__packageName__-simulation/src/lib/signals/index.ts +2 -4
- package/templates/pixi-react/__packageName__-simulation/src/lib/systems/apply-move-input.system.ts +9 -2
- package/templates/pixi-react/__packageName__-simulation/src/lib/systems/index.ts +0 -2
- package/templates/pixi-react/docs/08-physics2d.md +53 -39
- package/templates/pixi-react/docs/08-physics3d.md +60 -33
- package/templates/pixi-react/docs/09-recipes.md +60 -0
- package/templates/pixi-react/docs/10-common-mistakes.md +4 -9
- package/templates/pixi-react/docs/11-2d-map-generation.md +707 -0
- package/templates/pixi-react/__packageName__-simulation/src/lib/systems/hash-verification.system.ts +0 -17
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(
|
|
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
|
-
|
|
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
|
@@ -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
|
-
- [ ]
|
|
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
|
-
"@
|
|
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.
|
|
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
|
-
|
|
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('@
|
|
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
|
-
|
|
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
|
|
1
|
+
import { ISignalConstructor } from '@lagless/core';
|
|
2
2
|
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
export const <%= projectName %>Signals: ISignalConstructor[] = [DivergenceSignal];
|
|
3
|
+
export const <%= projectName %>Signals: ISignalConstructor[] = [];
|
package/templates/pixi-react/__packageName__-simulation/src/lib/systems/apply-move-input.system.ts
CHANGED
|
@@ -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
|
-
|
|
92
|
-
|
|
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
|
];
|