{"_id":"@alife-sdk/ai","_rev":"7-0915db831e75f29426bd414a3aa5b776","name":"@alife-sdk/ai","dist-tags":{"latest":"0.5.1"},"versions":{"0.1.0":{"name":"@alife-sdk/ai","version":"0.1.0","keywords":["alife","game-ai","simulation","npc","behavior","state-machine","goap"],"author":{"name":"Eugene Rusakov"},"license":"MIT","_id":"@alife-sdk/ai@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":"19ee1bfad56a0cdc82e5e0c4c1529b9b7d99d295","tarball":"https://registry.npmjs.org/@alife-sdk/ai/-/ai-0.1.0.tgz","fileCount":395,"integrity":"sha512-sPqDXXBCq0CmiXlkFIwtVyKodPujZ8VpBYZrQkFquq1SGg0KGjcs4H2DitLEWtZLFHXwRzgBsd9tlYGT9pJJYw==","signatures":[{"sig":"MEUCIQDIxyTQ9B82aKq+j+7rIYsCHuzQeNMDsISsd5YiJP2ZSAIgSPytsvFA476SxrT61ZW+ofkunCFNS0utw4yBOu8yYJc=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":866320},"main":"dist/index.js","type":"module","_from":"file:alife-sdk-ai-0.1.0.tgz","types":"dist/index.d.ts","engines":{"node":">=20.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./goap":{"types":"./dist/goap/index.d.ts","import":"./dist/goap/index.js"},"./cover":{"types":"./dist/cover/index.d.ts","import":"./dist/cover/index.js"},"./ports":{"types":"./dist/ports/index.d.ts","import":"./dist/ports/index.js"},"./sound":{"types":"./dist/sound/index.d.ts","import":"./dist/sound/index.js"},"./squad":{"types":"./dist/squad/index.d.ts","import":"./dist/squad/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"},"./config":{"types":"./dist/config/index.d.ts","import":"./dist/config/index.js"},"./plugin":{"types":"./dist/plugin/index.d.ts","import":"./dist/plugin/index.js"},"./states":{"types":"./dist/states/index.d.ts","import":"./dist/states/index.js"},"./animation":{"types":"./dist/animation/index.d.ts","import":"./dist/animation/index.js"},"./suspicion":{"types":"./dist/suspicion/index.d.ts","import":"./dist/suspicion/index.js"},"./conditions":{"types":"./dist/conditions/index.d.ts","import":"./dist/conditions/index.js"},"./navigation":{"types":"./dist/navigation/index.d.ts","import":"./dist/navigation/index.js"},"./perception":{"types":"./dist/perception/index.d.ts","import":"./dist/perception/index.js"},"./states/eat-corpse":{"types":"./dist/states/eat-corpse/index.d.ts","import":"./dist/states/eat-corpse/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/11387caa92e0c732631b3f5b286dc2b1/alife-sdk-ai-0.1.0.tgz","_integrity":"sha512-sPqDXXBCq0CmiXlkFIwtVyKodPujZ8VpBYZrQkFquq1SGg0KGjcs4H2DitLEWtZLFHXwRzgBsd9tlYGT9pJJYw==","repository":{"url":"git+https://github.com/eurusik/alife-sdk.git","type":"git","directory":"packages/alife-ai"},"_npmVersion":"10.8.1","description":"Online frame-based NPC behavior system","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/hazards":"0.1.0"},"_npmOperationalInternal":{"tmp":"tmp/ai_0.1.0_1772739947729_0.24854871475551987","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@alife-sdk/ai","version":"0.1.1","keywords":["alife","game-ai","simulation","npc","behavior","state-machine","goap"],"author":{"name":"Eugene Rusakov"},"license":"MIT","_id":"@alife-sdk/ai@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":"1c93cdb9ddfba668ec7ffbb52172dafd3103906f","tarball":"https://registry.npmjs.org/@alife-sdk/ai/-/ai-0.1.1.tgz","fileCount":395,"integrity":"sha512-O5p+Bcfhxgw+Jj4JLmMexoYe3pzQyW8QWCtPk6wzdxlbX3exP7gvC0J9hc52MU8ttZw5x2Ven71Hh5WyMIwiIA==","signatures":[{"sig":"MEQCIBZc+7QMQjuDF3rBlax88an3uhZHfMWCYNRfS1iOF2VpAiAfhYizFQtL/t3vqoXtMiSmYiZvz72I05CeTO5SwEQO5g==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":881289},"main":"dist/index.js","type":"module","_from":"file:alife-sdk-ai-0.1.1.tgz","types":"dist/index.d.ts","engines":{"node":">=20.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./goap":{"types":"./dist/goap/index.d.ts","import":"./dist/goap/index.js"},"./cover":{"types":"./dist/cover/index.d.ts","import":"./dist/cover/index.js"},"./ports":{"types":"./dist/ports/index.d.ts","import":"./dist/ports/index.js"},"./sound":{"types":"./dist/sound/index.d.ts","import":"./dist/sound/index.js"},"./squad":{"types":"./dist/squad/index.d.ts","import":"./dist/squad/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"},"./config":{"types":"./dist/config/index.d.ts","import":"./dist/config/index.js"},"./plugin":{"types":"./dist/plugin/index.d.ts","import":"./dist/plugin/index.js"},"./states":{"types":"./dist/states/index.d.ts","import":"./dist/states/index.js"},"./animation":{"types":"./dist/animation/index.d.ts","import":"./dist/animation/index.js"},"./suspicion":{"types":"./dist/suspicion/index.d.ts","import":"./dist/suspicion/index.js"},"./conditions":{"types":"./dist/conditions/index.d.ts","import":"./dist/conditions/index.js"},"./navigation":{"types":"./dist/navigation/index.d.ts","import":"./dist/navigation/index.js"},"./perception":{"types":"./dist/perception/index.d.ts","import":"./dist/perception/index.js"},"./states/eat-corpse":{"types":"./dist/states/eat-corpse/index.d.ts","import":"./dist/states/eat-corpse/index.js"}},"scripts":{"dev":"tsc --watch","test":"vitest run","build":"tsc","test:watch":"vitest"},"_npmUser":{"name":"eurusik","email":"john.rusakov@gmail.com"},"_resolved":"/tmp/2f00e342922c7fc4a75697e3fe0d99a7/alife-sdk-ai-0.1.1.tgz","_integrity":"sha512-O5p+Bcfhxgw+Jj4JLmMexoYe3pzQyW8QWCtPk6wzdxlbX3exP7gvC0J9hc52MU8ttZw5x2Ven71Hh5WyMIwiIA==","repository":{"url":"git+https://github.com/eurusik/alife-sdk.git","type":"git","directory":"packages/alife-ai"},"_npmVersion":"10.8.2","description":"Online frame-based NPC behavior system","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/hazards":"0.1.1"},"_npmOperationalInternal":{"tmp":"tmp/ai_0.1.1_1772742736229_0.4538152328924012","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@alife-sdk/ai","version":"0.3.0","keywords":["alife","game-ai","simulation","npc","behavior","state-machine","goap"],"author":{"name":"Eugene Rusakov"},"license":"MIT","_id":"@alife-sdk/ai@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":"0cdb9db58e5f91de033f7ab15c8d66d91ccea79c","tarball":"https://registry.npmjs.org/@alife-sdk/ai/-/ai-0.3.0.tgz","fileCount":395,"integrity":"sha512-fnvFLwHw88kWxCudi7Z6TSjXcl0AoiuCuBmJru3bqk56bp2IuB5ymG+rvmEMuN6Cm7mL/NJTH32DR1V7hvBVBQ==","signatures":[{"sig":"MEQCICw2jknRqDYjIBtH2tzMMF6D3ds5cILxwBrNHZyl1dUgAiAbPcoA0mzW9Iw/dwf60aIB6eXItYU1EXKVR35CAYQSSA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":881289},"main":"dist/index.js","type":"module","_from":"file:alife-sdk-ai-0.3.0.tgz","types":"dist/index.d.ts","engines":{"node":">=20.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./goap":{"types":"./dist/goap/index.d.ts","import":"./dist/goap/index.js"},"./cover":{"types":"./dist/cover/index.d.ts","import":"./dist/cover/index.js"},"./ports":{"types":"./dist/ports/index.d.ts","import":"./dist/ports/index.js"},"./sound":{"types":"./dist/sound/index.d.ts","import":"./dist/sound/index.js"},"./squad":{"types":"./dist/squad/index.d.ts","import":"./dist/squad/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"},"./config":{"types":"./dist/config/index.d.ts","import":"./dist/config/index.js"},"./plugin":{"types":"./dist/plugin/index.d.ts","import":"./dist/plugin/index.js"},"./states":{"types":"./dist/states/index.d.ts","import":"./dist/states/index.js"},"./animation":{"types":"./dist/animation/index.d.ts","import":"./dist/animation/index.js"},"./suspicion":{"types":"./dist/suspicion/index.d.ts","import":"./dist/suspicion/index.js"},"./conditions":{"types":"./dist/conditions/index.d.ts","import":"./dist/conditions/index.js"},"./navigation":{"types":"./dist/navigation/index.d.ts","import":"./dist/navigation/index.js"},"./perception":{"types":"./dist/perception/index.d.ts","import":"./dist/perception/index.js"},"./states/eat-corpse":{"types":"./dist/states/eat-corpse/index.d.ts","import":"./dist/states/eat-corpse/index.js"}},"scripts":{"dev":"tsc --watch","test":"vitest run","build":"tsc","test:watch":"vitest"},"_npmUser":{"name":"eurusik","email":"john.rusakov@gmail.com"},"_resolved":"/tmp/187e655d6cb6c0562019b09d0b89302f/alife-sdk-ai-0.3.0.tgz","_integrity":"sha512-fnvFLwHw88kWxCudi7Z6TSjXcl0AoiuCuBmJru3bqk56bp2IuB5ymG+rvmEMuN6Cm7mL/NJTH32DR1V7hvBVBQ==","repository":{"url":"git+https://github.com/eurusik/alife-sdk.git","type":"git","directory":"packages/alife-ai"},"_npmVersion":"10.8.2","description":"Online frame-based NPC behavior system","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/hazards":"0.3.0"},"_npmOperationalInternal":{"tmp":"tmp/ai_0.3.0_1772789683241_0.6444209233808105","host":"s3://npm-registry-packages-npm-production"}},"0.4.0":{"name":"@alife-sdk/ai","version":"0.4.0","keywords":["alife","game-ai","simulation","npc","behavior","state-machine","goap"],"author":{"name":"Eugene Rusakov"},"license":"MIT","_id":"@alife-sdk/ai@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":"157734be9b9a130f9b2e53c058433ff78a16236d","tarball":"https://registry.npmjs.org/@alife-sdk/ai/-/ai-0.4.0.tgz","fileCount":395,"integrity":"sha512-+MiauygKQ0NhkJW4yQVRgV4H1Kfwm9vnFYnkdnXXDS22274EMoAOHPIKSY+q1GJf4h5GAmp/pHG3UomGOM7+vw==","signatures":[{"sig":"MEQCIFdHq9K0tGOBDjFWv62LJ2s675GR3HRVnyiOMUMM9p77AiAYT+lUQ4nQSNWP+ZIlZw2GusAlf5Obtf79KgRbaJSUjw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":881289},"main":"dist/index.js","type":"module","_from":"file:alife-sdk-ai-0.4.0.tgz","types":"dist/index.d.ts","engines":{"node":">=20.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./goap":{"types":"./dist/goap/index.d.ts","import":"./dist/goap/index.js"},"./cover":{"types":"./dist/cover/index.d.ts","import":"./dist/cover/index.js"},"./ports":{"types":"./dist/ports/index.d.ts","import":"./dist/ports/index.js"},"./sound":{"types":"./dist/sound/index.d.ts","import":"./dist/sound/index.js"},"./squad":{"types":"./dist/squad/index.d.ts","import":"./dist/squad/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"},"./config":{"types":"./dist/config/index.d.ts","import":"./dist/config/index.js"},"./plugin":{"types":"./dist/plugin/index.d.ts","import":"./dist/plugin/index.js"},"./states":{"types":"./dist/states/index.d.ts","import":"./dist/states/index.js"},"./animation":{"types":"./dist/animation/index.d.ts","import":"./dist/animation/index.js"},"./suspicion":{"types":"./dist/suspicion/index.d.ts","import":"./dist/suspicion/index.js"},"./conditions":{"types":"./dist/conditions/index.d.ts","import":"./dist/conditions/index.js"},"./navigation":{"types":"./dist/navigation/index.d.ts","import":"./dist/navigation/index.js"},"./perception":{"types":"./dist/perception/index.d.ts","import":"./dist/perception/index.js"},"./states/eat-corpse":{"types":"./dist/states/eat-corpse/index.d.ts","import":"./dist/states/eat-corpse/index.js"}},"scripts":{"dev":"tsc --watch","test":"vitest run","build":"tsc","test:watch":"vitest"},"_npmUser":{"name":"eurusik","email":"john.rusakov@gmail.com"},"_resolved":"/tmp/713f232905b12673a503a3643e2cd39b/alife-sdk-ai-0.4.0.tgz","_integrity":"sha512-+MiauygKQ0NhkJW4yQVRgV4H1Kfwm9vnFYnkdnXXDS22274EMoAOHPIKSY+q1GJf4h5GAmp/pHG3UomGOM7+vw==","repository":{"url":"git+https://github.com/eurusik/alife-sdk.git","type":"git","directory":"packages/alife-ai"},"_npmVersion":"10.8.2","description":"Online frame-based NPC behavior system","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/hazards":"0.4.0"},"_npmOperationalInternal":{"tmp":"tmp/ai_0.4.0_1772823546971_0.36612938988325716","host":"s3://npm-registry-packages-npm-production"}},"0.4.1":{"name":"@alife-sdk/ai","version":"0.4.1","keywords":["alife","game-ai","simulation","npc","behavior","state-machine","goap"],"author":{"name":"Eugene Rusakov"},"license":"MIT","_id":"@alife-sdk/ai@0.4.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":"e5fba9939b1c34df198527ccb21cd1fa8955433c","tarball":"https://registry.npmjs.org/@alife-sdk/ai/-/ai-0.4.1.tgz","fileCount":395,"integrity":"sha512-qgGYLw0RznSdJxR3F58FoPTxQHw5svXktv2uAdFDpd8kAiz6CnR5FCI6TRJ67fENC/imJv8WQOj//e3uum7EqA==","signatures":[{"sig":"MEUCIQDsXj/LW+K85OlsPAO68pCWH1/Uq4UkupKXnPziQuamoAIgQAnsuhjSRzWt7K39VuQtxB/G/7h24bteQ/Mpkobsep4=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":900159},"main":"dist/index.js","type":"module","_from":"file:alife-sdk-ai-0.4.1.tgz","types":"dist/index.d.ts","engines":{"node":">=20.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./goap":{"types":"./dist/goap/index.d.ts","import":"./dist/goap/index.js"},"./cover":{"types":"./dist/cover/index.d.ts","import":"./dist/cover/index.js"},"./ports":{"types":"./dist/ports/index.d.ts","import":"./dist/ports/index.js"},"./sound":{"types":"./dist/sound/index.d.ts","import":"./dist/sound/index.js"},"./squad":{"types":"./dist/squad/index.d.ts","import":"./dist/squad/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"},"./config":{"types":"./dist/config/index.d.ts","import":"./dist/config/index.js"},"./plugin":{"types":"./dist/plugin/index.d.ts","import":"./dist/plugin/index.js"},"./states":{"types":"./dist/states/index.d.ts","import":"./dist/states/index.js"},"./animation":{"types":"./dist/animation/index.d.ts","import":"./dist/animation/index.js"},"./suspicion":{"types":"./dist/suspicion/index.d.ts","import":"./dist/suspicion/index.js"},"./conditions":{"types":"./dist/conditions/index.d.ts","import":"./dist/conditions/index.js"},"./navigation":{"types":"./dist/navigation/index.d.ts","import":"./dist/navigation/index.js"},"./perception":{"types":"./dist/perception/index.d.ts","import":"./dist/perception/index.js"},"./states/eat-corpse":{"types":"./dist/states/eat-corpse/index.d.ts","import":"./dist/states/eat-corpse/index.js"}},"scripts":{"dev":"tsc --watch","test":"vitest run","build":"tsc","test:watch":"vitest"},"_npmUser":{"name":"eurusik","email":"john.rusakov@gmail.com"},"_resolved":"/tmp/ef4b83cb256a2539646901b8619f47ba/alife-sdk-ai-0.4.1.tgz","_integrity":"sha512-qgGYLw0RznSdJxR3F58FoPTxQHw5svXktv2uAdFDpd8kAiz6CnR5FCI6TRJ67fENC/imJv8WQOj//e3uum7EqA==","repository":{"url":"git+https://github.com/eurusik/alife-sdk.git","type":"git","directory":"packages/alife-ai"},"_npmVersion":"10.8.2","description":"Online frame-based NPC behavior system","directories":{},"sideEffects":false,"_nodeVersion":"20.20.1","dependencies":{"@alife-sdk/core":"0.4.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.0.18","typescript":"^5.9.3","@alife-sdk/hazards":"0.4.1"},"_npmOperationalInternal":{"tmp":"tmp/ai_0.4.1_1773837145056_0.692428193140691","host":"s3://npm-registry-packages-npm-production"}},"0.5.0":{"name":"@alife-sdk/ai","version":"0.5.0","keywords":["alife","game-ai","simulation","npc","behavior","state-machine","goap"],"author":{"name":"Eugene Rusakov"},"license":"MIT","_id":"@alife-sdk/ai@0.5.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":"46f4c487d32909d84ef63f910119f1d7ca0d6455","tarball":"https://registry.npmjs.org/@alife-sdk/ai/-/ai-0.5.0.tgz","fileCount":399,"integrity":"sha512-ycOTR3pN9HK8QEyXzNctyI8bvGB1L2QY21uYE68PmjMq0RfbtafyqDJCKGtbk8U98SJgG55as/sR/vYtyd6GGA==","signatures":[{"sig":"MEQCIEEYMuG86vrkFAKfho4yBxZYnIoT5JYOYXWg8w4AwW2WAiAZEg4jaPfI39KOjNS9pWW+oangKJydqkv21dlsm9Bb9g==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":930738},"main":"dist/index.js","type":"module","_from":"file:alife-sdk-ai-0.5.0.tgz","types":"dist/index.d.ts","engines":{"node":">=20.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./goap":{"types":"./dist/goap/index.d.ts","import":"./dist/goap/index.js"},"./cover":{"types":"./dist/cover/index.d.ts","import":"./dist/cover/index.js"},"./ports":{"types":"./dist/ports/index.d.ts","import":"./dist/ports/index.js"},"./sound":{"types":"./dist/sound/index.d.ts","import":"./dist/sound/index.js"},"./squad":{"types":"./dist/squad/index.d.ts","import":"./dist/squad/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"},"./config":{"types":"./dist/config/index.d.ts","import":"./dist/config/index.js"},"./plugin":{"types":"./dist/plugin/index.d.ts","import":"./dist/plugin/index.js"},"./states":{"types":"./dist/states/index.d.ts","import":"./dist/states/index.js"},"./animation":{"types":"./dist/animation/index.d.ts","import":"./dist/animation/index.js"},"./suspicion":{"types":"./dist/suspicion/index.d.ts","import":"./dist/suspicion/index.js"},"./conditions":{"types":"./dist/conditions/index.d.ts","import":"./dist/conditions/index.js"},"./navigation":{"types":"./dist/navigation/index.d.ts","import":"./dist/navigation/index.js"},"./perception":{"types":"./dist/perception/index.d.ts","import":"./dist/perception/index.js"},"./states/eat-corpse":{"types":"./dist/states/eat-corpse/index.d.ts","import":"./dist/states/eat-corpse/index.js"}},"scripts":{"dev":"tsc --watch","test":"vitest run","build":"tsc","test:watch":"vitest"},"_npmUser":{"name":"eurusik","email":"john.rusakov@gmail.com"},"_resolved":"/tmp/284a427089542fd5865e5ff55235bb16/alife-sdk-ai-0.5.0.tgz","_integrity":"sha512-ycOTR3pN9HK8QEyXzNctyI8bvGB1L2QY21uYE68PmjMq0RfbtafyqDJCKGtbk8U98SJgG55as/sR/vYtyd6GGA==","repository":{"url":"git+https://github.com/eurusik/alife-sdk.git","type":"git","directory":"packages/alife-ai"},"_npmVersion":"10.8.2","description":"Online frame-based NPC behavior system","directories":{},"sideEffects":false,"_nodeVersion":"20.20.1","dependencies":{"@alife-sdk/core":"0.4.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.0.18","typescript":"^5.9.3","@alife-sdk/hazards":"0.4.1"},"_npmOperationalInternal":{"tmp":"tmp/ai_0.5.0_1773866621023_0.5391106346807022","host":"s3://npm-registry-packages-npm-production"}},"0.5.1":{"name":"@alife-sdk/ai","version":"0.5.1","description":"Online frame-based NPC behavior system","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"},"./config":{"types":"./dist/config/index.d.ts","import":"./dist/config/index.js"},"./cover":{"types":"./dist/cover/index.d.ts","import":"./dist/cover/index.js"},"./combat":{"types":"./dist/combat/index.d.ts","import":"./dist/combat/index.js"},"./navigation":{"types":"./dist/navigation/index.d.ts","import":"./dist/navigation/index.js"},"./squad":{"types":"./dist/squad/index.d.ts","import":"./dist/squad/index.js"},"./animation":{"types":"./dist/animation/index.d.ts","import":"./dist/animation/index.js"},"./sound":{"types":"./dist/sound/index.d.ts","import":"./dist/sound/index.js"},"./perception":{"types":"./dist/perception/index.d.ts","import":"./dist/perception/index.js"},"./goap":{"types":"./dist/goap/index.d.ts","import":"./dist/goap/index.js"},"./ports":{"types":"./dist/ports/index.d.ts","import":"./dist/ports/index.js"},"./plugin":{"types":"./dist/plugin/index.d.ts","import":"./dist/plugin/index.js"},"./conditions":{"types":"./dist/conditions/index.d.ts","import":"./dist/conditions/index.js"},"./suspicion":{"types":"./dist/suspicion/index.d.ts","import":"./dist/suspicion/index.js"},"./states":{"types":"./dist/states/index.d.ts","import":"./dist/states/index.js"},"./states/eat-corpse":{"types":"./dist/states/eat-corpse/index.d.ts","import":"./dist/states/eat-corpse/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-ai"},"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.2"},"devDependencies":{"typescript":"^5.9.3","vitest":"^4.0.18","@alife-sdk/hazards":"0.4.1"},"scripts":{"build":"tsc","dev":"tsc --watch","test":"vitest run","test:watch":"vitest"},"_id":"@alife-sdk/ai@0.5.1","_integrity":"sha512-0tQqt1NWKyLtPF9GCfA7ikJz1OtXnGPGFqW2cXBUPzwkbNoLdxWYUXuQF8d4kqkqbT3Y8hAaUP1kQP8ncjmOXg==","_resolved":"/tmp/c39065beb76915c284871c313267d076/alife-sdk-ai-0.5.1.tgz","_from":"file:alife-sdk-ai-0.5.1.tgz","_nodeVersion":"20.20.1","_npmVersion":"10.8.2","dist":{"integrity":"sha512-0tQqt1NWKyLtPF9GCfA7ikJz1OtXnGPGFqW2cXBUPzwkbNoLdxWYUXuQF8d4kqkqbT3Y8hAaUP1kQP8ncjmOXg==","shasum":"b0fb9271d9cabbcd3ac6c4a133e31cfde7aaeaa9","tarball":"https://registry.npmjs.org/@alife-sdk/ai/-/ai-0.5.1.tgz","fileCount":399,"unpackedSize":931824,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDTb76hZbrdaXwOEfl4o8+rnG4dYUiQzlDot3FfnXtL/AIgfdLj+wSKR0OVbm+Csp1BCVFZM+LprplYUNrNB4hmYjI="}]},"_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/ai_0.5.1_1773955886063_0.47877960922284934"},"_hasShrinkwrap":false}},"time":{"created":"2026-03-05T19:45:47.623Z","modified":"2026-03-19T21:31:26.362Z","0.1.0":"2026-03-05T19:45:47.979Z","0.1.1":"2026-03-05T20:32:16.467Z","0.3.0":"2026-03-06T09:34:43.423Z","0.4.0":"2026-03-06T18:59:07.173Z","0.4.1":"2026-03-18T12:32:25.281Z","0.5.0":"2026-03-18T20:43:41.160Z","0.5.1":"2026-03-19T21:31:26.245Z"},"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-ai"},"description":"Online frame-based NPC behavior system","maintainers":[{"name":"eurusik","email":"john.rusakov@gmail.com"}],"readme":"# @alife-sdk/ai\n\nOnline frame-based NPC behavior system.\n\nEngine-agnostic. Works with Phaser, PixiJS, Node.js, or any other runtime.\nDepends on [`@alife-sdk/core`](../alife-core/README.md).\n\n```\nnpm install @alife-sdk/ai\n```\n\n---\n\n## What this package does\n\n`@alife-sdk/ai` drives every online NPC — an NPC that is within the player's\nview and must behave in real-time.\n\n- **State machine driver** — `OnlineAIDriver` + `StateHandlerMap` runs a per-NPC FSM\n  over 18 built-in states (idle, patrol, combat, flee, wounded, monster abilities, …)\n- **Optional state handlers** — opt-in handlers for investigation, helping wounded allies, and combat transitions (`InvestigateState`, `HelpWoundedState`, `KillWoundedState`, `CombatTransitionHandler`)\n- **Cover system** — 6 evaluators, loophole peek/fire cycles, TTL-based cover locking\n- **Perception** — FOV queries, hearing radius, intel freshness filters\n- **GOAP** — elite NPC goal-oriented planning over a 16-property world state bitmask\n- **GOAPDirector** — built-in bridge between GOAP planner and FSM; replans on entry, dispatches actions as sub-states, handles interrupts and plan lifecycle\n- **Navigation** — Catmull-Rom + Dubins arc path smoothing, restricted zones, pack steering, pluggable `IPathfindingAccess` for grid/navmesh A*\n- **Squad tactics** — situational assessment, 6 commands, shared target table\n- **Animation** — 8-direction state→key mapping, layered debounced controller\n- **Suspicion** — stimulus accumulation → alert threshold crossing\n- **Conditions** — multi-channel boolean state for dialogue / event gating\n\nThe package is intentionally engine-agnostic. All engine interaction goes\nthrough **ports** — narrow interfaces you implement once for your engine.\n\n---\n\n## Quick start\n\n> `buildNPCContext` and `liveDrivers` below are **your** code, not SDK exports.\n> `INPCContext` is an interface — you assemble it once per NPC from your engine objects.\n> See the [Key concepts → INPCContext](#inpccontext--narrow-access-interfaces) section for the full interface shape.\n\n```ts\nimport { ALifeKernel }               from '@alife-sdk/core';\nimport { AIPlugin }                  from '@alife-sdk/ai/plugin';\nimport { buildDefaultHandlerMap,\n         OnlineAIDriver,\n         ONLINE_STATE }              from '@alife-sdk/ai/states';\nimport { SeededRandom }              from '@alife-sdk/core/ports';\n\n// 1. Set up the kernel (see @alife-sdk/core quick start for full kernel setup)\nconst kernel = new ALifeKernel({ /* your engine adapters */ });\n\n// 2. Install AIPlugin — owns shared cover registry and restricted zones\nconst random = new SeededRandom(42);\nconst aiPlugin = new AIPlugin(random);\nkernel.use(aiPlugin);\n\nawait kernel.init();\n\n// 3. Build a handler map — one instance, shared across all human NPCs\nconst handlers = buildDefaultHandlerMap({ combatRange: 350, meleeRange: 60 });\n\n// 4. Create an OnlineAIDriver per NPC (in your spawn / online transition)\nfunction spawnNPC(npcId: string): OnlineAIDriver {\n  const coverAccess = aiPlugin.createCoverAccess(npcId);\n  // buildNPCContext — your function that assembles INPCContext from engine objects\n  // (perception adapter, health adapter, cover access, danger access, etc.)\n  const ctx = buildNPCContext(npcId, coverAccess);\n  return new OnlineAIDriver(ctx, handlers, ONLINE_STATE.IDLE);\n}\n\n// 5. Game loop — call update() on each live driver\n// liveDrivers — your Map/Array of active OnlineAIDriver instances\nfunction update(deltaMs: number): void {\n  kernel.update(deltaMs);           // A-Life tick + events flush\n  for (const driver of liveDrivers) {\n    driver.update(deltaMs);         // per-NPC state machine\n  }\n}\n\n// 6. Read current state\nconst state = driver.currentStateId; // e.g. 'COMBAT'\n\n// 7. Force a state transition from OUTSIDE a handler (new!)\ndriver.forceTransition(ONLINE_STATE.DEAD);\n\n// 8. Listen to state transitions (new!)\nconst unsub = driver.onTransition((from, to) => {\n  console.log(`NPC ${npcId}: ${from} → ${to}`);\n});\n// unsub() to remove the listener\n\n// 9. Store game-specific data in the extensible state bag (new!)\nctx.state.custom ??= {};\nctx.state.custom.ammo = 30;\nctx.state.custom.goapPlan = planner.plan(worldState, goal);\n```\n\n---\n\n## Sub-path imports\n\nEach module has its own import path for optimal tree-shaking:\n\n| Import path | What's inside | Module docs |\n|-------------|--------------|-------------|\n| `@alife-sdk/ai` | Full re-export of all sub-modules | [src/](src/) |\n| `@alife-sdk/ai/plugin` | `AIPlugin`, `IAIPluginConfig` | [plugin/](src/plugin/) |\n| `@alife-sdk/ai/states` | `OnlineAIDriver`, `StateHandlerMap`, `ONLINE_STATE`, all handlers, builder functions | [states/](src/states/) |\n| `@alife-sdk/ai/cover` | `CoverRegistry`, `CoverLockRegistry`, 6 evaluators, `LoopholeGenerator` | [cover/](src/cover/) |\n| `@alife-sdk/ai/perception` | `NPCSensors`, `isInFOV`, `filterVisibleEntities`, `filterHearingEntities`, `filterHostileEntities`, `filterFriendlyEntities`, `filterFreshIntel`, `distanceSq`, `findClosest`, `scanForEnemies` | [perception/](src/perception/) |\n| `@alife-sdk/ai/goap` | `GOAPController`, `GOAPDirector`, `buildWorldState`, `selectGoal`, `EvadeHazardAction` | [goap/](src/goap/) |\n| `@alife-sdk/ai/navigation` | `smoothPath`, `smoothPathWithTurning`, `SmoothPathFollower`, `RestrictedZoneManager`, `SteeringBehaviors` | [navigation/](src/navigation/) |\n| `@alife-sdk/ai/squad` | `evaluateSituation`, `SquadCommand`, `SquadSharedTargetTable` | [squad/](src/squad/) |\n| `@alife-sdk/ai/animation` | `getDirection`, `getAnimationKey`, `getAnimationRequest`, `AnimationController`, `DirectionCache`, `CompassIndex`, `AnimLayer`, `DEFAULT_STATE_ANIM_MAP`, `DEFAULT_WEAPON_SUFFIXES` | [animation/](src/animation/) |\n| `@alife-sdk/ai/sound` | `VocalizationType`, `VocalizationTracker` | [sound/](src/sound/) |\n| `@alife-sdk/ai/suspicion` | `SuspicionAccumulator`, `SuspicionStimuli` | [suspicion/](src/suspicion/) |\n| `@alife-sdk/ai/conditions` | `ConditionBank`, `ConditionChannels` | [conditions/](src/conditions/) |\n| `@alife-sdk/ai/combat` | `selectBestWeapon`, `shouldThrowGrenade`, `shouldUseMedkit`, `LoadoutBuilder`, `createLoadout`, `FactionWeaponPreference`, `evaluateTransitions`, `DEFAULT_COMBAT_RULES`, `WoundedRule`, `NoAmmoRule`, `EvadeDangerRule`, `MoraleRule`, `GrenadeOpportunityRule`, `MonsterAbility`, `selectMonsterAbility` | [combat/](src/combat/) |\n| `@alife-sdk/ai/types` | Shared interfaces (`INPCContext`, `INPCOnlineState`, …) | [types/](src/types/) |\n| `@alife-sdk/ai/config` | `IStateConfig`, default config helpers | [config/](src/config/) |\n| `@alife-sdk/ai/ports` | AI-specific port interfaces | [ports/](src/ports/) |\n\n---\n\n## Architecture\n\n```\n┌───────────────────────────────────────────────────────────┐\n│                       ALifeKernel                         │\n│                  (from @alife-sdk/core)                    │\n└──────────────────────────┬────────────────────────────────┘\n                           │ kernel.use(aiPlugin)\n┌──────────────────────────▼────────────────────────────────┐\n│                       AIPlugin                            │\n│  CoverRegistry · CoverLockRegistry                        │\n│  RestrictedZoneManager                                    │\n│  createCoverAccess(npcId) ──► ICoverAccess (per NPC)      │\n└───────────────────────────────────────────────────────────┘\n\nPer-NPC (created on online transition):\n┌───────────────────────────────────────────────────────────┐\n│  OnlineAIDriver                                           │\n│                                                           │\n│  StateHandlerMap ──► IOnlineStateHandler                  │\n│    DEAD · IDLE · PATROL · ALERT · FLEE · SEARCH           │\n│    CAMP · SLEEP · COMBAT · TAKE_COVER · GRENADE           │\n│    EVADE_GRENADE · WOUNDED · RETREAT                       │\n│    CHARGE · STALK · LEAP · PSI_ATTACK                     │\n│                                                           │\n│  INPCContext ─┬─ INPCPerception     (FOV / hearing)       │\n│               ├─ INPCHealth         (hp / morale)         │\n│               ├─ ICoverAccess       (find / lock cover)   │\n│               ├─ IDangerAccess      (DangerManager port)  │\n│               ├─ IPathfindingAccess (A* / NavMesh port)   │\n│               ├─ ISquadAccess       (commands / target)   │\n│               ├─ ISuspicionAccess                         │\n│               └─ IConditionAccess                         │\n│                                                           │\n│  state.custom ── extensible Record<string, unknown> bag   │\n└───────────────────────────────────────────────────────────┘\n\nShared systems (optional, compose as needed):\n  NPCSensors            filterVisibleEntities / filterHearingEntities\n  GOAPController        elite NPC A* planning (rank >= 5)\n  GOAPDirector          GOAP -> FSM bridge (register as COMBAT handler)\n  SmoothPathFollower    Catmull-Rom + Dubins arc path cursor\n  AnimationController   layered, debounced animation dispatch\n  SuspicionAccumulator  stimulus -> alert threshold crossing\n  ConditionBank         multi-channel boolean state\n```\n\n---\n\n## Key concepts\n\n### OnlineAIDriver — per-NPC state machine\n\n`OnlineAIDriver` is created once per NPC when it enters the online zone\nand destroyed when it goes offline. It ticks one `IOnlineStateHandler`\nper frame: `enter → update (every frame) → exit`.\n\n```ts\nconst driver = new OnlineAIDriver(ctx, handlers, ONLINE_STATE.IDLE);\n\ndriver.update(deltaMs);             // call every frame\ndriver.currentStateId;              // current state ID string\n\n// Transitions from inside a handler:\nctx.transition('PATROL');\n\n// Transitions from OUTSIDE a handler (e.g. GOAP director, scripted events):\ndriver.forceTransition('DEAD');\n\n// Listen to transitions:\ndriver.onTransition((from, to) => console.log(`${from} → ${to}`));\n```\n\nEach state handler is a stateless object — all per-NPC runtime data lives in\n`INPCOnlineState` (position, target, phase flags, timer, etc.), not in the handler.\nThe handler map can be shared across all NPCs of the same type.\nGame-specific data (GOAP plans, ammo, personality) goes in `ctx.state.custom`.\n\n### StateHandlerMap — three built-in presets\n\nChoose the right preset for your entity type:\n\n| Builder function | States | Use for |\n|-----------------|--------|---------|\n| `buildDefaultHandlerMap()` | 14 (core + `CombatState`) | Human NPCs — ranged weapons, cover, grenades |\n| `buildMonsterHandlerMap()` | 14 (core + `MonsterCombatController`) | Monsters — melee only, no special abilities |\n| `buildChornobylMonsterHandlerMap()` | 18 (core + controller + 4 abilities) | Stalker-style monsters: `CHARGE / STALK / LEAP / PSI_ATTACK` |\n\nAll three return a fresh `StateHandlerMap` you can extend with `.register()`:\n\n```ts\n// Add a custom HUNT state to the default human map\nconst handlers = buildDefaultHandlerMap({ combatRange: 300 })\n  .register('HUNT', new HuntState(cfg));\n```\n\nMonster ability states map to entity types via `CHORNOBYL_ABILITY_SELECTOR`\n(or your own `IMonsterAbilityRule[]`):\n\n| Type | Ability state |\n|------|--------------|\n| `boar` | `CHARGE` — windup → ram at 2× speed |\n| `bloodsucker` | `STALK` — go invisible (alpha 0.08) → approach |\n| `snork` | `LEAP` — windup → airborne lerp → land |\n| `controller` | `PSI_ATTACK` — channel 2 s → PSI area damage |\n\n### AIPlugin — shared world state\n\n`AIPlugin` owns the data structures that must be shared across all NPCs:\n\n- `CoverRegistry` — all cover points in the scene\n- `CoverLockRegistry` — TTL-based locking so two NPCs don't pick the same cover\n- `RestrictedZoneManager` — zones where NPCs cannot enter / must leave\n\nInstall it once, then get a per-NPC adapter from `createCoverAccess(npcId)`:\n\n```ts\nconst aiPlugin = new AIPlugin(random);\nkernel.use(aiPlugin);\nawait kernel.init();\n\n// For each NPC going online:\nconst coverAccess = aiPlugin.createCoverAccess(npcId);\n```\n\n`AIPlugin.serialize()` saves `RestrictedZoneManager` state.\nCover locks are intentionally NOT serialized — they are ephemeral TTL data.\n\n### INPCContext — narrow access interfaces\n\nEvery state handler receives `INPCContext`, which is a bag of narrow port\ninterfaces. You implement each interface once and compose them per NPC:\n\n```ts\nconst ctx: INPCContext = {\n  npcId:      npc.id,\n  faction:    npc.faction,\n  perception: new MyPerception(npc),  // INPCPerception  | null\n  health:     new MyHealth(npc),      // INPCHealth      | null\n  cover:        coverAccess,            // ICoverAccess        | null  (from AIPlugin)\n  danger:       dangerAdapter,          // IDangerAccess       | null\n  pathfinding:  gridPathfinding,        // IPathfindingAccess  | null  (A* / NavMesh)\n  squad:        squadAccess,            // ISquadAccess        | null\n  suspicion:    suspicionAccess,        // ISuspicionAccess    | null\n  conditions:   conditionAccess,        // IConditionAccess    | null\n  // pack, restrictedZones also nullable — omit if not used\n  emitShoot:   (payload) => fireWeapon(npc, payload),\n  emitMeleeHit:(payload) => applyMelee(npc, payload),\n};\n```\n\n> **Important:** Many subsystems are nullable (`T | null`). State handlers\n> must null-check before use — always access optional subsystems with optional\n> chaining: `ctx.cover?.findCover(...)`, `ctx.health?.hp`, `ctx.perception?.hasVisibleEnemy()`.\n> Omitting a subsystem (setting it to `null`) silently disables the\n> features that depend on it, with no code changes required in the handlers.\n\n### GOAPDirector — GOAP-to-FSM bridge\n\n`GOAPDirector` bridges `GOAPPlanner` with the FSM. Register it as the `COMBAT`\nhandler — when NPCs enter combat, the director replans and dispatches each\naction as a sub-state with full lifecycle management.\n\n```ts\nimport { GOAPDirector } from '@alife-sdk/ai/goap';\nimport type { IGOAPActionHandler } from '@alife-sdk/ai/goap';\n\nconst director = new GOAPDirector(planner, {\n  buildWorldState: (ctx) => WorldState.from({\n    isHealthy: ctx.health!.hpPercent >= 0.5,\n    inCover:   ctx.state.hasTakenCover,\n    seeEnemy:  ctx.perception?.hasVisibleEnemy() ?? false,\n  }),\n  goal: WorldState.from({ targetEliminated: true }),\n  actionHandlers: {\n    TakeCover: {\n      enter(ctx) { ctx.pathfinding?.findPath(coverX, coverY); },\n      update(ctx, dt) { return ctx.pathfinding?.isNavigating() ? 'running' : 'success'; },\n      exit(ctx) {},\n    },\n    Suppress: {\n      enter(ctx) { /* start firing */ },\n      update(ctx, dt) { return shotsFired >= 4 ? 'success' : 'running'; },\n      exit(ctx) {},\n    },\n    Attack: {\n      enter(ctx) { /* engage */ },\n      update(ctx, dt) { return 'running'; }, // terminal — stays until interrupted\n      exit(ctx) {},\n    },\n  },\n  interrupts: [\n    { condition: ctx => ctx.state.moraleState === 'PANICKED', targetState: 'FLEE' },\n    { condition: ctx => ctx.health!.hpPercent < 0.2, targetState: 'WOUNDED' },\n  ],\n  onNoPlan: (ctx, dt) => { /* fallback when plan is empty */ },\n});\n\nhandlers.register(ONLINE_STATE.COMBAT, director);\n```\n\nThe director:\n- **Replans** on every entry (including after interrupts like WOUNDED → COMBAT)\n- **Dispatches** actions: calls `enter()` → `update()` each tick → `exit()` on success/failure\n- **Advances** to the next action on `'success'`, replans on `'failure'`\n- **Checks interrupts** every tick before the action handler runs\n- **Prevents infinite replans** — stops after 3 consecutive empty plans\n- Stores plan state in `ctx.state.custom` (keys prefixed with `__goap`)\n\n### IPathfindingAccess — pluggable pathfinding\n\nState handlers can use obstacle-aware navigation via the optional `IPathfindingAccess`\nsubsystem. When provided, `moveAlongPath()` follows waypoints from the pathfinder\ninstead of moving in a straight line.\n\n```ts\n// Host implements IPathfindingAccess (e.g. wrapping PathFinding.js, EasyStar, or NavMesh)\nclass GridPathfinding implements IPathfindingAccess {\n  findPath(targetX, targetY) { /* A* search on grid */ }\n  getNextWaypoint()          { /* return next waypoint, advance cursor */ }\n  setPath(waypoints)         { /* replace current path */ }\n  isNavigating()             { /* true if cursor < path.length */ }\n  clearPath()                { /* stop following path */ }\n}\n\n// Wire to NPC context:\nconst ctx = { ..., pathfinding: new GridPathfinding(npc) };\n\n// In action handlers:\nctx.pathfinding?.findPath(target.x, target.y);  // compute path\nctx.pathfinding?.isNavigating();                 // check if walking\nctx.pathfinding?.clearPath();                    // stop\n```\n\nWhen `ctx.pathfinding` is null, all handlers fall back to direct straight-line movement.\n\n### Cover system\n\nThe cover module provides a full pipeline from raw cover points to per-NPC\npeek/fire cycles:\n\n1. **Register** cover points into `CoverRegistry` (world space positions + normal)\n2. **Evaluate** — choose the right evaluator for the situation:\n   - `CloseCoverEvaluator` — nearest cover\n   - `FarCoverEvaluator` — cover far from threat\n   - `BalancedCoverEvaluator` — balanced distance + angle\n   - `BestCoverEvaluator` — best angle + distance combined\n   - `AmbushCoverEvaluator` — optimal ambush position\n   - `SafeCoverEvaluator` — maximum distance from all threats\n3. **Lock** — `CoverLockRegistry.tryLock(coverId, npcId, { ttlMs })` so no two NPCs share a point\n4. **Loopholes** — each cover point has 1–N loophole offsets (count randomized, cached per point); `TakeCoverState` cycles `WAIT → PEEK → FIRE → RETURN`\n\n#### Cover workflow inside a state handler\n\nThe typical cover workflow in a custom state handler mirrors what `TakeCoverState`\ndoes internally:\n\n```ts\n// In your state handler's enter():\nenter(ctx: INPCContext): void {\n  const enemies = ctx.perception?.getVisibleEnemies() ?? [];\n  const enemy   = enemies[0] ?? null;\n\n  // 1. findCover() — searches CoverRegistry for the best available point.\n  //    Returns { x, y } or null. Internally stores the found point's ID.\n  let coverPt: { x: number; y: number } | null = null;\n  if (ctx.cover !== null && enemy !== null) {\n    coverPt = ctx.cover.findCover(ctx.x, ctx.y, enemy.x, enemy.y);\n  }\n\n  if (coverPt !== null) {\n    // 2. lockLastFound() — acquires a TTL lock on the point just returned\n    //    by findCover(). Returns false if already locked by another NPC.\n    const locked = ctx.cover?.lockLastFound?.(ctx.npcId, 8000) ?? true;\n    if (locked) {\n      ctx.state.coverPointX = coverPt.x;\n      ctx.state.coverPointY = coverPt.y;\n    } else {\n      coverPt = null; // contested — do not move to this point\n    }\n  }\n\n  // 3. Initialise the loophole phase cycle.\n  ctx.state.loophole = { phase: 'WAIT', phaseStartMs: ctx.now() };\n}\n\n// In your state handler's update(), cycle WAIT → PEEK → FIRE → RETURN:\nupdate(ctx: INPCContext, deltaMs: number): void {\n  const loophole = ctx.state.loophole;\n  const now      = ctx.now();\n  const enemies  = ctx.perception?.getVisibleEnemies() ?? [];\n  const enemy    = enemies[0] ?? null;\n\n  switch (loophole?.phase) {\n    case 'WAIT':\n      ctx.halt();\n      if (now >= ctx.state.lastGrenadeMs) {\n        loophole.phase = 'PEEK';\n        loophole.phaseStartMs = now;\n      }\n      break;\n\n    case 'PEEK':\n      // Move slightly toward enemy to simulate peeking out.\n      if (enemy) moveToward(ctx, enemy.x, enemy.y, speed * 0.5);\n      if (now - loophole.phaseStartMs >= cfg.loopholePeekDurationMs) {\n        loophole.phase = 'FIRE';\n        loophole.phaseStartMs = now;\n      }\n      break;\n\n    case 'FIRE':\n      ctx.halt();\n      if (enemy) {\n        ctx.emitShoot({ npcId: ctx.npcId, x: ctx.x, y: ctx.y,\n                        targetX: enemy.x, targetY: enemy.y,\n                        weaponType: ctx.state.primaryWeapon ?? 'rifle' });\n      }\n      if (now - loophole.phaseStartMs >= cfg.loopholeFireDurationMs) {\n        loophole.phase = 'RETURN';\n        loophole.phaseStartMs = now;\n      }\n      break;\n\n    case 'RETURN':\n      // Move back to cover centre, then restart WAIT.\n      moveToward(ctx, ctx.state.coverPointX, ctx.state.coverPointY, speed);\n      if (now - loophole.phaseStartMs >= cfg.loopholeReturnDurationMs) {\n        loophole.phase = 'WAIT';\n        ctx.state.lastGrenadeMs = now + waitDuration;\n      }\n      break;\n  }\n}\n\n// In exit(): release the lock so other NPCs can take the point.\nexit(ctx: INPCContext): void {\n  ctx.state.hasTakenCover = false;\n  ctx.state.loophole = null;\n  ctx.cover?.unlockAll?.(ctx.npcId);\n}\n```\n\n```ts\nimport { recommendCoverType } from '@alife-sdk/ai/cover';\n\nconst evaluatorType = recommendCoverType({\n  threatCount: 3,\n  npcHp:       15,\n  npcHpMax:    100,\n  isElite:     false,\n});\n// → 'SAFE' (low HP + many threats → maximize distance)\n```\n\n### GOAP — elite NPC planning\n\nFor NPCs with rank ≥ 5, `GOAPController` wraps a `GOAPPlanner` (A* on a\n17-property `WorldState`) to select and execute goal-oriented action\nsequences. Replanning happens automatically on a configurable interval or\nimmediately when `invalidatePlan()` is called.\n\n#### World state properties\n\n`buildWorldState(snapshot)` maps an `INPCWorldSnapshot` to 17 boolean\nproperties. All property keys come from the `WorldProperty` constant:\n\n| Property key | Source field | What it means |\n|---|---|---|\n| `alive` | `snapshot.isAlive` | NPC is alive |\n| `criticallyWounded` | `hpRatio <= 0.3` | HP at or below 30 % |\n| `hasWeapon` | `snapshot.hasWeapon` | Has a usable weapon |\n| `hasAmmo` | `snapshot.hasAmmo` | Has ammunition |\n| `inCover` | `snapshot.inCover` | Currently at a cover point |\n| `seeEnemy` | `snapshot.seeEnemy` | Enemy visible in FOV |\n| `enemyPresent` | `snapshot.enemyPresent` | Enemy known (seen or heard) |\n| `enemyInRange` | `snapshot.enemyInRange` | Enemy within weapon range |\n| `danger` | `snapshot.hasDanger` | General danger signal active |\n| `dangerGrenade` | `snapshot.hasDangerGrenade` | Grenade danger signal active |\n| `enemyWounded` | `snapshot.enemyWounded` | Last known enemy is wounded |\n| `anomalyNear` | `snapshot.nearAnomalyZone` | Anomaly zone inside proximity |\n| `enemySeeMe` | `snapshot.seeEnemy` | (derived) enemy has line of sight |\n| `readyToKill` | `hasWeapon && hasAmmo && seeEnemy && enemyInRange` | Can fire immediately |\n| `positionHeld` | `inCover && !seeEnemy` | Holding cover without exposure |\n| `lookedOut` | always `false` | One-shot peek flag (actions set it) |\n| `atTarget` | `!enemyPresent && !hasDanger` | Safe at destination |\n\n#### Goal selection\n\nGoals are chosen by evaluating `DEFAULT_GOAL_RULES` in priority order (lowest\nnumber wins):\n\n| Priority | Goal | Trigger |\n|---|---|---|\n| 0 `CRITICALLY_WOUNDED` | Heal + disengage | `hpRatio <= healHpThreshold` |\n| 1 `PANIC_FLEE` | Flee (morale collapsed) | `isPanicked && hasDanger` |\n| 2 `ENEMY_PRESENT` | Eliminate enemy | `snapshot.enemyPresent` |\n| 3 `DANGER` | Evade danger | `snapshot.hasDanger` |\n| 4 `ANOMALY_AVOID` | Exit anomaly zone | `snapshot.nearAnomalyZone` |\n| 5 `DEFAULT` | Patrol / idle | always (fallback) |\n\n#### Integration example\n\n```ts\nimport { GOAPController }         from '@alife-sdk/ai/goap';\nimport { GOAPPlanner }             from '@alife-sdk/core';\nimport type { IGOAPConfig }        from '@alife-sdk/ai/types';\nimport type { INPCWorldSnapshot }  from '@alife-sdk/ai/types';\n\n// 1. Create a planner and register actions once (shared across NPCs of same type).\nconst planner = new GOAPPlanner();\nplanner.registerAction(new PatrolAction());\nplanner.registerAction(new TakeCoverAction());\nplanner.registerAction(new EngageEnemyAction());\n\n// 2. Build config — replanIntervalMs drives periodic replanning.\nconst goapConfig: IGOAPConfig = {\n  replanIntervalMs:    5000,   // replan every 5 s at minimum\n  eliteRankThreshold:  5,\n  healHpThreshold:     0.3,\n  maxPlanDepth:        6,\n  dangerMemoryMaxAge:  10000,\n};\n\n// 3. Create one GOAPController per NPC (in your per-NPC setup / state handler).\nconst goap = new GOAPController(planner, goapConfig);\n\n// 4. Inside a state handler's update(), build a snapshot and tick GOAP.\nupdate(ctx: INPCContext, deltaMs: number): void {\n  const snapshot: INPCWorldSnapshot = {\n    isAlive:       ctx.health?.isAlive()  ?? true,\n    hpRatio:       ctx.health?.hpRatio()  ?? 1,\n    hasWeapon:     ctx.state.primaryWeapon !== null,\n    hasAmmo:       ctx.state.hasAmmo,\n    inCover:       ctx.state.hasTakenCover,\n    seeEnemy:      (ctx.perception?.getVisibleEnemies().length ?? 0) > 0,\n    enemyPresent:  ctx.state.lastKnownEnemyX !== 0,\n    enemyInRange:  ctx.state.enemyInRange,\n    hasDanger:     (ctx.danger?.getActiveZones().length ?? 0) > 0,\n    hasDangerGrenade: ctx.state.dangerGrenade,\n    enemyWounded:  ctx.state.enemyWounded,\n    nearAnomalyZone: ctx.state.nearAnomaly,\n  };\n\n  const entity = ctx.entity; // IEntity — your game object adapter\n  const result = goap.update(deltaMs, entity, snapshot);\n\n  if (!result.handled) {\n    // GOAP has no plan — fall back to FSM transition\n    ctx.transition('IDLE');\n  }\n\n  // Force immediate replan when significant world change occurs:\n  // goap.invalidatePlan();\n}\n\n// Custom world property builders and goal rules are opt-in:\nimport { DEFAULT_WORLD_PROPERTY_BUILDERS,\n         DEFAULT_GOAL_RULES } from '@alife-sdk/ai/goap';\n```\n\n### Animation integration\n\n`AnimationController` is a stateful per-NPC controller with debounce and\nlayer priority. It sits in front of your engine's animation API and\nprevents redundant `play()` calls.\n\n#### Layers\n\nAnimations are tagged with one of three `AnimLayer` values (defined as numeric\nconstants for priority comparison):\n\n| Layer | Value | Typical use |\n|---|---|---|\n| `LEGS` | `0` | Walking, running, crouching, idle |\n| `TORSO` | `1` | Combat stance, throw, fire |\n| `HEAD` | `2` | (reserved — for facial rigs etc.) |\n\nA higher numeric value wins when two layers compete. For example, a `TORSO`\nanimation (value `1`) overrides a `LEGS` animation (value `0`). The priority\nmapping can be overridden via `ILayerPriorityMap`.\n\n#### Debouncing\n\n`request()` is a no-op if the same `key + layer` is already playing. This\nmeans you can call it every frame without spamming the engine renderer.\n`force()` bypasses both the debounce and the priority check — use it for\none-shot events such as death animations or ability effects.\n\n#### Creating and using AnimationController\n\n```ts\nimport { AnimationController }     from '@alife-sdk/ai/animation';\nimport { getAnimationRequest }      from '@alife-sdk/ai/animation';\nimport type { IAnimationDriver }    from '@alife-sdk/ai/animation';\n\n// 1. Implement IAnimationDriver once for your engine.\n//    Phaser example:\nclass PhaserAnimDriver implements IAnimationDriver {\n  constructor(private readonly sprite: Phaser.GameObjects.Sprite) {}\n  play(key: string, opts: { loop: boolean; frameRate: number }): void {\n    this.sprite.anims.play({ key, loop: opts.loop, frameRate: opts.frameRate }, true);\n  }\n  hasAnimation(key: string): boolean {\n    return this.sprite.anims.exists(key);\n  }\n}\n\n// 2. Create one AnimationController per NPC.\nconst animController = new AnimationController({\n  driver: new PhaserAnimDriver(sprite),\n  // layerPriority: { [AnimLayer.TORSO]: 10 } — optional override\n});\n\n// 3. Inside a state handler's update(), resolve and request the animation.\nupdate(ctx: INPCContext, deltaMs: number): void {\n  const req = getAnimationRequest({\n    state:          ctx.driver.currentStateId,   // e.g. 'COMBAT'\n    weaponCategory: ctx.state.primaryWeaponType, // e.g. 2 → 'rifle'\n    velocity:       { x: ctx.vx, y: ctx.vy },\n    directionCache: this.dirCache,               // DirectionCache — avoids atan2 every frame\n  });\n\n  // request() skips play() if the same key+layer is already active (debounce).\n  animController.request(req);\n\n  // For one-shot events — bypasses debounce and priority:\n  // animController.force({ key: 'death_rifle', loop: false, frameRate: 8,\n  //                        layer: AnimLayer.LEGS });\n}\n\n// 4. On respawn or object-pool recycle:\nanimController.reset();\n```\n\nKey points:\n- `getAnimationRequest()` builds the animation key from `state + weaponCategory + direction`.\n  Default key format: `{base}_{weapon}_{direction}`, e.g. `combat_rifle_SE`.\n  States with `omitDirection: true` (e.g. `DEAD`, `GRENADE`, `SLEEP`) use\n  `{base}_{weapon}` only.\n- `DirectionCache.resolve(vx, vy)` caches the last 8-way compass direction and\n  only re-runs `atan2` when velocity changes by more than ~2 px/s.\n- The controller is stateless in terms of NPC data — one instance per NPC,\n  but one `StateHandlerMap` can carry a shared `AnimationController` factory.\n\n---\n\n## Lifecycle\n\n```\nkernel.use(aiPlugin)                ← register AI plugin\n  ↓\nkernel.init()                       ← freeze registries, init plugin\n  ↓\nNPC goes online:\n  coverAccess = aiPlugin.createCoverAccess(npcId)\n  driver = new OnlineAIDriver(ctx, handlers, ONLINE_STATE.IDLE)\n  ↓\nEvery frame:\n  kernel.update(delta)              ← A-Life tick + events flush\n  driver.update(delta)              ← state enter/update/exit\n  ↓\nNPC goes offline:\n  driver is discarded               ← no serialization needed\n  ↓\nSave / restore:\n  aiPlugin.serialize()              ← RestrictedZoneManager state\n  aiPlugin.restore(state)           ← rebuilds zones\n  ↓\nkernel.destroy()                    ← cleanup\n```\n\n---\n\n## Testing\n\nRun only the AI package tests:\n\n```\npnpm test --filter @alife-sdk/ai\n```\n\nEach system is designed for isolated unit tests:\n\n```ts\nimport { buildDefaultHandlerMap, OnlineAIDriver, ONLINE_STATE } from '@alife-sdk/ai/states';\n\n// Minimal stub context — implement only the interfaces your test exercises\nconst ctx = buildStubContext({ hp: 100, targetId: 'enemy1' });\nconst driver = new OnlineAIDriver(ctx, buildDefaultHandlerMap(), ONLINE_STATE.IDLE);\n\n// Transitions happen via ctx.transition() inside a state handler.\n// To test a forced transition, put it inside a stub handler's enter/update:\n//   enter: (ctx) => ctx.transition(ONLINE_STATE.COMBAT)\ndriver.update(16);\n\nexpect(driver.currentStateId).toBe(ONLINE_STATE.COMBAT);\n```\n\nTips:\n- State handlers are stateless objects — instantiate once, reuse across tests\n- `INPCOnlineState` is a plain object — mutate it directly in tests to simulate phase changes\n- `CoverRegistry` / `CoverLockRegistry` have no external dependencies — construct them directly\n- `SuspicionAccumulator` and `ConditionBank` are standalone classes with no kernel dependency\n\n---\n\n## Module map\n\n```\nsrc/\n├── plugin/       AIPlugin, IAIPluginConfig, createDefaultAIPluginConfig\n├── states/       OnlineAIDriver, StateHandlerMap, ONLINE_STATE, builder functions\n│   ├── handlers/ DeadState, IdleState, PatrolState, AlertState, FleeState,\n│   │             SearchState, CampState, SleepState, CombatState, TakeCoverState,\n│   │             GrenadeState, EvadeGrenadeState, WoundedState, RetreatState,\n│   │             MonsterCombatController, ChargeState, StalkState, LeapState, PsiAttackState\n│   └── eat-corpse/ EatCorpseState (opt-in monster sub-state)\n├── cover/        CoverRegistry, CoverLockRegistry, CoverAccessAdapter,\n│                 6 evaluators, LoopholeGenerator, findBestLoophole, recommendCoverType\n├── perception/   NPCSensors, isInFOV, filterVisibleEntities, filterHearingEntities,\n│                 filterHostileEntities, filterFriendlyEntities, filterFreshIntel\n├── goap/         GOAPController, buildWorldState, selectGoal, EvadeHazardAction,\n│                 DEFAULT_WORLD_PROPERTY_BUILDERS, DEFAULT_GOAL_RULES\n├── navigation/   smoothPath, smoothPathWithTurning, SmoothPathFollower,\n│                 RestrictedZoneManager, RestrictionType, SteeringBehaviors\n├── squad/        evaluateSituation, SquadCommand, canApplyCommand,\n│                 SquadSharedTargetTable, PROTECTED_STATES\n├── animation/    getDirection, getAnimationKey, AnimationController,\n│                 DirectionCache, DEFAULT_STATE_ANIM_MAP, DEFAULT_WEAPON_SUFFIXES\n├── sound/        VocalizationType, VocalizationTracker, createDefaultVocalizationConfig\n├── suspicion/    SuspicionAccumulator, SuspicionStimuli, createDefaultSuspicionConfig\n├── conditions/   ConditionBank, ConditionChannels, createDefaultConditionBankConfig\n├── combat/       combat helpers used by state handlers\n├── types/        INPCContext, INPCOnlineState, IOnlineStateHandler, INPCPerception,\n│                 INPCHealth, ICoverAccess, IDangerAccess, ISquadAccess, IPackAccess,\n│                 ISuspicionAccess, IConditionAccess, IShootPayload, IMeleeHitPayload\n├── config/       IStateConfig, IStateTransitionMap, createDefaultStateConfig\n└── ports/        AI-specific port interfaces (IRestrictedZoneAccess, IHazardZoneAccess)\n```\n\n---\n\n## Common pitfalls\n\n**NPC stuck in a state — update() is called but the state never changes**\n\nState transitions are only triggered by `ctx.transition()` being called from\ninside a state handler's `enter()` or `update()`. If your state never calls\n`ctx.transition()`, the driver stays in that state indefinitely.\nCheck the transition conditions in your handler: perception null-checks, timer\ncomparisons, and morale/health thresholds are the most common culprits.\n\n**Transitions not firing — `ctx.transition()` is called externally but ignored**\n\n`ctx.transition()` must be called from within a state handler's `enter()` or\n`update()` method. Calling it from outside (e.g. from your game's event\nhandler directly) does nothing because the driver only processes transitions\nduring its own `update()` call. To force a state change from outside,\nmutate `ctx.state` to put the NPC in the correct pre-condition, then let\nthe next `driver.update()` frame evaluate the transition naturally.\n\n**Cover system returns null — NPC never finds a cover point**\n\nWork through this checklist:\n1. `CoverRegistry` populated? Call `registry.getSize()` — if it returns `0`,\n   no points have been registered. Call `registry.addPoints([...])` during\n   scene setup.\n2. Cover points within search radius? The default `searchRadius` from `ICoverConfig`\n   must be larger than the distance between the NPC and the nearest point.\n3. All points locked? If `CoverLockRegistry` is in use, points expire\n   automatically after their TTL. Check that `ttlMs` is not set so high\n   that points remain locked indefinitely.\n4. Score threshold too strict? `minScoreThreshold` in `ICoverConfig` filters\n   out low-quality candidates. Lower it for sparse maps.\n\n**Subsystem returning null — calls like `ctx.cover.findCover()` crash**\n\nAll subsystems on `INPCContext` are typed `T | null`. You must access them\nwith optional chaining:\n\n```ts\n// Safe — no-op if cover is null:\nconst pt = ctx.cover?.findCover(ctx.x, ctx.y, ex, ey) ?? null;\n\n// Safe — defaults to empty array:\nconst enemies = ctx.perception?.getVisibleEnemies() ?? [];\n\n// Safe — defaults to 1 (full HP):\nconst hp = ctx.health?.hpRatio() ?? 1;\n```\n\nSetting a subsystem to `null` on `INPCContext` silently disables the\nfeatures that depend on it — the built-in state handlers all guard with\noptional chaining, so no code changes are needed in the handlers themselves.\n\n**GOAP plan never advances — `result.handled` is always `false`**\n\nThe most common causes:\n- No actions registered in `GOAPPlanner`. Call `planner.registerAction(...)` before\n  creating `GOAPController`.\n- `INPCWorldSnapshot` already satisfies the selected goal. When the world state\n  already matches the goal, the planner returns an empty plan and the controller\n  yields `handled: false`. This is correct behaviour — GOAP is done.\n- `action.isValid(entity)` returns `false` on every action. Each action\n  aborts immediately and the plan is marked invalid, triggering a replan loop.\n  Log `goap.getCurrentPlanIds()` and `goap.getLastGoalResult()` to diagnose.\n","readmeFilename":"README.md"}