@umicat/phaser-sdk 1.0.16 → 1.0.18

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
@@ -79,9 +79,14 @@ if (umicat) {
79
79
  umicat.isAuthenticated; // boolean
80
80
  umicat.gameId; // stable platform-issued game id
81
81
  umicat.host; // 'umicat-home-ui' | 'standalone' | 'discord' (future)
82
+ umicat.locale; // player's UI language, e.g. 'en' | 'zh-CN' (string)
82
83
  }
83
84
  ```
84
85
 
86
+ `umicat.locale` (since 1.0.17) is the player's preferred language from the host —
87
+ default your game's UI + AI-NPC language to it instead of asking. For multi-language
88
+ games + the **CJK font handling** that avoids tofu boxes, see the `game-i18n` skill.
89
+
85
90
  ### Save data — `umicat.saves`
86
91
 
87
92
  Per-user key-value store scoped to `(gameId, userId)`. Backed by the Umicat backend when authenticated, by localStorage when standalone/anonymous — **the API is identical either way**. Games should never branch on auth state for save logic.
@@ -600,11 +605,48 @@ function handleBlocked(reason) {
600
605
  → same `AiActResult`. `npc()` just keeps the conversation `history` for you (also
601
606
  `npc.note('the player drew a sword')` to feed a world event, `npc.reset()` to forget).
602
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
+
603
645
  **API**
604
646
 
605
647
  | call | returns | notes |
606
648
  |---|---|---|
607
- | `umicat.ai.npc(config)` | `Npc` | persona + `actions`; keeps history |
649
+ | `umicat.ai.npc(config)` | `Npc` | `playbook` (or inline persona) + `actions`; keeps history |
608
650
  | `npc.say(text, { observation? })` | `Promise<AiActResult>` | one turn |
609
651
  | `umicat.ai.act(params)` | `Promise<AiActResult>` | stateless; you pass `history` |
610
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,
@@ -19,6 +19,9 @@ export interface Transport {
19
19
  * `umicat.rooms.*` is unavailable in that case.
20
20
  */
21
21
  readonly realtimeUrl?: string;
22
+ /** The player's preferred language (BCP-47-ish, e.g. 'en', 'zh-CN'). The host
23
+ * provides it at handshake; standalone falls back to the browser locale. */
24
+ readonly locale?: string;
22
25
  /** `timeoutMs` overrides the default RPC timeout — runtime-AI calls need
23
26
  * longer than the 10s default for LLM latency. */
24
27
  call<T = unknown>(method: string, params?: unknown, timeoutMs?: number): Promise<T>;
@@ -33,6 +33,9 @@ export declare class Umicat {
33
33
  get isAuthenticated(): boolean;
34
34
  /** Stable game identifier issued by the host. May be a placeholder when standalone. */
35
35
  get gameId(): string;
36
+ /** The player's preferred language (e.g. 'en', 'zh-CN'). Use it to default the
37
+ * game's UI + AI NPC language. See the game-i18n skill. */
38
+ get locale(): string;
36
39
  /** Which transport Umicat.init() selected. Useful for debugging. */
37
40
  get host(): TransportKind;
38
41
  /**
@@ -40,6 +40,9 @@ export class Umicat {
40
40
  get isAuthenticated() { return this.transport.user !== null; }
41
41
  /** Stable game identifier issued by the host. May be a placeholder when standalone. */
42
42
  get gameId() { return this.transport.gameId; }
43
+ /** The player's preferred language (e.g. 'en', 'zh-CN'). Use it to default the
44
+ * game's UI + AI NPC language. See the game-i18n skill. */
45
+ get locale() { return this.transport.locale ?? 'en'; }
43
46
  /** Which transport Umicat.init() selected. Useful for debugging. */
44
47
  get host() { return this.transport.kind; }
45
48
  /**
@@ -9,6 +9,7 @@ export declare class LocalStorageTransport implements Transport {
9
9
  readonly gameId: string;
10
10
  readonly kind: "standalone";
11
11
  readonly user: UmicatUser | null;
12
+ readonly locale: string;
12
13
  constructor(gameId: string);
13
14
  call<T = unknown>(method: string, params?: unknown): Promise<T>;
14
15
  private savesPrefix;
@@ -9,6 +9,8 @@ export class LocalStorageTransport {
9
9
  this.gameId = gameId;
10
10
  this.kind = 'standalone';
11
11
  this.user = null;
12
+ // No host to tell us the player's language — best-effort from the browser.
13
+ this.locale = (typeof navigator !== 'undefined' && navigator.language) ? navigator.language : 'en';
12
14
  }
13
15
  async call(method, params) {
14
16
  switch (method) {
@@ -10,6 +10,7 @@ export declare class PostMessageTransport implements Transport {
10
10
  user: UmicatUser | null;
11
11
  gameId: string;
12
12
  realtimeUrl?: string;
13
+ locale?: string;
13
14
  private pending;
14
15
  private seq;
15
16
  private constructor();
@@ -68,6 +68,7 @@ export class PostMessageTransport {
68
68
  transport.user = init.user;
69
69
  transport.gameId = init.gameId;
70
70
  transport.realtimeUrl = init.realtimeUrl;
71
+ transport.locale = init.locale;
71
72
  transport.installResultListener();
72
73
  return transport;
73
74
  }
@@ -28,6 +28,12 @@ export interface InitMessage {
28
28
  * here after fetching a JWT via the 'realtime.getToken' RPC.
29
29
  */
30
30
  realtimeUrl?: string;
31
+ /**
32
+ * The player's preferred language (the host's UI locale, e.g. 'en', 'zh-CN',
33
+ * 'ja'). Lets a game default its UI + AI NPC language to the player's without
34
+ * asking. Absent in standalone mode → SDK falls back to the browser locale.
35
+ */
36
+ locale?: string;
31
37
  }
32
38
  export interface RpcResultOk {
33
39
  type: 'umicat:rpc.result';
@@ -927,6 +933,14 @@ export interface AiActOptions {
927
933
  temperature?: number;
928
934
  }
929
935
  export interface AiActParams {
936
+ /**
937
+ * Name of a playbook shipped with the game at `public/playbooks/<name>.md`
938
+ * (ADR-018) — a natural-language behavior pack (persona + strategy) the platform
939
+ * loads and injects. Pass just the NAME; the host resolves it under the game's
940
+ * own id (tamper-resistant). Supplies the character behavior; can be combined
941
+ * with `persona` (the playbook leads, `persona` augments).
942
+ */
943
+ playbook?: string;
930
944
  persona?: AiPersona;
931
945
  /** Arbitrary game-defined JSON of what the AI perceives this turn. */
932
946
  observation?: unknown;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umicat/phaser-sdk",
3
- "version": "1.0.16",
3
+ "version": "1.0.18",
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",