sci2-ts 0.11.0 → 0.13.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/996.sc +81 -24
- package/lib/997.sc +40 -0
- package/package.json +1 -1
- package/packages/content/room.schema.json +66 -15
- package/packages/content/src/room.d.ts +12 -0
- package/packages/content/src/room.js +103 -20
- package/packages/content/src/room.ts +107 -21
- package/packages/content/src/target.d.ts +3 -1
- package/packages/content/src/target.ts +3 -2
- package/tools/game/target.js +2 -2
- package/tools/game/target.ts +2 -2
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.13.0](https://github.com/rlueder/sci-ts/compare/v0.12.0...v0.13.0) (2026-10-02)
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
* **content:** perspective measured across the floor, for rooms painted at an angle ([#15](https://github.com/rlueder/sci-ts/issues/15)) ([8020031](https://github.com/rlueder/sci-ts/commit/8020031785b75c966ba449c7bcbe159002868fec))
|
|
12
|
+
|
|
13
|
+
## [0.12.0](https://github.com/rlueder/sci-ts/compare/v0.11.0...v0.12.0) (2026-10-02)
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
* **lib:** portraits that face the middle from their character's side, framed, and one for the hero ([#14](https://github.com/rlueder/sci-ts/issues/14)) ([e7be055](https://github.com/rlueder/sci-ts/commit/e7be0555af2c49743c539e7231cf979ac274617e))
|
|
18
|
+
|
|
7
19
|
## [0.11.0](https://github.com/rlueder/sci-ts/compare/v0.10.0...v0.11.0) (2026-10-02)
|
|
8
20
|
|
|
9
21
|
### Added
|
package/lib/996.sc
CHANGED
|
@@ -17,7 +17,10 @@
|
|
|
17
17
|
back 255 ; paper
|
|
18
18
|
border 0 ; a one-pixel border's colour; -1: none
|
|
19
19
|
frame -1 ; a frame view, or -1
|
|
20
|
-
margin 4
|
|
20
|
+
margin 4 ; between the text and the border or frame
|
|
21
|
+
portraitFrame -1 ; a view around portraits: loop 0 behind the face, loop 1 over its edges
|
|
22
|
+
portraitX 8 ; where a portrait's face goes on the left (mirrored on the right)
|
|
23
|
+
portraitY 8))
|
|
21
24
|
|
|
22
25
|
;; A box of text over the room: drawn by the interpreter into a bitmap, shown as a screen
|
|
23
26
|
;; item in the UI plane. Set text (and width, x, y), then init; it's as tall as the text
|
|
@@ -191,34 +194,80 @@
|
|
|
191
194
|
(super say: line &rest whoCares)))
|
|
192
195
|
|
|
193
196
|
;; Someone with a portrait: a view whose loop 0 is the bust, loop 1 the mouth and loop 2 the
|
|
194
|
-
;; eyes, every cel the same size and anchored at its top-left corner
|
|
195
|
-
;;
|
|
196
|
-
;;
|
|
197
|
-
;; the line is said the mouth moves, then closes; the eyes blink.
|
|
197
|
+
;; eyes, every cel the same size and anchored at its top-left corner; loops 3 to 5, if there
|
|
198
|
+
;; are any, are the same facing left. A room's talker is handed its parts (init: mouth bust
|
|
199
|
+
;; eyes frame, where frame is drawn as the bust); one with a view of its own (the hero's)
|
|
200
|
+
;; makes them. While the line is said the mouth moves, then closes; the eyes blink.
|
|
201
|
+
;;
|
|
202
|
+
;; The portrait goes at the top left facing right, or the top right facing left, with the
|
|
203
|
+
;; text beside it on the side towards the middle. A character takes the side of the hero
|
|
204
|
+
;; it stands on (who: is its body in the room; without one, the right); the hero takes the
|
|
205
|
+
;; side opposite whoever spoke last. The textStyle places it (portraitX, portraitY) and can
|
|
206
|
+
;; frame it (portraitFrame).
|
|
198
207
|
(class PortraitTalker of Talker
|
|
199
208
|
(properties
|
|
200
209
|
mouth 0 bust 0 eyes 0 frame 0
|
|
210
|
+
view -1 ; the portrait's view, for a talker that makes its own parts
|
|
211
|
+
who 0 ; the character's body in the room
|
|
212
|
+
side 0 ; where the portrait is: 0 the left, 1 the right
|
|
213
|
+
otherSide 1 ; the hero's: the side of whoever spoke last
|
|
214
|
+
back 0 front 0 ; the portraitFrame, behind the face and over its edges
|
|
201
215
|
priority 150
|
|
202
216
|
mouthFor 0) ; cycles the mouth moves for
|
|
203
217
|
|
|
204
218
|
(method (init theMouth theBust theEyes theFrame)
|
|
205
|
-
(
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
(
|
|
213
|
-
|
|
214
|
-
(=
|
|
215
|
-
(=
|
|
216
|
-
|
|
219
|
+
(if argc
|
|
220
|
+
(= mouth theMouth)
|
|
221
|
+
(= bust theBust)
|
|
222
|
+
(= eyes theEyes)
|
|
223
|
+
(= frame theFrame)))
|
|
224
|
+
|
|
225
|
+
(method (makeParts)
|
|
226
|
+
(if (and (!= view -1) (not bust) (not frame))
|
|
227
|
+
(= bust ((View new:) view: view yourself:))
|
|
228
|
+
(= mouth ((Prop new:) view: view yourself:))
|
|
229
|
+
(= eyes ((Prop new:) view: view yourself:))))
|
|
230
|
+
|
|
231
|
+
;; This line's side: see above.
|
|
232
|
+
(method (pickSide &tmp s)
|
|
233
|
+
(if (== self heroTalker) (return (- 1 otherSide)))
|
|
234
|
+
(= s (if (and who ego (< (who x?) (ego x?))) 0 else 1))
|
|
235
|
+
(if (and heroTalker (!= heroTalker self) (heroTalker respondsTo: #otherSide)) (heroTalker otherSide: s))
|
|
236
|
+
(return s))
|
|
237
|
+
|
|
238
|
+
(method (say txt whoCares &tmp face l pw f fx fy over)
|
|
239
|
+
(self init: makeParts:)
|
|
240
|
+
(= face (if frame frame else bust))
|
|
241
|
+
(if face
|
|
242
|
+
(= side (self pickSide:))
|
|
243
|
+
(= l (if (and side (>= (NumLoops face) 6)) 3 else 0))
|
|
244
|
+
(= pw (CelWide (face view?) l 0))
|
|
245
|
+
(= f (textStyle portraitFrame?))
|
|
246
|
+
(= over (if (!= f -1) (/ (- (CelWide f 0 0) pw) 2) else 0))
|
|
247
|
+
(= fx (if side (- SCREEN_WIDTH (+ (textStyle portraitX?) pw)) else (textStyle portraitX?)))
|
|
248
|
+
(= fy (textStyle portraitY?))
|
|
249
|
+
(face x: fx y: fy loop: l cel: 0)
|
|
250
|
+
(if mouth (mouth x: fx y: fy loop: (+ l 1) cel: 0))
|
|
251
|
+
(if eyes (eyes x: fx y: fy loop: (+ l 2) cel: 0))
|
|
252
|
+
(if (!= f -1)
|
|
253
|
+
(if (not back) (= back ((View new:) view: f loop: 0 cel: 0 yourself:)))
|
|
254
|
+
(back x: fx y: fy)
|
|
255
|
+
(if (and (not front) (>= (NumLoops back) 2)) (= front ((View new:) view: f loop: 1 cel: 0 yourself:)))
|
|
256
|
+
(if front (front x: fx y: fy)))
|
|
257
|
+
;; The text beside it, towards the middle.
|
|
258
|
+
(= y fy)
|
|
259
|
+
(if side
|
|
260
|
+
(= x 6)
|
|
261
|
+
(= width (- fx (+ over 12)))
|
|
262
|
+
else
|
|
263
|
+
(= x (+ fx pw over 6))
|
|
264
|
+
(= width (- SCREEN_WIDTH (+ x 6)))))
|
|
217
265
|
(super say: txt &rest whoCares)
|
|
218
|
-
(self showPart:
|
|
219
|
-
(self showPart:
|
|
220
|
-
(self showPart: eyes (+ priority 1))
|
|
266
|
+
(self showPart: back (- priority 1))
|
|
267
|
+
(self showPart: face priority)
|
|
221
268
|
(self showPart: mouth (+ priority 1))
|
|
269
|
+
(self showPart: eyes (+ priority 1))
|
|
270
|
+
(self showPart: front (+ priority 2))
|
|
222
271
|
(if eyes (eyes setCycle: Blink))
|
|
223
272
|
(if mouth
|
|
224
273
|
(mouth setCycle: Forward)
|
|
@@ -236,11 +285,19 @@
|
|
|
236
285
|
(part init:)
|
|
237
286
|
(part plane: uiPlane setPri: pri)))
|
|
238
287
|
|
|
288
|
+
;; Off the screen, kept for the next line.
|
|
289
|
+
(method (hidePart part)
|
|
290
|
+
(if part
|
|
291
|
+
(DeleteScreenItem part)
|
|
292
|
+
(cast delete: part)))
|
|
293
|
+
|
|
239
294
|
(method (clear)
|
|
240
|
-
(
|
|
241
|
-
(
|
|
242
|
-
(
|
|
243
|
-
(
|
|
295
|
+
(self hidePart: front)
|
|
296
|
+
(self hidePart: back)
|
|
297
|
+
(self hidePart: frame)
|
|
298
|
+
(self hidePart: bust)
|
|
299
|
+
(self hidePart: eyes)
|
|
300
|
+
(self hidePart: mouth)
|
|
244
301
|
(super clear:)))
|
|
245
302
|
|
|
246
303
|
;; Says the lines of a message file for a noun, verb and condition, in sequence:
|
package/lib/997.sc
CHANGED
|
@@ -224,3 +224,43 @@
|
|
|
224
224
|
scaleX: (/ (* pct 128) 100)
|
|
225
225
|
scaleY: (/ (* pct 128) 100)
|
|
226
226
|
scaleSignal: (| (client scaleSignal?) SCALE_ON))))
|
|
227
|
+
|
|
228
|
+
;; Sizes its client on a floor seen at an angle, where one line across the screen isn't the
|
|
229
|
+
;; same depth all the way along. Sizes are measured at the back and front of the floor at
|
|
230
|
+
;; three places across it (left, middle, right): at each of those, size changes in a straight
|
|
231
|
+
;; line with y, as perspective does; between them, in a straight line with x.
|
|
232
|
+
;; (actor setScaler: FloorScaler x1 backY1 back1 frontY1 front1 x2 ... x3 ...)
|
|
233
|
+
(class FloorScaler of Scaler
|
|
234
|
+
(properties
|
|
235
|
+
x1 0 yb1 0 sb1 100 yf1 1 sf1 100
|
|
236
|
+
x2 1 yb2 0 sb2 100 yf2 1 sf2 100
|
|
237
|
+
x3 2 yb3 0 sb3 100 yf3 1 sf3 100)
|
|
238
|
+
|
|
239
|
+
(method (init who a1 b1 c1 d1 e1 a2 b2 c2 d2 e2 a3 b3 c3 d3 e3)
|
|
240
|
+
(= client who)
|
|
241
|
+
(= x1 a1) (= yb1 b1) (= sb1 c1) (= yf1 d1) (= sf1 e1)
|
|
242
|
+
(= x2 a2) (= yb2 b2) (= sb2 c2) (= yf2 d2) (= sf2 e2)
|
|
243
|
+
(= x3 a3) (= yb3 b3) (= sb3 c3) (= yf3 d3) (= sf3 e3)
|
|
244
|
+
(self doit:))
|
|
245
|
+
|
|
246
|
+
;; The size at y where one place was measured: the straight line through its back and front.
|
|
247
|
+
(method (along y yb sb yf sf)
|
|
248
|
+
(return (+ sb (/ (* (- sf sb) (- y yb)) (- yf yb)))))
|
|
249
|
+
|
|
250
|
+
(method (doit &tmp x y a b c pct)
|
|
251
|
+
(= x (client x?))
|
|
252
|
+
(= y (client y?))
|
|
253
|
+
(= a (self along: y yb1 sb1 yf1 sf1))
|
|
254
|
+
(= b (self along: y yb2 sb2 yf2 sf2))
|
|
255
|
+
(= c (self along: y yb3 sb3 yf3 sf3))
|
|
256
|
+
(= pct
|
|
257
|
+
(cond
|
|
258
|
+
((<= x x1) a)
|
|
259
|
+
((< x x2) (+ a (/ (* (- b a) (- x x1)) (- x2 x1))))
|
|
260
|
+
((< x x3) (+ b (/ (* (- c b) (- x x2)) (- x3 x2))))
|
|
261
|
+
(else c)))
|
|
262
|
+
(if (< pct 1) (= pct 1))
|
|
263
|
+
(client
|
|
264
|
+
scaleX: (/ (* pct 128) 100)
|
|
265
|
+
scaleY: (/ (* pct 128) 100)
|
|
266
|
+
scaleSignal: (| (client scaleSignal?) SCALE_ON))))
|
package/package.json
CHANGED
|
@@ -123,23 +123,74 @@
|
|
|
123
123
|
}
|
|
124
124
|
},
|
|
125
125
|
"perspective": {
|
|
126
|
-
"
|
|
127
|
-
"
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
"
|
|
136
|
-
|
|
126
|
+
"description": "How people are sized by where they stand: a level camera's horizon and full-size line, or sizes measured at up to three places across the floor (for a picture painted at an angle)",
|
|
127
|
+
"oneOf": [
|
|
128
|
+
{
|
|
129
|
+
"type": "object",
|
|
130
|
+
"additionalProperties": false,
|
|
131
|
+
"required": [
|
|
132
|
+
"horizon",
|
|
133
|
+
"fullSize"
|
|
134
|
+
],
|
|
135
|
+
"properties": {
|
|
136
|
+
"horizon": {
|
|
137
|
+
"type": "integer",
|
|
138
|
+
"description": "The y the floor's lines meet at"
|
|
139
|
+
},
|
|
140
|
+
"fullSize": {
|
|
141
|
+
"type": "integer",
|
|
142
|
+
"description": "The y of a figure's feet where it's drawn at full size"
|
|
143
|
+
}
|
|
144
|
+
}
|
|
137
145
|
},
|
|
138
|
-
|
|
139
|
-
"type": "
|
|
140
|
-
"
|
|
146
|
+
{
|
|
147
|
+
"type": "object",
|
|
148
|
+
"additionalProperties": false,
|
|
149
|
+
"required": [
|
|
150
|
+
"columns"
|
|
151
|
+
],
|
|
152
|
+
"properties": {
|
|
153
|
+
"columns": {
|
|
154
|
+
"type": "array",
|
|
155
|
+
"minItems": 1,
|
|
156
|
+
"maxItems": 3,
|
|
157
|
+
"items": {
|
|
158
|
+
"type": "object",
|
|
159
|
+
"additionalProperties": false,
|
|
160
|
+
"required": [
|
|
161
|
+
"x",
|
|
162
|
+
"back",
|
|
163
|
+
"front"
|
|
164
|
+
],
|
|
165
|
+
"properties": {
|
|
166
|
+
"x": {
|
|
167
|
+
"type": "integer",
|
|
168
|
+
"description": "Where across the floor it was measured"
|
|
169
|
+
},
|
|
170
|
+
"back": {
|
|
171
|
+
"type": "array",
|
|
172
|
+
"items": {
|
|
173
|
+
"type": "integer"
|
|
174
|
+
},
|
|
175
|
+
"minItems": 2,
|
|
176
|
+
"maxItems": 2,
|
|
177
|
+
"description": "[y, size in percent] at the back of the floor"
|
|
178
|
+
},
|
|
179
|
+
"front": {
|
|
180
|
+
"type": "array",
|
|
181
|
+
"items": {
|
|
182
|
+
"type": "integer"
|
|
183
|
+
},
|
|
184
|
+
"minItems": 2,
|
|
185
|
+
"maxItems": 2,
|
|
186
|
+
"description": "[y, size in percent] at the front of the floor"
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
141
192
|
}
|
|
142
|
-
|
|
193
|
+
]
|
|
143
194
|
},
|
|
144
195
|
"walkable": {
|
|
145
196
|
"$ref": "#/$defs/polygon",
|
|
@@ -43,10 +43,16 @@ export interface RoomSpec {
|
|
|
43
43
|
* size. A figure's height is in proportion to how far below the horizon it stands, so it
|
|
44
44
|
* shrinks towards the back and grows past fullSize at the front. The hero and props that
|
|
45
45
|
* move are sized as they go; a still prop only with scale: true.
|
|
46
|
+
*
|
|
47
|
+
* A picture painted at an angle, where a line across the screen isn't one depth all the
|
|
48
|
+
* way along, gives sizes measured instead: `columns`, up to three places across the floor
|
|
49
|
+
* (left, middle, right), each with a size at the back and at the front of the floor.
|
|
46
50
|
*/
|
|
47
51
|
perspective?: {
|
|
48
52
|
horizon: number;
|
|
49
53
|
fullSize: number;
|
|
54
|
+
} | {
|
|
55
|
+
columns: FloorColumn[];
|
|
50
56
|
};
|
|
51
57
|
/** The floor: a polygon the hero stays inside. */
|
|
52
58
|
walkable?: Point[];
|
|
@@ -70,6 +76,12 @@ interface CharacterSpec {
|
|
|
70
76
|
portrait?: number;
|
|
71
77
|
}
|
|
72
78
|
type Point = [number, number];
|
|
79
|
+
/** A place across the floor where sizes were measured: x, and [y, percent] at the back and front. */
|
|
80
|
+
export interface FloorColumn {
|
|
81
|
+
x: number;
|
|
82
|
+
back: [number, number];
|
|
83
|
+
front: [number, number];
|
|
84
|
+
}
|
|
73
85
|
type Rect = [number, number, number, number];
|
|
74
86
|
interface FeatureSpec {
|
|
75
87
|
/** left, top, right, bottom (room coordinates). */
|
|
@@ -64,11 +64,37 @@ class SpecChecker {
|
|
|
64
64
|
}
|
|
65
65
|
if (s.perspective !== undefined) {
|
|
66
66
|
const pv = this.map(s.perspective, ["perspective"]);
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
67
|
+
if (pv.columns !== undefined) {
|
|
68
|
+
this.known(pv, ["perspective"], ["columns"]);
|
|
69
|
+
if (!Array.isArray(pv.columns) || pv.columns.length < 1 || pv.columns.length > 3)
|
|
70
|
+
this.fail(["perspective", "columns"], "expected one to three places across the floor");
|
|
71
|
+
let lastX = -Infinity;
|
|
72
|
+
pv.columns.forEach((value, i) => {
|
|
73
|
+
const path = ["perspective", "columns", i];
|
|
74
|
+
const c = this.map(value, path);
|
|
75
|
+
this.known(c, path, ["x", "back", "front"]);
|
|
76
|
+
this.int(c.x, [...path, "x"], 0, 319);
|
|
77
|
+
if (c.x <= lastX)
|
|
78
|
+
this.fail([...path, "x"], "places go left to right");
|
|
79
|
+
lastX = c.x;
|
|
80
|
+
for (const end of ["back", "front"]) {
|
|
81
|
+
const v = c[end];
|
|
82
|
+
if (!Array.isArray(v) || v.length !== 2)
|
|
83
|
+
this.fail([...path, end], "expected [y, size in percent]");
|
|
84
|
+
this.int(v[0], [...path, end, 0], 0, 199);
|
|
85
|
+
this.int(v[1], [...path, end, 1], 1, 400);
|
|
86
|
+
}
|
|
87
|
+
if (c.back[0] >= c.front[0])
|
|
88
|
+
this.fail([...path, "front"], "the front is lower on the screen than the back (a bigger y)");
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
this.known(pv, ["perspective"], ["horizon", "fullSize", "columns"]);
|
|
93
|
+
this.int(pv.horizon, ["perspective", "horizon"], -1000, 199);
|
|
94
|
+
this.int(pv.fullSize, ["perspective", "fullSize"], 0, 1000);
|
|
95
|
+
if (pv.fullSize <= pv.horizon)
|
|
96
|
+
this.fail(["perspective", "fullSize"], "must be below the horizon (a bigger y)");
|
|
97
|
+
}
|
|
72
98
|
}
|
|
73
99
|
if (s.walkable !== undefined)
|
|
74
100
|
this.polygon(s.walkable, ["walkable"]);
|
|
@@ -188,6 +214,30 @@ class SpecChecker {
|
|
|
188
214
|
}
|
|
189
215
|
}
|
|
190
216
|
const SCREEN_BOTTOM = 200;
|
|
217
|
+
/** A column's size at y: the straight line through its back and front. */
|
|
218
|
+
const along = (c, y) => c.back[1] + ((c.front[1] - c.back[1]) * (y - c.back[0])) / (c.front[0] - c.back[0]);
|
|
219
|
+
/** Three columns for the FloorScaler: one is used for all three, two get one between them. */
|
|
220
|
+
function floorColumns(cols) {
|
|
221
|
+
if (cols.length === 3)
|
|
222
|
+
return cols;
|
|
223
|
+
const [a, b] = [cols[0], cols.at(-1)];
|
|
224
|
+
if (cols.length === 1)
|
|
225
|
+
return [a, { ...a, x: a.x + 1 }, { ...a, x: a.x + 2 }];
|
|
226
|
+
const mid = (y) => Math.round((along(a, y) + along(b, y)) / 2);
|
|
227
|
+
return [a, { x: Math.round((a.x + b.x) / 2), back: [a.back[0], mid(a.back[0])], front: [a.front[0], mid(a.front[0])] }, b];
|
|
228
|
+
}
|
|
229
|
+
/** A figure's size at x, y among three columns: straight lines in y at each, then in x between them. */
|
|
230
|
+
function floorSize(cols, x, y) {
|
|
231
|
+
const [a, b, c] = cols.map((col) => along(col, y));
|
|
232
|
+
const [c1, c2, c3] = cols;
|
|
233
|
+
if (x <= c1.x)
|
|
234
|
+
return a;
|
|
235
|
+
if (x < c2.x)
|
|
236
|
+
return a + ((b - a) * (x - c1.x)) / (c2.x - c1.x);
|
|
237
|
+
if (x < c3.x)
|
|
238
|
+
return b + ((c - b) * (x - c2.x)) / (c3.x - c2.x);
|
|
239
|
+
return c;
|
|
240
|
+
}
|
|
191
241
|
const num = (n) => (n < 0 ? `-$${(-n).toString(16)}` : String(n));
|
|
192
242
|
function pushVal(v) {
|
|
193
243
|
if (typeof v === "number")
|
|
@@ -761,7 +811,7 @@ class RoomCompiler {
|
|
|
761
811
|
if (hero?.scale)
|
|
762
812
|
egoCalls.push(["setScaler", [{ cls: "Scaler" }, hero.scale.front, hero.scale.back, hero.scale.frontY, hero.scale.backY]]);
|
|
763
813
|
else if (perspective)
|
|
764
|
-
egoCalls.push(["setScaler", [{ cls:
|
|
814
|
+
egoCalls.push(["setScaler", [{ cls: perspective.cls }, ...perspective.args]]);
|
|
765
815
|
else
|
|
766
816
|
egoCalls.push(["setScaler", [0]]);
|
|
767
817
|
c.push(...send({ global: target.globals.ego }, egoCalls));
|
|
@@ -772,7 +822,7 @@ class RoomCompiler {
|
|
|
772
822
|
for (const [name, p] of Object.entries(spec.props ?? {})) {
|
|
773
823
|
c.push(...send({ obj: name }, [
|
|
774
824
|
["init", []],
|
|
775
|
-
...(perspective && p.moves && p.scale !== false ? [["setScaler", [{ cls:
|
|
825
|
+
...(perspective && p.moves && p.scale !== false ? [["setScaler", [{ cls: perspective.cls }, ...perspective.args]]] : []),
|
|
776
826
|
...(p.cycle === "forward" ? [["setCycle", [{ cls: this.target.forwardCycle }]]] : []),
|
|
777
827
|
// A new instance of the game's Script class, running on the prop.
|
|
778
828
|
...(p.script ? [["setScript", [{ code: ["pushi #new", "push0", `class ${p.script}`, "send 4"] }]]] : []),
|
|
@@ -837,6 +887,8 @@ class RoomCompiler {
|
|
|
837
887
|
"&rest 1", `super ${p.class} 12`,
|
|
838
888
|
"pushi #setPri", "push1", "pTos priority", "ldi 1", "add", "push", `lofsa @${key}Mouth`, "send 6",
|
|
839
889
|
"pushi #setPri", "push1", "pTos priority", "ldi 1", "add", "push", `lofsa @${key}Eyes`, "send 6",
|
|
890
|
+
// Its body in the room, if a prop has its name: where it stands picks the portrait's side.
|
|
891
|
+
...(p.who && this.spec.props?.[key] ? send("self", [["who", [{ obj: key }]]]) : []),
|
|
840
892
|
"ret",
|
|
841
893
|
], "");
|
|
842
894
|
}
|
|
@@ -889,34 +941,65 @@ class RoomCompiler {
|
|
|
889
941
|
if (cases.length)
|
|
890
942
|
this.doVerb(name, "Feature", cases);
|
|
891
943
|
}
|
|
944
|
+
/** The y range people stand in: the floor, and where the hero and props that move start. */
|
|
945
|
+
standingYs() {
|
|
946
|
+
return [
|
|
947
|
+
...(this.spec.walkable ?? []).map((p) => p[1]),
|
|
948
|
+
...Object.values(this.spec.props ?? {}).filter((p) => p.moves).map((p) => p.at[1]),
|
|
949
|
+
...[this.spec.hero?.at, this.spec.hero?.enterTo].flatMap((p) => (p ? [p[1]] : [])),
|
|
950
|
+
];
|
|
951
|
+
}
|
|
892
952
|
/**
|
|
893
|
-
* The room's perspective as
|
|
894
|
-
*
|
|
895
|
-
*
|
|
896
|
-
*
|
|
953
|
+
* The room's perspective as a scaler class and its arguments.
|
|
954
|
+
*
|
|
955
|
+
* With a horizon, a Scaler (front, back, frontY, backY): sizes at the nearest and farthest
|
|
956
|
+
* places anyone stands. Size is a straight line in y, so the Scaler's in-between is exact;
|
|
957
|
+
* keeping to that range keeps its 16-bit arithmetic from overflowing.
|
|
958
|
+
*
|
|
959
|
+
* With measured columns, a FloorScaler with three of them (left, middle, right): one is
|
|
960
|
+
* used for all three, two get one halfway between them.
|
|
897
961
|
*/
|
|
898
962
|
perspective() {
|
|
899
963
|
const pv = this.spec.perspective;
|
|
900
964
|
if (!pv)
|
|
901
965
|
return undefined;
|
|
902
|
-
const
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
966
|
+
const fail = (why) => { throw new ContentError(`room ${this.spec.room}: perspective: ${why}`); };
|
|
967
|
+
if ("columns" in pv) {
|
|
968
|
+
const cols = floorColumns(pv.columns);
|
|
969
|
+
const ys = this.standingYs();
|
|
970
|
+
const [lo, hi] = [Math.min(...ys, ...cols.map((c) => c.back[0])), Math.max(...ys, ...cols.map((c) => c.front[0]))];
|
|
971
|
+
// FloorScaler works out (front - back) * (y - backY), then (b - a) * (x - x1), in 16 bits.
|
|
972
|
+
for (const c of cols) {
|
|
973
|
+
for (const y of [lo, hi]) {
|
|
974
|
+
if (Math.abs((c.front[1] - c.back[1]) * (y - c.back[0])) > 32767)
|
|
975
|
+
fail(`sizes from ${c.back[1]}% to ${c.front[1]}% at x ${c.x} are too much for the scaler at y ${y}`);
|
|
976
|
+
const size = along(c, y);
|
|
977
|
+
if (size < 1 || size > 255)
|
|
978
|
+
fail(`at x ${c.x} a figure standing at y ${y} would be ${Math.round(size)}%`);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
return { cls: "FloorScaler", args: cols.flatMap((c) => [c.x, c.back[0], c.back[1], c.front[0], c.front[1]]) };
|
|
982
|
+
}
|
|
983
|
+
const ys = this.standingYs().filter((y) => y > pv.horizon);
|
|
907
984
|
const backY = ys.length ? Math.min(...ys) : pv.horizon + 1, frontY = ys.length ? Math.max(...ys) : SCREEN_BOTTOM - 1;
|
|
908
985
|
const size = (y) => Math.round(((y - pv.horizon) / (pv.fullSize - pv.horizon)) * 100);
|
|
909
986
|
const [front, back] = [size(frontY), size(backY)];
|
|
910
987
|
// Scaler works out (front - back) * (y - backY) on the way.
|
|
911
988
|
if ((front - back) * (frontY - backY) > 32767)
|
|
912
|
-
|
|
913
|
-
return [front, back, Math.max(frontY, backY + 1), backY];
|
|
989
|
+
fail(`sizes from ${back}% to ${front}% over ${frontY - backY} lines are too much for the Scaler`);
|
|
990
|
+
return { cls: "Scaler", args: [front, back, Math.max(frontY, backY + 1), backY] };
|
|
991
|
+
}
|
|
992
|
+
/** The size, in percent, of a figure standing at x, y. */
|
|
993
|
+
sizeAt(x, y) {
|
|
994
|
+
const pv = this.spec.perspective;
|
|
995
|
+
if ("columns" in pv)
|
|
996
|
+
return floorSize(floorColumns(pv.columns), x, y);
|
|
997
|
+
return ((y - pv.horizon) / (pv.fullSize - pv.horizon)) * 100;
|
|
914
998
|
}
|
|
915
999
|
prop(name, p) {
|
|
916
1000
|
const cases = this.scriptedVerbs.get(name) ?? [];
|
|
917
1001
|
// A still prop is sized once, for where it is.
|
|
918
|
-
const
|
|
919
|
-
const fixed = pv && p.scale === true && !p.moves ? Math.round(((p.at[1] - pv.horizon) / (pv.fullSize - pv.horizon)) * 128) : undefined;
|
|
1002
|
+
const fixed = this.spec.perspective && p.scale === true && !p.moves ? Math.round((this.sizeAt(p.at[0], p.at[1]) / 100) * 128) : undefined;
|
|
920
1003
|
this.instance(name, p.moves ? "Actor" : "Prop", {
|
|
921
1004
|
noun: this.nouns.get(name), modNum: -1, sightAngle: p.sightAngle ?? 180, x: p.at[0], y: p.at[1], view: p.view, loop: p.loop ?? 0, cel: p.cel ?? 0,
|
|
922
1005
|
...(fixed === undefined ? {} : { scaleSignal: 1, scaleX: fixed, scaleY: fixed }),
|
|
@@ -41,8 +41,12 @@ export interface RoomSpec {
|
|
|
41
41
|
* size. A figure's height is in proportion to how far below the horizon it stands, so it
|
|
42
42
|
* shrinks towards the back and grows past fullSize at the front. The hero and props that
|
|
43
43
|
* move are sized as they go; a still prop only with scale: true.
|
|
44
|
+
*
|
|
45
|
+
* A picture painted at an angle, where a line across the screen isn't one depth all the
|
|
46
|
+
* way along, gives sizes measured instead: `columns`, up to three places across the floor
|
|
47
|
+
* (left, middle, right), each with a size at the back and at the front of the floor.
|
|
44
48
|
*/
|
|
45
|
-
perspective?: { horizon: number; fullSize: number };
|
|
49
|
+
perspective?: { horizon: number; fullSize: number } | { columns: FloorColumn[] };
|
|
46
50
|
/** The floor: a polygon the hero stays inside. */
|
|
47
51
|
walkable?: Point[];
|
|
48
52
|
/** Polygons the hero walks around. */
|
|
@@ -67,6 +71,13 @@ interface CharacterSpec {
|
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
type Point = [number, number];
|
|
74
|
+
|
|
75
|
+
/** A place across the floor where sizes were measured: x, and [y, percent] at the back and front. */
|
|
76
|
+
export interface FloorColumn {
|
|
77
|
+
x: number;
|
|
78
|
+
back: [number, number];
|
|
79
|
+
front: [number, number];
|
|
80
|
+
}
|
|
70
81
|
type Rect = [number, number, number, number];
|
|
71
82
|
|
|
72
83
|
interface FeatureSpec {
|
|
@@ -212,10 +223,31 @@ class SpecChecker {
|
|
|
212
223
|
}
|
|
213
224
|
if (s.perspective !== undefined) {
|
|
214
225
|
const pv = this.map(s.perspective, ["perspective"]);
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
226
|
+
if (pv.columns !== undefined) {
|
|
227
|
+
this.known(pv, ["perspective"], ["columns"]);
|
|
228
|
+
if (!Array.isArray(pv.columns) || pv.columns.length < 1 || pv.columns.length > 3) this.fail(["perspective", "columns"], "expected one to three places across the floor");
|
|
229
|
+
let lastX = -Infinity;
|
|
230
|
+
(pv.columns as unknown[]).forEach((value, i) => {
|
|
231
|
+
const path = ["perspective", "columns", i];
|
|
232
|
+
const c = this.map(value, path);
|
|
233
|
+
this.known(c, path, ["x", "back", "front"]);
|
|
234
|
+
this.int(c.x, [...path, "x"], 0, 319);
|
|
235
|
+
if ((c.x as number) <= lastX) this.fail([...path, "x"], "places go left to right");
|
|
236
|
+
lastX = c.x as number;
|
|
237
|
+
for (const end of ["back", "front"] as const) {
|
|
238
|
+
const v = c[end];
|
|
239
|
+
if (!Array.isArray(v) || v.length !== 2) this.fail([...path, end], "expected [y, size in percent]");
|
|
240
|
+
this.int((v as unknown[])[0], [...path, end, 0], 0, 199);
|
|
241
|
+
this.int((v as unknown[])[1], [...path, end, 1], 1, 400);
|
|
242
|
+
}
|
|
243
|
+
if ((c.back as number[])[0]! >= (c.front as number[])[0]!) this.fail([...path, "front"], "the front is lower on the screen than the back (a bigger y)");
|
|
244
|
+
});
|
|
245
|
+
} else {
|
|
246
|
+
this.known(pv, ["perspective"], ["horizon", "fullSize", "columns"]);
|
|
247
|
+
this.int(pv.horizon, ["perspective", "horizon"], -1000, 199);
|
|
248
|
+
this.int(pv.fullSize, ["perspective", "fullSize"], 0, 1000);
|
|
249
|
+
if ((pv.fullSize as number) <= (pv.horizon as number)) this.fail(["perspective", "fullSize"], "must be below the horizon (a bigger y)");
|
|
250
|
+
}
|
|
219
251
|
}
|
|
220
252
|
if (s.walkable !== undefined) this.polygon(s.walkable, ["walkable"]);
|
|
221
253
|
if (s.obstacles !== undefined) {
|
|
@@ -313,6 +345,28 @@ type Val = number | { obj: string } | { cls: string } | "self" | { code: string[
|
|
|
313
345
|
type To = { global: number } | { obj: string } | { cls: string } | "self" | { superOf: string };
|
|
314
346
|
|
|
315
347
|
const SCREEN_BOTTOM = 200;
|
|
348
|
+
|
|
349
|
+
/** A column's size at y: the straight line through its back and front. */
|
|
350
|
+
const along = (c: FloorColumn, y: number) => c.back[1] + ((c.front[1] - c.back[1]) * (y - c.back[0])) / (c.front[0] - c.back[0]);
|
|
351
|
+
|
|
352
|
+
/** Three columns for the FloorScaler: one is used for all three, two get one between them. */
|
|
353
|
+
function floorColumns(cols: FloorColumn[]): [FloorColumn, FloorColumn, FloorColumn] {
|
|
354
|
+
if (cols.length === 3) return cols as [FloorColumn, FloorColumn, FloorColumn];
|
|
355
|
+
const [a, b] = [cols[0]!, cols.at(-1)!];
|
|
356
|
+
if (cols.length === 1) return [a, { ...a, x: a.x + 1 }, { ...a, x: a.x + 2 }];
|
|
357
|
+
const mid = (y: number) => Math.round((along(a, y) + along(b, y)) / 2);
|
|
358
|
+
return [a, { x: Math.round((a.x + b.x) / 2), back: [a.back[0], mid(a.back[0])], front: [a.front[0], mid(a.front[0])] }, b];
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** A figure's size at x, y among three columns: straight lines in y at each, then in x between them. */
|
|
362
|
+
function floorSize(cols: [FloorColumn, FloorColumn, FloorColumn], x: number, y: number): number {
|
|
363
|
+
const [a, b, c] = cols.map((col) => along(col, y)) as [number, number, number];
|
|
364
|
+
const [c1, c2, c3] = cols;
|
|
365
|
+
if (x <= c1.x) return a;
|
|
366
|
+
if (x < c2.x) return a + ((b - a) * (x - c1.x)) / (c2.x - c1.x);
|
|
367
|
+
if (x < c3.x) return b + ((c - b) * (x - c2.x)) / (c3.x - c2.x);
|
|
368
|
+
return c;
|
|
369
|
+
}
|
|
316
370
|
const num = (n: number) => (n < 0 ? `-$${(-n).toString(16)}` : String(n));
|
|
317
371
|
|
|
318
372
|
function pushVal(v: Val): string[] {
|
|
@@ -899,7 +953,7 @@ class RoomCompiler {
|
|
|
899
953
|
// Each room sizes the hero its own way, or not at all.
|
|
900
954
|
const perspective = this.perspective();
|
|
901
955
|
if (hero?.scale) egoCalls.push(["setScaler", [{ cls: "Scaler" }, hero.scale.front, hero.scale.back, hero.scale.frontY, hero.scale.backY]]);
|
|
902
|
-
else if (perspective) egoCalls.push(["setScaler", [{ cls:
|
|
956
|
+
else if (perspective) egoCalls.push(["setScaler", [{ cls: perspective.cls }, ...perspective.args]]);
|
|
903
957
|
else egoCalls.push(["setScaler", [0]]);
|
|
904
958
|
c.push(...send({ global: target.globals.ego }, egoCalls));
|
|
905
959
|
if (spec.music !== undefined) c.push(...this.playMusic(spec.music));
|
|
@@ -907,7 +961,7 @@ class RoomCompiler {
|
|
|
907
961
|
for (const [name, p] of Object.entries(spec.props ?? {})) {
|
|
908
962
|
c.push(...send({ obj: name }, [
|
|
909
963
|
["init", []],
|
|
910
|
-
...(perspective && p.moves && p.scale !== false ? [["setScaler", [{ cls:
|
|
964
|
+
...(perspective && p.moves && p.scale !== false ? [["setScaler", [{ cls: perspective.cls }, ...perspective.args]] as [string, Val[]]] : []),
|
|
911
965
|
...(p.cycle === "forward" ? [["setCycle", [{ cls: this.target.forwardCycle }]] as [string, Val[]]] : []),
|
|
912
966
|
// A new instance of the game's Script class, running on the prop.
|
|
913
967
|
...(p.script ? [["setScript", [{ code: ["pushi #new", "push0", `class ${p.script}`, "send 4"] }]] as [string, Val[]]] : []),
|
|
@@ -975,6 +1029,8 @@ class RoomCompiler {
|
|
|
975
1029
|
"&rest 1", `super ${p.class} 12`,
|
|
976
1030
|
"pushi #setPri", "push1", "pTos priority", "ldi 1", "add", "push", `lofsa @${key}Mouth`, "send 6",
|
|
977
1031
|
"pushi #setPri", "push1", "pTos priority", "ldi 1", "add", "push", `lofsa @${key}Eyes`, "send 6",
|
|
1032
|
+
// Its body in the room, if a prop has its name: where it stands picks the portrait's side.
|
|
1033
|
+
...(p.who && this.spec.props?.[key] ? send("self", [["who", [{ obj: key }]]]) : []),
|
|
978
1034
|
"ret",
|
|
979
1035
|
],
|
|
980
1036
|
"",
|
|
@@ -1039,33 +1095,63 @@ class RoomCompiler {
|
|
|
1039
1095
|
if (cases.length) this.doVerb(name, "Feature", cases);
|
|
1040
1096
|
}
|
|
1041
1097
|
|
|
1098
|
+
/** The y range people stand in: the floor, and where the hero and props that move start. */
|
|
1099
|
+
private standingYs(): number[] {
|
|
1100
|
+
return [
|
|
1101
|
+
...(this.spec.walkable ?? []).map((p) => p[1]),
|
|
1102
|
+
...Object.values(this.spec.props ?? {}).filter((p) => p.moves).map((p) => p.at[1]),
|
|
1103
|
+
...[this.spec.hero?.at, this.spec.hero?.enterTo].flatMap((p) => (p ? [p[1]] : [])),
|
|
1104
|
+
];
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1042
1107
|
/**
|
|
1043
|
-
* The room's perspective as
|
|
1044
|
-
*
|
|
1045
|
-
*
|
|
1046
|
-
*
|
|
1108
|
+
* The room's perspective as a scaler class and its arguments.
|
|
1109
|
+
*
|
|
1110
|
+
* With a horizon, a Scaler (front, back, frontY, backY): sizes at the nearest and farthest
|
|
1111
|
+
* places anyone stands. Size is a straight line in y, so the Scaler's in-between is exact;
|
|
1112
|
+
* keeping to that range keeps its 16-bit arithmetic from overflowing.
|
|
1113
|
+
*
|
|
1114
|
+
* With measured columns, a FloorScaler with three of them (left, middle, right): one is
|
|
1115
|
+
* used for all three, two get one halfway between them.
|
|
1047
1116
|
*/
|
|
1048
|
-
private perspective():
|
|
1117
|
+
private perspective(): { cls: string; args: number[] } | undefined {
|
|
1049
1118
|
const pv = this.spec.perspective;
|
|
1050
1119
|
if (!pv) return undefined;
|
|
1051
|
-
const
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1120
|
+
const fail = (why: string): never => { throw new ContentError(`room ${this.spec.room}: perspective: ${why}`); };
|
|
1121
|
+
if ("columns" in pv) {
|
|
1122
|
+
const cols = floorColumns(pv.columns);
|
|
1123
|
+
const ys = this.standingYs();
|
|
1124
|
+
const [lo, hi] = [Math.min(...ys, ...cols.map((c) => c.back[0])), Math.max(...ys, ...cols.map((c) => c.front[0]))];
|
|
1125
|
+
// FloorScaler works out (front - back) * (y - backY), then (b - a) * (x - x1), in 16 bits.
|
|
1126
|
+
for (const c of cols) {
|
|
1127
|
+
for (const y of [lo, hi]) {
|
|
1128
|
+
if (Math.abs((c.front[1] - c.back[1]) * (y - c.back[0])) > 32767) fail(`sizes from ${c.back[1]}% to ${c.front[1]}% at x ${c.x} are too much for the scaler at y ${y}`);
|
|
1129
|
+
const size = along(c, y);
|
|
1130
|
+
if (size < 1 || size > 255) fail(`at x ${c.x} a figure standing at y ${y} would be ${Math.round(size)}%`);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
return { cls: "FloorScaler", args: cols.flatMap((c) => [c.x, c.back[0], c.back[1], c.front[0], c.front[1]]) };
|
|
1134
|
+
}
|
|
1135
|
+
const ys = this.standingYs().filter((y) => y > pv.horizon);
|
|
1056
1136
|
const backY = ys.length ? Math.min(...ys) : pv.horizon + 1, frontY = ys.length ? Math.max(...ys) : SCREEN_BOTTOM - 1;
|
|
1057
1137
|
const size = (y: number) => Math.round(((y - pv.horizon) / (pv.fullSize - pv.horizon)) * 100);
|
|
1058
1138
|
const [front, back] = [size(frontY), size(backY)];
|
|
1059
1139
|
// Scaler works out (front - back) * (y - backY) on the way.
|
|
1060
|
-
if ((front - back) * (frontY - backY) > 32767)
|
|
1061
|
-
return [front, back, Math.max(frontY, backY + 1), backY];
|
|
1140
|
+
if ((front - back) * (frontY - backY) > 32767) fail(`sizes from ${back}% to ${front}% over ${frontY - backY} lines are too much for the Scaler`);
|
|
1141
|
+
return { cls: "Scaler", args: [front, back, Math.max(frontY, backY + 1), backY] };
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
/** The size, in percent, of a figure standing at x, y. */
|
|
1145
|
+
private sizeAt(x: number, y: number): number {
|
|
1146
|
+
const pv = this.spec.perspective!;
|
|
1147
|
+
if ("columns" in pv) return floorSize(floorColumns(pv.columns), x, y);
|
|
1148
|
+
return ((y - pv.horizon) / (pv.fullSize - pv.horizon)) * 100;
|
|
1062
1149
|
}
|
|
1063
1150
|
|
|
1064
1151
|
private prop(name: string, p: PropSpec): void {
|
|
1065
1152
|
const cases = this.scriptedVerbs.get(name) ?? [];
|
|
1066
1153
|
// A still prop is sized once, for where it is.
|
|
1067
|
-
const
|
|
1068
|
-
const fixed = pv && p.scale === true && !p.moves ? Math.round(((p.at[1] - pv.horizon) / (pv.fullSize - pv.horizon)) * 128) : undefined;
|
|
1154
|
+
const fixed = this.spec.perspective && p.scale === true && !p.moves ? Math.round((this.sizeAt(p.at[0], p.at[1]) / 100) * 128) : undefined;
|
|
1069
1155
|
this.instance(
|
|
1070
1156
|
name, p.moves ? "Actor" : "Prop",
|
|
1071
1157
|
{
|
|
@@ -40,7 +40,8 @@ export interface Target {
|
|
|
40
40
|
/**
|
|
41
41
|
* A character with a portrait view: loop 0 the bust, loop 1 mouth shapes, loop 2 eyes
|
|
42
42
|
* (cel 0 open), all cels the same size so the parts line up. The talker class gets
|
|
43
|
-
* `init: mouth bust eyes frame`; parts are a View (bust) and Props at `at`.
|
|
43
|
+
* `init: mouth bust eyes frame`; parts are a View (bust) and Props at `at`. With `who`,
|
|
44
|
+
* the talker is also told the character's prop in the room, if it has one (`who:`).
|
|
44
45
|
*/
|
|
45
46
|
portrait: {
|
|
46
47
|
class: string;
|
|
@@ -48,6 +49,7 @@ export interface Target {
|
|
|
48
49
|
at: [number, number];
|
|
49
50
|
frameSignal: number;
|
|
50
51
|
partSignal: number;
|
|
52
|
+
who?: boolean;
|
|
51
53
|
};
|
|
52
54
|
/**
|
|
53
55
|
* Conversations: an instance of this class runs a menu of topics for a thing (e.g. a
|
|
@@ -37,9 +37,10 @@ export interface Target {
|
|
|
37
37
|
/**
|
|
38
38
|
* A character with a portrait view: loop 0 the bust, loop 1 mouth shapes, loop 2 eyes
|
|
39
39
|
* (cel 0 open), all cels the same size so the parts line up. The talker class gets
|
|
40
|
-
* `init: mouth bust eyes frame`; parts are a View (bust) and Props at `at`.
|
|
40
|
+
* `init: mouth bust eyes frame`; parts are a View (bust) and Props at `at`. With `who`,
|
|
41
|
+
* the talker is also told the character's prop in the room, if it has one (`who:`).
|
|
41
42
|
*/
|
|
42
|
-
portrait: { class: string; props: Record<string, number>; at: [number, number]; frameSignal: number; partSignal: number };
|
|
43
|
+
portrait: { class: string; props: Record<string, number>; at: [number, number]; frameSignal: number; partSignal: number; who?: boolean };
|
|
43
44
|
/**
|
|
44
45
|
* Conversations: an instance of this class runs a menu of topics for a thing (e.g. a
|
|
45
46
|
* Teller with `init: client modNum sayNoun verb rootNoun`; topic labels are messages at
|
package/tools/game/target.js
CHANGED
|
@@ -31,8 +31,8 @@ export function libraryTarget(globals, items = {}) {
|
|
|
31
31
|
heroTalker: 98,
|
|
32
32
|
firstRoomTalker: 200,
|
|
33
33
|
voice: { class: "Talker", props: {} },
|
|
34
|
-
// Portraits stand at the top left
|
|
35
|
-
portrait: { class: "PortraitTalker", props: {}, at: [8, 8], frameSignal: 0, partSignal: 0 },
|
|
34
|
+
// Portraits stand at the top, left or right by where the character is (who:).
|
|
35
|
+
portrait: { class: "PortraitTalker", props: {}, at: [8, 8], frameSignal: 0, partSignal: 0, who: true },
|
|
36
36
|
teller: { class: "Teller", props: {}, verb: 2 },
|
|
37
37
|
forwardCycle: "Forward",
|
|
38
38
|
closeUp: "CloseUp",
|
package/tools/game/target.ts
CHANGED
|
@@ -33,8 +33,8 @@ export function libraryTarget(globals: readonly string[], items: Record<string,
|
|
|
33
33
|
heroTalker: 98,
|
|
34
34
|
firstRoomTalker: 200,
|
|
35
35
|
voice: { class: "Talker", props: {} },
|
|
36
|
-
// Portraits stand at the top left
|
|
37
|
-
portrait: { class: "PortraitTalker", props: {}, at: [8, 8], frameSignal: 0, partSignal: 0 },
|
|
36
|
+
// Portraits stand at the top, left or right by where the character is (who:).
|
|
37
|
+
portrait: { class: "PortraitTalker", props: {}, at: [8, 8], frameSignal: 0, partSignal: 0, who: true },
|
|
38
38
|
teller: { class: "Teller", props: {}, verb: 2 },
|
|
39
39
|
forwardCycle: "Forward",
|
|
40
40
|
closeUp: "CloseUp",
|