@umicat/phaser-sdk 1.0.17 → 1.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SDK-GUIDE.md CHANGED
@@ -605,11 +605,48 @@ function handleBlocked(reason) {
605
605
  → same `AiActResult`. `npc()` just keeps the conversation `history` for you (also
606
606
  `npc.note('the player drew a sword')` to feed a world event, `npc.reset()` to forget).
607
607
 
608
+ #### Playbooks — keep persona + strategy out of code (since 1.0.18)
609
+
610
+ For anything past a one-line shopkeeper, write the NPC's **persona + strategy as
611
+ natural language** in `public/playbooks/<name>.md` and reference it by name. Code
612
+ keeps owning the **action vocabulary** (what the NPC *can* do) and the handlers
613
+ that validate + execute; the playbook owns **who it is and how it plays** (what to
614
+ do, when, why). You tune behavior by editing the `.md` — no code change, and a
615
+ non-coder can do it.
616
+
617
+ `public/playbooks/wolf.md`:
618
+ ```md
619
+ You are a Werewolf, secretly on the wolf team. Your partner is another wolf.
620
+
621
+ Goal: survive to the end. Never reveal your team.
622
+ - By day, blend in. Sound like a worried villager hunting wolves.
623
+ - Cast suspicion subtly — agree with the room, then nudge it toward a villager.
624
+ - If accused, stay calm; over-defending looks guilty. Deflect to someone quieter.
625
+ - Don't out your partner; don't obviously defend them either.
626
+ ```
627
+
628
+ ```ts
629
+ const wolf = umicat.ai.npc({
630
+ playbook: 'wolf', // → public/playbooks/wolf.md
631
+ actions: [ // the vocabulary stays in code
632
+ { name: 'vote', description: 'Vote to eliminate a player', args: { target: 'string' } },
633
+ ],
634
+ });
635
+ const r = await wolf.say('It is day. Discuss.', { observation: { alive, votesSoFar } });
636
+ ```
637
+
638
+ The host resolves the playbook under the game's own id, so pass **only the name** —
639
+ a player can't point it at another game. The playbook is content *inside* the
640
+ game's fiction: the platform safety rules and your `actions` still bound it; it
641
+ can't grant powers your code didn't declare. One playbook per NPC keeps each call
642
+ focused (a Werewolf game has `wolf.md` / `seer.md` / `villager.md`, loaded per
643
+ speaker). Edit + re-publish to ship a behavior change.
644
+
608
645
  **API**
609
646
 
610
647
  | call | returns | notes |
611
648
  |---|---|---|
612
- | `umicat.ai.npc(config)` | `Npc` | persona + `actions`; keeps history |
649
+ | `umicat.ai.npc(config)` | `Npc` | `playbook` (or inline persona) + `actions`; keeps history |
613
650
  | `npc.say(text, { observation? })` | `Promise<AiActResult>` | one turn |
614
651
  | `umicat.ai.act(params)` | `Promise<AiActResult>` | stateless; you pass `history` |
615
652
 
@@ -18,6 +18,13 @@ export declare class AiModule {
18
18
  npc(config: NpcConfig): Npc;
19
19
  }
20
20
  export interface NpcConfig {
21
+ /**
22
+ * Name of a playbook shipped at `public/playbooks/<name>.md` (ADR-018) — the
23
+ * NPC's persona + strategy as editable natural language, kept OUT of code.
24
+ * Preferred over the inline `role`/`goals`/`style` for any non-trivial
25
+ * character (you can tune behavior by editing the `.md`, no code change).
26
+ */
27
+ playbook?: string;
21
28
  role?: string;
22
29
  goals?: string[];
23
30
  style?: string;
@@ -45,6 +45,7 @@ export class Npc {
45
45
  async say(playerLine, opts) {
46
46
  this.history.push({ from: 'player', text: playerLine });
47
47
  const res = await this.ai.act({
48
+ playbook: this.config.playbook,
48
49
  persona: {
49
50
  role: this.config.role,
50
51
  goals: this.config.goals,
@@ -79,7 +79,7 @@ function handleMessage(game, msg) {
79
79
  applyPanZoomToWorld(game, msg);
80
80
  break;
81
81
  case 'umicat:editor:createEntity':
82
- void createEntity(game, msg.entity, msg.manifestAsset);
82
+ void createEntity(game, msg.entity, msg.manifestAsset, msg.tilemapFile);
83
83
  break;
84
84
  case 'umicat:editor:deleteEntity':
85
85
  deleteEntity(game, msg.entityId);
@@ -1206,7 +1206,7 @@ function applyVisualPatch(go, v) {
1206
1206
  * the host would have to coordinate a build before showing a dropped
1207
1207
  * sprite, which defeats the point of drag-to-place.
1208
1208
  */
1209
- async function createEntity(game, entity, manifestAsset) {
1209
+ async function createEntity(game, entity, manifestAsset, tilemapFile) {
1210
1210
  // HUD mode dispatches to the HUD-runtime spawner (slice 5). The host
1211
1211
  // sends HudEntity records (not WorldEntity), so we type-erase.
1212
1212
  if (getEditorMode(game) === 'hud') {
@@ -1288,13 +1288,26 @@ async function createEntity(game, entity, manifestAsset) {
1288
1288
  // dropped tilemap renders LIVE without a save+reload round-trip.
1289
1289
  const tilemapId = entity.tilemapId;
1290
1290
  if (tilemapId) {
1291
- try {
1292
- await loadTilemapFileIntoScene(sceneRef, tilemapId);
1291
+ const key = tilemapFileCacheKey(tilemapId);
1292
+ // Prefer the host-supplied file CONTENT (the workspace truth). The dist
1293
+ // copy that `loadTilemapFileIntoScene` fetches is STALE until the next
1294
+ // game build, so a freshly-authored/edited tilemap 404s there → magenta
1295
+ // placeholder until save+rebuild. Injecting the passed content renders it
1296
+ // LIVE. Fall back to the dist fetch when the host didn't supply it.
1297
+ if (tilemapFile) {
1298
+ if (sceneRef.cache.json.exists(key))
1299
+ sceneRef.cache.json.remove(key);
1300
+ sceneRef.cache.json.add(key, tilemapFile);
1293
1301
  }
1294
- catch (e) {
1295
- console.warn('[umicat/editor] createEntity: tilemap-ref file load failed:', e);
1302
+ else {
1303
+ try {
1304
+ await loadTilemapFileIntoScene(sceneRef, tilemapId);
1305
+ }
1306
+ catch (e) {
1307
+ console.warn('[umicat/editor] createEntity: tilemap-ref file load failed:', e);
1308
+ }
1296
1309
  }
1297
- const file = sceneRef.cache.json.get(tilemapFileCacheKey(tilemapId));
1310
+ const file = sceneRef.cache.json.get(key);
1298
1311
  for (const layer of file?.layers ?? []) {
1299
1312
  for (const tid of layer.tilesetIds ?? []) {
1300
1313
  queueIfMissing(resolveById(tid));
@@ -194,6 +194,15 @@ export interface EditorCreateEntityMessage {
194
194
  entity: unknown;
195
195
  /** Asset record to add to runtime cache before spawn. Omit if assetId is already loaded. */
196
196
  manifestAsset?: unknown;
197
+ /**
198
+ * For a `tilemap-ref`: the standalone tilemap file's CONTENT (the parsed
199
+ * `public/tilemaps/{id}.json`). The host fetches it from the WORKSPACE
200
+ * (current truth) and passes it so the SDK injects it into the JSON cache
201
+ * directly. Without this the SDK fetches the file from the deployed dist,
202
+ * which is STALE until the next game build — so a freshly-authored/edited
203
+ * tilemap renders as the magenta placeholder until save+reload.
204
+ */
205
+ tilemapFile?: unknown;
197
206
  }
198
207
  export interface EditorDeleteEntityMessage {
199
208
  type: 'umicat:editor:deleteEntity';
@@ -933,6 +942,14 @@ export interface AiActOptions {
933
942
  temperature?: number;
934
943
  }
935
944
  export interface AiActParams {
945
+ /**
946
+ * Name of a playbook shipped with the game at `public/playbooks/<name>.md`
947
+ * (ADR-018) — a natural-language behavior pack (persona + strategy) the platform
948
+ * loads and injects. Pass just the NAME; the host resolves it under the game's
949
+ * own id (tamper-resistant). Supplies the character behavior; can be combined
950
+ * with `persona` (the playbook leads, `persona` augments).
951
+ */
952
+ playbook?: string;
936
953
  persona?: AiPersona;
937
954
  /** Arbitrary game-defined JSON of what the AI perceives this turn. */
938
955
  observation?: unknown;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umicat/phaser-sdk",
3
- "version": "1.0.17",
3
+ "version": "1.0.19",
4
4
  "description": "Umicat Phaser 3 SDK — game infrastructure for the Umicat platform",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",