{"_id":"@alife-sdk/simulation","_rev":"6-e8d6f9009f3dead5f4b639314b2727dc","name":"@alife-sdk/simulation","dist-tags":{"latest":"0.4.1"},"versions":{"0.1.0":{"name":"@alife-sdk/simulation","version":"0.1.0","keywords":["alife","game-ai","simulation","npc","behavior","state-machine","goap"],"author":{"name":"Eugene Rusakov"},"license":"MIT","_id":"@alife-sdk/simulation@0.1.0","maintainers":[{"name":"eurusik","email":"john.rusakov@gmail.com"}],"homepage":"https://github.com/eurusik/alife-sdk#readme","bugs":{"url":"https://github.com/eurusik/alife-sdk/issues"},"dist":{"shasum":"78a0345dc31bb83beb66138ad2533cb8da68d1d2","tarball":"https://registry.npmjs.org/@alife-sdk/simulation/-/simulation-0.1.0.tgz","fileCount":159,"integrity":"sha512-tH/3SD6a8fOnaTBbPDUN/L5A38SpTTh5WQY7vfRi2ZR96fEfpCjS590+Z3W2YVc8/GH0V9ahRTWNOB1znsiEHQ==","signatures":[{"sig":"MEUCIAX3aW3SJvOHv0PyJLQwqM/ayfc3X/3Y6CvjS2wUIHoJAiEAvlmvDE66uAJu38w5/GwhSoJHNzoLqT4Ehp4QY/feIpo=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":461530},"main":"dist/index.js","type":"module","_from":"file:alife-sdk-simulation-0.1.0.tgz","types":"dist/index.d.ts","engines":{"node":">=20.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./npc":{"types":"./dist/npc/index.d.ts","import":"./dist/npc/index.js"},"./brain":{"types":"./dist/brain/index.d.ts","import":"./dist/brain/index.js"},"./ports":{"types":"./dist/ports/index.d.ts","import":"./dist/ports/index.js"},"./squad":{"types":"./dist/squad/index.d.ts","import":"./dist/squad/index.js"},"./surge":{"types":"./dist/surge/index.d.ts","import":"./dist/surge/index.js"},"./types":{"types":"./dist/types/index.d.ts","import":"./dist/types/index.js"},"./combat":{"types":"./dist/combat/index.d.ts","import":"./dist/combat/index.js"},"./plugin":{"types":"./dist/plugin/index.d.ts","import":"./dist/plugin/index.js"},"./terrain":{"types":"./dist/terrain/index.d.ts","import":"./dist/terrain/index.js"},"./movement":{"types":"./dist/movement/index.d.ts","import":"./dist/movement/index.js"}},"scripts":{"dev":"tsc --watch","test":"vitest run","build":"tsc","test:watch":"vitest"},"_npmUser":{"name":"eurusik","email":"john.rusakov@gmail.com"},"_resolved":"/private/var/folders/jv/cpf6x8mj1g11mppj8rpdhlmr0000gn/T/adaeb722473f53fca3835e0932987202/alife-sdk-simulation-0.1.0.tgz","_integrity":"sha512-tH/3SD6a8fOnaTBbPDUN/L5A38SpTTh5WQY7vfRi2ZR96fEfpCjS590+Z3W2YVc8/GH0V9ahRTWNOB1znsiEHQ==","repository":{"url":"git+https://github.com/eurusik/alife-sdk.git","type":"git","directory":"packages/alife-simulation"},"_npmVersion":"10.8.1","description":"Offline tick-based A-Life world simulation","directories":{},"sideEffects":false,"_nodeVersion":"20.16.0","dependencies":{"@alife-sdk/core":"0.1.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.0.18","typescript":"^5.9.3","@alife-sdk/social":"0.1.0","@alife-sdk/economy":"0.1.0","@alife-sdk/hazards":"0.1.0","@alife-sdk/persistence":"0.1.0"},"_npmOperationalInternal":{"tmp":"tmp/simulation_0.1.0_1772739947109_0.9022458522030898","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@alife-sdk/simulation","version":"0.1.1","keywords":["alife","game-ai","simulation","npc","behavior","state-machine","goap"],"author":{"name":"Eugene Rusakov"},"license":"MIT","_id":"@alife-sdk/simulation@0.1.1","maintainers":[{"name":"eurusik","email":"john.rusakov@gmail.com"}],"homepage":"https://github.com/eurusik/alife-sdk#readme","bugs":{"url":"https://github.com/eurusik/alife-sdk/issues"},"dist":{"shasum":"6ace645725e0979702c7fe1b067f837ba87dcc82","tarball":"https://registry.npmjs.org/@alife-sdk/simulation/-/simulation-0.1.1.tgz","fileCount":159,"integrity":"sha512-BYN1w6d74V/Ze6EWhDADRVCB203kWCTQLq5eQOZkECTr9T+7Tjr80ogJQuaF//ZvlwV6RcT5Uedx0x0Cs0+eTA==","signatures":[{"sig":"MEUCID2+4LAGio0W6fxFfQkvX3EnjDdhK2Nru+5jWmYKHAiBAiEAp4IcAnIVxuvYxcYTGcY1jtT0gH6Z5IwdOztlEhaKLJ0=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":473243},"main":"dist/index.js","type":"module","_from":"file:alife-sdk-simulation-0.1.1.tgz","types":"dist/index.d.ts","engines":{"node":">=20.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./npc":{"types":"./dist/npc/index.d.ts","import":"./dist/npc/index.js"},"./brain":{"types":"./dist/brain/index.d.ts","import":"./dist/brain/index.js"},"./ports":{"types":"./dist/ports/index.d.ts","import":"./dist/ports/index.js"},"./squad":{"types":"./dist/squad/index.d.ts","import":"./dist/squad/index.js"},"./surge":{"types":"./dist/surge/index.d.ts","import":"./dist/surge/index.js"},"./types":{"types":"./dist/types/index.d.ts","import":"./dist/types/index.js"},"./combat":{"types":"./dist/combat/index.d.ts","import":"./dist/combat/index.js"},"./plugin":{"types":"./dist/plugin/index.d.ts","import":"./dist/plugin/index.js"},"./terrain":{"types":"./dist/terrain/index.d.ts","import":"./dist/terrain/index.js"},"./movement":{"types":"./dist/movement/index.d.ts","import":"./dist/movement/index.js"}},"scripts":{"dev":"tsc --watch","test":"vitest run","build":"tsc","test:watch":"vitest"},"_npmUser":{"name":"eurusik","email":"john.rusakov@gmail.com"},"_resolved":"/tmp/91f59bb8cd43c547a961e45c91c442e3/alife-sdk-simulation-0.1.1.tgz","_integrity":"sha512-BYN1w6d74V/Ze6EWhDADRVCB203kWCTQLq5eQOZkECTr9T+7Tjr80ogJQuaF//ZvlwV6RcT5Uedx0x0Cs0+eTA==","repository":{"url":"git+https://github.com/eurusik/alife-sdk.git","type":"git","directory":"packages/alife-simulation"},"_npmVersion":"10.8.2","description":"Offline tick-based A-Life world simulation","directories":{},"sideEffects":false,"_nodeVersion":"20.20.0","dependencies":{"@alife-sdk/core":"0.1.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.0.18","typescript":"^5.9.3","@alife-sdk/social":"0.1.1","@alife-sdk/economy":"0.1.1","@alife-sdk/hazards":"0.1.1","@alife-sdk/persistence":"0.1.1"},"_npmOperationalInternal":{"tmp":"tmp/simulation_0.1.1_1772742736084_0.5829605366802717","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@alife-sdk/simulation","version":"0.2.0","keywords":["alife","game-ai","simulation","npc","behavior","state-machine","goap"],"author":{"name":"Eugene Rusakov"},"license":"MIT","_id":"@alife-sdk/simulation@0.2.0","maintainers":[{"name":"eurusik","email":"john.rusakov@gmail.com"}],"homepage":"https://github.com/eurusik/alife-sdk#readme","bugs":{"url":"https://github.com/eurusik/alife-sdk/issues"},"dist":{"shasum":"44202aa5f26f63254c0c20b947951db44a50cbc5","tarball":"https://registry.npmjs.org/@alife-sdk/simulation/-/simulation-0.2.0.tgz","fileCount":159,"integrity":"sha512-7ZqCxiBxgGJ7EMEBDvQBJPHThvgX2fFbmZvqiFP7Rm/6uit5IZP/nfKPHpU8gaEk0rub6FetVfqcqhJiQrrhrw==","signatures":[{"sig":"MEYCIQDlFi9RXvsYOQ0+0o+a3WVl6m2YoYlRTdcZYlIknNYewQIhAJonrgqCv4KSXnK5HhSfZCdOnA6mXG/TZIrNgMh5cpJp","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":473798},"main":"dist/index.js","type":"module","_from":"file:alife-sdk-simulation-0.2.0.tgz","types":"dist/index.d.ts","engines":{"node":">=20.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./npc":{"types":"./dist/npc/index.d.ts","import":"./dist/npc/index.js"},"./brain":{"types":"./dist/brain/index.d.ts","import":"./dist/brain/index.js"},"./ports":{"types":"./dist/ports/index.d.ts","import":"./dist/ports/index.js"},"./squad":{"types":"./dist/squad/index.d.ts","import":"./dist/squad/index.js"},"./surge":{"types":"./dist/surge/index.d.ts","import":"./dist/surge/index.js"},"./types":{"types":"./dist/types/index.d.ts","import":"./dist/types/index.js"},"./combat":{"types":"./dist/combat/index.d.ts","import":"./dist/combat/index.js"},"./plugin":{"types":"./dist/plugin/index.d.ts","import":"./dist/plugin/index.js"},"./terrain":{"types":"./dist/terrain/index.d.ts","import":"./dist/terrain/index.js"},"./movement":{"types":"./dist/movement/index.d.ts","import":"./dist/movement/index.js"}},"scripts":{"dev":"tsc --watch","test":"vitest run","build":"tsc","test:watch":"vitest"},"_npmUser":{"name":"eurusik","email":"john.rusakov@gmail.com"},"_resolved":"/tmp/4e8507abbb7421aff0dbbeb11921c8e6/alife-sdk-simulation-0.2.0.tgz","_integrity":"sha512-7ZqCxiBxgGJ7EMEBDvQBJPHThvgX2fFbmZvqiFP7Rm/6uit5IZP/nfKPHpU8gaEk0rub6FetVfqcqhJiQrrhrw==","repository":{"url":"git+https://github.com/eurusik/alife-sdk.git","type":"git","directory":"packages/alife-simulation"},"_npmVersion":"10.8.2","description":"Offline tick-based A-Life world simulation","directories":{},"sideEffects":false,"_nodeVersion":"20.20.0","dependencies":{"@alife-sdk/core":"0.1.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.0.18","typescript":"^5.9.3","@alife-sdk/social":"0.1.1","@alife-sdk/economy":"0.1.1","@alife-sdk/hazards":"0.1.1","@alife-sdk/persistence":"0.1.1"},"_npmOperationalInternal":{"tmp":"tmp/simulation_0.2.0_1772774311435_0.1983996075592369","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@alife-sdk/simulation","version":"0.3.0","keywords":["alife","game-ai","simulation","npc","behavior","state-machine","goap"],"author":{"name":"Eugene Rusakov"},"license":"MIT","_id":"@alife-sdk/simulation@0.3.0","maintainers":[{"name":"eurusik","email":"john.rusakov@gmail.com"}],"homepage":"https://github.com/eurusik/alife-sdk#readme","bugs":{"url":"https://github.com/eurusik/alife-sdk/issues"},"dist":{"shasum":"b5178f24a5d16b5dfe6b79c20e9bedbe287d8e22","tarball":"https://registry.npmjs.org/@alife-sdk/simulation/-/simulation-0.3.0.tgz","fileCount":159,"integrity":"sha512-mQeaBqtowlnwdJVNJfa/B2Qyox9AWtuEmu+v1mdOmUiwvz/fSkDFA0BeXcsvdVMSue5nApSHNa18s7uwD0HQpw==","signatures":[{"sig":"MEUCIFZIze+sgCqD/AEVP14w8wnWa5gIwXxb/lplB6z7Gu7VAiEA4sBdXZM4vfSeyMyUUvlB2at9AJddhUVaFOkQ3IGbW58=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":473798},"main":"dist/index.js","type":"module","_from":"file:alife-sdk-simulation-0.3.0.tgz","types":"dist/index.d.ts","engines":{"node":">=20.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./npc":{"types":"./dist/npc/index.d.ts","import":"./dist/npc/index.js"},"./brain":{"types":"./dist/brain/index.d.ts","import":"./dist/brain/index.js"},"./ports":{"types":"./dist/ports/index.d.ts","import":"./dist/ports/index.js"},"./squad":{"types":"./dist/squad/index.d.ts","import":"./dist/squad/index.js"},"./surge":{"types":"./dist/surge/index.d.ts","import":"./dist/surge/index.js"},"./types":{"types":"./dist/types/index.d.ts","import":"./dist/types/index.js"},"./combat":{"types":"./dist/combat/index.d.ts","import":"./dist/combat/index.js"},"./plugin":{"types":"./dist/plugin/index.d.ts","import":"./dist/plugin/index.js"},"./terrain":{"types":"./dist/terrain/index.d.ts","import":"./dist/terrain/index.js"},"./movement":{"types":"./dist/movement/index.d.ts","import":"./dist/movement/index.js"}},"scripts":{"dev":"tsc --watch","test":"vitest run","build":"tsc","test:watch":"vitest"},"_npmUser":{"name":"eurusik","email":"john.rusakov@gmail.com"},"_resolved":"/tmp/412ecb9dadc00e58165a4a02b1b1ac7b/alife-sdk-simulation-0.3.0.tgz","_integrity":"sha512-mQeaBqtowlnwdJVNJfa/B2Qyox9AWtuEmu+v1mdOmUiwvz/fSkDFA0BeXcsvdVMSue5nApSHNa18s7uwD0HQpw==","repository":{"url":"git+https://github.com/eurusik/alife-sdk.git","type":"git","directory":"packages/alife-simulation"},"_npmVersion":"10.8.2","description":"Offline tick-based A-Life world simulation","directories":{},"sideEffects":false,"_nodeVersion":"20.20.0","dependencies":{"@alife-sdk/core":"0.3.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.0.18","typescript":"^5.9.3","@alife-sdk/social":"0.3.0","@alife-sdk/economy":"0.3.0","@alife-sdk/hazards":"0.3.0","@alife-sdk/persistence":"0.3.0"},"_npmOperationalInternal":{"tmp":"tmp/simulation_0.3.0_1772789682731_0.8382196937829167","host":"s3://npm-registry-packages-npm-production"}},"0.4.0":{"name":"@alife-sdk/simulation","version":"0.4.0","keywords":["alife","game-ai","simulation","npc","behavior","state-machine","goap"],"author":{"name":"Eugene Rusakov"},"license":"MIT","_id":"@alife-sdk/simulation@0.4.0","maintainers":[{"name":"eurusik","email":"john.rusakov@gmail.com"}],"homepage":"https://github.com/eurusik/alife-sdk#readme","bugs":{"url":"https://github.com/eurusik/alife-sdk/issues"},"dist":{"shasum":"56c212561152f6478505825b75084f2b335ef2ab","tarball":"https://registry.npmjs.org/@alife-sdk/simulation/-/simulation-0.4.0.tgz","fileCount":159,"integrity":"sha512-WIIed4jpeUCsJJWqpD+7BVgMCXdVxnXG5N4dlMFZwUUCYRZD8Kl8idGIrHnloNJATMteYIYaooNsQXb2/OGPAA==","signatures":[{"sig":"MEYCIQD06oaqtGJGGfzL+9iCdjzqqDY9iY45a9WMwcb8qOlDRgIhAMfyYIKz5ER0z0cCVUI+AnWbMeTXp2vsSdaDp3IteADx","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":475145},"main":"dist/index.js","type":"module","_from":"file:alife-sdk-simulation-0.4.0.tgz","types":"dist/index.d.ts","engines":{"node":">=20.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./npc":{"types":"./dist/npc/index.d.ts","import":"./dist/npc/index.js"},"./brain":{"types":"./dist/brain/index.d.ts","import":"./dist/brain/index.js"},"./ports":{"types":"./dist/ports/index.d.ts","import":"./dist/ports/index.js"},"./squad":{"types":"./dist/squad/index.d.ts","import":"./dist/squad/index.js"},"./surge":{"types":"./dist/surge/index.d.ts","import":"./dist/surge/index.js"},"./types":{"types":"./dist/types/index.d.ts","import":"./dist/types/index.js"},"./combat":{"types":"./dist/combat/index.d.ts","import":"./dist/combat/index.js"},"./plugin":{"types":"./dist/plugin/index.d.ts","import":"./dist/plugin/index.js"},"./terrain":{"types":"./dist/terrain/index.d.ts","import":"./dist/terrain/index.js"},"./movement":{"types":"./dist/movement/index.d.ts","import":"./dist/movement/index.js"}},"scripts":{"dev":"tsc --watch","test":"vitest run","build":"tsc","test:watch":"vitest"},"_npmUser":{"name":"eurusik","email":"john.rusakov@gmail.com"},"_resolved":"/tmp/f413ce8aee30b818f9a91ee106c1e9ee/alife-sdk-simulation-0.4.0.tgz","_integrity":"sha512-WIIed4jpeUCsJJWqpD+7BVgMCXdVxnXG5N4dlMFZwUUCYRZD8Kl8idGIrHnloNJATMteYIYaooNsQXb2/OGPAA==","repository":{"url":"git+https://github.com/eurusik/alife-sdk.git","type":"git","directory":"packages/alife-simulation"},"_npmVersion":"10.8.2","description":"Offline tick-based A-Life world simulation","directories":{},"sideEffects":false,"_nodeVersion":"20.20.0","dependencies":{"@alife-sdk/core":"0.4.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.0.18","typescript":"^5.9.3","@alife-sdk/social":"0.4.0","@alife-sdk/economy":"0.4.0","@alife-sdk/hazards":"0.4.0","@alife-sdk/persistence":"0.4.0"},"_npmOperationalInternal":{"tmp":"tmp/simulation_0.4.0_1772823546776_0.15310168572795435","host":"s3://npm-registry-packages-npm-production"}},"0.4.1":{"name":"@alife-sdk/simulation","version":"0.4.1","description":"Offline tick-based A-Life world simulation","type":"module","sideEffects":false,"main":"dist/index.js","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./types":{"types":"./dist/types/index.d.ts","import":"./dist/types/index.js"},"./ports":{"types":"./dist/ports/index.d.ts","import":"./dist/ports/index.js"},"./terrain":{"types":"./dist/terrain/index.d.ts","import":"./dist/terrain/index.js"},"./npc":{"types":"./dist/npc/index.d.ts","import":"./dist/npc/index.js"},"./brain":{"types":"./dist/brain/index.d.ts","import":"./dist/brain/index.js"},"./combat":{"types":"./dist/combat/index.d.ts","import":"./dist/combat/index.js"},"./surge":{"types":"./dist/surge/index.d.ts","import":"./dist/surge/index.js"},"./squad":{"types":"./dist/squad/index.d.ts","import":"./dist/squad/index.js"},"./movement":{"types":"./dist/movement/index.d.ts","import":"./dist/movement/index.js"},"./plugin":{"types":"./dist/plugin/index.d.ts","import":"./dist/plugin/index.js"}},"keywords":["alife","game-ai","simulation","npc","behavior","state-machine","goap"],"license":"MIT","author":{"name":"Eugene Rusakov"},"repository":{"type":"git","url":"git+https://github.com/eurusik/alife-sdk.git","directory":"packages/alife-simulation"},"homepage":"https://github.com/eurusik/alife-sdk#readme","bugs":{"url":"https://github.com/eurusik/alife-sdk/issues"},"engines":{"node":">=20.0.0"},"publishConfig":{"access":"public"},"dependencies":{"@alife-sdk/core":"0.4.1"},"devDependencies":{"typescript":"^5.9.3","vitest":"^4.0.18","@alife-sdk/economy":"0.4.1","@alife-sdk/hazards":"0.4.1","@alife-sdk/persistence":"0.4.1","@alife-sdk/social":"0.4.1"},"scripts":{"build":"tsc","dev":"tsc --watch","test":"vitest run","test:watch":"vitest"},"_id":"@alife-sdk/simulation@0.4.1","_integrity":"sha512-kv7e8iO05WCwg9eXUfVxGuQ9GkjDONvqdJqHKgEAJAOZsYgEYk66uO7M3sA39oWFvRLXn3MxINuKv+zSzwPUFw==","_resolved":"/tmp/abde849bd079ae72cff6e2f8a1597db2/alife-sdk-simulation-0.4.1.tgz","_from":"file:alife-sdk-simulation-0.4.1.tgz","_nodeVersion":"20.20.1","_npmVersion":"10.8.2","dist":{"integrity":"sha512-kv7e8iO05WCwg9eXUfVxGuQ9GkjDONvqdJqHKgEAJAOZsYgEYk66uO7M3sA39oWFvRLXn3MxINuKv+zSzwPUFw==","shasum":"070c85905fcff6219ba77e765d2f473ca22e975b","tarball":"https://registry.npmjs.org/@alife-sdk/simulation/-/simulation-0.4.1.tgz","fileCount":159,"unpackedSize":476758,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDYRDOcwRJR6FzXnoUqYLnHvkQRlnxyV++8KE+MAAcOtwIgEzXUe9iBd1B5RXzRBu+8D5ru57Al22qV4m/rIobGnaU="}]},"_npmUser":{"name":"eurusik","email":"john.rusakov@gmail.com"},"directories":{},"maintainers":[{"name":"eurusik","email":"john.rusakov@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/simulation_0.4.1_1773837144854_0.6376439787855117"},"_hasShrinkwrap":false}},"time":{"created":"2026-03-05T19:45:46.996Z","modified":"2026-03-18T12:32:25.143Z","0.1.0":"2026-03-05T19:45:47.332Z","0.1.1":"2026-03-05T20:32:16.236Z","0.2.0":"2026-03-06T05:18:31.592Z","0.3.0":"2026-03-06T09:34:42.882Z","0.4.0":"2026-03-06T18:59:06.988Z","0.4.1":"2026-03-18T12:32:25.026Z"},"bugs":{"url":"https://github.com/eurusik/alife-sdk/issues"},"author":{"name":"Eugene Rusakov"},"license":"MIT","homepage":"https://github.com/eurusik/alife-sdk#readme","keywords":["alife","game-ai","simulation","npc","behavior","state-machine","goap"],"repository":{"type":"git","url":"git+https://github.com/eurusik/alife-sdk.git","directory":"packages/alife-simulation"},"description":"Offline tick-based A-Life world simulation","maintainers":[{"name":"eurusik","email":"john.rusakov@gmail.com"}],"readme":"# @alife-sdk/simulation\n\nOffline tick-based A-Life world simulation — NPC brains, terrain management,\nsquad grouping, probabilistic combat, and zone-wide surge events.\nFramework-free: all engine operations go through injected ports.\n\n```\nnpm install @alife-sdk/simulation\n```\n\n---\n\n## Quick start\n\n### `createInMemoryKernel` — no adapters needed\n\n> **Note:** `createInMemoryKernel` uses no-op adapters — entities always stay\n> alive (`isAlive` returns `true`), damage is ignored (`applyDamage` returns\n> `false`, effective damage is `0`). Suitable for testing and CLI tools.\n> For production use, wire a real `ISimulationBridge` (see Full wiring below).\n\n```ts\nimport { createInMemoryKernel } from '@alife-sdk/simulation';\nimport { FactionBuilder, SmartTerrain } from '@alife-sdk/core';\n\n// Kernel is already init()'d and start()'d — no port wiring required.\nconst { kernel, sim, factions } = createInMemoryKernel({ tickIntervalMs: 5_000 });\n\nfactions.factions.register('stalker', new FactionBuilder('stalker').build());\nsim.addTerrain(new SmartTerrain({ id: 'camp', name: 'Camp',\n  bounds: { x: 0, y: 0, width: 200, height: 200 }, capacity: 6, jobs: [] }));\nsim.registerNPC({ entityId: 'wolf', factionId: 'stalker',\n  position: { x: 50, y: 50 }, rank: 2, combatPower: 50, currentHp: 100,\n  options: { type: 'human' } });\n\nkernel.events.on('alife:tick', ({ tick }) => console.log('tick', tick));\nkernel.update(5_001); // advance one tick\nkernel.destroy();\n```\n\n### Full wiring (production / real engine)\n\n```ts\nimport { ALifeKernel, FactionsPlugin } from '@alife-sdk/core';\nimport { SimulationPlugin, createDefaultPluginConfig } from '@alife-sdk/simulation/plugin';\nimport { createDefaultBehaviorConfig } from '@alife-sdk/simulation';\nimport { SimulationPorts } from '@alife-sdk/simulation/ports';\n\n// 1. Build the kernel\nconst kernel = new ALifeKernel();\n\n// 2. Register your engine bridge (required port)\nkernel.provide(SimulationPorts.SimulationBridge, {\n  isAlive:            (id) => entities.get(id)?.health > 0,\n  applyDamage:        (id, dmg, type) => entities.get(id)?.takeDamage(dmg, type) ?? false,\n  getEffectiveDamage: (id, dmg, type) => dmg * (immunities.get(id)?.[type] ?? 1),\n  adjustMorale:       (id, delta) => { entities.get(id)?.morale.adjust(delta); },\n});\n\n// 3. Register required plugins\nkernel.use(new FactionsPlugin({ factions: myFactionDefs }));\nkernel.use(new SimulationPlugin({\n  tickIntervalMs: 5_000,\n  simulation: {\n    brain: { moraleFleeThreshold: -0.6 },\n  },\n}));\n\n// 4. Add terrains before or after init\nconst sim = kernel.getPlugin<SimulationPlugin>('simulation');\nsim.addTerrain(mySmartTerrain);\n\n// 5. Initialize\nkernel.init();\n\n// 6. Register NPCs\nsim.registerNPC({\n  entityId:       'npc_soldier_1',\n  factionId:      'military',\n  combatPower:    60,\n  currentHp:      100,\n  rank:           3,\n  position:       { x: 400, y: 300 },\n  behaviorConfig: createDefaultBehaviorConfig({ retreatThreshold: 0.2 }),\n  options:        { type: 'human' },\n});\n\n// 7. Drive the simulation — call every frame\nfunction gameLoop(deltaMs: number) {\n  sim.setNPCOnline('npc_soldier_1', playerIsNear);\n  kernel.update(deltaMs);\n}\n```\n\n---\n\n## Sub-paths\n\n| Import path | What it contains |\n|-------------|-----------------|\n| `@alife-sdk/simulation/plugin` | `SimulationPlugin` — kernel entry point, owns all state |\n| `@alife-sdk/simulation/types` | `INPCRecord`, `ISimulationConfig` and 7 sub-configs |\n| `@alife-sdk/simulation/ports` | `ISimulationBridge`, `SimulationPorts` token |\n| `@alife-sdk/simulation/brain` | `NPCBrain`, `HumanBrain`, `MonsterBrain`, `BrainScheduleManager` |\n| `@alife-sdk/simulation/terrain` | `TerrainStateManager`, `TerrainSelector`, `JobSlotSystem`, `resolveScheme` |\n| `@alife-sdk/simulation/npc` | `NPCRegistrar`, `StoryRegistry`, `Schedule`, `NPCRelationRegistry` |\n| `@alife-sdk/simulation/movement` | `MovementSimulator`, `GraphMovementSimulator` |\n| `@alife-sdk/simulation/combat` | `OfflineCombatResolver` |\n| `@alife-sdk/simulation/squad` | `Squad`, `SquadManager` |\n| `@alife-sdk/simulation/surge` | `SurgeManager`, `SurgePhase` |\n\n---\n\n## Architecture\n\n```\n┌────────────────────────────────────────────────────────────┐\n│                      SimulationPlugin                      │\n│  (IALifePlugin — registered in ALifeKernel)                │\n│                                                            │\n│  7-step tick pipeline (every tickIntervalMs)               │\n│  ┌──────────┬──────────┬──────────┬──────────┬──────────┐  │\n│  │ Terrain  │ Brains   │ Movement │ Combat   │ Morale   │  │\n│  │ states   │ round-   │simulator │ resolver │ restore  │  │\n│  │ decay    │ robin    │ update   │(offline) │ + decay  │  │\n│  │          │          │          │SKIPPED   │          │  │\n│  │          │          │          │during    │          │  │\n│  │          │          │          │ACTIVE    │          │  │\n│  │          │          │          │surge     │          │  │\n│  └──────────┴──────────┴──────────┴──────────┴──────────┘  │\n│                                                            │\n│  Every frame (smooth):                                     │\n│  ┌──────────────────────────┬───────────────────────────┐  │\n│  │  SurgeManager.update()   │  Morale panic eval        │  │\n│  └──────────────────────────┴───────────────────────────┘  │\n└────────────────────────────────────────────────────────────┘\n          │ ports                        │ events\n          ▼                              ▼\n┌────────────────────────┐  ┌────────────────────────┐\n│  ISimulationBridge     │  │  EventBus<ALifeEvents>  │\n│    (host owns)         │  │    TICK, NPC_MOVED      │\n│    isAlive             │  │    FACTION_CONFLICT     │\n│    applyDamage         │  │    NPC_PANICKED         │\n│    adjustMorale        │  │    SURGE_*              │\n│    getEffectiveDamage  │  │    SQUAD_*              │\n└────────────────────────┘  │    TERRAIN_STATE_*      │\n                            └────────────────────────┘\n```\n\n---\n\n## Key concepts\n\n### Online / offline split\n\nThe SDK ticks **only offline NPCs** (`isOnline === false`). When a player\napproaches an NPC, the host switches it online and takes over with its own\nphysics and combat system. The SDK never reads camera or viewport data.\n\n```ts\nsim.setNPCOnline(npcId, true);   // host engine drives this NPC now\nsim.setNPCOnline(npcId, false);  // SDK tick pipeline resumes\n```\n\n**Concrete sync workflow:**\n\n```ts\n// NPC enters render range — sync offline brain state to live entity, then\n// hand control to the host engine.\nconst brain = sim.getNPCBrain(npcId);\nif (brain) {\n  myEntity.morale = brain.morale; // read authoritative morale from the brain\n}\nsim.setNPCOnline(npcId, true);    // SDK stops ticking this NPC\n\n// NPC leaves render range — sync live position back to the record, then\n// return control to the SDK.\nconst record = sim.getNPCRecord(npcId);\nif (record) {\n  record.lastPosition = myEntity.position; // write current world position\n}\nsim.setNPCOnline(npcId, false);   // SDK tick pipeline resumes\n```\n\n### Brain hierarchy\n\n```\nNPCBrain           — 11-step update, terrain selection, morale, movement dispatch\n  HumanBrain       — equipment scoring bonuses, money management\n  MonsterBrain     — lair affinity +1000, danger preference, no schedule/surge flee\n```\n\nOverride `selectBestTerrain()`, `buildJobContext()`, or `buildTerrainQuery()`\nto customise selection logic without modifying the brain update loop.\n\nSee [brain/README.md](src/brain/README.md) for detailed brain extension examples.\n\n### Terrain threat FSM\n\nEach `SmartTerrain` gets a `TerrainStateManager` that escalates threat level\nwhen hostile NPCs share the terrain:\n\n```\nPEACEFUL → ALERT → COMBAT  (escalate on detection)\nCOMBAT → ALERT → PEACEFUL  (decay after combatDecayMs / alertDecayMs)\n```\n\nBrains read the threat level to select behavior schemes (patrol → guard → combat_patrol).\n\n### Surge lifecycle\n\n```\nINACTIVE → WARNING → ACTIVE → AFTERMATH → INACTIVE\n```\n\nDuring **ACTIVE**: every unsheltered NPC takes PSI damage every\n`damageTickIntervalMs`. During **AFTERMATH**: all spawn cooldowns reset\n(mass repopulation wave) and survivors get a morale bonus.\n\n### Story NPC protection\n\nNPCs registered in `StoryRegistry` are immune to offline combat death and\nredundancy cleanup — quest NPCs always survive.\n\n```ts\nsim.getStoryRegistry().register('main_quest_act1', npcId);\n```\n\n---\n\n## Serialisation\n\n```ts\n// Save — JSON-serialisable snapshot\nconst state = sim.serialize();\n\n// Load\nsim.restore(state);\n\n// After restore, rebuild brain instances. Two options:\n\n// Option 1: registerNPC() — full re-registration.\n// Rebuilds the brain AND re-runs squad assignment, relation tracking, and\n// story registry wiring. Use this when the NPC record may have changed\n// (e.g. faction swap, rank change) or when you are loading a fresh scene.\nfor (const record of sim.getAllNPCRecords().values()) {\n  sim.registerNPC({ entityId: record.entityId, factionId: record.factionId, ... });\n}\n\n// Option 2: rebuildBrain(npcId) — faster, brain-only rebuild.\n// Recreates the brain instance from the existing NPC record without touching\n// squads, relations, or the story registry. Preserves the restored state\n// exactly as serialized. Preferred after a save/load round-trip.\nfor (const record of sim.getAllNPCRecords().values()) {\n  sim.rebuildBrain(record.entityId);\n}\n```\n\nBrains cannot be serialised (they hold terrain references and a movement\ndispatcher). The restore contract requires the caller to rebuild brain instances\nvia one of the two options above.\n\n---\n\n## Testing\n\nThe package has **793 tests** (vitest). Run them:\n\n```\npnpm --filter @alife-sdk/simulation test\n```\n\nAll subsystems are pure — no kernel needed for unit tests. Brains can be\ninstantiated directly and bridges can be mocked:\n\n```ts\nimport { HumanBrain } from '@alife-sdk/simulation/brain';\n\n// Instantiate a brain directly — no kernel required\nconst brain = new HumanBrain({ /* config */ });\n\n// Mock ISimulationBridge for unit tests\nconst mockBridge = {\n  isAlive:            (id: string) => true,\n  applyDamage:        (id: string, dmg: number, type: string) => true,\n  getEffectiveDamage: (id: string, dmg: number, type: string) => dmg,\n  adjustMorale:       (id: string, delta: number) => {},\n};\n```\n\n---\n\n## Events emitted\n\nAll events flow through `kernel.events` (an `EventBus<ALifeEventPayloads>`).\nSubscribe with:\n\n```ts\nkernel.events.on('alife:tick', ({ tick, delta }) => { /* ... */ });\n```\n\nThe table below lists every event the simulation package emits. Events from\nother packages (AI perception, anomaly, social, monster) are not included.\n\n| Event | String key | Payload | When |\n|-------|-----------|---------|------|\n| `TICK` | `'alife:tick'` | `{ tick: number; delta: number }` | End of every tick pipeline execution (every `tickIntervalMs`) |\n| `NPC_MOVED` | `'alife:npc_moved'` | `{ npcId: string; fromZone: string; toZone: string }` | NPC moves between terrain zones (movement simulator) |\n| `NPC_DIED` | `'alife:npc_died'` | `{ npcId: string; killedBy: string; zoneId: string }` | NPC HP reaches zero (offline combat or surge damage) |\n| `NPC_RELEASED` | `'alife:npc_released'` | `{ npcId: string; terrainId: string }` | NPC leaves a terrain/job slot |\n| `TASK_ASSIGNED` | `'alife:task_assigned'` | `{ npcId: string; terrainId: string; taskType: string }` | Brain assigns NPC to a terrain job |\n| `FACTION_CONFLICT` | `'alife:faction_conflict'` | `{ factionA: string; factionB: string; zoneId: string }` | Two hostile factions share a terrain zone (once per tick per pair) |\n| `NPC_PANICKED` | `'ai:npc_panicked'` | `{ npcId: string; squadId: string \\| null }` | NPC morale drops below `panicThreshold` |\n| `TERRAIN_STATE_CHANGED` | `'alife:terrain_state_changed'` | `{ terrainId: string; oldState: number; newState: number }` | Terrain FSM transitions (PEACEFUL↔ALERT↔COMBAT) |\n| `SURGE_WARNING` | `'surge:warning'` | `{ timeUntilSurge: number }` | Surge enters WARNING phase |\n| `SURGE_STARTED` | `'surge:started'` | `{ surgeNumber: number }` | Surge enters ACTIVE phase |\n| `SURGE_ENDED` | `'surge:ended'` | `{ surgeNumber: number }` | Surge enters AFTERMATH phase |\n| `SURGE_DAMAGE` | `'surge:damage'` | `{ npcId: string; damage: number }` | PSI damage tick applied to an unsheltered NPC |\n| `SQUAD_FORMED` | `'squad:formed'` | `{ squadId: string; factionId: string; memberIds: string[] }` | New squad created from faction members |\n| `SQUAD_DISBANDED` | `'squad:disbanded'` | `{ squadId: string }` | Squad dissolved (leader died or last member left) |\n| `SQUAD_MEMBER_ADDED` | `'squad:member_added'` | `{ squadId: string; npcId: string }` | NPC joined an existing squad |\n| `SQUAD_MEMBER_REMOVED` | `'squad:member_removed'` | `{ squadId: string; npcId: string }` | NPC left or was removed from a squad |\n| `SQUAD_GOAL_SET` | `'squad:goal_set'` | `{ squadId: string; goalType: string; terrainId: string \\| null; priority: number }` | Squad receives a new movement/combat goal |\n| `SQUAD_GOAL_CLEARED` | `'squad:goal_cleared'` | `{ squadId: string; previousGoalType: string }` | Squad goal removed or completed |\n\n---\n\n## Porting to your engine\n\nThe simulation package is engine-agnostic. The only integration point is\n`ISimulationBridge` — a four-method interface your engine implements once.\n\n### Step 1 — Implement ISimulationBridge\n\n```ts\nimport type { ISimulationBridge } from '@alife-sdk/simulation/ports';\n\nclass MyEngineBridge implements ISimulationBridge {\n  isAlive(entityId: string): boolean {\n    // Return false when the entity has been destroyed or HP ≤ 0\n    return this.entityRegistry.get(entityId)?.health.isAlive ?? false;\n  }\n\n  applyDamage(entityId: string, amount: number, damageTypeId: string): boolean {\n    const entity = this.entityRegistry.get(entityId);\n    if (!entity) return false;\n    const effective = this.immunitySystem.reduce(entity, amount, damageTypeId);\n    entity.health.applyDamage(effective);\n    return !entity.health.isAlive; // true = entity just died\n  }\n\n  getEffectiveDamage(entityId: string, rawDamage: number, damageTypeId: string): number {\n    // Apply immunity/resistance multipliers WITHOUT mutating HP\n    const entity = this.entityRegistry.get(entityId);\n    return entity ? this.immunitySystem.reduce(entity, rawDamage, damageTypeId) : 0;\n  }\n\n  adjustMorale(entityId: string, delta: number, _reason: string): void {\n    // Write the delta to your morale component — the brain is authoritative\n    // while offline; sync back to brain.morale when the NPC goes online.\n    this.entityRegistry.get(entityId)?.alife.adjustMorale(delta);\n  }\n}\n```\n\nThe `damageTypeId` values used by the simulation are `'physical'` (offline\ncombat) and `'psi'` (surge damage) unless overridden in config.\n\n### Step 2 — Register the bridge before kernel.init()\n\n```ts\nimport { SimulationPorts } from '@alife-sdk/simulation/ports';\n\nkernel.provide(\n  SimulationPorts.SimulationBridge,\n  new MyEngineBridge(entityRegistry, immunitySystem),\n);\n// kernel.init() validates all required ports — missing bridge throws immediately.\nkernel.init();\n```\n\n### Step 3 — Sync brain state when an NPC goes online\n\nWhen a player enters render range the SDK stops ticking that NPC. Read the\nauthoritative morale from the brain before handing off to the host engine:\n\n```ts\nconst brain = sim.getNPCBrain(npcId);\nif (brain) {\n  myEntity.morale = brain.morale;\n  myEntity.position = brain.lastPosition ?? myEntity.position;\n}\nsim.setNPCOnline(npcId, true); // SDK tick pipeline skips this NPC\n```\n\nWhen the NPC leaves render range, write the current position back before\nreturning control to the SDK:\n\n```ts\nconst record = sim.getNPCRecord(npcId);\nif (record) {\n  record.lastPosition = myEntity.position;\n}\nsim.setNPCOnline(npcId, false); // SDK tick pipeline resumes\n```\n\n### Step 4 — Call kernel.update(deltaMs) in your game loop\n\nThe kernel's `update` call drives surge (every frame) and the tick pipeline\n(gated by `tickIntervalMs`):\n\n```ts\nfunction gameLoop(deltaMs: number) {\n  // Toggle online/offline before update so the current tick sees the right state\n  sim.setNPCOnline('npc_soldier_1', playerIsNear('npc_soldier_1'));\n  kernel.update(deltaMs);\n}\n```\n\n### Step 5 — Handle events\n\nReact to simulation outcomes without polling:\n\n```ts\n// Spawn a death effect\nkernel.events.on('alife:npc_died', ({ npcId, killedBy, zoneId }) => {\n  vfx.playDeathEffect(npcId, worldMap.getZoneCenter(zoneId));\n});\n\n// Show surge HUD warning\nkernel.events.on('surge:warning', ({ timeUntilSurge }) => {\n  hud.showSurgeCountdown(timeUntilSurge);\n});\n\n// React to offline panic — maybe play a distant scream\nkernel.events.on('ai:npc_panicked', ({ npcId }) => {\n  audio.playDistantScream(npcId);\n});\n```\n\n---\n\n## Performance tuning\n\nAll knobs live in `ISimulationPluginConfig` (plugin-level) and\n`ISimulationConfig` sub-sections (simulation-level). Pass overrides to\n`SimulationPlugin` or `createInMemoryKernel`.\n\n### Plugin-level knobs\n\n| Knob | Default | Effect |\n|------|---------|--------|\n| `tickIntervalMs` | `5000` ms | How often the full tick pipeline runs. Increase to 10 000 ms for 200+ NPCs; decrease to 2 000 ms for a more reactive world. |\n| `maxBrainUpdatesPerTick` | `20` | Round-robin budget: at most this many offline brains are updated per tick. Raise for faster NPC reactions; lower to spread CPU across more frames. |\n| `moraleRestoreRate` | `0.02` | Morale delta per tick toward baseline. Higher values = NPCs recover from fear faster. |\n| `moraleBaseline` | `0.5` | Morale target all NPCs drift toward over time. |\n| `moraleEvalIntervalMs` | `2000` ms | How often panic threshold is evaluated (runs every frame, gated by this interval). |\n| `redundancyCleanupInterval` | `3` ticks | Dead NPCs are unregistered every N ticks. Lower = faster memory recovery; higher = less per-tick overhead. |\n\n### Simulation sub-config knobs\n\n| Knob | Path | Default | Effect |\n|------|------|---------|--------|\n| `combatDecayMs` | `terrainState.combatDecayMs` | `30 000` ms | Time for terrain COMBAT → ALERT decay. |\n| `alertDecayMs` | `terrainState.alertDecayMs` | `15 000` ms | Time for terrain ALERT → PEACEFUL decay. |\n| `maxResolutionsPerTick` | `offlineCombat.maxResolutionsPerTick` | `10` | Max faction-pair combat exchanges per tick. Reduce for lower CPU cost with many hostile pairs. |\n| `detectionProbability` | `offlineCombat.detectionProbability` | `70` (%) | Chance two co-located hostile factions detect each other per tick. Lower for sparser fights. |\n| `combatLockMs` | `offlineCombat.combatLockMs` | `15 000` ms | Cooldown between exchanges for the same pair. Raise to throttle combat frequency. |\n| `reEvaluateIntervalMs` | `brain.reEvaluateIntervalMs` | `30 000` ms | How often a brain reconsiders its terrain assignment. Raise to reduce terrain churn CPU. |\n\n### Practical guidance by NPC count\n\n| Scale | Recommended settings |\n|-------|---------------------|\n| **50 NPCs** | Defaults work well. `tickIntervalMs: 5_000`, `maxBrainUpdatesPerTick: 20`. All NPCs updated every tick. |\n| **150 NPCs** | Raise `tickIntervalMs` to `8_000`. Keep `maxBrainUpdatesPerTick` at `20` — each brain gets a turn every ~2 ticks. |\n| **300 NPCs** | `tickIntervalMs: 10_000`, `maxBrainUpdatesPerTick: 30`. Each brain updated roughly every ~2 ticks. Raise `reEvaluateIntervalMs` to `60_000` to reduce terrain selection churn. |\n| **500 NPCs** | `tickIntervalMs: 15_000`, `maxBrainUpdatesPerTick: 40`, `maxResolutionsPerTick: 5`. Consider disabling graph movement (`levelGraph: undefined`) if not needed. |\n\n```ts\n// Example: tuning for ~300 offline NPCs\nconst kernel = new ALifeKernel();\nkernel.use(new SimulationPlugin({\n  tickIntervalMs:          10_000,\n  maxBrainUpdatesPerTick:  30,\n  moraleEvalIntervalMs:     3_000,\n  redundancyCleanupInterval: 5,\n  simulation: {\n    brain:        { reEvaluateIntervalMs: 60_000 },\n    offlineCombat: { maxResolutionsPerTick: 5 },\n  },\n}));\n```\n\n---\n\n## See also\n\n- [`@alife-sdk/ai`](../alife-ai/README.md) — online frame-based AI for NPCs that come within player range\n- [`@alife-sdk/phaser`](../alife-phaser/README.md) — Phaser 3 adapter that wires online/offline transitions automatically\n- [`@alife-sdk/persistence`](../alife-persistence/README.md) — save and restore simulation state between sessions\n","readmeFilename":"README.md"}