sci2-ts 0.15.1 → 0.17.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
@@ -4,6 +4,18 @@ All notable changes to this project. Versions follow [Semantic Versioning](https
4
4
  and the entries are generated from commit messages
5
5
  ([Conventional Commits](https://www.conventionalcommits.org/)) when a release is made.
6
6
 
7
+ ## [0.17.0](https://github.com/rlueder/sci-ts/compare/v0.16.0...v0.17.0) (2026-10-02)
8
+
9
+ ### Added
10
+
11
+ * **lib:** a sliding icon bar, and portraits at the bottom of the screen ([#20](https://github.com/rlueder/sci-ts/issues/20)) ([31e82ca](https://github.com/rlueder/sci-ts/commit/31e82ca33478c163b80b4f4fb16c5718b3c36bae))
12
+
13
+ ## [0.16.0](https://github.com/rlueder/sci-ts/compare/v0.15.1...v0.16.0) (2026-10-02)
14
+
15
+ ### Added
16
+
17
+ * **lib:** a lens magnifies what's under it, and portraits don't cover faces ([#19](https://github.com/rlueder/sci-ts/issues/19)) ([cc901d2](https://github.com/rlueder/sci-ts/commit/cc901d23716fad1b6466950fb77d7809f00b7f00))
18
+
7
19
  ## [0.15.1](https://github.com/rlueder/sci-ts/compare/v0.15.0...v0.15.1) (2026-10-02)
8
20
 
9
21
  ### Fixed
package/lib/991.sc CHANGED
@@ -31,10 +31,16 @@
31
31
  top -1
32
32
  box 0
33
33
  icons 0
34
- height 0)
34
+ height 0
35
+ slides TRUE ; slides down from above the screen, and back up when it closes
36
+ shown 0 ; how far down it is: 0 all the way, -height out of sight
37
+ leaving 0) ; sliding back up
35
38
 
36
39
  (method (show &tmp x y step)
37
- (if box (return))
40
+ ;; Opened again while it slides away: it comes back down.
41
+ (if box
42
+ (if leaving (= leaving 0) (= dialog self))
43
+ (return))
38
44
  (if (!= skin -1)
39
45
  (= box ((View new:) view: skin loop: 0 cel: 0 x: 0 y: 0 yourself:))
40
46
  (box init:)
@@ -66,7 +72,37 @@
66
72
  (self addIcon: (+ x (* 4 step)) y -1 V_ITEM)
67
73
  (self addIcon: (+ x (* 5 step)) y 4 ICON_INVENTORY)
68
74
  (self addIcon: (+ x (* 6 step)) y 5 ICON_MENU)
69
- (= dialog self))
75
+ (= dialog self)
76
+ (if slides
77
+ (= shown 0)
78
+ (self moveBy: (- height))
79
+ (= shown (- height))))
80
+
81
+ ;; Everything in the bar up or down by dy.
82
+ (method (moveBy dy &tmp node icon)
83
+ (box y: (+ (box y?) dy))
84
+ (UpdateScreenItem box)
85
+ (for ((= node (FirstNode (icons elements?)))) node ((= node (NextNode node)))
86
+ (= icon (NodeValue node))
87
+ (icon y: (+ (icon y?) dy))
88
+ (UpdateScreenItem icon)))
89
+
90
+ ;; A cycle of sliding (the User calls it while the bar is up): down while it opens, up
91
+ ;; while it closes, quickly at first and easing to a stop.
92
+ (method (step &tmp d)
93
+ (cond
94
+ ((not box) 0)
95
+ (leaving
96
+ (= d (+ 4 (/ (+ height shown) 3)))
97
+ (if (> d (+ height shown)) (= d (+ height shown)))
98
+ (self moveBy: (- d))
99
+ (-= shown d)
100
+ (if (<= shown (- height)) (self remove:)))
101
+ ((< shown 0)
102
+ (= d (+ 2 (/ (- shown) 3)))
103
+ (if (> d (- shown)) (= d (- shown)))
104
+ (self moveBy: d)
105
+ (+= shown d))))
70
106
 
71
107
  ;; An icon at (x, y): cel c of the bar's view (-1: the item in use), standing for `what`.
72
108
  (method (addIcon theX theY c what &tmp icon)
@@ -85,8 +121,17 @@
85
121
  (icon plane: uiPlane setPri: (+ (box priority?) 1))
86
122
  (icons add: icon))
87
123
 
124
+ ;; Closes it: it slides away (or goes at once, without slides), and stops taking clicks.
88
125
  (method (hide)
89
126
  (if (== dialog self) (= dialog 0))
127
+ (if (and slides box)
128
+ (= leaving 1)
129
+ else
130
+ (self remove:)))
131
+
132
+ (method (remove)
133
+ (= leaving 0)
134
+ (= shown 0)
90
135
  (if icons
91
136
  (icons eachElementDo: #dispose)
92
137
  (icons dispose:)
package/lib/993.sc CHANGED
@@ -5,13 +5,15 @@
5
5
 
6
6
  ;; Something the hero can carry. Its view's loop 0 is its icon in the inventory window and
7
7
  ;; loop 1 the cursor while it's being used (the cel's anchor is the hotspot); both anchored
8
- ;; at the top-left corner. Using it on something sends that thing doVerb: with the item's
8
+ ;; at the top-left corner. One that magnifies (a lens) has its glass as loop 2, placed like
9
+ ;; the cursor: while it's in use, the room shows through the glass `magnify` times larger. Using it on something sends that thing doVerb: with the item's
9
10
  ;; own verb, 10 and up, so the room answers with noun and that verb (in Yarn, a node like
10
11
  ;; filings.lens: the game names its items' verbs in items.yaml).
11
12
  (class InvItem of Obj
12
13
  (properties
13
14
  view -1
14
15
  verb 0
16
+ magnify 0 ; 0, or how much bigger things look through it
15
17
  description 0) ; what looking at it in the window says
16
18
 
17
19
  ;; Looked at in the inventory window.
package/lib/995.sc CHANGED
@@ -65,10 +65,15 @@
65
65
  (if (or (!= v cursor) (!= l cursorLoop))
66
66
  (= cursor v)
67
67
  (= cursorLoop l)
68
- (SetCursor v l 0)))
68
+ (SetCursor v l 0)
69
+ (if (and (== l 1) theItem (theItem magnify?))
70
+ (AddMagnify v 2 0 (theItem magnify?))
71
+ else
72
+ (DeleteMagnify))))
69
73
 
70
74
  (method (doit)
71
75
  (self showCursor:)
76
+ (if (and iconBar (iconBar box?)) (iconBar step:))
72
77
  (while (GetEvent EV_ALL theEvent)
73
78
  (self handleEvent: theEvent))
74
79
  ;; theEvent now says where the pointer is: at the top edge, the icon bar comes down.
package/lib/996.sc CHANGED
@@ -20,7 +20,7 @@
20
20
  margin 4 ; between the text and the border or frame
21
21
  portraitFrame -1 ; a view around portraits: loop 0 behind the face, loop 1 over its edges
22
22
  portraitX 8 ; where a portrait's face goes on the left (mirrored on the right)
23
- portraitY 8
23
+ portraitY 8 ; its top; below 0, its bottom that far above the screen's, its text with it
24
24
  nameFont -1)) ; the font for a speaker's name before their line (-1: the text's)
25
25
 
26
26
  ;; A box of text over the room: drawn by the interpreter into a bitmap, shown as a screen
@@ -233,24 +233,62 @@
233
233
  (= mouth ((Prop new:) view: view yourself:))
234
234
  (= eyes ((Prop new:) view: view yourself:))))
235
235
 
236
- ;; This line's side: see above.
237
- (method (pickSide &tmp s)
238
- (if (== self heroTalker) (return (- 1 otherSide)))
239
- (= s (if (and who ego (< (who x?) (ego x?))) 0 else 1))
240
- (if (and heroTalker (!= heroTalker self) (heroTalker respondsTo: #otherSide)) (heroTalker otherSide: s))
236
+ ;; This line's side: see above. A side whose portrait would cover the speaker or the hero
237
+ ;; (a face under the frame) gives way to the other, if that one covers no one.
238
+ (method (pickSide face &tmp s)
239
+ (= s
240
+ (cond
241
+ ((== self heroTalker) (- 1 otherSide))
242
+ ((and who ego (< (who x?) (ego x?))) 0)
243
+ (else 1)))
244
+ (if (and face (self covers: face s) (not (self covers: face (- 1 s)))) (= s (- 1 s)))
245
+ (if (and (!= self heroTalker) heroTalker (heroTalker respondsTo: #otherSide)) (heroTalker otherSide: s))
241
246
  (return s))
242
247
 
248
+ ;; Where the face's top goes: textStyle's portraitY, or below 0, counted up from the bottom.
249
+ (method (faceTop face &tmp py)
250
+ (= py (textStyle portraitY?))
251
+ (return (if (< py 0) (- (+ SCREEN_HEIGHT py) (CelHigh (face view?) 0 0)) else py)))
252
+
253
+ ;; Whether the portrait on side s (frame and all) would be over the speaker's or the hero's
254
+ ;; head.
255
+ (method (covers face s &tmp pw ph over left right top bottom f)
256
+ (= pw (CelWide (face view?) 0 0))
257
+ (= ph (CelHigh (face view?) 0 0))
258
+ (= f (textStyle portraitFrame?))
259
+ (= over (if (!= f -1) (/ (- (CelWide f 0 0) pw) 2) else 0))
260
+ (= left (if s (- SCREEN_WIDTH (+ (textStyle portraitX?) pw over)) else (- (textStyle portraitX?) over)))
261
+ (= right (+ left pw over over))
262
+ (= top (- (self faceTop: face) (if (!= f -1) (- (- (CelHigh f 0 0) ph) over) else 0)))
263
+ (= bottom (+ (self faceTop: face) ph over))
264
+ (return
265
+ (or
266
+ (self over: (if (== self heroTalker) ego else who) left right top bottom)
267
+ (and (!= self heroTalker) (self over: ego left right top bottom)))))
268
+
269
+ ;; Whether someone's head (the top quarter of their figure) is in the box.
270
+ (method (over body left right top bottom &tmp w h)
271
+ (if (or (not body) (not (body respondsTo: #view)) (& (body signal?) SIG_HIDDEN)) (return FALSE))
272
+ (= w (CelWide (body view?) (body loop?) (body cel?)))
273
+ (= h (CelHigh (body view?) (body loop?) (body cel?)))
274
+ (if (& (body scaleSignal?) SCALE_ON)
275
+ (= w (/ (* w (body scaleX?)) 128))
276
+ (= h (/ (* h (body scaleY?)) 128)))
277
+ (return
278
+ (and (< (- (body x?) (/ w 2)) right) (> (+ (body x?) (/ w 2)) left)
279
+ (< (- (body y?) h) bottom) (> (+ (- (body y?) h) (/ h 4)) top))))
280
+
243
281
  (method (say txt whoCares &tmp face l pw f fx fy over)
244
282
  (self init: makeParts:)
245
283
  (= face (if frame frame else bust))
246
284
  (if face
247
- (= side (self pickSide:))
285
+ (= side (self pickSide: face))
248
286
  (= l (if (and side (>= (NumLoops face) 6)) 3 else 0))
249
287
  (= pw (CelWide (face view?) l 0))
250
288
  (= f (textStyle portraitFrame?))
251
289
  (= over (if (!= f -1) (/ (- (CelWide f 0 0) pw) 2) else 0))
252
290
  (= fx (if side (- SCREEN_WIDTH (+ (textStyle portraitX?) pw)) else (textStyle portraitX?)))
253
- (= fy (textStyle portraitY?))
291
+ (= fy (self faceTop: face))
254
292
  (face x: fx y: fy loop: l cel: 0)
255
293
  (if mouth (mouth x: fx y: fy loop: (+ l 1) cel: 0))
256
294
  (if eyes (eyes x: fx y: fy loop: (+ l 2) cel: 0))
@@ -259,8 +297,9 @@
259
297
  (back x: fx y: fy)
260
298
  (if (and (not front) (>= (NumLoops back) 2)) (= front ((View new:) view: f loop: 1 cel: 0 yourself:)))
261
299
  (if front (front x: fx y: fy)))
262
- ;; The text beside it, towards the middle.
263
- (= y fy)
300
+ ;; The text beside it, towards the middle: level with its top, or at the bottom of the
301
+ ;; screen, ending level with its frame's bottom and growing upwards.
302
+ (= y (if (< (textStyle portraitY?) 0) (- (+ fy (CelHigh (face view?) l 0) over) SCREEN_HEIGHT) else fy))
264
303
  (if side
265
304
  (= x 6)
266
305
  (= width (- fx (+ over 12)))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sci2-ts",
3
- "version": "0.15.1",
3
+ "version": "0.17.0",
4
4
  "description": "An SCI2 adventure game engine in TypeScript, and the tools to make games with it",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -56,6 +56,16 @@ export declare class Graphics {
56
56
  x: number;
57
57
  y: number;
58
58
  };
59
+ /**
60
+ * The cursor as a magnifying glass (AddMagnify): where this cel is opaque, placed like the
61
+ * cursor, the screen under the cursor's hotspot shows `zoom` times larger.
62
+ */
63
+ magnify: {
64
+ view: number;
65
+ loop: number;
66
+ cel: number;
67
+ zoom: number;
68
+ } | undefined;
59
69
  /** Palette range affected by fade show styles (SetPalStyleRange). */
60
70
  styleRange: {
61
71
  from: number;
@@ -85,6 +85,11 @@ export class Graphics {
85
85
  transitions = new Map();
86
86
  /** The mouse cursor: a view cel drawn on top at the mouse position (its origin = hotspot). */
87
87
  cursor = { view: -1, loop: 0, cel: 0, visible: true, x: 160, y: 100 };
88
+ /**
89
+ * The cursor as a magnifying glass (AddMagnify): where this cel is opaque, placed like the
90
+ * cursor, the screen under the cursor's hotspot shows `zoom` times larger.
91
+ */
92
+ magnify;
88
93
  /** Palette range affected by fade show styles (SetPalStyleRange). */
89
94
  styleRange = { from: 0, to: 255 };
90
95
  lastFrame;
@@ -218,8 +223,28 @@ export class Graphics {
218
223
  for (const d of draws)
219
224
  blit(pixels, d, clip, remap);
220
225
  }
221
- // The cursor goes on top of everything.
226
+ // The cursor goes on top of everything: first what a magnifier shows through its glass.
222
227
  const cur = this.cursor;
228
+ const mag = this.magnify;
229
+ const glass = cur.visible && mag ? this.cel(mag.view, mag.loop, mag.cel) : undefined;
230
+ if (glass && mag) {
231
+ const scene = pixels.slice();
232
+ const o = celOrigin(glass.cel, glass.mirror);
233
+ const { width: w, height: h, skipColor } = glass.cel;
234
+ for (let gy = 0; gy < h; gy++) {
235
+ for (let gx = 0; gx < w; gx++) {
236
+ if (glass.cel.pixels[gy * w + (glass.mirror ? w - 1 - gx : gx)] === skipColor)
237
+ continue;
238
+ const x = cur.x - o.x + gx, y = cur.y - o.y + gy;
239
+ if (x < 0 || y < 0 || x >= SCREEN_WIDTH || y >= SCREEN_HEIGHT)
240
+ continue;
241
+ const sx = cur.x + Math.floor((x - cur.x) / mag.zoom), sy = cur.y + Math.floor((y - cur.y) / mag.zoom);
242
+ if (sx < 0 || sy < 0 || sx >= SCREEN_WIDTH || sy >= SCREEN_HEIGHT)
243
+ continue;
244
+ pixels[y * SCREEN_WIDTH + x] = scene[sy * SCREEN_WIDTH + sx];
245
+ }
246
+ }
247
+ }
223
248
  if (cur.visible && cur.view >= 0) {
224
249
  const found = this.cel(cur.view, cur.loop, cur.cel);
225
250
  if (found) {
@@ -467,8 +492,17 @@ export const graphicsKernels = {
467
492
  HaveMouse: () => 1,
468
493
  ShakeScreen: () => 0,
469
494
  SetScroll: () => 0,
470
- AddMagnify: () => 0,
471
- DeleteMagnify: () => 0,
495
+ // AddMagnify(view, loop, cel, zoom): sci-ts makes the cursor a magnifying glass, the cel's
496
+ // opaque pixels its glass (placed like the cursor), showing what's under the hotspot zoom
497
+ // times larger. DeleteMagnify() stops it.
498
+ AddMagnify: (vm, [view = 0, loop = 0, cel = 0, zoom = 2]) => {
499
+ graphics(vm).magnify = { view: toSigned(view), loop, cel, zoom: Math.max(1, toSigned(zoom)) };
500
+ return 0;
501
+ },
502
+ DeleteMagnify: (vm) => {
503
+ graphics(vm).magnify = undefined;
504
+ return 0;
505
+ },
472
506
  GetHighItemPri: () => 0,
473
507
  SetFontRes: () => 0,
474
508
  NULL_: () => NULL,
@@ -90,6 +90,11 @@ export class Graphics {
90
90
  readonly transitions = new Map<Value, Transition>();
91
91
  /** The mouse cursor: a view cel drawn on top at the mouse position (its origin = hotspot). */
92
92
  cursor = { view: -1, loop: 0, cel: 0, visible: true, x: 160, y: 100 };
93
+ /**
94
+ * The cursor as a magnifying glass (AddMagnify): where this cel is opaque, placed like the
95
+ * cursor, the screen under the cursor's hotspot shows `zoom` times larger.
96
+ */
97
+ magnify: { view: number; loop: number; cel: number; zoom: number } | undefined;
93
98
  /** Palette range affected by fade show styles (SetPalStyleRange). */
94
99
  styleRange = { from: 0, to: 255 };
95
100
  lastFrame: Frame | undefined;
@@ -224,8 +229,25 @@ export class Graphics {
224
229
  }
225
230
  for (const d of draws) blit(pixels, d, clip, remap);
226
231
  }
227
- // The cursor goes on top of everything.
232
+ // The cursor goes on top of everything: first what a magnifier shows through its glass.
228
233
  const cur = this.cursor;
234
+ const mag = this.magnify;
235
+ const glass = cur.visible && mag ? this.cel(mag.view, mag.loop, mag.cel) : undefined;
236
+ if (glass && mag) {
237
+ const scene = pixels.slice();
238
+ const o = celOrigin(glass.cel, glass.mirror);
239
+ const { width: w, height: h, skipColor } = glass.cel;
240
+ for (let gy = 0; gy < h; gy++) {
241
+ for (let gx = 0; gx < w; gx++) {
242
+ if (glass.cel.pixels[gy * w + (glass.mirror ? w - 1 - gx : gx)] === skipColor) continue;
243
+ const x = cur.x - o.x + gx, y = cur.y - o.y + gy;
244
+ if (x < 0 || y < 0 || x >= SCREEN_WIDTH || y >= SCREEN_HEIGHT) continue;
245
+ const sx = cur.x + Math.floor((x - cur.x) / mag.zoom), sy = cur.y + Math.floor((y - cur.y) / mag.zoom);
246
+ if (sx < 0 || sy < 0 || sx >= SCREEN_WIDTH || sy >= SCREEN_HEIGHT) continue;
247
+ pixels[y * SCREEN_WIDTH + x] = scene[sy * SCREEN_WIDTH + sx]!;
248
+ }
249
+ }
250
+ }
229
251
  if (cur.visible && cur.view >= 0) {
230
252
  const found = this.cel(cur.view, cur.loop, cur.cel);
231
253
  if (found) {
@@ -501,8 +523,17 @@ export const graphicsKernels: Record<string, KernelFn> = {
501
523
  HaveMouse: () => 1,
502
524
  ShakeScreen: () => 0,
503
525
  SetScroll: () => 0,
504
- AddMagnify: () => 0,
505
- DeleteMagnify: () => 0,
526
+ // AddMagnify(view, loop, cel, zoom): sci-ts makes the cursor a magnifying glass, the cel's
527
+ // opaque pixels its glass (placed like the cursor), showing what's under the hotspot zoom
528
+ // times larger. DeleteMagnify() stops it.
529
+ AddMagnify: (vm, [view = 0, loop = 0, cel = 0, zoom = 2]) => {
530
+ graphics(vm).magnify = { view: toSigned(view), loop, cel, zoom: Math.max(1, toSigned(zoom)) };
531
+ return 0;
532
+ },
533
+ DeleteMagnify: (vm) => {
534
+ graphics(vm).magnify = undefined;
535
+ return 0;
536
+ },
506
537
  GetHighItemPri: () => 0,
507
538
  SetFontRes: () => 0,
508
539
  NULL_: () => NULL,
@@ -60,12 +60,14 @@ export interface GameItem {
60
60
  verb: number;
61
61
  view?: number;
62
62
  description?: string;
63
+ /** How much bigger things look through it (a lens): its view's loop 2 is the glass. */
64
+ magnify?: number;
63
65
  }
64
66
  /**
65
67
  * items.yaml: each thing the hero can carry, by name. `lens: 10` names just its verb (10 to
66
68
  * 255; 1 to 4 are the player's), for a game whose scripts make the item; with a view, the
67
69
  * build makes it too, in script ITEMS_SCRIPT, and rooms can give it (`<<get lens>>`):
68
70
  *
69
- * lens: { verb: 10, view: 250, description: "Holmes's lens." }
71
+ * lens: { verb: 10, view: 250, description: "Holmes's lens.", magnify: 2 }
70
72
  */
71
73
  export declare function gameItems(dir: string): Record<string, GameItem>;
@@ -340,7 +340,7 @@ export async function buildGame(dir, options = {}) {
340
340
  * 255; 1 to 4 are the player's), for a game whose scripts make the item; with a view, the
341
341
  * build makes it too, in script ITEMS_SCRIPT, and rooms can give it (`<<get lens>>`):
342
342
  *
343
- * lens: { verb: 10, view: 250, description: "Holmes's lens." }
343
+ * lens: { verb: 10, view: 250, description: "Holmes's lens.", magnify: 2 }
344
344
  */
345
345
  export function gameItems(dir) {
346
346
  const file = join(dir, "items.yaml");
@@ -366,7 +366,7 @@ export function gameItems(dir) {
366
366
  const spec = typeof value === "number" ? { verb: value } : value;
367
367
  if (!spec || typeof spec !== "object" || Array.isArray(spec))
368
368
  return fail(`${name}: expected a verb, or { verb, view, description }`);
369
- const { verb, view, description, ...rest } = spec;
369
+ const { verb, view, description, magnify, ...rest } = spec;
370
370
  const unknown = Object.keys(rest)[0];
371
371
  if (unknown)
372
372
  fail(`${name}: unknown key ${unknown}`);
@@ -380,7 +380,11 @@ export function gameItems(dir) {
380
380
  fail(`${name}: description is text without quotes or backslashes`);
381
381
  if (description !== undefined && view === undefined)
382
382
  fail(`${name}: an item the build makes needs a view`);
383
- items[name] = { verb: verb, view: view, description: description };
383
+ if (magnify !== undefined && (typeof magnify !== "number" || !Number.isInteger(magnify) || magnify < 2 || magnify > 8))
384
+ fail(`${name}: magnify is how much bigger, 2 to 8`);
385
+ if (magnify !== undefined && view === undefined)
386
+ fail(`${name}: an item that magnifies needs a view (its loop 2 is the glass)`);
387
+ items[name] = { verb: verb, view: view, description: description, ...(magnify === undefined ? {} : { magnify: magnify }) };
384
388
  }
385
389
  return items;
386
390
  }
@@ -394,7 +398,7 @@ function itemsScript(items) {
394
398
  `(script ${ITEMS_SCRIPT})`,
395
399
  '(include "system.sh")',
396
400
  `(public ${made.map(([name], i) => `${name} ${i}`).join(" ")})`,
397
- ...made.map(([name, i]) => `(instance ${name} of InvItem (properties view ${i.view} verb ${i.verb}${i.description ? ` description "${i.description}"` : ""}))`),
401
+ ...made.map(([name, i]) => `(instance ${name} of InvItem (properties view ${i.view} verb ${i.verb}${i.magnify ? ` magnify ${i.magnify}` : ""}${i.description ? ` description "${i.description}"` : ""}))`),
398
402
  "",
399
403
  ].join("\n");
400
404
  }
@@ -370,6 +370,8 @@ export interface GameItem {
370
370
  verb: number;
371
371
  view?: number;
372
372
  description?: string;
373
+ /** How much bigger things look through it (a lens): its view's loop 2 is the glass. */
374
+ magnify?: number;
373
375
  }
374
376
 
375
377
  /**
@@ -377,7 +379,7 @@ export interface GameItem {
377
379
  * 255; 1 to 4 are the player's), for a game whose scripts make the item; with a view, the
378
380
  * build makes it too, in script ITEMS_SCRIPT, and rooms can give it (`<<get lens>>`):
379
381
  *
380
- * lens: { verb: 10, view: 250, description: "Holmes's lens." }
382
+ * lens: { verb: 10, view: 250, description: "Holmes's lens.", magnify: 2 }
381
383
  */
382
384
  export function gameItems(dir: string): Record<string, GameItem> {
383
385
  const file = join(dir, "items.yaml");
@@ -394,7 +396,7 @@ export function gameItems(dir: string): Record<string, GameItem> {
394
396
  if (!/^[a-z][A-Za-z0-9]*$/.test(name) || ["look", "talk", "walk", "do", "enter"].includes(name)) fail(`"${name}" can't name an item (lower camelCase, and not a verb)`);
395
397
  const spec = typeof value === "number" ? { verb: value } : value;
396
398
  if (!spec || typeof spec !== "object" || Array.isArray(spec)) return fail(`${name}: expected a verb, or { verb, view, description }`);
397
- const { verb, view, description, ...rest } = spec as Record<string, unknown>;
399
+ const { verb, view, description, magnify, ...rest } = spec as Record<string, unknown>;
398
400
  const unknown = Object.keys(rest)[0];
399
401
  if (unknown) fail(`${name}: unknown key ${unknown}`);
400
402
  if (typeof verb !== "number" || !Number.isInteger(verb) || verb < 10 || verb > 255) fail(`${name}: the verb is a number from 10 to 255`);
@@ -402,7 +404,9 @@ export function gameItems(dir: string): Record<string, GameItem> {
402
404
  if (view !== undefined && (typeof view !== "number" || !Number.isInteger(view) || view < 0 || view > 65535)) fail(`${name}: view is a view number`);
403
405
  if (description !== undefined && (typeof description !== "string" || /["\\]/.test(description))) fail(`${name}: description is text without quotes or backslashes`);
404
406
  if (description !== undefined && view === undefined) fail(`${name}: an item the build makes needs a view`);
405
- items[name] = { verb: verb as number, view: view as number | undefined, description: description as string | undefined };
407
+ if (magnify !== undefined && (typeof magnify !== "number" || !Number.isInteger(magnify) || magnify < 2 || magnify > 8)) fail(`${name}: magnify is how much bigger, 2 to 8`);
408
+ if (magnify !== undefined && view === undefined) fail(`${name}: an item that magnifies needs a view (its loop 2 is the glass)`);
409
+ items[name] = { verb: verb as number, view: view as number | undefined, description: description as string | undefined, ...(magnify === undefined ? {} : { magnify: magnify as number }) };
406
410
  }
407
411
  return items;
408
412
  }
@@ -416,7 +420,7 @@ function itemsScript(items: Record<string, GameItem>): string | undefined {
416
420
  `(script ${ITEMS_SCRIPT})`,
417
421
  '(include "system.sh")',
418
422
  `(public ${made.map(([name], i) => `${name} ${i}`).join(" ")})`,
419
- ...made.map(([name, i]) => `(instance ${name} of InvItem (properties view ${i.view} verb ${i.verb}${i.description ? ` description "${i.description}"` : ""}))`),
423
+ ...made.map(([name, i]) => `(instance ${name} of InvItem (properties view ${i.view} verb ${i.verb}${i.magnify ? ` magnify ${i.magnify}` : ""}${i.description ? ` description "${i.description}"` : ""}))`),
420
424
  "",
421
425
  ].join("\n");
422
426
  }