sci2-ts 0.12.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 +6 -0
- 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 +101 -20
- package/packages/content/src/room.ts +105 -21
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,12 @@ 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
|
+
|
|
7
13
|
## [0.12.0](https://github.com/rlueder/sci-ts/compare/v0.11.0...v0.12.0) (2026-10-02)
|
|
8
14
|
|
|
9
15
|
### Added
|
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"] }]]] : []),
|
|
@@ -891,34 +941,65 @@ class RoomCompiler {
|
|
|
891
941
|
if (cases.length)
|
|
892
942
|
this.doVerb(name, "Feature", cases);
|
|
893
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
|
+
}
|
|
894
952
|
/**
|
|
895
|
-
* The room's perspective as
|
|
896
|
-
*
|
|
897
|
-
*
|
|
898
|
-
*
|
|
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.
|
|
899
961
|
*/
|
|
900
962
|
perspective() {
|
|
901
963
|
const pv = this.spec.perspective;
|
|
902
964
|
if (!pv)
|
|
903
965
|
return undefined;
|
|
904
|
-
const
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
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);
|
|
909
984
|
const backY = ys.length ? Math.min(...ys) : pv.horizon + 1, frontY = ys.length ? Math.max(...ys) : SCREEN_BOTTOM - 1;
|
|
910
985
|
const size = (y) => Math.round(((y - pv.horizon) / (pv.fullSize - pv.horizon)) * 100);
|
|
911
986
|
const [front, back] = [size(frontY), size(backY)];
|
|
912
987
|
// Scaler works out (front - back) * (y - backY) on the way.
|
|
913
988
|
if ((front - back) * (frontY - backY) > 32767)
|
|
914
|
-
|
|
915
|
-
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;
|
|
916
998
|
}
|
|
917
999
|
prop(name, p) {
|
|
918
1000
|
const cases = this.scriptedVerbs.get(name) ?? [];
|
|
919
1001
|
// A still prop is sized once, for where it is.
|
|
920
|
-
const
|
|
921
|
-
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;
|
|
922
1003
|
this.instance(name, p.moves ? "Actor" : "Prop", {
|
|
923
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,
|
|
924
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[]]] : []),
|
|
@@ -1041,33 +1095,63 @@ class RoomCompiler {
|
|
|
1041
1095
|
if (cases.length) this.doVerb(name, "Feature", cases);
|
|
1042
1096
|
}
|
|
1043
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
|
+
|
|
1044
1107
|
/**
|
|
1045
|
-
* The room's perspective as
|
|
1046
|
-
*
|
|
1047
|
-
*
|
|
1048
|
-
*
|
|
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.
|
|
1049
1116
|
*/
|
|
1050
|
-
private perspective():
|
|
1117
|
+
private perspective(): { cls: string; args: number[] } | undefined {
|
|
1051
1118
|
const pv = this.spec.perspective;
|
|
1052
1119
|
if (!pv) return undefined;
|
|
1053
|
-
const
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
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);
|
|
1058
1136
|
const backY = ys.length ? Math.min(...ys) : pv.horizon + 1, frontY = ys.length ? Math.max(...ys) : SCREEN_BOTTOM - 1;
|
|
1059
1137
|
const size = (y: number) => Math.round(((y - pv.horizon) / (pv.fullSize - pv.horizon)) * 100);
|
|
1060
1138
|
const [front, back] = [size(frontY), size(backY)];
|
|
1061
1139
|
// Scaler works out (front - back) * (y - backY) on the way.
|
|
1062
|
-
if ((front - back) * (frontY - backY) > 32767)
|
|
1063
|
-
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;
|
|
1064
1149
|
}
|
|
1065
1150
|
|
|
1066
1151
|
private prop(name: string, p: PropSpec): void {
|
|
1067
1152
|
const cases = this.scriptedVerbs.get(name) ?? [];
|
|
1068
1153
|
// A still prop is sized once, for where it is.
|
|
1069
|
-
const
|
|
1070
|
-
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;
|
|
1071
1155
|
this.instance(
|
|
1072
1156
|
name, p.moves ? "Actor" : "Prop",
|
|
1073
1157
|
{
|