{"_id":"@alife-sdk/hazards","_rev":"5-06e7009c5984b502e89b7bb269930a65","name":"@alife-sdk/hazards","dist-tags":{"latest":"0.4.1"},"versions":{"0.1.0":{"name":"@alife-sdk/hazards","version":"0.1.0","keywords":["alife","hazards","anomaly","artefact","zone"],"author":{"name":"Eugene Rusakov"},"license":"MIT","_id":"@alife-sdk/hazards@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":"05040f55e874b7fa3ca7a72d4a6ed2c5e18c31f5","tarball":"https://registry.npmjs.org/@alife-sdk/hazards/-/hazards-0.1.0.tgz","fileCount":67,"integrity":"sha512-qWA92sG9xc6a8FqkWtp31JIn8vkXwoGRpLP/Kp6iXIs56XWnC8PQpkAETrZ8nBjR1Gpnh+In//fIUN4cBPuCPA==","signatures":[{"sig":"MEUCIQDKcFWTJXs+7VzpCQmYU7BckKX5jVcgz5VSs5BsL1Uv2gIgbypyc/rLydL2O0s0xyMNPm6nkKctjfGkaDGT45uP+Rc=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":75815},"main":"dist/index.js","type":"module","_from":"file:alife-sdk-hazards-0.1.0.tgz","types":"dist/index.d.ts","engines":{"node":">=20.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./zone":{"types":"./dist/zone/index.d.ts","import":"./dist/zone/index.js"},"./ports":{"types":"./dist/ports/index.d.ts","import":"./dist/ports/index.js"},"./events":{"types":"./dist/events/index.d.ts","import":"./dist/events/index.js"},"./plugin":{"types":"./dist/plugin/index.d.ts","import":"./dist/plugin/index.js"},"./manager":{"types":"./dist/manager/index.d.ts","import":"./dist/manager/index.js"},"./artefact":{"types":"./dist/artefact/index.d.ts","import":"./dist/artefact/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/5575f50f01077f10ea3205bdfde7880e/alife-sdk-hazards-0.1.0.tgz","_integrity":"sha512-qWA92sG9xc6a8FqkWtp31JIn8vkXwoGRpLP/Kp6iXIs56XWnC8PQpkAETrZ8nBjR1Gpnh+In//fIUN4cBPuCPA==","repository":{"url":"git+https://github.com/eurusik/alife-sdk.git","type":"git","directory":"packages/alife-hazards"},"_npmVersion":"10.8.1","description":"Hazard zones, anomalies and artefact spawning for A-Life SDK","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"},"_npmOperationalInternal":{"tmp":"tmp/hazards_0.1.0_1772739946581_0.22319776807334302","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@alife-sdk/hazards","version":"0.1.1","keywords":["alife","hazards","anomaly","artefact","zone"],"author":{"name":"Eugene Rusakov"},"license":"MIT","_id":"@alife-sdk/hazards@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":"a4ad76758194de81fdb3ac1a86770414bc3883a0","tarball":"https://registry.npmjs.org/@alife-sdk/hazards/-/hazards-0.1.1.tgz","fileCount":67,"integrity":"sha512-v0PZ1Z8o/JTg7HLr6vGYtkbnEJbgHnGt5hMCSZCXCKs3JTSfQBuNF/pc9+Myb8QL5PeJKLgBdL7lZL7IZqemnA==","signatures":[{"sig":"MEUCICo6c4WYBcIgjmoZzmQU6DrJTsFRONb3rPtfZRW1hcobAiEA6nDN8kFb1JAKKp2Z915eE0pRCeGho8tty7Aj4ILsBb8=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":78124},"main":"dist/index.js","type":"module","_from":"file:alife-sdk-hazards-0.1.1.tgz","types":"dist/index.d.ts","engines":{"node":">=20.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./zone":{"types":"./dist/zone/index.d.ts","import":"./dist/zone/index.js"},"./ports":{"types":"./dist/ports/index.d.ts","import":"./dist/ports/index.js"},"./events":{"types":"./dist/events/index.d.ts","import":"./dist/events/index.js"},"./plugin":{"types":"./dist/plugin/index.d.ts","import":"./dist/plugin/index.js"},"./manager":{"types":"./dist/manager/index.d.ts","import":"./dist/manager/index.js"},"./artefact":{"types":"./dist/artefact/index.d.ts","import":"./dist/artefact/index.js"}},"scripts":{"dev":"tsc --watch","test":"vitest run","build":"tsc","test:watch":"vitest"},"_npmUser":{"name":"eurusik","email":"john.rusakov@gmail.com"},"_resolved":"/tmp/9c0a16e17eb77c2982ecb5ca88512877/alife-sdk-hazards-0.1.1.tgz","_integrity":"sha512-v0PZ1Z8o/JTg7HLr6vGYtkbnEJbgHnGt5hMCSZCXCKs3JTSfQBuNF/pc9+Myb8QL5PeJKLgBdL7lZL7IZqemnA==","repository":{"url":"git+https://github.com/eurusik/alife-sdk.git","type":"git","directory":"packages/alife-hazards"},"_npmVersion":"10.8.2","description":"Hazard zones, anomalies and artefact spawning for A-Life SDK","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"},"_npmOperationalInternal":{"tmp":"tmp/hazards_0.1.1_1772742735314_0.42794639713201654","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@alife-sdk/hazards","version":"0.3.0","keywords":["alife","hazards","anomaly","artefact","zone"],"author":{"name":"Eugene Rusakov"},"license":"MIT","_id":"@alife-sdk/hazards@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":"c24f0db5924af67204c970f092dff54b568079ac","tarball":"https://registry.npmjs.org/@alife-sdk/hazards/-/hazards-0.3.0.tgz","fileCount":67,"integrity":"sha512-qZBUGOA3RII1p9RM0M5TpnQeECuGf9jDiP04c7IDvg4QV2r7AJvACVtIbrJxlMdHer/C/TxeWyV/uG2B1XyczA==","signatures":[{"sig":"MEUCIQCHXjMcDxBcmVyot/cxNmp0nWCSEYvjbENrZn5GJ17/KwIgL+hq7wY80wVp305lUXdk5yDmjsg8CBimFR6iIlqaU1Y=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":78124},"main":"dist/index.js","type":"module","_from":"file:alife-sdk-hazards-0.3.0.tgz","types":"dist/index.d.ts","engines":{"node":">=20.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./zone":{"types":"./dist/zone/index.d.ts","import":"./dist/zone/index.js"},"./ports":{"types":"./dist/ports/index.d.ts","import":"./dist/ports/index.js"},"./events":{"types":"./dist/events/index.d.ts","import":"./dist/events/index.js"},"./plugin":{"types":"./dist/plugin/index.d.ts","import":"./dist/plugin/index.js"},"./manager":{"types":"./dist/manager/index.d.ts","import":"./dist/manager/index.js"},"./artefact":{"types":"./dist/artefact/index.d.ts","import":"./dist/artefact/index.js"}},"scripts":{"dev":"tsc --watch","test":"vitest run","build":"tsc","test:watch":"vitest"},"_npmUser":{"name":"eurusik","email":"john.rusakov@gmail.com"},"_resolved":"/tmp/fe6f5107b90c8af52610bc0a6e55165b/alife-sdk-hazards-0.3.0.tgz","_integrity":"sha512-qZBUGOA3RII1p9RM0M5TpnQeECuGf9jDiP04c7IDvg4QV2r7AJvACVtIbrJxlMdHer/C/TxeWyV/uG2B1XyczA==","repository":{"url":"git+https://github.com/eurusik/alife-sdk.git","type":"git","directory":"packages/alife-hazards"},"_npmVersion":"10.8.2","description":"Hazard zones, anomalies and artefact spawning for A-Life SDK","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"},"_npmOperationalInternal":{"tmp":"tmp/hazards_0.3.0_1772789682507_0.7639083525045993","host":"s3://npm-registry-packages-npm-production"}},"0.4.0":{"name":"@alife-sdk/hazards","version":"0.4.0","keywords":["alife","hazards","anomaly","artefact","zone"],"author":{"name":"Eugene Rusakov"},"license":"MIT","_id":"@alife-sdk/hazards@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":"d1eab4711c3a7121cf5a2c74cce4e11ed5aded96","tarball":"https://registry.npmjs.org/@alife-sdk/hazards/-/hazards-0.4.0.tgz","fileCount":67,"integrity":"sha512-AcwSdM2hsY/mVf3qIoJV9216ucGrAvnzHr+I/O4wE+31JS5JxBl3i0GqwoXXwf7PNfT4id3JjsGu3vFFBmFThg==","signatures":[{"sig":"MEYCIQDhZVBM1WFY30sVlfysjN+42Bifyy39XvwCERlMUTOCEQIhAOZau/M0Jtk8c3ercuSoJWDg+jHc1ExAQBlGO1632Pmf","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":78124},"main":"dist/index.js","type":"module","_from":"file:alife-sdk-hazards-0.4.0.tgz","types":"dist/index.d.ts","engines":{"node":">=20.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./zone":{"types":"./dist/zone/index.d.ts","import":"./dist/zone/index.js"},"./ports":{"types":"./dist/ports/index.d.ts","import":"./dist/ports/index.js"},"./events":{"types":"./dist/events/index.d.ts","import":"./dist/events/index.js"},"./plugin":{"types":"./dist/plugin/index.d.ts","import":"./dist/plugin/index.js"},"./manager":{"types":"./dist/manager/index.d.ts","import":"./dist/manager/index.js"},"./artefact":{"types":"./dist/artefact/index.d.ts","import":"./dist/artefact/index.js"}},"scripts":{"dev":"tsc --watch","test":"vitest run","build":"tsc","test:watch":"vitest"},"_npmUser":{"name":"eurusik","email":"john.rusakov@gmail.com"},"_resolved":"/tmp/6e9620cbab595f5c47808edf6f30990c/alife-sdk-hazards-0.4.0.tgz","_integrity":"sha512-AcwSdM2hsY/mVf3qIoJV9216ucGrAvnzHr+I/O4wE+31JS5JxBl3i0GqwoXXwf7PNfT4id3JjsGu3vFFBmFThg==","repository":{"url":"git+https://github.com/eurusik/alife-sdk.git","type":"git","directory":"packages/alife-hazards"},"_npmVersion":"10.8.2","description":"Hazard zones, anomalies and artefact spawning for A-Life SDK","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"},"_npmOperationalInternal":{"tmp":"tmp/hazards_0.4.0_1772823546522_0.3510845544327774","host":"s3://npm-registry-packages-npm-production"}},"0.4.1":{"name":"@alife-sdk/hazards","version":"0.4.1","description":"Hazard zones, anomalies and artefact spawning for A-Life SDK","type":"module","sideEffects":false,"main":"dist/index.js","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./zone":{"types":"./dist/zone/index.d.ts","import":"./dist/zone/index.js"},"./artefact":{"types":"./dist/artefact/index.d.ts","import":"./dist/artefact/index.js"},"./manager":{"types":"./dist/manager/index.d.ts","import":"./dist/manager/index.js"},"./events":{"types":"./dist/events/index.d.ts","import":"./dist/events/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"}},"keywords":["alife","hazards","anomaly","artefact","zone"],"license":"MIT","author":{"name":"Eugene Rusakov"},"repository":{"type":"git","url":"git+https://github.com/eurusik/alife-sdk.git","directory":"packages/alife-hazards"},"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"},"scripts":{"build":"tsc","dev":"tsc --watch","test":"vitest run","test:watch":"vitest"},"_id":"@alife-sdk/hazards@0.4.1","_integrity":"sha512-AvI+l9mNMIiKEQi6SlT4yEVjrNDsugTXvmh4VC00mQXMOhZ83xd463bVwGc1jx964ZT6BmDuC7a1eRRw4Fn9GA==","_resolved":"/tmp/1f37db1dda3535a83e88a151d988e4e1/alife-sdk-hazards-0.4.1.tgz","_from":"file:alife-sdk-hazards-0.4.1.tgz","_nodeVersion":"20.20.1","_npmVersion":"10.8.2","dist":{"integrity":"sha512-AvI+l9mNMIiKEQi6SlT4yEVjrNDsugTXvmh4VC00mQXMOhZ83xd463bVwGc1jx964ZT6BmDuC7a1eRRw4Fn9GA==","shasum":"52b5ff2095799c434f445c50c11d0cbf25171ae7","tarball":"https://registry.npmjs.org/@alife-sdk/hazards/-/hazards-0.4.1.tgz","fileCount":67,"unpackedSize":78124,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCaqgEsNlmeV1oilBNJaGroKSplW8IVlZEqYGQF4IYREwIhAJeRFK2dLh4xpyqsKgYGK/CPIEsIrgln08Itj8Errx8k"}]},"_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/hazards_0.4.1_1773837144587_0.9887508640547511"},"_hasShrinkwrap":false}},"time":{"created":"2026-03-05T19:45:46.489Z","modified":"2026-03-18T12:32:24.882Z","0.1.0":"2026-03-05T19:45:46.749Z","0.1.1":"2026-03-05T20:32:15.457Z","0.3.0":"2026-03-06T09:34:42.659Z","0.4.0":"2026-03-06T18:59:06.669Z","0.4.1":"2026-03-18T12:32:24.736Z"},"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","hazards","anomaly","artefact","zone"],"repository":{"type":"git","url":"git+https://github.com/eurusik/alife-sdk.git","directory":"packages/alife-hazards"},"description":"Hazard zones, anomalies and artefact spawning for A-Life SDK","maintainers":[{"name":"eurusik","email":"john.rusakov@gmail.com"}],"readme":"# @alife-sdk/hazards\n\nHazard zones, anomaly damage, and artefact spawning for A-Life simulations.\n\nDepends on @alife-sdk/core\n\n```ts\nimport { HazardsPlugin, createDefaultHazardsConfig } from '@alife-sdk/hazards/plugin';\nimport { HazardEvents } from '@alife-sdk/hazards/events';\n```\n\nEngine-agnostic. Works with Phaser, PixiJS, Node.js, or any other runtime.\nZero rendering dependencies — all output goes through your event bus and one\nfactory callback.\n\n```\nnpm install @alife-sdk/hazards\n```\n\n---\n\n## Quick start (7 steps)\n\n```ts\nimport { ALifeKernel, SeededRandom }                             from '@alife-sdk/core';\nimport { HazardsPlugin, createDefaultHazardsConfig }             from '@alife-sdk/hazards/plugin';\nimport { HazardEvents }                                          from '@alife-sdk/hazards/events';\n\n// 1. Create the plugin\nconst random  = new SeededRandom(42);\nconst hazards = new HazardsPlugin(random, createDefaultHazardsConfig({\n  zones: anomaliesJson,            // IHazardZoneConfig[] loaded from your data file\n  artefactFactory: {\n    // Only callback you must implement — create a pickup object in your engine\n    create(ev) { scene.spawnPickup(ev.x, ev.y, ev.artefactId); },\n  },\n}));\n\n// 2. Register artefact definitions\nhazards.artefacts\n  .register({ id: 'soul',     zoneTypes: ['radiation'],        weight: 3 })\n  .register({ id: 'fireball', zoneTypes: ['fire'],             weight: 2 })\n  .register({ id: 'jellyfish', zoneTypes: ['radiation', 'psi'], weight: 1 });\n\n// 3. Register with kernel\nconst kernel = new ALifeKernel({ /* … */ });\nkernel.use(hazards);\n\n// 4. Subscribe to hazard events\nhazards.events.on(HazardEvents.HAZARD_DAMAGE, ({ entityId, damage, zoneType }) => {\n  world.getEntity(entityId)?.takeDamage(damage, zoneType);\n});\nhazards.events.on(HazardEvents.ARTEFACT_COLLECTED, ({ artefactId, collectorId }) => {\n  inventory.of(collectorId).addItem(artefactId, 1);\n});\n\n// 5. Init (freezes artefact registry)\nkernel.init();\n\n// 6. Each frame — tick with live entities\nfunction update(deltaMs: number) {\n  kernel.update(deltaMs);\n  hazards.manager.tick(deltaMs, world.getLiveEntities());\n}\n\n// 7. When a player collects an artefact\nhazards.manager.notifyArtefactCollected(zoneId, instanceId, artefactId, player.id);\n```\n\n> **Why manual tick?** `HazardsPlugin.update()` is a no-op because only your host code knows how to fetch the live entity list. Call `manager.tick()` each frame right after `kernel.update()`.\n\n---\n\n## Sub-path imports\n\n| Import | What you get |\n|--------|-------------|\n| `@alife-sdk/hazards/plugin` | `HazardsPlugin`, `HazardsPluginToken`, `createDefaultHazardsConfig` |\n| `@alife-sdk/hazards/zone` | `HazardZone`, `IHazardZoneConfig`, `HazardZoneType` |\n| `@alife-sdk/hazards/artefact` | `ArtefactRegistry`, `WeightedArtefactSelector`, `IArtefactDefinition` |\n| `@alife-sdk/hazards/manager` | `HazardManager`, `IHazardEntity`, `IHazardManagerConfig` |\n| `@alife-sdk/hazards/events` | `HazardEvents`, `HazardEventKey`, `HazardEventPayloads` |\n| `@alife-sdk/hazards/ports` | `IArtefactFactory`, `IArtefactSpawnEvent` |\n\n---\n\n## Architecture\n\n```\n                        ┌─────────────────────────────────────────┐\n                        │             HazardsPlugin                │\n                        │  artefacts  │  events  │  manager        │\n                        └─────────────┼──────────┼─────────────────┘\n                                      │          │\n              ┌───────────────────────┘          │\n              ▼                                  ▼\n   ┌──────────────────────┐          ┌─────────────────────────────┐\n   │   ArtefactRegistry   │          │       HazardManager         │\n   │  register / freeze   │          │  addZone / tick / queries   │\n   │  pickForZone()       │          │  SpatialGrid (O(k) lookup)  │\n   └──────────┬───────────┘          └──────────┬──────────────────┘\n              │                                  │\n              ▼                                  ▼\n   ┌──────────────────────┐          ┌─────────────────────────────┐\n   │  WeightedArtefact    │          │        HazardZone           │\n   │    Selector          │          │  advance / isDamageReady    │\n   └──────────────────────┘          │  containsPoint / getDmg     │\n                                     └─────────────────────────────┘\n                      │ spawn event\n                      ▼\n            ┌──────────────────┐\n            │ IArtefactFactory │  ← you implement\n            │   create(event)  │\n            └──────────────────┘\n```\n\n---\n\n## Key concepts\n\n### IHazardEntity\n\nThe minimal shape your entity objects must satisfy when passed to `manager.tick()`.\nImported from `@alife-sdk/hazards/manager`.\n\n```ts\ninterface IHazardEntity {\n  readonly id: string;                         // unique entity identifier\n  readonly position: Vec2;                     // current world-space position { x, y }\n  readonly immunity?: ReadonlyMap<string, number>; // optional per-type resistance (see Immunity system below)\n  isAlive?(): boolean;                         // optional — entities returning false are skipped during damage ticks\n}\n```\n\nYour full entity class does not need to extend this — TypeScript structural typing means any\nobject with the required fields is accepted.\n\n---\n\n### HazardZone\n\nA circular area defined by centre `(x, y)` and `radius`. Maintains two\nindependent timers:\n\n- **Damage timer** — fires every `damageTickIntervalMs` (default 500 ms), applies\n  `damagePerSecond × interval / 1000` to every entity inside the zone.\n- **Artefact timer** — fires every `artefactSpawnCycleMs` (default 60 s), runs\n  a lottery (`artefactChance`) to attempt a spawn if `maxArtefacts` not reached.\n\nAdditional config options:\n\n- `entityFilter?: (entity: IHazardEntity) => boolean` — per-zone entity predicate; only entities for which this returns `true` receive damage. If omitted, all entities inside the zone are affected.\n- `expiresAtMs?: number` — auto-expiration timestamp in accumulated elapsed ms. When the manager's internal elapsed time reaches this value, the zone is removed and `hazard:zone_expired` is emitted. If omitted, the zone lives indefinitely.\n\nZone type is an **open enum** — use built-ins or add your own:\n\n```ts\ntype HazardZoneType = 'fire' | 'radiation' | 'chemical' | 'psi' | (string & {});\n```\n\n### Spatial queries\n\n`HazardManager` provides two O(k) spatial query methods backed by an internal `SpatialGrid`:\n\n```ts\n// Returns the first zone whose circle contains the point, or null if none.\nconst zone = manager.getZoneAtPoint(npc.x, npc.y);\nif (zone) console.log(`NPC is inside zone: ${zone.config.id}`);\n\n// Returns all zones whose circles overlap a search radius around the point.\n// Useful for NPC terrain scoring — find nearby hazards before moving.\nconst nearby = manager.getZonesInRadius(npc.x, npc.y, 150);\nfor (const z of nearby) {\n  ai.avoidPoint(z.config.x, z.config.y, z.config.radius);\n}\n```\n\n---\n\n### Immunity system\n\nEach entity can carry per-type resistance factors `[0–1]`:\n\n```ts\nconst immunity = new Map([\n  // Resistance fraction [0–1]: damage = rawDamage × (1 - resistance)\n  // 1.0 = full immunity — no damage dealt and no hazard:damage event fired.\n  ['radiation', 0.5],   // 50% resistance → entity takes rawDamage × 0.5\n  ['fire',      1.0],   // fully immune to fire — no damage, no event\n]);\n```\n\nThe resistance key matches the zone's `type` string. No resistance = full damage.\n\n### Artefact spawning\n\nSpawn position is sampled on the **perimeter band** (60–95% of zone radius from\ncentre) — artefacts appear near the edge, not dead-centre.\n\n`WeightedArtefactSelector` picks by weight. Override with any `IArtefactSelector`\nfor custom logic (rarest-first, quest-specific pools, etc.).\n\n### Event bus\n\n`HazardsPlugin` owns a **dedicated** `EventBus<HazardEventPayloads>` — separate\nfrom the kernel's core bus. Events are flushed in a single batch after all zones\nare processed each tick:\n\n| Event | When |\n|-------|------|\n| `hazard:damage` | Entity inside zone when damage tick fires |\n| `hazard:artefact_spawned` | Artefact spawned successfully |\n| `hazard:artefact_collected` | `notifyArtefactCollected()` called |\n| `hazard:zone_expired` | Zone auto-removes when elapsed time reaches `expiresAtMs` |\n\n---\n\n## Lifecycle\n\n```\nHazardsPlugin constructor\n  └─ register artefact definitions (plugin.artefacts)\n\nkernel.use(hazards)          — install(): creates EventBus + HazardManager\n  └─ subscribe to plugin.events here\n\nkernel.init()                — init(): freezes artefact registry\n\ngame loop:\n  hazards.manager.tick(deltaMs, entities)\n\ndynamic zone removal:\n  hazards.manager.removeZone(id)   // removes zone immediately; does NOT emit hazard:zone_expired\n\nplayer collects artefact:\n  hazards.manager.notifyArtefactCollected(…)\n\nkernel.destroy()             — cleans up manager + event bus\n```\n\n---\n\n## Save/Load\n\n`HazardManager` (and `HazardsPlugin`) expose `serialize()` / `restore()` for save-game integration.\n\n**What is serialized:** timer state (damage timer, artefact timer) and artefact counts for each zone, plus the manager's accumulated `elapsedMs`.\n\n**What is NOT serialized:** zone configs. You must re-add all zones via `addZone()` (or the `zones` constructor option) before calling `restore()`.\n\n```ts\n// On save\nconst snapshot = hazards.serialize();\nsaveFile.write('hazards', snapshot);\n\n// On load — zones must be re-registered first\nconst kernel2 = new ALifeKernel({ /* … */ });\nkernel2.use(hazards2);   // install() re-creates manager and re-adds config zones\nkernel2.init();\nhazards2.restore(saveFile.read('hazards'));\n```\n\n---\n\n## Tests\n\n58 tests, 0 failures:\n\n```\npnpm --filter @alife-sdk/hazards test\n```\n\nCovers: zone timers (advance/consume/carry-over), damage with immunity, artefact\nspawn lottery, spatial queries, plugin lifecycle, registry freeze/duplicate guards,\nand 4 integration scenarios.\n\n---\n\n## Module map\n\n| Module | README |\n|--------|--------|\n| `plugin/` | [`plugin/README.md`](src/plugin/README.md) — entry point, kernel integration |\n| `manager/` | [`manager/README.md`](src/manager/README.md) — tick, spatial queries, zone lifecycle |\n| `zone/` | [`zone/README.md`](src/zone/README.md) — HazardZone timers, config reference |\n| `artefact/` | [`artefact/README.md`](src/artefact/README.md) — registry, weighted selection, spawner |\n| `events/` | [`events/README.md`](src/events/README.md) — event constants and payload types |\n| `ports/` | [`ports/README.md`](src/ports/README.md) — IArtefactFactory (the one callback you implement) |\n\n## See also\n\n- [`@alife-sdk/simulation`](../alife-simulation/README.md) — surge events in simulation trigger hazard zone behaviour\n- [`@alife-sdk/economy`](../alife-economy/README.md) — artefacts spawned by hazard zones can be collected and traded\n- [`@alife-sdk/persistence`](../alife-persistence/README.md) — hazard zone state and artefact positions are included in save data\n","readmeFilename":"README.md"}