{"_id":"@alife-sdk/social","_rev":"5-cacdbde16b768158e6e99d8f09666523","name":"@alife-sdk/social","dist-tags":{"latest":"0.4.1"},"versions":{"0.1.0":{"name":"@alife-sdk/social","version":"0.1.0","keywords":["alife","game-ai","social","npc","campfire","dialogue"],"author":{"name":"Eugene Rusakov"},"license":"MIT","_id":"@alife-sdk/social@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":"12576e61b02bb0040f632052adfd338e5b7f21f1","tarball":"https://registry.npmjs.org/@alife-sdk/social/-/social-0.1.0.tgz","fileCount":99,"integrity":"sha512-7G1fwjnMIMWrpsBHc6/fG6TfBIVbp60Ao1fSKu69jEZ7I/X8CjQe707qYxBOsCAQugOMGZ5LN0+g//W8gZlb9A==","signatures":[{"sig":"MEUCIQCE87mNdaQBZKwPswc3INkZzcyyu+uFs6RCYKBDKODIogIgcDVGC1GyCA5N4qC48R+hQ1ZVJlUt4wxY+UKt7KcDD0M=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":156017},"main":"dist/index.js","type":"module","_from":"file:alife-sdk-social-0.1.0.tgz","types":"dist/index.d.ts","engines":{"node":">=20.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./meet":{"types":"./dist/meet/index.d.ts","import":"./dist/meet/index.js"},"./ports":{"types":"./dist/ports/index.d.ts","import":"./dist/ports/index.js"},"./types":{"types":"./dist/types/index.d.ts","import":"./dist/types/index.js"},"./plugin":{"types":"./dist/plugin/index.d.ts","import":"./dist/plugin/index.js"},"./remark":{"types":"./dist/remark/index.d.ts","import":"./dist/remark/index.js"},"./content":{"types":"./dist/content/index.d.ts","import":"./dist/content/index.js"},"./campfire":{"types":"./dist/campfire/index.d.ts","import":"./dist/campfire/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/7365fb12b80faf88cd5b66fa48887af5/alife-sdk-social-0.1.0.tgz","_integrity":"sha512-7G1fwjnMIMWrpsBHc6/fG6TfBIVbp60Ao1fSKu69jEZ7I/X8CjQe707qYxBOsCAQugOMGZ5LN0+g//W8gZlb9A==","repository":{"url":"git+https://github.com/eurusik/alife-sdk.git","type":"git","directory":"packages/alife-social"},"_npmVersion":"10.8.1","description":"NPC social interaction systems — greetings, remarks, campfire storytelling","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/social_0.1.0_1772739946881_0.3513410253563869","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@alife-sdk/social","version":"0.1.1","keywords":["alife","game-ai","social","npc","campfire","dialogue"],"author":{"name":"Eugene Rusakov"},"license":"MIT","_id":"@alife-sdk/social@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":"b57ca02498cbe17cae29d8c659f5fc6f0ac0fc4d","tarball":"https://registry.npmjs.org/@alife-sdk/social/-/social-0.1.1.tgz","fileCount":87,"integrity":"sha512-di9Vzf1Zztu+jlGAcKBBMnOjvstt9z6RWu+szYlOHiLvwxUW29PlwlM8H/oYMY6q8VnP1MXnN9rrZ3BMB5VRXg==","signatures":[{"sig":"MEUCIQD6zS1yUYHpPsK2oRHeZoRehDhE3GLsM3xNAwHifHhqvAIgHwNrhlGxaO/qw5J+D77mDPiHb2qrL7UQ9UvGdanIxLc=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":136652},"main":"dist/index.js","type":"module","_from":"file:alife-sdk-social-0.1.1.tgz","types":"dist/index.d.ts","engines":{"node":">=20.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./meet":{"types":"./dist/meet/index.d.ts","import":"./dist/meet/index.js"},"./ports":{"types":"./dist/ports/index.d.ts","import":"./dist/ports/index.js"},"./types":{"types":"./dist/types/index.d.ts","import":"./dist/types/index.js"},"./plugin":{"types":"./dist/plugin/index.d.ts","import":"./dist/plugin/index.js"},"./remark":{"types":"./dist/remark/index.d.ts","import":"./dist/remark/index.js"},"./content":{"types":"./dist/content/index.d.ts","import":"./dist/content/index.js"},"./campfire":{"types":"./dist/campfire/index.d.ts","import":"./dist/campfire/index.js"}},"scripts":{"dev":"tsc --watch","test":"vitest run","build":"tsc","test:watch":"vitest"},"_npmUser":{"name":"eurusik","email":"john.rusakov@gmail.com"},"_resolved":"/tmp/7cbbc5444813d02b4a8d361c8cc4968f/alife-sdk-social-0.1.1.tgz","_integrity":"sha512-di9Vzf1Zztu+jlGAcKBBMnOjvstt9z6RWu+szYlOHiLvwxUW29PlwlM8H/oYMY6q8VnP1MXnN9rrZ3BMB5VRXg==","repository":{"url":"git+https://github.com/eurusik/alife-sdk.git","type":"git","directory":"packages/alife-social"},"_npmVersion":"10.8.2","description":"NPC social interaction systems — greetings, remarks, campfire storytelling","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/social_0.1.1_1772742735723_0.42842480248619963","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@alife-sdk/social","version":"0.3.0","keywords":["alife","game-ai","social","npc","campfire","dialogue"],"author":{"name":"Eugene Rusakov"},"license":"MIT","_id":"@alife-sdk/social@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":"f4e8e523f5a90c04bc654b7dfa70d0fbdd61ef39","tarball":"https://registry.npmjs.org/@alife-sdk/social/-/social-0.3.0.tgz","fileCount":87,"integrity":"sha512-3SQa4zKn8mdf65atyl1cmzCOfHcWoat1iHjoy/pLU0D0zAIpGSCM4okQb++6o8pOYBsDow6C48Wojuk+d/EtpA==","signatures":[{"sig":"MEUCIE98EKDBWg+mFaScsAG2N1UvWyLNgVAX7aNKscdBzb1SAiEAkzYhCABfVDbX11r/gyebihju/xn59YHfG2wRm/0l+NE=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":136652},"main":"dist/index.js","type":"module","_from":"file:alife-sdk-social-0.3.0.tgz","types":"dist/index.d.ts","engines":{"node":">=20.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./meet":{"types":"./dist/meet/index.d.ts","import":"./dist/meet/index.js"},"./ports":{"types":"./dist/ports/index.d.ts","import":"./dist/ports/index.js"},"./types":{"types":"./dist/types/index.d.ts","import":"./dist/types/index.js"},"./plugin":{"types":"./dist/plugin/index.d.ts","import":"./dist/plugin/index.js"},"./remark":{"types":"./dist/remark/index.d.ts","import":"./dist/remark/index.js"},"./content":{"types":"./dist/content/index.d.ts","import":"./dist/content/index.js"},"./campfire":{"types":"./dist/campfire/index.d.ts","import":"./dist/campfire/index.js"}},"scripts":{"dev":"tsc --watch","test":"vitest run","build":"tsc","test:watch":"vitest"},"_npmUser":{"name":"eurusik","email":"john.rusakov@gmail.com"},"_resolved":"/tmp/0d042b0980b80fb6af588a81a42623fb/alife-sdk-social-0.3.0.tgz","_integrity":"sha512-3SQa4zKn8mdf65atyl1cmzCOfHcWoat1iHjoy/pLU0D0zAIpGSCM4okQb++6o8pOYBsDow6C48Wojuk+d/EtpA==","repository":{"url":"git+https://github.com/eurusik/alife-sdk.git","type":"git","directory":"packages/alife-social"},"_npmVersion":"10.8.2","description":"NPC social interaction systems — greetings, remarks, campfire storytelling","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/social_0.3.0_1772789682759_0.0165258752502353","host":"s3://npm-registry-packages-npm-production"}},"0.4.0":{"name":"@alife-sdk/social","version":"0.4.0","keywords":["alife","game-ai","social","npc","campfire","dialogue"],"author":{"name":"Eugene Rusakov"},"license":"MIT","_id":"@alife-sdk/social@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":"0734eb0a569eed98802f61a86d761b7ebd17eadf","tarball":"https://registry.npmjs.org/@alife-sdk/social/-/social-0.4.0.tgz","fileCount":87,"integrity":"sha512-/+sQXvaNM6ZgsqyN4V02ZfjOPTD7P/F/OE1/80CDqj7tI4sigwjDyPEKaTzIUgsbRq24bVNZH6g8XBO5Cqz90A==","signatures":[{"sig":"MEUCID+gpGb7HnYAP1zc/lR6ZCn0h7Ggaj0hdQ/f0OPTiSP1AiEA4DQkpZphupKWnGL8tmvgzAyDQV9NuvH5iC5XIe5LpLc=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":136652},"main":"dist/index.js","type":"module","_from":"file:alife-sdk-social-0.4.0.tgz","types":"dist/index.d.ts","engines":{"node":">=20.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./meet":{"types":"./dist/meet/index.d.ts","import":"./dist/meet/index.js"},"./ports":{"types":"./dist/ports/index.d.ts","import":"./dist/ports/index.js"},"./types":{"types":"./dist/types/index.d.ts","import":"./dist/types/index.js"},"./plugin":{"types":"./dist/plugin/index.d.ts","import":"./dist/plugin/index.js"},"./remark":{"types":"./dist/remark/index.d.ts","import":"./dist/remark/index.js"},"./content":{"types":"./dist/content/index.d.ts","import":"./dist/content/index.js"},"./campfire":{"types":"./dist/campfire/index.d.ts","import":"./dist/campfire/index.js"}},"scripts":{"dev":"tsc --watch","test":"vitest run","build":"tsc","test:watch":"vitest"},"_npmUser":{"name":"eurusik","email":"john.rusakov@gmail.com"},"_resolved":"/tmp/bc3289ed1d718f7e266ee601290ba44b/alife-sdk-social-0.4.0.tgz","_integrity":"sha512-/+sQXvaNM6ZgsqyN4V02ZfjOPTD7P/F/OE1/80CDqj7tI4sigwjDyPEKaTzIUgsbRq24bVNZH6g8XBO5Cqz90A==","repository":{"url":"git+https://github.com/eurusik/alife-sdk.git","type":"git","directory":"packages/alife-social"},"_npmVersion":"10.8.2","description":"NPC social interaction systems — greetings, remarks, campfire storytelling","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/social_0.4.0_1772823546515_0.38854095700208724","host":"s3://npm-registry-packages-npm-production"}},"0.4.1":{"name":"@alife-sdk/social","version":"0.4.1","description":"NPC social interaction systems — greetings, remarks, campfire storytelling","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"},"./content":{"types":"./dist/content/index.d.ts","import":"./dist/content/index.js"},"./meet":{"types":"./dist/meet/index.d.ts","import":"./dist/meet/index.js"},"./remark":{"types":"./dist/remark/index.d.ts","import":"./dist/remark/index.js"},"./campfire":{"types":"./dist/campfire/index.d.ts","import":"./dist/campfire/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","game-ai","social","npc","campfire","dialogue"],"license":"MIT","author":{"name":"Eugene Rusakov"},"repository":{"type":"git","url":"git+https://github.com/eurusik/alife-sdk.git","directory":"packages/alife-social"},"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/social@0.4.1","_integrity":"sha512-01EMgSY3rt8BdmNsr97/1/lPI2fRqDtKi/zSOI1YOw612xjt5CT4Vdx58S+RBe+ru0wK99bSOIvRJdXvzOm0VA==","_resolved":"/tmp/fdf5fa462720cc0c54dd12442fc5957a/alife-sdk-social-0.4.1.tgz","_from":"file:alife-sdk-social-0.4.1.tgz","_nodeVersion":"20.20.1","_npmVersion":"10.8.2","dist":{"integrity":"sha512-01EMgSY3rt8BdmNsr97/1/lPI2fRqDtKi/zSOI1YOw612xjt5CT4Vdx58S+RBe+ru0wK99bSOIvRJdXvzOm0VA==","shasum":"5b6e8ae0af411268f9e643e7502f097c76018ecc","tarball":"https://registry.npmjs.org/@alife-sdk/social/-/social-0.4.1.tgz","fileCount":87,"unpackedSize":136743,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIHQPlxr7En8ZJjuf5Xk5e7e28xA0i2Hlnthc8ntzMVHtAiEA7DZi+n2Cwxn0VfAPMw5rt6iJLKr7s96+BayqoZVWc1s="}]},"_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/social_0.4.1_1773837144679_0.9548840127865643"},"_hasShrinkwrap":false}},"time":{"created":"2026-03-05T19:45:46.790Z","modified":"2026-03-18T12:32:25.008Z","0.1.0":"2026-03-05T19:45:47.033Z","0.1.1":"2026-03-05T20:32:15.866Z","0.3.0":"2026-03-06T09:34:42.912Z","0.4.0":"2026-03-06T18:59:06.659Z","0.4.1":"2026-03-18T12:32:24.865Z"},"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","social","npc","campfire","dialogue"],"repository":{"type":"git","url":"git+https://github.com/eurusik/alife-sdk.git","directory":"packages/alife-social"},"description":"NPC social interaction systems — greetings, remarks, campfire storytelling","maintainers":[{"name":"eurusik","email":"john.rusakov@gmail.com"}],"readme":"# @alife-sdk/social\n\nNPC social interaction systems — proximity greetings, ambient idle remarks,\nand campfire group storytelling sessions.\nFramework-free: rendering and NPC data are supplied by host-side ports.\n\nDepends on @alife-sdk/core\n\n---\n\n## Install\n\n```\nnpm install @alife-sdk/social\n```\n\n---\n\n## Quick start\n\n```ts\nimport { SocialPlugin }  from '@alife-sdk/social/plugin';\nimport { SocialPorts }   from '@alife-sdk/social/ports';\nimport { Plugins }       from '@alife-sdk/core/plugins';\n\n// ISocialData — the shape your social.json must match:\nconst socialJson = {\n  greetings: {\n    friendly: ['Hey, good to see you!', 'Morning, stalker.'],\n    neutral:  ['Hmm.', 'Move along.'],\n    evening:  ['Getting late…', 'Stay safe out there.'],\n  },\n  remarks: {\n    zone:    ['Something feels off around here.', 'Watch your step.'],\n    weather: ['Rain again.', 'Sky looks ugly today.'],\n    gossip:  {\n      default: ['Heard a squad went missing near the factory.'],\n    },\n  },\n  campfire: {\n    stories:   ['Back in \\'03 I saw something in the fog…'],\n    jokes:     ['Why did the stalker cross the road? He didn\\'t — the road crossed him.'],\n    reactions: {\n      laughter:    ['Ha! Good one.'],\n      story_react: ['Gives me chills every time.'],\n      eating:      ['*eating sounds*'],\n    },\n  },\n  // custom: { my_category: ['Line A', 'Line B'] },  // optional extra pools\n};\n\n// 1. Implement the two required ports\nkernel.portRegistry.register(SocialPorts.SocialPresenter, {\n  showBubble(npcId, text, durationMs) {\n    const sprite = scene.npcSprites.get(npcId);\n    if (sprite) new SocialBubble(scene, sprite.x, sprite.y - 40, text, durationMs);\n  },\n});\n\nkernel.portRegistry.register(SocialPorts.NPCSocialProvider, {\n  getOnlineNPCs() {\n    // ISocialNPC shape: { id: string, position: { x, y }, factionId: string, state: string }\n    // Construct on the fly — the SDK never holds a reference to your full NPC objects.\n    return [...onlineNPCs.values()].map(npc => ({\n      id:        npc.id,\n      position:  { x: npc.worldX, y: npc.worldY },\n      factionId: npc.faction,\n      state:     npc.aiState,   // e.g. 'idle', 'patrol', 'camp', 'dead'\n    }));\n  },\n  areFactionsFriendly(a, b)    { return factions.getRelation(a, b) > 0; },\n  areFactionsHostile(a, b)     { return factions.getRelation(a, b) < -30; },\n  getNPCTerrainId(id)          { return sim.getNPCBrain(id)?.currentTerrainId ?? null; },\n});\n\n// Both ports are required. If either is missing, the plugin will not emit bubbles.\n\n// INPCSocialProvider method contracts:\n//   getOnlineNPCs() returns []     → no remarks or campfires are triggered (graceful no-op).\n//   getNPCTerrainId() returns null → that NPC is excluded from remark terrain grouping\n//                                    and from campfire participant discovery.\n\n// 2. Register the plugin with your content data\nkernel.use(new SocialPlugin(random, { data: socialJson }));\nkernel.init();\n\n// 3. Drive greetings from the player position each frame\nconst social = kernel.getPlugin(Plugins.SOCIAL);\nfunction gameLoop(deltaMs: number) {\n  kernel.update(deltaMs); // runs remarks + campfire automatically\n\n  const bubbles = social.meetOrchestrator.update({\n    deltaMs, targetX: player.x, targetY: player.y,\n    currentTime: Date.now(), npcs: provider.getOnlineNPCs(),\n    isHostile: (a, b) => factions.isHostile(a, b),\n    isAlly:    (a, b) => factions.isAlly(a, b),\n    targetFactionId: 'loner',\n  });\n  for (const b of bubbles) presenter.showBubble(b.npcId, b.text, b.durationMs);\n  // b.category is also available (SocialCategory) for custom styling/filtering\n}\n```\n\n---\n\n## Sub-paths\n\n| Import path | What it contains |\n|-------------|-----------------|\n| `@alife-sdk/social/plugin` | `SocialPlugin` — kernel entry point |\n| `@alife-sdk/social/types` | `SocialCategory`, `CampfireState`, `ISocialData`, `ISocialConfig`, `IBubbleRequest` |\n| `@alife-sdk/social/ports` | `SocialPorts`, `ISocialPresenter`, `INPCSocialProvider` |\n| `@alife-sdk/social/content` | `ContentPool`, `loadSocialData` |\n| `@alife-sdk/social/meet` | `MeetOrchestrator`, `isMeetEligible`, `selectGreetingCategory` |\n| `@alife-sdk/social/remark` | `RemarkDispatcher` |\n| `@alife-sdk/social/campfire` | `CampfireFSM`, `CampfireParticipants`, `IGatheringFSM` |\n\n---\n\n## Architecture\n\n```\n┌─────────────────────────────────────────────────┐\n│                  SocialPlugin                   │\n│                                                 │\n│  Every frame (via kernel.update):               │\n│  ┌──────────────────┬──────────────────────┐    │\n│  │ RemarkDispatcher │ CampfireFSM per       │    │\n│  │ (gated by        │ terrain (gated by     │    │\n│  │  check interval) │  sync interval)       │    │\n│  └──────────────────┴──────────────────────┘    │\n│                                                 │\n│  Host-driven (call manually):                   │\n│  ┌──────────────────────────────────────────┐   │\n│  │  MeetOrchestrator (player position)      │   │\n│  └──────────────────────────────────────────┘   │\n└─────────────────────────────────────────────────┘\n       │ ports                  │ bubble output\n       ▼                        ▼\n┌──────────────────┐   ┌──────────────────────┐\n│ INPCSocialProvider│   │  ISocialPresenter    │\n│  getOnlineNPCs   │   │  showBubble(id,       │\n│  areFriendly     │   │    text, durationMs)  │\n│  areHostile      │   └──────────────────────┘\n│  getTerrainId    │\n└──────────────────┘\n```\n\n---\n\n## Key concepts\n\n### Three interaction types\n\n| System | Trigger | Who speaks | Content |\n|--------|---------|-----------|---------|\n| **Meet** | NPC within `meetDistance` of target | That NPC | Greeting by faction/state |\n| **Remark** | Random check every 5 s | One NPC per terrain | Zone, weather, or gossip |\n| **Campfire** | ≥ 2 NPCs in `gatheringStates` share a terrain | Director then audience | Story/joke/eating cycle |\n\n### Greeting selection\n\n`selectGreetingCategory` picks the greeting pool using this priority chain:\n\n1. **State override** — if the NPC's `state` matches a key in `stateGreetingMap`, that category wins.\n   Default map: `{ camp: 'greeting_evening', sleep: 'greeting_evening' }`.\n2. **Faction ally** — if `isAlly(npcFactionId, targetFactionId)` returns true (or factions match), use `greeting_friendly`.\n3. **Fallback** — use `greeting_neutral`.\n\nYou can supply a custom `stateGreetingMap` via `IMeetConfig.stateGreetingMap` to add or override state entries without touching the fallback logic.\n\n### Content pool\n\nAll text is loaded from `ISocialData` JSON into a `ContentPool` at startup.\nNo-repeat selection: the same line is never picked twice in a row per category.\nAdd custom categories via `ISocialData.custom` or `pool.addLines()` at runtime.\n\n### Configuration tuning\n\nAll defaults come from `createDefaultSocialConfig()`. Pass overrides via the `social` key of `ISocialPluginConfig`.\n\n| Parameter | Default | Effect |\n|-----------|---------|--------|\n| `meet.meetDistance` | `150` px | Radius around the target in which an NPC triggers a greeting |\n| `meet.meetCooldownMs` | `60 000` ms | Per-NPC silence window after a greeting fires |\n| `remark.remarkChance` | `0.3` | Probability (0–1) that an eligible NPC speaks on each check interval |\n| `remark.remarkCheckIntervalMs` | `5 000` ms | How often the remark system evaluates candidates |\n| `campfire.weightStory` | `0.35` | Weight for the STORY branch in the idle→next transition |\n| `campfire.weightJokeCumulative` | `0.65` | Cumulative weight for JOKE (i.e. joke probability = 0.65 − 0.35 = 0.30) |\n\nPractical tips:\n\n- **Lower `meetCooldownMs`** (e.g. `20_000`) for more frequent greetings in busy social hubs.\n- **Raise `remarkChance`** (e.g. `0.6`) for noisier zones where NPCs should feel chatty.\n- **Lower `remarkCheckIntervalMs`** to make NPCs react to events faster, at the cost of slightly more CPU per second.\n- **Adjust `weightStory` / `weightJokeCumulative`** to shift the campfire mood — higher `weightStory` means more dramatic sessions, narrowing the gap between them increases joke frequency.\n\n```ts\nkernel.use(new SocialPlugin(random, {\n  data: socialJson,\n  social: {\n    meet:   { meetCooldownMs: 20_000 },          // greet more often\n    remark: { remarkChance: 0.6 },               // busier zones\n  },\n}));\n```\n\n---\n\n### Save/load integration\n\n`SocialPlugin` exposes `serialize()` and `restore()` for seamless save/load support.\n\n**What IS persisted:**\n\n- Per-NPC greeting cooldowns (`meetCooldowns`) — prevents NPCs from re-greeting immediately after a load.\n- Per-NPC remark cooldowns (`remarkCooldowns`) — preserves the time-independent remaining cooldown so NPCs don't burst-speak on resume.\n\n**What is NOT persisted:**\n\n- Campfire sessions — they are transient and auto-reconstruct from live NPC positions on the next sync tick (within `syncIntervalMs`, default 3 s).\n\n```ts\n// On save\nconst saveData = {\n  social: kernel.getPlugin(Plugins.SOCIAL).serialize(),\n  // ...other plugin saves\n};\nfs.writeFileSync('save.json', JSON.stringify(saveData));\n\n// On load\nconst saveData = JSON.parse(fs.readFileSync('save.json', 'utf8'));\nkernel.getPlugin(Plugins.SOCIAL).restore(saveData.social);\n```\n\nThe `serialize()` return shape:\n\n```ts\n{\n  campfireTerrains: string[];                 // informational only — not restored\n  meetCooldowns:   Array<[npcId, expiryTs]>; // absolute timestamps (ms)\n  remarkCooldowns: Array<[npcId, remainMs]>; // remaining ms (time-independent)\n}\n```\n\n---\n\n### Custom gathering FSM\n\nReplace the built-in campfire with your own group behavior:\n\n```ts\nkernel.use(new SocialPlugin(random, {\n  data: socialJson,\n  social: {\n    createGatheringFSM: (terrainId) => new TavernFSM(terrainId, tavernConfig),\n  },\n}));\n```\n\n`TavernFSM` only needs to implement `IGatheringFSM` (3 methods).\n\n### Campfire state machine\n\nThe built-in `CampfireFSM` cycles through five states:\n\n```\nIDLE ──(weighted roll)──► STORY   ──► REACTING ──► IDLE\n                     └──► JOKE    ──► REACTING ──┘\n                     └──► EATING  ──────────────► IDLE\n```\n\nTransition weights (defaults, all configurable):\n\n| From IDLE | Weight | Cumulative |\n|-----------|--------|------------|\n| STORY     | 0.35   | 0–0.35     |\n| JOKE      | 0.30   | 0.35–0.65  |\n| EATING    | 0.35   | 0.65–1.0   |\n\nDefault timing ranges:\n\n| State     | Min     | Max     |\n|-----------|---------|---------|\n| IDLE      | 10 000 ms | 20 000 ms |\n| STORY     | 8 000 ms  | 15 000 ms |\n| JOKE      | 5 000 ms  | 8 000 ms  |\n| EATING    | 5 000 ms  | 10 000 ms |\n| REACTING  | 3 000 ms  | 5 000 ms  |\n\nAudience reactions are staggered by `reactionStaggerMs` (default 500 ms) so bubbles\ndon't all appear at once. After a JOKE the reaction pool is `campfire_laughter`;\nafter a STORY it is `campfire_story_react`.\n\n### Bubble duration\n\nComputed automatically: `max(2000ms, text.length × 80ms)`.\nThe host receives `durationMs` in every `IBubbleRequest` and should use it to\ncontrol animation/auto-dismiss timing.\n\n---\n\n## Testing\n\nThe package has **198 tests** (vitest). Run them:\n\n```\npnpm --filter @alife-sdk/social test\n```\n\nAll subsystems are pure — no kernel needed for unit tests:\n\n```ts\nimport { MeetOrchestrator } from '@alife-sdk/social/meet';\nimport { ContentPool } from '@alife-sdk/social/content';\n\nconst pool = new ContentPool();\npool.addLines('greeting.friendly', ['Hello!', 'Hey there!']);\n\nconst orchestrator = new MeetOrchestrator(pool, { /* config */ });\nconst bubbles = orchestrator.update({ npcs: [...], player: { x: 0, y: 0 } });\n\n## See also\n\n- [`@alife-sdk/simulation`](../alife-simulation/README.md) — offline NPC simulation that drives which NPCs are active\n- [`@alife-sdk/ai`](../alife-ai/README.md) — online AI plugin that provides NPC proximity and line-of-sight for social triggers\n- [`@alife-sdk/phaser`](../alife-phaser/README.md) — includes `PhaserNPCSocialProvider` and `PhaserSocialPresenter` adapters\n```\n","readmeFilename":"README.md"}