sci2-ts 0.13.0 → 0.15.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 +12 -0
- package/lib/991.sc +44 -30
- package/lib/993.sc +40 -6
- package/lib/996.sc +8 -3
- package/lib/997.sc +48 -14
- package/package.json +1 -1
- package/packages/content/src/room.js +46 -3
- package/packages/content/src/room.ts +41 -3
- package/packages/content/src/target.d.ts +9 -0
- package/packages/content/src/target.ts +5 -0
- package/packages/sci/src/index.d.ts +1 -0
- package/packages/sci/src/index.js +1 -0
- package/packages/sci/src/index.ts +1 -0
- package/packages/sci/src/text/font.d.ts +19 -2
- package/packages/sci/src/text/font.js +62 -14
- package/packages/sci/src/text/font.ts +64 -15
- package/packages/sci/src/text/styled.d.ts +40 -0
- package/packages/sci/src/text/styled.js +92 -0
- package/packages/sci/src/text/styled.ts +110 -0
- package/packages/sci/src/vm/kernels/text.js +40 -29
- package/packages/sci/src/vm/kernels/text.ts +41 -28
- package/tools/art/build.d.ts +9 -3
- package/tools/art/build.js +33 -2
- package/tools/art/build.ts +27 -4
- package/tools/art/font.d.ts +20 -0
- package/tools/art/font.js +35 -0
- package/tools/art/font.ts +41 -0
- package/tools/game/build.d.ts +3 -1
- package/tools/game/build.js +13 -1
- package/tools/game/build.ts +17 -3
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.15.0](https://github.com/rlueder/sci-ts/compare/v0.14.0...v0.15.0) (2026-10-02)
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
* bitmap fonts with bearings and advances, styled text, and skinned icon bar and inventory ([#17](https://github.com/rlueder/sci-ts/issues/17)) ([0cec83b](https://github.com/rlueder/sci-ts/commit/0cec83be7211ee532c6846a08826a2b09f462ee1))
|
|
12
|
+
|
|
13
|
+
## [0.14.0](https://github.com/rlueder/sci-ts/compare/v0.13.0...v0.14.0) (2026-10-02)
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
* **lib:** figures drawn smaller walk and step slower, and change size in steps ([#16](https://github.com/rlueder/sci-ts/issues/16)) ([06f2b1d](https://github.com/rlueder/sci-ts/commit/06f2b1d91fd95d5b648104b435eb58e3355ba808))
|
|
18
|
+
|
|
7
19
|
## [0.13.0](https://github.com/rlueder/sci-ts/compare/v0.12.0...v0.13.0) (2026-10-02)
|
|
8
20
|
|
|
9
21
|
### Added
|
package/lib/991.sc
CHANGED
|
@@ -13,50 +13,63 @@
|
|
|
13
13
|
(return (and (>= theX x) (< theX (+ x size)) (>= theY y) (< theY (+ y size))))))
|
|
14
14
|
|
|
15
15
|
;; Slides down when the pointer reaches the top edge of the screen, or on a tap there. Its
|
|
16
|
-
;; icons are the cels of `view`, loop 0 as they are and loop 1 picked, each
|
|
16
|
+
;; icons are the cels of `view`, loop 0 as they are and loop 1 picked, each `size`
|
|
17
17
|
;; square and anchored at the top-left corner: walk, look, do, talk, the inventory, the
|
|
18
18
|
;; game menu (the item in use shows its own icon). A game with its own sets view in its
|
|
19
19
|
;; init: (iconBar view: 266). Picking one, or a click below the bar, closes it.
|
|
20
|
+
;; With `skin`, a view drawn across the top of the screen (loop 0, cel 0, anchored at its top
|
|
21
|
+
;; left) instead of the box, the icons `size` square at `left`, `left` + `spacing` and on, at
|
|
22
|
+
;; `top`; the item in use goes in the fifth place, between the verbs and the inventory.
|
|
23
|
+
;; (iconBar view: 266 skin: 267 size: 32 left: 16 spacing: 42 top: 8)
|
|
20
24
|
(class IconBar of Obj
|
|
21
25
|
(properties
|
|
22
26
|
view ICON_BAR_VIEW
|
|
27
|
+
skin -1
|
|
28
|
+
size ICON_SIZE
|
|
29
|
+
left -1 ; -1 here and below: laid out inside the box
|
|
30
|
+
spacing -1
|
|
31
|
+
top -1
|
|
23
32
|
box 0
|
|
24
33
|
icons 0
|
|
25
34
|
height 0)
|
|
26
35
|
|
|
27
|
-
(method (show &tmp x)
|
|
36
|
+
(method (show &tmp x y step)
|
|
28
37
|
(if box (return))
|
|
29
|
-
(
|
|
30
|
-
((
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
(
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
(if (!= skin -1)
|
|
39
|
+
(= box ((View new:) view: skin loop: 0 cel: 0 x: 0 y: 0 yourself:))
|
|
40
|
+
(box init:)
|
|
41
|
+
(box plane: uiPlane setPri: 100)
|
|
42
|
+
(= height (CelHigh skin 0 0))
|
|
43
|
+
(= x (if (!= left -1) left else ICON_GAP))
|
|
44
|
+
(= y (if (!= top -1) top else ICON_GAP))
|
|
45
|
+
else
|
|
46
|
+
(= box
|
|
47
|
+
((TextItem new:)
|
|
48
|
+
text: ""
|
|
49
|
+
width: SCREEN_WIDTH
|
|
50
|
+
height: (+ size (* 2 ICON_GAP) (* 2 (textStyle margin?)))
|
|
51
|
+
x: 0 y: 0
|
|
52
|
+
yourself:))
|
|
53
|
+
(if (!= (textStyle frame?) -1)
|
|
54
|
+
(box height: (+ (box height?) (CelHigh (textStyle frame?) 0 4) (CelHigh (textStyle frame?) 0 5))))
|
|
55
|
+
(box init:)
|
|
56
|
+
(= height (box height?))
|
|
57
|
+
(= x (if (!= left -1) left else (+ (box textLeft?) ICON_GAP)))
|
|
58
|
+
(= y (if (!= top -1) top else (+ (box textTop?) ICON_GAP))))
|
|
59
|
+
(= step (if (!= spacing -1) spacing else (+ size ICON_GAP)))
|
|
40
60
|
(= icons (List new:))
|
|
41
|
-
(= x (+ (box textLeft?) ICON_GAP))
|
|
42
61
|
;; The verbs, then the item, the inventory and the menu.
|
|
43
|
-
(self addIcon: x 0 V_WALK)
|
|
44
|
-
(
|
|
45
|
-
(self addIcon: x
|
|
46
|
-
(
|
|
47
|
-
(self addIcon: x
|
|
48
|
-
(
|
|
49
|
-
(self addIcon: x
|
|
50
|
-
(+= x (+ ICON_SIZE ICON_GAP))
|
|
51
|
-
(self addIcon: x -1 V_ITEM)
|
|
52
|
-
(+= x (+ ICON_SIZE ICON_GAP))
|
|
53
|
-
(self addIcon: x 4 ICON_INVENTORY)
|
|
54
|
-
(+= x (+ ICON_SIZE ICON_GAP))
|
|
55
|
-
(self addIcon: x 5 ICON_MENU)
|
|
62
|
+
(self addIcon: x y 0 V_WALK)
|
|
63
|
+
(self addIcon: (+ x step) y 1 V_LOOK)
|
|
64
|
+
(self addIcon: (+ x (* 2 step)) y 2 V_DO)
|
|
65
|
+
(self addIcon: (+ x (* 3 step)) y 3 V_TALK)
|
|
66
|
+
(self addIcon: (+ x (* 4 step)) y -1 V_ITEM)
|
|
67
|
+
(self addIcon: (+ x (* 5 step)) y 4 ICON_INVENTORY)
|
|
68
|
+
(self addIcon: (+ x (* 6 step)) y 5 ICON_MENU)
|
|
56
69
|
(= dialog self))
|
|
57
70
|
|
|
58
|
-
;; An icon at x: cel c of the bar's view (-1: the item in use), standing for `what`.
|
|
59
|
-
(method (addIcon theX c what &tmp icon)
|
|
71
|
+
;; An icon at (x, y): cel c of the bar's view (-1: the item in use), standing for `what`.
|
|
72
|
+
(method (addIcon theX theY c what &tmp icon)
|
|
60
73
|
(if (and (== c -1) (not theItem)) (return))
|
|
61
74
|
(= icon
|
|
62
75
|
((Icon new:)
|
|
@@ -64,8 +77,9 @@
|
|
|
64
77
|
loop: (if (and (!= c -1) (== what (user verb?))) 1 else 0)
|
|
65
78
|
cel: (if (== c -1) 0 else c)
|
|
66
79
|
actions: what
|
|
80
|
+
size: size
|
|
67
81
|
x: theX
|
|
68
|
-
y:
|
|
82
|
+
y: theY
|
|
69
83
|
yourself:))
|
|
70
84
|
(icon init:)
|
|
71
85
|
(icon plane: uiPlane setPri: (+ (box priority?) 1))
|
package/lib/993.sc
CHANGED
|
@@ -21,16 +21,27 @@
|
|
|
21
21
|
;; What the hero carries (the `inventory` global): (inventory add: lens), (inventory
|
|
22
22
|
;; contains: lens). showSelf opens the window: the items in a row, one click picks one to
|
|
23
23
|
;; use (it becomes the cursor), a right-click on one looks at it, a click elsewhere closes.
|
|
24
|
+
;;
|
|
25
|
+
;; With `skin`, the window is that view (loop 0, cel 0, anchored at its top left) at x, y,
|
|
26
|
+
;; and the items go in a grid on it: `cols` across, the first slot at (slotLeft, slotTop) in
|
|
27
|
+
;; the skin and the rest slotWidth and slotHeight on, each icon `inset` into its slot.
|
|
28
|
+
;; (inventory skin: 268 x: 32 y: 30 cols: 4 slotLeft: 13 slotTop: 28 slotWidth: 60
|
|
29
|
+
;; slotHeight: 47 inset: 7 iconSize: 32)
|
|
24
30
|
(class Inventory of Set
|
|
25
31
|
(properties
|
|
26
32
|
window 0
|
|
27
33
|
icons 0
|
|
28
|
-
|
|
34
|
+
x -1 ; -1: centred
|
|
35
|
+
y 24
|
|
36
|
+
skin -1
|
|
37
|
+
cols 4 slotLeft 0 slotTop 0 slotWidth 0 slotHeight 0 inset 0
|
|
38
|
+
iconSize INV_ICON)
|
|
29
39
|
|
|
30
|
-
(method (showSelf &tmp node item icon
|
|
40
|
+
(method (showSelf &tmp node item icon ix i)
|
|
31
41
|
(if (not size)
|
|
32
42
|
(narrator say: "You aren't carrying anything.")
|
|
33
43
|
(return))
|
|
44
|
+
(if (!= skin -1) (self showCase:) (return))
|
|
34
45
|
(= window
|
|
35
46
|
((TextItem new:)
|
|
36
47
|
text: ""
|
|
@@ -41,17 +52,40 @@
|
|
|
41
52
|
(window
|
|
42
53
|
width: (+ (window width?) (CelWide (textStyle frame?) 0 6) (CelWide (textStyle frame?) 0 7))
|
|
43
54
|
height: (+ (window height?) (CelHigh (textStyle frame?) 0 4) (CelHigh (textStyle frame?) 0 5))))
|
|
44
|
-
(window x: (/ (- SCREEN_WIDTH (window width?)) 2) y: y init:)
|
|
55
|
+
(window x: (if (== x -1) (/ (- SCREEN_WIDTH (window width?)) 2) else x) y: y init:)
|
|
45
56
|
(= icons (List new:))
|
|
46
|
-
(=
|
|
57
|
+
(= ix (+ (window x?) (window textLeft?) INV_GAP))
|
|
47
58
|
(for ((= node (FirstNode elements))) node ((= node (NextNode node)))
|
|
48
59
|
(= item (NodeValue node))
|
|
49
60
|
;; The icon keeps its item in `actions`.
|
|
50
|
-
(= icon ((Icon new:) view: (item view?) loop: 0 cel: 0 actions: item
|
|
61
|
+
(= icon ((Icon new:) view: (item view?) loop: 0 cel: 0 actions: item size: iconSize x: ix y: (+ (window y?) (window textTop?) INV_GAP) yourself:))
|
|
51
62
|
(icon init:)
|
|
52
63
|
(icon plane: uiPlane setPri: (+ (window priority?) 1))
|
|
53
64
|
(icons add: icon)
|
|
54
|
-
(+=
|
|
65
|
+
(+= ix (+ iconSize INV_GAP)))
|
|
66
|
+
(= dialog self))
|
|
67
|
+
|
|
68
|
+
;; The window as the skin, with the items in its slots (as many as it has).
|
|
69
|
+
(method (showCase &tmp node item icon i wx wy)
|
|
70
|
+
(= wx (if (== x -1) (/ (- SCREEN_WIDTH (CelWide skin 0 0)) 2) else x))
|
|
71
|
+
(= wy y)
|
|
72
|
+
(= window ((View new:) view: skin loop: 0 cel: 0 x: wx y: wy yourself:))
|
|
73
|
+
(window init:)
|
|
74
|
+
(window plane: uiPlane setPri: 100)
|
|
75
|
+
(= icons (List new:))
|
|
76
|
+
(= i 0)
|
|
77
|
+
(for ((= node (FirstNode elements))) node ((= node (NextNode node)))
|
|
78
|
+
(= item (NodeValue node))
|
|
79
|
+
(= icon
|
|
80
|
+
((Icon new:)
|
|
81
|
+
view: (item view?) loop: 0 cel: 0 actions: item size: iconSize
|
|
82
|
+
x: (+ wx slotLeft (* (mod i cols) slotWidth) inset)
|
|
83
|
+
y: (+ wy slotTop (* (/ i cols) slotHeight) inset)
|
|
84
|
+
yourself:))
|
|
85
|
+
(icon init:)
|
|
86
|
+
(icon plane: uiPlane setPri: 101)
|
|
87
|
+
(icons add: icon)
|
|
88
|
+
(++ i))
|
|
55
89
|
(= dialog self))
|
|
56
90
|
|
|
57
91
|
;; The item whose icon is at (x, y), or 0.
|
package/lib/996.sc
CHANGED
|
@@ -20,7 +20,8 @@
|
|
|
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
|
|
24
|
+
nameFont -1)) ; the font for a speaker's name before their line (-1: the text's)
|
|
24
25
|
|
|
25
26
|
;; A box of text over the room: drawn by the interpreter into a bitmap, shown as a screen
|
|
26
27
|
;; item in the UI plane. Set text (and width, x, y), then init; it's as tall as the text
|
|
@@ -183,14 +184,18 @@
|
|
|
183
184
|
(= caller 0)
|
|
184
185
|
(if c (c cue:))))
|
|
185
186
|
|
|
186
|
-
;; Someone with a name: their lines start with it.
|
|
187
|
+
;; Someone with a name: their lines start with it (in the textStyle's nameFont, if it has one).
|
|
187
188
|
(class Talker of Narrator
|
|
188
189
|
(properties
|
|
189
190
|
line 0)
|
|
190
191
|
|
|
191
192
|
(method (say txt whoCares)
|
|
192
193
|
(if line (String ARRAY_FREE line))
|
|
193
|
-
(= line
|
|
194
|
+
(= line
|
|
195
|
+
(if (!= (textStyle nameFont?) -1)
|
|
196
|
+
(String 11 "|f%d|%s:|f| %s" (textStyle nameFont?) name txt)
|
|
197
|
+
else
|
|
198
|
+
(String 11 "%s: %s" name txt)))
|
|
194
199
|
(super say: line &rest whoCares)))
|
|
195
200
|
|
|
196
201
|
;; Someone with a portrait: a view whose loop 0 is the bust, loop 1 the mouth and loop 2 the
|
package/lib/997.sc
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
;; Walks its client in a straight line to (x, y), a step (xStep, yStep) every
|
|
7
7
|
;; moveSpeed cycles. The interpreter does the stepping (InitBresen, DoBresen) and sends
|
|
8
8
|
;; moveDone on arrival; the client then calls motionCue:, which cues the caller.
|
|
9
|
+
;; A client drawn smaller than full size (farther off) steps less often, in proportion,
|
|
10
|
+
;; so it doesn't slide across the floor.
|
|
9
11
|
(class Motion of Obj
|
|
10
12
|
(properties
|
|
11
13
|
client 0
|
|
@@ -13,6 +15,7 @@
|
|
|
13
15
|
x 0 y 0
|
|
14
16
|
dx 0 dy 0
|
|
15
17
|
b-moveCnt 0 b-i1 0 b-i2 0 b-di 0 b-xAxis 0 b-incr 0
|
|
18
|
+
progress 0 ; towards the next step, in 128ths of a cycle at full size
|
|
16
19
|
completed 0)
|
|
17
20
|
|
|
18
21
|
(method (init who toX toY whoCares)
|
|
@@ -25,12 +28,21 @@
|
|
|
25
28
|
(= x toX)
|
|
26
29
|
(= y toY)
|
|
27
30
|
(= b-moveCnt gameTime)
|
|
31
|
+
(= progress 0)
|
|
28
32
|
(client setHeading: (GetAngle (client x?) (client y?) x y))
|
|
29
33
|
(InitBresen self))
|
|
30
34
|
|
|
31
|
-
(
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
;; Each cycle counts for the client's size (128 at full size); a step takes moveSpeed
|
|
36
|
+
;; cycles' worth.
|
|
37
|
+
(method (doit &tmp t e)
|
|
38
|
+
(= t (* (client moveSpeed?) 128))
|
|
39
|
+
;; (After a long pause, at most 16 cycles count, so the sum stays in 16 bits.)
|
|
40
|
+
(if (> (= e (- gameTime b-moveCnt)) 16) (= e 16))
|
|
41
|
+
(+= progress (* e (if (& (client scaleSignal?) SCALE_ON) (client scaleX?) else 128)))
|
|
42
|
+
(= b-moveCnt gameTime)
|
|
43
|
+
(if (> progress t) (= progress t))
|
|
44
|
+
(if (>= progress t)
|
|
45
|
+
(-= progress t)
|
|
34
46
|
(DoBresen self)))
|
|
35
47
|
|
|
36
48
|
(method (moveDone)
|
|
@@ -139,8 +151,22 @@
|
|
|
139
151
|
(if (> c (self lastCel:)) (= c 0))
|
|
140
152
|
(client cel: c)))
|
|
141
153
|
|
|
142
|
-
;; Round and round while the client moves; cel 0 when it stands.
|
|
154
|
+
;; Round and round while the client moves; cel 0 when it stands. Its legs keep pace with
|
|
155
|
+
;; its steps: drawn smaller, it turns its cels less often, in proportion.
|
|
143
156
|
(class Walk of Forward
|
|
157
|
+
(properties
|
|
158
|
+
progress 0)
|
|
159
|
+
|
|
160
|
+
(method (nextCel &tmp t e)
|
|
161
|
+
(= t (* (client cycleSpeed?) 128))
|
|
162
|
+
(if (> (= e (- gameTime cycleCnt)) 16) (= e 16))
|
|
163
|
+
(+= progress (* e (if (& (client scaleSignal?) SCALE_ON) (client scaleX?) else 128)))
|
|
164
|
+
(= cycleCnt gameTime)
|
|
165
|
+
(if (> progress t) (= progress t))
|
|
166
|
+
(if (< progress t) (return (client cel?)))
|
|
167
|
+
(-= progress t)
|
|
168
|
+
(return (+ (client cel?) cycleDir)))
|
|
169
|
+
|
|
144
170
|
(method (doit)
|
|
145
171
|
(if (client mover?)
|
|
146
172
|
(super doit:)
|
|
@@ -198,12 +224,15 @@
|
|
|
198
224
|
(client cel: c)))))
|
|
199
225
|
|
|
200
226
|
;; Sizes its client by how far up the screen it stands: frontSize percent at frontY,
|
|
201
|
-
;; backSize at backY, in between in proportion.
|
|
227
|
+
;; backSize at backY, in between in proportion. The size changes in steps of at least `step`
|
|
228
|
+
;; percent: a figure redrawn at a new size for every line it moves shimmers.
|
|
202
229
|
(class Scaler of Obj
|
|
203
230
|
(properties
|
|
204
231
|
client 0
|
|
205
232
|
frontSize 100 backSize 100
|
|
206
|
-
frontY 190 backY 0
|
|
233
|
+
frontY 190 backY 0
|
|
234
|
+
step 2
|
|
235
|
+
shown -1) ; the size the client is drawn at
|
|
207
236
|
|
|
208
237
|
(method (init who fs bs fy by)
|
|
209
238
|
(= client who)
|
|
@@ -220,10 +249,18 @@
|
|
|
220
249
|
((>= y frontY) frontSize)
|
|
221
250
|
((<= y backY) backSize)
|
|
222
251
|
(else (+ backSize (/ (* (- frontSize backSize) (- y backY)) (- frontY backY))))))
|
|
223
|
-
(
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
252
|
+
(self show: pct))
|
|
253
|
+
|
|
254
|
+
;; Draws the client at pct percent, if that's a step away from its size now.
|
|
255
|
+
(method (show pct &tmp d)
|
|
256
|
+
(= d (- pct shown))
|
|
257
|
+
(if (< d 0) (= d (- d)))
|
|
258
|
+
(if (or (== shown -1) (>= d step))
|
|
259
|
+
(= shown pct)
|
|
260
|
+
(client
|
|
261
|
+
scaleX: (/ (* pct 128) 100)
|
|
262
|
+
scaleY: (/ (* pct 128) 100)
|
|
263
|
+
scaleSignal: (| (client scaleSignal?) SCALE_ON)))))
|
|
227
264
|
|
|
228
265
|
;; Sizes its client on a floor seen at an angle, where one line across the screen isn't the
|
|
229
266
|
;; same depth all the way along. Sizes are measured at the back and front of the floor at
|
|
@@ -260,7 +297,4 @@
|
|
|
260
297
|
((< x x3) (+ b (/ (* (- c b) (- x x2)) (- x3 x2))))
|
|
261
298
|
(else c)))
|
|
262
299
|
(if (< pct 1) (= pct 1))
|
|
263
|
-
(
|
|
264
|
-
scaleX: (/ (* pct 128) 100)
|
|
265
|
-
scaleY: (/ (* pct 128) 100)
|
|
266
|
-
scaleSignal: (| (client scaleSignal?) SCALE_ON))))
|
|
300
|
+
(self show: pct)))
|
package/package.json
CHANGED
|
@@ -760,7 +760,8 @@ class RoomCompiler {
|
|
|
760
760
|
message(noun, verb, cond, seq, l, label, spoken = true) {
|
|
761
761
|
const where = `${this.yarnFile}:${l.line}`;
|
|
762
762
|
const prev = this.messages.at(-1);
|
|
763
|
-
const
|
|
763
|
+
const shown = styledText(plainText(l.text, l.id === undefined ? where : `${where} (#line:${l.id})`), this.target.fonts, where);
|
|
764
|
+
const m = { noun, verb, cond, seq, talker: this.talkerOf(l.speaker, where), speaker: l.speaker ?? "Narrator", text: shown.text, label };
|
|
764
765
|
this.messages.push(m);
|
|
765
766
|
if (!spoken)
|
|
766
767
|
return;
|
|
@@ -768,8 +769,8 @@ class RoomCompiler {
|
|
|
768
769
|
if (twice)
|
|
769
770
|
throw new ContentError(`${where}: #line:${l.id} is already the id of ${twice.where}`);
|
|
770
771
|
this.lines.push({
|
|
771
|
-
...(l.id === undefined ? {} : { id: l.id }), noun, verb, cond, seq, talker: m.talker, speaker: m.speaker, text:
|
|
772
|
-
...(prev?.label === label ? { before: { speaker: prev.speaker, text: prev.text } } : {}), where,
|
|
772
|
+
...(l.id === undefined ? {} : { id: l.id }), noun, verb, cond, seq, talker: m.talker, speaker: m.speaker, text: shown.plain, node: label,
|
|
773
|
+
...(prev?.label === label ? { before: { speaker: prev.speaker, text: stripCodes(prev.text) } } : {}), where,
|
|
773
774
|
});
|
|
774
775
|
}
|
|
775
776
|
talkerOf(speaker, where) {
|
|
@@ -1056,6 +1057,48 @@ const counter = (from) => {
|
|
|
1056
1057
|
return () => n++;
|
|
1057
1058
|
};
|
|
1058
1059
|
/** Message text is single bytes: typographic punctuation becomes ASCII, anything else is an error. */
|
|
1060
|
+
/** Message text without SCI's font and colour codes: what's said, and what recordings are for. */
|
|
1061
|
+
const stripCodes = (text) => text.replace(/\|[fc]\d*\|/g, "");
|
|
1062
|
+
/**
|
|
1063
|
+
* Yarn's [b] and [i] markup (nestable, closed with [/b] and [/i]) as SCI's font codes: bold,
|
|
1064
|
+
* italic or both, back to the text's own font with |f|. `plain` is the text without it, for
|
|
1065
|
+
* speech and the script. \[ is a bracket.
|
|
1066
|
+
*/
|
|
1067
|
+
function styledText(text, fonts, where) {
|
|
1068
|
+
if (!text.includes("["))
|
|
1069
|
+
return { text, plain: text };
|
|
1070
|
+
const fail = (why) => { throw new ContentError(`${where}: ${why}`); };
|
|
1071
|
+
let shown = "", plain = "", bold = 0, italic = 0;
|
|
1072
|
+
const font = () => (bold && italic ? fonts.boldItalic : bold ? fonts.bold : italic ? fonts.italic : 0);
|
|
1073
|
+
for (let i = 0; i < text.length;) {
|
|
1074
|
+
if (text.startsWith("\\[", i)) {
|
|
1075
|
+
(shown += "["), (plain += "["), (i += 2);
|
|
1076
|
+
continue;
|
|
1077
|
+
}
|
|
1078
|
+
const m = /^\[(\/?)([a-z]+)\]/.exec(text.slice(i));
|
|
1079
|
+
if (text[i] === "[") {
|
|
1080
|
+
if (!m || (m[2] !== "b" && m[2] !== "i"))
|
|
1081
|
+
fail(`unknown markup "${m?.[0] ?? text.slice(i, i + 8)}" (supported: [b] and [i], closed with [/b] and [/i]; \\[ for a bracket)`);
|
|
1082
|
+
if (!fonts)
|
|
1083
|
+
fail("[b] and [i] need the game's bold and italic fonts (\"fonts\" in game.json)");
|
|
1084
|
+
const delta = m[1] ? -1 : 1;
|
|
1085
|
+
if (m[2] === "b")
|
|
1086
|
+
bold += delta;
|
|
1087
|
+
else
|
|
1088
|
+
italic += delta;
|
|
1089
|
+
if (bold < 0 || italic < 0)
|
|
1090
|
+
fail(`${m[0]} without its opening`);
|
|
1091
|
+
const f = font();
|
|
1092
|
+
shown += f ? `|f${f}|` : "|f|";
|
|
1093
|
+
i += m[0].length;
|
|
1094
|
+
continue;
|
|
1095
|
+
}
|
|
1096
|
+
(shown += text[i]), (plain += text[i]), i++;
|
|
1097
|
+
}
|
|
1098
|
+
if (bold || italic)
|
|
1099
|
+
fail(`[${bold ? "b" : "i"}] isn't closed`);
|
|
1100
|
+
return { text: shown.replace(/\|f\d*\|$/, (code) => (code === "|f|" ? "" : code)), plain };
|
|
1101
|
+
}
|
|
1059
1102
|
function plainText(text, where) {
|
|
1060
1103
|
const out = text
|
|
1061
1104
|
.replace(/[‘’]/g, "'")
|
|
@@ -902,14 +902,15 @@ class RoomCompiler {
|
|
|
902
902
|
private message(noun: number, verb: number, cond: number, seq: number, l: YarnLine, label: string, spoken = true): void {
|
|
903
903
|
const where = `${this.yarnFile}:${l.line}`;
|
|
904
904
|
const prev = this.messages.at(-1);
|
|
905
|
-
const
|
|
905
|
+
const shown = styledText(plainText(l.text, l.id === undefined ? where : `${where} (#line:${l.id})`), this.target.fonts, where);
|
|
906
|
+
const m: Message = { noun, verb, cond, seq, talker: this.talkerOf(l.speaker, where), speaker: l.speaker ?? "Narrator", text: shown.text, label };
|
|
906
907
|
this.messages.push(m);
|
|
907
908
|
if (!spoken) return;
|
|
908
909
|
const twice = l.id !== undefined && this.lines.find((x) => x.id === l.id);
|
|
909
910
|
if (twice) throw new ContentError(`${where}: #line:${l.id} is already the id of ${twice.where}`);
|
|
910
911
|
this.lines.push({
|
|
911
|
-
...(l.id === undefined ? {} : { id: l.id }), noun, verb, cond, seq, talker: m.talker, speaker: m.speaker, text:
|
|
912
|
-
...(prev?.label === label ? { before: { speaker: prev.speaker, text: prev.text } } : {}), where,
|
|
912
|
+
...(l.id === undefined ? {} : { id: l.id }), noun, verb, cond, seq, talker: m.talker, speaker: m.speaker, text: shown.plain, node: label,
|
|
913
|
+
...(prev?.label === label ? { before: { speaker: prev.speaker, text: stripCodes(prev.text) } } : {}), where,
|
|
913
914
|
});
|
|
914
915
|
}
|
|
915
916
|
|
|
@@ -1214,6 +1215,43 @@ const counter = (from: number) => {
|
|
|
1214
1215
|
};
|
|
1215
1216
|
|
|
1216
1217
|
/** Message text is single bytes: typographic punctuation becomes ASCII, anything else is an error. */
|
|
1218
|
+
/** Message text without SCI's font and colour codes: what's said, and what recordings are for. */
|
|
1219
|
+
const stripCodes = (text: string) => text.replace(/\|[fc]\d*\|/g, "");
|
|
1220
|
+
|
|
1221
|
+
/**
|
|
1222
|
+
* Yarn's [b] and [i] markup (nestable, closed with [/b] and [/i]) as SCI's font codes: bold,
|
|
1223
|
+
* italic or both, back to the text's own font with |f|. `plain` is the text without it, for
|
|
1224
|
+
* speech and the script. \[ is a bracket.
|
|
1225
|
+
*/
|
|
1226
|
+
function styledText(text: string, fonts: Target["fonts"], where: string): { text: string; plain: string } {
|
|
1227
|
+
if (!text.includes("[")) return { text, plain: text };
|
|
1228
|
+
const fail = (why: string): never => { throw new ContentError(`${where}: ${why}`); };
|
|
1229
|
+
let shown = "", plain = "", bold = 0, italic = 0;
|
|
1230
|
+
const font = () => (bold && italic ? fonts!.boldItalic : bold ? fonts!.bold : italic ? fonts!.italic : 0);
|
|
1231
|
+
for (let i = 0; i < text.length; ) {
|
|
1232
|
+
if (text.startsWith("\\[", i)) {
|
|
1233
|
+
(shown += "["), (plain += "["), (i += 2);
|
|
1234
|
+
continue;
|
|
1235
|
+
}
|
|
1236
|
+
const m = /^\[(\/?)([a-z]+)\]/.exec(text.slice(i));
|
|
1237
|
+
if (text[i] === "[") {
|
|
1238
|
+
if (!m || (m[2] !== "b" && m[2] !== "i")) fail(`unknown markup "${m?.[0] ?? text.slice(i, i + 8)}" (supported: [b] and [i], closed with [/b] and [/i]; \\[ for a bracket)`);
|
|
1239
|
+
if (!fonts) fail("[b] and [i] need the game's bold and italic fonts (\"fonts\" in game.json)");
|
|
1240
|
+
const delta = m![1] ? -1 : 1;
|
|
1241
|
+
if (m![2] === "b") bold += delta;
|
|
1242
|
+
else italic += delta;
|
|
1243
|
+
if (bold < 0 || italic < 0) fail(`${m![0]} without its opening`);
|
|
1244
|
+
const f = font();
|
|
1245
|
+
shown += f ? `|f${f}|` : "|f|";
|
|
1246
|
+
i += m![0].length;
|
|
1247
|
+
continue;
|
|
1248
|
+
}
|
|
1249
|
+
(shown += text[i]), (plain += text[i]), i++;
|
|
1250
|
+
}
|
|
1251
|
+
if (bold || italic) fail(`[${bold ? "b" : "i"}] isn't closed`);
|
|
1252
|
+
return { text: shown.replace(/\|f\d*\|$/, (code) => (code === "|f|" ? "" : code)), plain };
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1217
1255
|
function plainText(text: string, where: string): string {
|
|
1218
1256
|
const out = text
|
|
1219
1257
|
.replace(/[‘’]/g, "'")
|
|
@@ -94,4 +94,13 @@ export interface Target {
|
|
|
94
94
|
first: number;
|
|
95
95
|
last: number;
|
|
96
96
|
};
|
|
97
|
+
/**
|
|
98
|
+
* The game's fonts for Yarn's [b] and [i] markup, which become SCI's font codes (|f3|...|f|)
|
|
99
|
+
* in the message text. Without them, markup is refused.
|
|
100
|
+
*/
|
|
101
|
+
fonts?: {
|
|
102
|
+
bold: number;
|
|
103
|
+
italic: number;
|
|
104
|
+
boldItalic: number;
|
|
105
|
+
};
|
|
97
106
|
}
|
|
@@ -70,4 +70,9 @@ export interface Target {
|
|
|
70
70
|
* of `script`, each taking the flag number), and the range a mod may use.
|
|
71
71
|
*/
|
|
72
72
|
flags: { script: number; set: number; clear: number; test: number; first: number; last: number };
|
|
73
|
+
/**
|
|
74
|
+
* The game's fonts for Yarn's [b] and [i] markup, which become SCI's font codes (|f3|...|f|)
|
|
75
|
+
* in the message text. Without them, markup is refused.
|
|
76
|
+
*/
|
|
77
|
+
fonts?: { bold: number; italic: number; boldItalic: number };
|
|
73
78
|
}
|
|
@@ -11,6 +11,7 @@ export * from "./gfx/pic-file.ts";
|
|
|
11
11
|
export * from "./gfx/quantize.ts";
|
|
12
12
|
export * from "./gfx/view-file.ts";
|
|
13
13
|
export * from "./text/font.ts";
|
|
14
|
+
export * from "./text/styled.ts";
|
|
14
15
|
export * from "./text/message.ts";
|
|
15
16
|
export * from "./script/kernel-names.ts";
|
|
16
17
|
export * from "./script/vocab.ts";
|
|
@@ -11,6 +11,7 @@ export * from "./gfx/pic-file.js";
|
|
|
11
11
|
export * from "./gfx/quantize.js";
|
|
12
12
|
export * from "./gfx/view-file.js";
|
|
13
13
|
export * from "./text/font.js";
|
|
14
|
+
export * from "./text/styled.js";
|
|
14
15
|
export * from "./text/message.js";
|
|
15
16
|
export * from "./script/kernel-names.js";
|
|
16
17
|
export * from "./script/vocab.js";
|
|
@@ -11,6 +11,7 @@ export * from "./gfx/pic-file.ts";
|
|
|
11
11
|
export * from "./gfx/quantize.ts";
|
|
12
12
|
export * from "./gfx/view-file.ts";
|
|
13
13
|
export * from "./text/font.ts";
|
|
14
|
+
export * from "./text/styled.ts";
|
|
14
15
|
export * from "./text/message.ts";
|
|
15
16
|
export * from "./script/kernel-names.ts";
|
|
16
17
|
export * from "./script/vocab.ts";
|
|
@@ -3,7 +3,13 @@ export interface Glyph {
|
|
|
3
3
|
height: number;
|
|
4
4
|
/** 1 byte per pixel, 1 = ink. */
|
|
5
5
|
pixels: Uint8Array;
|
|
6
|
+
/** Where the bitmap starts from the pen (default 0); it can be left of it, an italic j's tail. */
|
|
7
|
+
bearingX?: number;
|
|
8
|
+
/** How far the pen moves on (default the width); ink can reach past it. */
|
|
9
|
+
advance?: number;
|
|
6
10
|
}
|
|
11
|
+
/** How far the pen moves on after a glyph. */
|
|
12
|
+
export declare const advanceOf: (g: Glyph) => number;
|
|
7
13
|
export interface Font {
|
|
8
14
|
/** Line height in pixels. */
|
|
9
15
|
height: number;
|
|
@@ -20,6 +26,11 @@ export interface Font {
|
|
|
20
26
|
*
|
|
21
27
|
* Each glyph: u8 width, u8 height, then rows of 1bpp pixels, MSB first,
|
|
22
28
|
* each row padded to a whole byte.
|
|
29
|
+
*
|
|
30
|
+
* A font can also carry each glyph's bearing and advance (writeFont adds them when any glyph
|
|
31
|
+
* has its own): after the glyphs, i8 bearing and u8 advance per character, then "XMT1". SCI
|
|
32
|
+
* reads glyphs by their offsets and never gets there; without it, bearings are 0 and
|
|
33
|
+
* advances the widths, as fonts always were.
|
|
23
34
|
*/
|
|
24
35
|
export declare function parseFont(data: Uint8Array): Font;
|
|
25
36
|
/** A font resource (the inverse of parseFont). Missing glyphs are written as 0×0. */
|
|
@@ -33,5 +44,11 @@ export interface TextBitmap {
|
|
|
33
44
|
height: number;
|
|
34
45
|
pixels: Uint8Array;
|
|
35
46
|
}
|
|
36
|
-
/**
|
|
37
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Renders already-wrapped lines, each aligned within the line width. Ink that starts left of
|
|
49
|
+
* the pen or runs past the last advance (an italic's first and last letters) is kept: the
|
|
50
|
+
* bitmap grows to hold it, and `originX` says where the pen started.
|
|
51
|
+
*/
|
|
52
|
+
export declare function renderText(font: Font, lines: string[], align?: "left" | "center"): TextBitmap & {
|
|
53
|
+
originX: number;
|
|
54
|
+
};
|