@runmu.sh/ext-scene 1.0.3 → 1.2.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.2.0
4
+
5
+ - Alt+R (*Go to scene*) is now the extension's own command, `focus.scene`, in Settings → Keys under Scene: Focus. On μClient with extension API 1.14 it focuses the Scene panel when it is open; a rebinding you made keeps working. On an older μClient the client's own Alt+R stays in place.
6
+ - The context menus on exits and room items use the kinds `scene.exit` (`data: { exit }`) and `scene.item` (`data: { item }`), which the extension registers with a schema. On a μClient before API 1.14 it keeps publishing `scene-exit` and `scene-item`.
7
+ - `src/types.ts` types the two kinds for other extensions (`ContextKinds`, `SceneItemData`, `SceneExitData`), and the module exports `ITEM_KIND`, `EXIT_KIND` and `KIND_SCHEMAS`.
8
+ - Built against SDK 1.14. Still needs extension API 1.12 or later.
9
+
10
+ ## 1.1.0
11
+
12
+ - Present now comes before Exits, and a room with no exits shows no Exits heading at all, as on Underspire.
13
+ - The Scene adds itself the first time a session knows its room, however the game sends it (GMCP, MSDP or another extension), not only on GMCP Room messages. A new **Show panel** setting per world (off / auto / on) controls it.
14
+ - Someone arriving or an item dropping no longer scrolls the panel back to the top; moving to another room still does.
15
+ - Right-click or long-press an exit or a room item for the actions other extensions offer on them.
16
+ - Exit buttons are at least 24px tall and show the standard focus ring.
17
+ - Needs μClient with extension API 1.12. It no longer asks to read game output; it only sends the exits you click.
18
+
3
19
  ## 1.0.3
4
20
 
5
21
  - No longer bundled with μClient: `builtin` and `defaultEnabled` are gone from the manifest. Install Scene from Extensions → Discover like any other extension. The panel is unchanged.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Scene (`@runmu.sh/ext-scene`, id `scene`)
2
2
 
3
- The Scene panel of [μClient](https://runmu.sh) (R-SCENE): the room you are in, its exits and who is there.
3
+ The Scene panel of [μClient](https://runmu.sh): the room you are in, who is there and its exits.
4
4
 
5
- First-party, on the [marketplace](https://runmu.sh/marketplace/x/scene). Install it from **☰ → Extensions → Discover**; once installed it is on in every world and takes the Scene slot of the default layout (right top). Turn it off per world in **Extensions → Installed**.
5
+ First-party, on the [marketplace](https://runmu.sh/marketplace/x/scene). Install it from **☰ → Extensions → Discover**; once installed it is on in every world and adds itself (right top) the first time a session knows its room. Turn it off per world in **Extensions → Installed**.
6
6
 
7
7
  Until 2026-09-29 the panel was part of the client core, and until 1.0.3 it was bundled with the client. The core still tracks the room for every session; this extension only draws it.
8
8
 
@@ -20,17 +20,34 @@ The host keeps one scene per session and fills it from:
20
20
  Package names are case-insensitive. Entering a new room clears Present. The room title is also the session's location (the world switcher shows it).
21
21
 
22
22
  ## What it looks like
23
- On `--bg-elev`: the title in uppercase `--accent-bright` with a glow and a `--border-bright` rule under it, the area in small faint capitals, the atmosphere in italic `--fg-dim`, the description, and the pose line in italic with a 2px left rule. Then `┤EXITS├` and `┤PRESENT├` section heads (gold, the host's `.sec-head`) over `▸` lists. Each exit is a button: clicking it sends the direction as if typed. A hostile item carries a small `hostile` tag in `--alert`. An empty list says `none`, and before any room arrives the panel says NO ROOM YET (an upright, uppercase, tracked faint label).
23
+ On `--bg-elev`: the title in uppercase `--accent-bright` with a glow and a `--border-bright` rule under it, the area in small faint capitals, the atmosphere in italic `--fg-dim`, the description, and the pose line in italic with a 2px left rule. Then `┤PRESENT├` over a `▸` list of the people and room items present (a hostile item carries a small `hostile` tag in `--alert`; nobody reads `none`), and, when the room has exits, `┤EXITS├` over a `▸` list. Each exit is a button: clicking it sends the direction as if typed. Before any room arrives the panel says NO ROOM YET (an upright, uppercase, tracked faint label).
24
24
 
25
- The panel is the region `scene`, so **Alt+R** (*Go to scene*) focuses it.
25
+ The panel is the region `scene`. **Alt+R** (*Go to scene*, command `focus.scene`) focuses it; rebind it in Settings → Keys. From 1.2.0 the extension registers the command itself (μClient with extension API 1.14); on an older μClient the client's own Alt+R does the same.
26
26
 
27
27
  ## Behaviour
28
28
  - Panel `scene`, singleton, default position right top, Views order 10 (after Terminal, before Channels).
29
- - The first `Room.*` package of a session adds the panel if it is not open (R-AUTO-PANELS), once per world on this device, so a Scene you closed stays closed.
30
- - It sends nothing to the game except the exits you click. It adds no `Core.Supports` of its own (the backend's base set already has `Room 1`).
29
+ - **Show panel: auto** (per world, on the extension's settings page: off / auto / on). On auto the Scene joins Views and adds itself the first time a session knows its room, whatever the room came from (GMCP, MSDP, another extension); once per world on this device, so a Scene you closed stays closed.
30
+ - A redraw in the same room (someone arrives, an item drops) keeps your scroll position; moving to another room scrolls back to the top. When the game sends no room id, every update counts as a move.
31
+ - Right-click (or long-press) an exit or a room item for the menus other extensions add to them. The extension registers the context kinds `scene.exit` (title *Exit*, `data: { exit }`) and `scene.item` (title *Scene item*, `data: { item: { id, name, hostile? } }`), each with a schema the host checks. On a μClient before API 1.14 it publishes the 1.12 kinds `scene-exit` and `scene-item` instead; on 1.14 an entry written for the old kinds still receives the new targets in the old shape.
32
+ - It sends nothing to the game except the exits you click, and reads no GMCP itself: the room adapters fill the scene.
33
+
34
+ ## Contract
35
+ The extension reads no GMCP and sends none, so it declares no GMCP packages or payload contracts; the room adapters declare `Room 1` and feed `mu.scene` (`SceneView`). Capability: `send-commands` (the exit buttons).
36
+
37
+ ## Exported API (`@runmu.sh/ext-scene`, types in `src/types.ts`)
38
+ No `ctx.exports` API. The module exports, for tests and other renderers:
39
+
40
+ | Export | |
41
+ |---|---|
42
+ | `renderScene(scene, go, opts?)` | The panel's children for one `SceneView` (`null` or `known: false` → NO ROOM YET). `go(dir)` is called on an exit click. `opts.css` passes the host classes (`mu.ui.css`), `opts.target(el, what)` is called for each exit button and present row (to mark context targets). The 1.0 two-argument form still works. |
43
+ | `presentOf(scene)` | People first, then room items, each name once: `{ name, hostile? }[]`. |
44
+ | `COPY`, `SCENE_CSS` | The drawn copy and the stylesheet (every rule under `.ext-panel[data-ext="scene"] .mu-scene`). |
45
+ | `ITEM_KIND`, `EXIT_KIND`, `KIND_SCHEMAS` | `'scene.item'`, `'scene.exit'` and their `data` schemas. |
46
+
47
+ `src/types.ts` augments the SDK's `ContextKinds` with `scene.item` (`SceneItemData`) and `scene.exit` (`SceneExitData`), so an extension that imports it gets `t.data` typed in `mu.menus.context({ target: 'scene.exit', … })`.
31
48
 
32
49
  ## SDK
33
- Uses `mu.panels.register` (with `order`, SDK 1.4), `mu.scene.watch` (SDK 1.4), `mu.sessions.send`, `mu.gmcp.on`, `mu.panels.autoAdd` and `mu.ui.style`. It exports no API. `renderScene` and `presentOf` are exported for tests.
50
+ `api` `^1.12`. SDK 1.12: `mu.panels.register` with `show: 'auto'`, `mu.panels.touch`, `mu.sessions.each`, `mu.scene.watch` (and `SceneView.id`), `mu.sessions.send(dir, { sid })`, `mu.menus.target`, `h` and `mu.ui.css`, `mu.ui.style`. SDK 1.14, used when the host has them: `mu.menus.kind` (with `JsonSchema` and `ContextKinds`), `mu.panels.focus` with `mu.commands.register`. The manifest declares `focus.scene` in `contributes.commands`, so Alt+R is listed and bound before the extension activates.
34
51
 
35
52
  ## Develop
36
53
  Made with `npm create @runmu.sh/extension` ([the quickstart](https://runmu.sh/docs/extensions/quickstart)).
@@ -39,6 +56,7 @@ Made with `npm create @runmu.sh/extension` ([the quickstart](https://runmu.sh/do
39
56
  npm install
40
57
  npm run build # src/index.ts → dist/index.js, then the manifest check
41
58
  npm run typecheck # against @muclient/sdk (npm:@runmu.sh/sdk)
59
+ npm test # the headless host (@runmu.sh/dev/test) with happy-dom
42
60
  npm run dev # dev server on http://localhost:5199/ with hot reload
43
61
  ```
44
62
 
package/dist/index.js CHANGED
@@ -1,5 +1,11 @@
1
1
  // src/index.ts
2
- import { defineExtension } from "@muclient/sdk";
2
+ import { defineExtension, h } from "@muclient/sdk";
3
+
4
+ // src/types.ts
5
+ var ITEM_KIND = "scene.item";
6
+ var EXIT_KIND = "scene.exit";
7
+
8
+ // src/index.ts
3
9
  var COPY = {
4
10
  title: "Scene",
5
11
  awaiting: "No room yet",
@@ -7,104 +13,162 @@ var COPY = {
7
13
  exits: "Exits",
8
14
  none: "none",
9
15
  hostile: "hostile",
10
- go: (dir) => `go ${dir}`
16
+ go: (dir) => `go ${dir}`,
17
+ focus: "Go to scene",
18
+ itemKind: "Scene item",
19
+ exitKind: "Exit"
20
+ };
21
+ var KIND_SCHEMAS = {
22
+ [ITEM_KIND]: {
23
+ type: "object",
24
+ required: ["item"],
25
+ properties: { item: { type: "object", required: ["id", "name"], properties: { id: { type: "string" }, name: { type: "string" }, hostile: { type: "boolean" } } } }
26
+ },
27
+ [EXIT_KIND]: { type: "object", required: ["exit"], properties: { exit: { type: "string" } } }
11
28
  };
29
+ var R = '.ext-panel[data-ext="scene"] .mu-scene';
12
30
  var SCENE_CSS = `
13
- .mu-scene { height: 100%; overflow-y: auto; background: var(--bg-elev); padding: .7rem .85rem 1rem; font-size: .9rem; color: var(--fg); box-sizing: border-box; }
14
- .mu-scene:focus { outline: none; }
15
- .mu-scene .title { margin: 0 0 .5rem; padding-bottom: .4rem; font-weight: 400; font-size: .9rem; letter-spacing: .16em; text-transform: uppercase; color: var(--accent-bright); border-bottom: 1px solid var(--border-bright); }
16
- .mu-scene .area { font-size: .66rem; letter-spacing: .2em; text-transform: uppercase; color: var(--fg-faint); margin: -.2rem 0 .4rem; }
17
- .mu-scene .atmo { color: var(--fg-dim); font-style: italic; margin: 0 0 .4rem; }
18
- .mu-scene .desc { margin: 0 0 .4rem; white-space: pre-wrap; }
19
- .mu-scene .pose { color: var(--fg-dim); font-style: italic; margin: 0 0 .5rem; padding-left: .5rem; border-left: 2px solid var(--border-bright); }
20
- .mu-scene .list { list-style: none; margin: 0; padding: 0; }
21
- .mu-scene .list li { padding: .08rem 0; color: var(--fg); }
22
- .mu-scene .sigil { color: var(--accent); margin-right: .6ch; }
23
- .mu-scene .exit { color: inherit; text-align: left; transition: color .12s ease; }
24
- .mu-scene .exit:hover { color: var(--accent-bright); }
25
- .mu-scene .exit:focus-visible { outline: 2px solid var(--accent-bright); outline-offset: 1px; }
26
- .mu-scene .hostile { color: var(--alert); font-size: .72rem; margin-left: 6px; }
27
- .mu-scene .none { margin: .1rem 0; font-style: normal; }
28
- .mu-scene .awaiting { margin: 0; color: var(--fg-faint); font-style: normal; font-size: .64rem; letter-spacing: .14em; text-transform: uppercase; }
31
+ ${R} { height: 100%; overflow-y: auto; background: var(--bg-elev); padding: .7rem .85rem 1rem; font-family: var(--font-mono); font-size: .9rem; color: var(--fg); box-sizing: border-box; }
32
+ ${R}:focus { outline: none; }
33
+ ${R} .title { margin: 0 0 .5rem; padding-bottom: .4rem; font-weight: 400; font-size: .9rem; letter-spacing: .16em; text-transform: uppercase; color: var(--accent-bright); border-bottom: 1px solid var(--border-bright); }
34
+ ${R} .area { font-size: .66rem; letter-spacing: .2em; text-transform: uppercase; color: var(--fg-faint); margin: -.2rem 0 .4rem; }
35
+ ${R} .atmo { color: var(--fg-dim); font-style: italic; margin: 0 0 .4rem; }
36
+ ${R} .desc { margin: 0 0 .4rem; white-space: pre-wrap; }
37
+ ${R} .pose { color: var(--fg-dim); font-style: italic; margin: 0 0 .5rem; padding-left: .5rem; border-left: 2px solid var(--border-bright); }
38
+ ${R} .list { list-style: none; margin: 0; padding: 0; }
39
+ ${R} .list li { padding: .08rem 0; color: var(--fg); }
40
+ ${R} .sigil { color: var(--accent); margin-right: .6ch; }
41
+ ${R} .exit { display: inline-flex; align-items: center; min-height: 24px; color: inherit; text-align: left; transition: color .12s ease; }
42
+ ${R} .exit:hover { color: var(--accent-bright); }
43
+ ${R} .exit:focus-visible { outline: 2px solid var(--accent-bright); outline-offset: -2px; }
44
+ ${R} .hostile { color: var(--alert); font-size: .72rem; margin-left: 6px; }
45
+ ${R} .none { margin: .1rem 0; font-style: normal; color: var(--fg-faint); }
46
+ ${R} .awaiting { margin: 0; color: var(--fg-faint); font-style: normal; font-size: .64rem; letter-spacing: .14em; text-transform: uppercase; }
29
47
  `;
30
- function el(tag, props = {}, ...kids) {
31
- const e = document.createElement(tag);
32
- for (const [k, v] of Object.entries(props)) {
33
- if (v === void 0 || v === false) continue;
34
- if (k === "class") e.className = String(v);
35
- else e.setAttribute(k, v === true ? "" : String(v));
36
- }
37
- for (const k of kids) if (k !== null && k !== false) e.append(k);
38
- return e;
39
- }
48
+ var HOST_CSS = { secHead: "sec-head", secClose: "sec-close", empty: "empty", glow: "glow-text" };
40
49
  function presentOf(s) {
41
50
  const seen = /* @__PURE__ */ new Set();
42
51
  const out = [];
43
- for (const n of s.present) if (!seen.has(n)) {
52
+ for (const n of s.present ?? []) if (!seen.has(n)) {
44
53
  seen.add(n);
45
54
  out.push({ name: n });
46
55
  }
47
- for (const it of s.items) if (!seen.has(it.name)) {
56
+ for (const it of s.items ?? []) if (!seen.has(it.name)) {
48
57
  seen.add(it.name);
49
58
  out.push(it.hostile ? { name: it.name, hostile: true } : { name: it.name });
50
59
  }
51
60
  return out;
52
61
  }
53
- var sectionHead = (label) => el("div", { class: "sec-head", role: "heading", "aria-level": "3" }, label, el("span", { class: "sec-close", "aria-hidden": "true" }));
54
- var sigil = () => el("span", { class: "sigil", "aria-hidden": "true" }, "\u25B8");
55
- function renderScene(s, go) {
56
- if (!s || !s.known) return [el("p", { class: "empty awaiting", "data-testid": "scene-empty" }, COPY.awaiting)];
57
- const out = [el("h3", { class: "title glow-text", role: "heading", "aria-level": "2", "data-testid": "scene-title" }, s.title)];
58
- if (s.area) out.push(el("div", { class: "area", "data-testid": "scene-area" }, s.area));
59
- if (s.atmosphere) out.push(el("p", { class: "atmo" }, s.atmosphere));
60
- if (s.desc) out.push(el("p", { class: "desc" }, s.desc));
61
- if (s.pose) out.push(el("p", { class: "pose" }, s.pose));
62
- out.push(sectionHead(COPY.exits));
62
+ var sigil = () => h("span", { class: "sigil", "aria-hidden": "true" }, "\u25B8");
63
+ function renderScene(s, go, opts = {}) {
64
+ const c = { ...HOST_CSS, ...opts.css };
65
+ const mark = (el, what) => {
66
+ opts.target?.(el, what);
67
+ return el;
68
+ };
69
+ if (!s || !s.known) return [h("p", { class: `${c.empty} awaiting`, "data-testid": "scene-empty" }, COPY.awaiting)];
70
+ const head = (label) => h("div", { class: c.secHead, role: "heading", "aria-level": "3" }, label, h("span", { class: c.secClose, "aria-hidden": "true" }));
71
+ const out = [h("h3", { class: `title ${c.glow}`, role: "heading", "aria-level": "2", "data-testid": "scene-title" }, s.title)];
72
+ if (s.area) out.push(h("div", { class: "area", "data-testid": "scene-area" }, s.area));
73
+ if (s.atmosphere) out.push(h("p", { class: "atmo" }, s.atmosphere));
74
+ if (s.desc) out.push(h("p", { class: "desc" }, s.desc));
75
+ if (s.pose) out.push(h("p", { class: "pose" }, s.pose));
76
+ out.push(head(COPY.present));
77
+ const items = new Map((s.items ?? []).map((it) => [it.name, it]));
78
+ const people = new Set(s.present ?? []);
79
+ const present = presentOf(s);
80
+ if (present.length) {
81
+ const ul = h("ul", { class: "list", "data-testid": "scene-present" });
82
+ for (const p of present) {
83
+ const it = people.has(p.name) ? void 0 : items.get(p.name);
84
+ const li = h("li", { class: "entity-ref", "data-item-id": it?.id || void 0 }, sigil(), p.name, p.hostile ? h("span", { class: "hostile" }, COPY.hostile) : null);
85
+ ul.append(it ? mark(li, { item: it }) : mark(li, { person: p.name }));
86
+ }
87
+ out.push(ul);
88
+ } else out.push(h("p", { class: `${c.empty} none` }, COPY.none));
63
89
  if (s.exits.length) {
64
- const ul = el("ul", { class: "list", "data-testid": "scene-exits" });
90
+ out.push(head(COPY.exits));
91
+ const ul = h("ul", { class: "list", "data-testid": "scene-exits" });
65
92
  for (const dir of s.exits) {
66
- const b = el("button", { class: "exit", type: "button", title: COPY.go(dir) }, sigil(), dir);
67
- b.addEventListener("click", () => go(dir));
68
- ul.append(el("li", {}, b));
93
+ const b = h("button", { class: "exit", type: "button", title: COPY.go(dir), onclick: () => go(dir) }, sigil(), dir);
94
+ ul.append(h("li", {}, mark(b, { exit: dir })));
69
95
  }
70
96
  out.push(ul);
71
- } else out.push(el("p", { class: "empty none" }, COPY.none));
72
- out.push(sectionHead(COPY.present));
73
- const present = presentOf(s);
74
- if (present.length) {
75
- const ul = el("ul", { class: "list", "data-testid": "scene-present" });
76
- for (const p of present) ul.append(el("li", {}, sigil(), p.name, p.hostile ? el("span", { class: "hostile" }, COPY.hostile) : null));
77
- out.push(ul);
78
- } else out.push(el("p", { class: "empty none" }, COPY.none));
97
+ }
79
98
  return out;
80
99
  }
81
100
  var index_default = defineExtension({
82
101
  activate(ctx) {
83
102
  const mu = ctx.mu;
84
103
  mu.ui.style(SCENE_CSS);
104
+ const css = { secHead: mu.ui.css.secHead, secClose: mu.ui.css.secClose, empty: mu.ui.css.empty, glow: mu.ui.css.glow };
105
+ const kinds = typeof mu.menus.kind === "function";
106
+ if (kinds) {
107
+ ctx.subscriptions.push(
108
+ mu.menus.kind({ id: ITEM_KIND, title: COPY.itemKind, schema: KIND_SCHEMAS[ITEM_KIND] }),
109
+ mu.menus.kind({ id: EXIT_KIND, title: COPY.exitKind, schema: KIND_SCHEMAS[EXIT_KIND] })
110
+ );
111
+ }
112
+ const exitTarget = (sid, exit) => kinds ? { kind: EXIT_KIND, sid, data: { exit } } : { kind: "scene-exit", sid, exit };
113
+ const itemTarget = (sid, item) => kinds ? { kind: ITEM_KIND, sid, data: { item } } : { kind: "scene-item", sid, item };
85
114
  const mount = (host, pc) => {
86
- const root = el("section", { class: "mu-scene", "aria-label": COPY.title, "data-focus-region": "scene", tabindex: "-1", "data-testid": "scene" });
115
+ const root = h("section", { class: "mu-scene", "aria-label": COPY.title, "data-focus-region": "scene", tabindex: "-1", "data-testid": "scene" });
87
116
  host.append(root);
88
117
  const sid = pc.sid;
89
- const go = (dir) => {
90
- if (sid) void mu.sessions.send(dir, sid);
91
- };
92
118
  if (!sid) {
93
- root.replaceChildren(...renderScene(null, go));
119
+ root.replaceChildren(...renderScene(null, () => {
120
+ }, { css }));
94
121
  return () => root.remove();
95
122
  }
96
- const off = mu.scene.watch((s) => root.replaceChildren(...renderScene(s, go)), sid);
123
+ const go = (dir) => {
124
+ void mu.sessions.send(dir, { sid });
125
+ };
126
+ let targets = [];
127
+ const untarget = () => {
128
+ for (const d of targets) d();
129
+ targets = [];
130
+ };
131
+ const target = (el, what) => {
132
+ if ("exit" in what) targets.push(mu.menus.target(el, exitTarget(sid, what.exit)));
133
+ else if ("item" in what) targets.push(mu.menus.target(el, itemTarget(sid, what.item)));
134
+ };
135
+ let room = null;
136
+ const off = mu.scene.watch((s) => {
137
+ const moved = !s.known || !s.id || s.id !== room;
138
+ room = s.known && s.id ? s.id : null;
139
+ const top = root.scrollTop;
140
+ untarget();
141
+ root.replaceChildren(...renderScene(s, go, { css, target }));
142
+ root.scrollTop = moved ? 0 : top;
143
+ }, sid);
97
144
  return () => {
98
145
  off();
146
+ untarget();
99
147
  root.remove();
100
148
  };
101
149
  };
102
- mu.panels.register({ id: "scene", title: COPY.title, singleton: true, defaultPosition: "right-top", order: 10, mount });
103
- mu.gmcp.on("Room", (_d, { sid }) => mu.panels.autoAdd("scene", sid));
150
+ mu.panels.register({ id: "scene", title: COPY.title, singleton: true, defaultPosition: "right-top", order: 10, show: "auto", mount });
151
+ if (typeof mu.panels.focus === "function") {
152
+ mu.commands.register({ id: "focus.scene", title: COPY.focus, keys: ["Alt+R"], group: "Focus", when: "session", run: () => {
153
+ mu.panels.focus?.("scene");
154
+ } });
155
+ }
156
+ ctx.subscriptions.push(mu.sessions.each((s) => {
157
+ let done = false;
158
+ return mu.scene.watch((v) => {
159
+ if (!done && v.known) {
160
+ done = true;
161
+ mu.panels.touch("scene", s.id);
162
+ }
163
+ }, s.id);
164
+ }));
104
165
  }
105
166
  });
106
167
  export {
107
168
  COPY,
169
+ EXIT_KIND,
170
+ ITEM_KIND,
171
+ KIND_SCHEMAS,
108
172
  SCENE_CSS,
109
173
  index_default as default,
110
174
  presentOf,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@runmu.sh/ext-scene",
3
- "version": "1.0.3",
4
- "description": "The Scene panel: the room you are in (title, area, atmosphere, description, pose), its exits as buttons and who is present, from GMCP Room.*, Char.Items.*, MSDP or another extension.",
3
+ "version": "1.2.0",
4
+ "description": "The Scene panel: the room you are in (title, area, atmosphere, description, pose), who is present and its exits as buttons, from GMCP Room.*, Char.Items.*, MSDP or another extension.",
5
5
  "keywords": [
6
6
  "scene",
7
7
  "room",
@@ -23,6 +23,7 @@
23
23
  "exports": "./dist/index.js",
24
24
  "files": [
25
25
  "dist",
26
+ "src/types.ts",
26
27
  "README.md",
27
28
  "CHANGELOG.md"
28
29
  ],
@@ -30,12 +31,14 @@
30
31
  "build": "node scripts/build.mjs",
31
32
  "dev": "node scripts/dev.mjs",
32
33
  "typecheck": "tsc --noEmit",
33
- "check": "node scripts/build.mjs --check"
34
+ "check": "node scripts/build.mjs --check",
35
+ "test": "node --test test/*.test.mjs"
34
36
  },
35
37
  "devDependencies": {
36
- "@muclient/sdk": "npm:@runmu.sh/sdk@^1.6.0",
37
- "@runmu.sh/dev": "^0.1.0",
38
+ "@muclient/sdk": "npm:@runmu.sh/sdk@^1.14.0",
39
+ "@runmu.sh/dev": "^0.3.0",
38
40
  "esbuild": "^0.28.2",
41
+ "happy-dom": "^20.0.0",
39
42
  "typescript": "^5.9.0"
40
43
  },
41
44
  "engines": {
@@ -43,10 +46,10 @@
43
46
  },
44
47
  "muclient": {
45
48
  "id": "scene",
46
- "api": "^1.4",
49
+ "api": "^1.12",
47
50
  "source": "src/index.ts",
48
51
  "displayName": "Scene",
49
- "description": "The Scene panel: the current room, its exits as buttons and who is present, from GMCP Room.*, Char.Items.*, MSDP or another extension.",
52
+ "description": "The Scene panel: the current room, who is present and its exits as buttons, from GMCP Room.*, Char.Items.*, MSDP or another extension.",
50
53
  "categories": [
51
54
  "interface",
52
55
  "mapping"
@@ -55,17 +58,26 @@
55
58
  "panels": [
56
59
  {
57
60
  "id": "scene",
58
- "title": "Scene"
61
+ "title": "Scene",
62
+ "order": 10,
63
+ "defaultPosition": "right-top",
64
+ "singleton": true
59
65
  }
60
66
  ],
61
- "gmcp": [
62
- "Room 1"
67
+ "commands": [
68
+ {
69
+ "id": "focus.scene",
70
+ "title": "Go to scene",
71
+ "keys": [
72
+ "Alt+R"
73
+ ]
74
+ }
63
75
  ]
64
76
  },
65
77
  "capabilities": [
66
- "read-output",
67
78
  "send-commands"
68
79
  ],
69
80
  "publisher": "bitmuse"
70
- }
81
+ },
82
+ "types": "./src/types.ts"
71
83
  }
package/src/types.ts ADDED
@@ -0,0 +1,71 @@
1
+ /**
2
+ * The exported API of `@runmu.sh/ext-scene` (id `scene`). The extension reads no GMCP and sends none: it draws the
3
+ * host's scene model (`mu.scene.watch`, `SceneView`), which the room adapters fill from GMCP `Room.*`,
4
+ * `Char.Items.*`, MSDP and other extensions (`mu.scene.set`). This file and the README change together.
5
+ */
6
+ import type { SceneView } from '@muclient/sdk';
7
+
8
+ /** One row of ┤PRESENT├: a person (from `present`) or a room item (from `items`). */
9
+ export interface PresentEntry {
10
+ name: string;
11
+ /** Set on hostile room items; drawn as a small `hostile` tag. */
12
+ hostile?: boolean;
13
+ /** The room item's id, when the row is an item with one (people have none). @since 1.1.0 */
14
+ id?: string;
15
+ }
16
+
17
+ /** The class names `renderScene` uses for the host primitives; defaults are the host's (`mu.ui.css`). @since 1.1.0 */
18
+ export interface SceneCss {
19
+ secHead: string;
20
+ secClose: string;
21
+ empty: string;
22
+ glow: string;
23
+ }
24
+
25
+ /** Options for {@link renderScene} beyond the 1.0 signature. @since 1.1.0 */
26
+ export interface RenderOptions {
27
+ /** `mu.ui.css` (or the part of it used). */
28
+ css?: Partial<SceneCss>;
29
+ /**
30
+ * Called for every exit button and every present row as it is created, so the caller can mark it as a context
31
+ * target (`mu.menus.target`). Return a function to undo it; those run on the next render or unmount.
32
+ */
33
+ target?: (el: HTMLElement, what: { exit: string } | { item: SceneView['items'][number] } | { person: string }) => void | (() => void);
34
+ }
35
+
36
+ /** Drawn copy. */
37
+ export interface SceneCopy {
38
+ title: string;
39
+ awaiting: string;
40
+ present: string;
41
+ exits: string;
42
+ none: string;
43
+ hostile: string;
44
+ go(dir: string): string;
45
+ /** The `focus.scene` command's title. @since 1.2.0 */
46
+ focus: string;
47
+ /** The context kinds' titles (the menu's accessible label). @since 1.2.0 */
48
+ itemKind: string;
49
+ exitKind: string;
50
+ }
51
+
52
+ /** The context kind of a room item in ┤PRESENT├ (SDK 1.14). @since 1.2.0 */
53
+ export const ITEM_KIND = 'scene.item';
54
+ /** The context kind of an exit button (SDK 1.14). @since 1.2.0 */
55
+ export const EXIT_KIND = 'scene.exit';
56
+
57
+ /** `data` of a `scene.item` target. @since 1.2.0 */
58
+ export interface SceneItemData { item: SceneView['items'][number] }
59
+ /** `data` of a `scene.exit` target. @since 1.2.0 */
60
+ export interface SceneExitData { exit: string }
61
+
62
+ /**
63
+ * Types `t.data` in another extension's `mu.menus.context({ target: 'scene.item' | 'scene.exit', … })` once it
64
+ * imports this file. @since 1.2.0
65
+ */
66
+ declare module '@muclient/sdk' {
67
+ interface ContextKinds {
68
+ 'scene.item': SceneItemData;
69
+ 'scene.exit': SceneExitData;
70
+ }
71
+ }