@bongos/core 1.24.94 → 1.24.95

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.
Files changed (36) hide show
  1. package/.bongos-core.json +107 -27
  2. package/docs/adr/0405-each-character-has-one-colour-and-the-visor-wears-it.md +31 -0
  3. package/docs/adr/README.md +1 -0
  4. package/docs/copy-inventory.md +7 -7
  5. package/docs/copy-registry.json +7 -7
  6. package/docs/design/characters/v2/artisan-visor.png +0 -0
  7. package/docs/design/characters/v2/artisan.png +0 -0
  8. package/docs/design/characters/v2/artist-visor.png +0 -0
  9. package/docs/design/characters/v2/artist.png +0 -0
  10. package/docs/design/characters/v2/draw-characters.js +465 -0
  11. package/docs/design/characters/v2/patron-visor.png +0 -0
  12. package/docs/design/characters/v2/patron.png +0 -0
  13. package/docs/design/characters/v2/sheet.html +114 -0
  14. package/docs/module-api-changelog.md +2 -0
  15. package/docs/page-readings.json +35 -35
  16. package/modules/hall-ui/public/characters/artisan-face.png +0 -0
  17. package/modules/hall-ui/public/characters/artisan-visor.png +0 -0
  18. package/modules/hall-ui/public/characters/artisan.png +0 -0
  19. package/modules/hall-ui/public/characters/artist-face.png +0 -0
  20. package/modules/hall-ui/public/characters/artist-visor.png +0 -0
  21. package/modules/hall-ui/public/characters/artist.png +0 -0
  22. package/modules/hall-ui/public/characters/patron-face.png +0 -0
  23. package/modules/hall-ui/public/characters/patron-visor.png +0 -0
  24. package/modules/hall-ui/public/characters/patron.png +0 -0
  25. package/modules/hall-ui/public/craft-mode.js +15 -4
  26. package/modules/hall-ui/public/style.css +18 -5
  27. package/modules/hall-ui/public/visor.css +20 -5
  28. package/modules/hall-ui/public/visor.js +6 -3
  29. package/modules/hall-ui/records/characters.md +19 -0
  30. package/modules/hall-ui/records/visor.md +3 -0
  31. package/package-lock.json +2 -2
  32. package/package.json +1 -1
  33. package/release-notes.json +6 -0
  34. package/src/module-api.js +1 -1
  35. package/tests/character_creation.mjs +44 -7
  36. package/tests/hall_visor.mjs +28 -2
@@ -0,0 +1,465 @@
1
+ #!/usr/bin/env node
2
+ // draw-characters.js — the second look for the three characters (exploration, 2026-11-11).
3
+ //
4
+ // The owner's brief: keep "two animals combined", leave ancient Egypt for an alien,
5
+ // space-opera feel that sits with the Helm UI, draw at 64x64, give each character ONE
6
+ // colour of its own, and draw a second version of each wearing the VR visor with the rim
7
+ // in that colour.
8
+ //
9
+ // The sprites are DRAWN BY THIS FILE, pixel by pixel, so a change to a character is an edit
10
+ // here and a re-run, never a hand-edited PNG:
11
+ //
12
+ // node docs/design/characters/v2/draw-characters.js
13
+ //
14
+ // writes, beside this file: <id>.png and <id>-visor.png (64x64, transparent), and
15
+ // contact.png (all six, enlarged, on the hall's night ground) for looking at.
16
+ //
17
+ // node docs/design/characters/v2/draw-characters.js --hall
18
+ //
19
+ // also writes the three files per character the hall serves (task 1005218), into
20
+ // modules/hall-ui/public/characters/: <id>.png (the whole figure, 64x64), <id>-face.png
21
+ // (the head, 32x32, for the top-bar chip) and <id>-visor.png (the head wearing the visor,
22
+ // 32x32, for the visor's corner). The hall's pictures are never edited by hand.
23
+ 'use strict';
24
+ const fs = require('node:fs');
25
+ const path = require('node:path');
26
+ const zlib = require('node:zlib');
27
+
28
+ const N = 64;
29
+ const OUT = '#04080f';
30
+
31
+ // ---------- a 64x64 sheet of colours ----------
32
+ function sheet() {
33
+ const px = new Array(N * N).fill(null);
34
+ const S = {
35
+ px,
36
+ set(x, y, c) { x = Math.round(x); y = Math.round(y); if (x >= 0 && y >= 0 && x < N && y < N) px[y * N + x] = c; },
37
+ get(x, y) { return x >= 0 && y >= 0 && x < N && y < N ? px[y * N + x] : null; },
38
+ rect(x0, y0, x1, y1, c) { for (let y = y0; y <= y1; y++) for (let x = x0; x <= x1; x++) S.set(x, y, c); },
39
+ ell(cx, cy, rx, ry, c) {
40
+ for (let y = Math.floor(cy - ry); y <= Math.ceil(cy + ry); y++) for (let x = Math.floor(cx - rx); x <= Math.ceil(cx + rx); x++) {
41
+ const dx = (x - cx) / rx, dy = (y - cy) / ry;
42
+ if (dx * dx + dy * dy <= 1) S.set(x, y, c);
43
+ }
44
+ },
45
+ poly(pts, c) {
46
+ const ys = pts.map((p) => p[1]);
47
+ for (let y = Math.min(...ys); y <= Math.max(...ys); y++) {
48
+ const xs = [];
49
+ for (let i = 0; i < pts.length; i++) {
50
+ const [x0, y0] = pts[i], [x1, y1] = pts[(i + 1) % pts.length];
51
+ if ((y0 <= y && y1 > y) || (y1 <= y && y0 > y)) xs.push(x0 + ((y - y0) / (y1 - y0)) * (x1 - x0));
52
+ }
53
+ xs.sort((a, b) => a - b);
54
+ for (let i = 0; i + 1 < xs.length; i += 2) for (let x = Math.round(xs[i]); x <= Math.round(xs[i + 1]); x++) S.set(x, y, c);
55
+ }
56
+ },
57
+ // a line with a round brush; `w` may taper from w to w1
58
+ line(x0, y0, x1, y1, c, w = 1, w1 = w) {
59
+ const n = Math.max(Math.abs(x1 - x0), Math.abs(y1 - y0)) * 2 || 1;
60
+ for (let i = 0; i <= n; i++) {
61
+ const t = i / n, x = x0 + (x1 - x0) * t, y = y0 + (y1 - y0) * t, r = (w + (w1 - w) * t) / 2;
62
+ if (r <= 0.6) S.set(x, y, c); else S.ell(x, y, r, r, c);
63
+ }
64
+ },
65
+ path(pts, c, w = 1, w1 = w) {
66
+ for (let i = 0; i + 1 < pts.length; i++) {
67
+ const a = w + (w1 - w) * (i / (pts.length - 1)), b = w + (w1 - w) * ((i + 1) / (pts.length - 1));
68
+ S.line(pts[i][0], pts[i][1], pts[i + 1][0], pts[i + 1][1], c, a, b);
69
+ }
70
+ },
71
+ dots(list, c) { for (const [x, y] of list) S.set(x, y, c); },
72
+ // one light from the upper right: the silhouette's lit edge up a tone, its far edge down
73
+ shade(skip) {
74
+ const src = px.slice(), at = (x, y) => (x >= 0 && y >= 0 && x < N && y < N ? src[y * N + x] : null);
75
+ const mix = (c, to, k) => '#' + [1, 3, 5].map((i) => Math.round(parseInt(c.slice(i, i + 2), 16) * (1 - k) + parseInt(to.slice(i, i + 2), 16) * k).toString(16).padStart(2, '0')).join('');
76
+ for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) {
77
+ const c = at(x, y);
78
+ if (!c || skip.has(c)) continue;
79
+ const lit = !at(x, y - 1) || !at(x + 1, y), far = !at(x - 1, y) || !at(x, y + 1);
80
+ if (lit && !far) px[y * N + x] = mix(c, '#ffffff', 0.2);
81
+ else if (far && !lit) px[y * N + x] = mix(c, '#04080f', 0.3);
82
+ }
83
+ },
84
+ // one dark pixel round the whole figure
85
+ outline(c = OUT) {
86
+ const add = [];
87
+ for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) {
88
+ if (S.get(x, y)) continue;
89
+ if (S.get(x - 1, y) || S.get(x + 1, y) || S.get(x, y - 1) || S.get(x, y + 1)) add.push([x, y]);
90
+ }
91
+ S.dots(add, c);
92
+ },
93
+ };
94
+ return S;
95
+ }
96
+
97
+ // ---------- shared cloth and metal ----------
98
+ const SUIT = { d: '#111e2d', s: '#182a3c', m: '#1f3549', l: '#34566f', hi: '#5d8aa6' };
99
+ const SHADOW = '#081320';
100
+ const ICE = '#c8f1f7';
101
+ const GLASS = '#0a1624';
102
+
103
+ // The three colours. One per character; the visor rim, the lit parts of the suit and the
104
+ // glass tint all take it.
105
+ const COLOUR = {
106
+ artisan: { c: '#fb9569', l: '#ffc7ab', d: '#b3562f', name: 'Ember' },
107
+ artist: { c: '#c59bff', l: '#e8d8ff', d: '#7a52c4', name: 'Violet' },
108
+ patron: { c: '#ffd166', l: '#fff0bd', d: '#b3851c', name: 'Gold' },
109
+ };
110
+
111
+ // THE RULES OF THIS LOOK (owner, 2026-11-11, four rounds):
112
+ // 1. "the animals weren't very recognizable": one animal gives the HEAD, whole, the other
113
+ // gives the BODY, whole (limbs, feet, tail). Never a blend of the two in one part.
114
+ // 2. "way too kiddish": no big heads, no round eyes, no fluffy or candy shapes. They stand
115
+ // tall and lean, in armour and long coats, the eye a narrow slit of light in the
116
+ // character's colour. Natural colours, held back.
117
+ // 3. each holds the one tool its role is known by, carried like a staff.
118
+ // 4. "the originals were more 2.5D, which felt more alive than our side view": the body is
119
+ // turned three-quarters to the viewer (both shoulders, a hand on the hip, a stride, the
120
+ // far foot higher than the near one), the head stays in profile, and they stand on a
121
+ // lit disc as the originals stood on a plinth.
122
+ // 5. "simplify": at 64 pixels a detail smaller than two pixels is noise. Big flat shapes,
123
+ // three tones each, the character's colour in a few strong places, no speckle.
124
+ // 6. one light, from the upper right, where they are looking: every figure's lit edge is
125
+ // raised and its far edge dropped by shade() below, so the three share one light without
126
+ // a hand-placed highlight. And each has its own build: the Artisan broad in heavy plate,
127
+ // the Artist narrow in cloth, the Patron upright under a cape.
128
+
129
+ // what the light leaves alone: anything that glows, the glass, the disc
130
+ function lit(A) { return new Set([A.c, A.l, A.d, ICE, GLASS, SHADOW, '#0d1a29', '#12233a']); }
131
+
132
+ // a spiral with a round brush
133
+ function spiral(S, cx, cy, r0, r1, a0, a1, c, w0, w1) {
134
+ const n = 220;
135
+ for (let i = 0; i <= n; i++) {
136
+ const t = i / n, a = (a0 + (a1 - a0) * t) * Math.PI / 180, r = r0 + (r1 - r0) * t, w = (w0 + (w1 - w0) * t) / 2;
137
+ S.ell(cx + Math.cos(a) * r, cy + Math.sin(a) * r, w, w, c);
138
+ }
139
+ }
140
+ // a ram's horn: a thick curl that thins to a tip, ridged across, lit along its outer edge
141
+ function horn(S, cx, cy, r0, r1, a0, a1, w0, w1, H, ridges) {
142
+ spiral(S, cx, cy, r0, r1, a0, a1, H.d, w0 + 1.4, w1 + 1);
143
+ spiral(S, cx, cy, r0, r1, a0, a1, H.m, w0, w1);
144
+ for (let k = 1; k < ridges; k++) {
145
+ const t = k / ridges, a = (a0 + (a1 - a0) * t) * Math.PI / 180, r = r0 + (r1 - r0) * t, w = (w0 + (w1 - w0) * t) / 2;
146
+ S.line(cx + Math.cos(a) * (r - w), cy + Math.sin(a) * (r - w), cx + Math.cos(a) * (r + w), cy + Math.sin(a) * (r + w), H.d, 1);
147
+ const b = (a0 + (a1 - a0) * (t - 0.5 / ridges)) * Math.PI / 180;
148
+ S.set(cx + Math.cos(b) * (r + w - 0.6), cy + Math.sin(b) * (r + w - 0.6), H.l);
149
+ }
150
+ }
151
+
152
+ // the lit disc they stand on
153
+ function ground(S, A) {
154
+ S.ell(31, 59.6, 20, 3.6, A.d);
155
+ S.ell(31, 59, 20, 3.2, '#0d1a29');
156
+ S.ell(31, 58.6, 15, 2, '#12233a');
157
+ S.ell(30, 58.8, 12, 1.8, SHADOW); // their shadow, falling away from the light
158
+ }
159
+ // plate armour, turned three-quarters: both shoulders, the breastplate off-centre
160
+ function torso(S, A) {
161
+ S.poly([[20, 24], [40, 23], [41, 28], [38, 35], [36, 42], [25, 42], [23, 35], [19, 29]], SUIT.m);
162
+ S.poly([[27, 25], [39, 24], [40, 28], [37, 35], [35, 41], [28, 41], [26, 34]], SUIT.l);
163
+ S.poly([[20, 24], [24, 24], [26, 34], [27, 41], [25, 41], [23, 35], [19, 29]], SUIT.s); // the near flank, out of the light
164
+ S.path([[35, 25], [39, 25], [40, 28]], SUIT.hi, 1); // the plate's lit rim
165
+ S.rect(25, 41, 36, 42, SUIT.d); S.rect(31, 41, 33, 42, A.c);
166
+ S.rect(28, 21, 36, 23, SUIT.d); S.rect(28, 21, 36, 21, SUIT.l); // high collar
167
+ }
168
+ // a stride: the far leg behind and higher, the near leg forward and lower
169
+ function legs(S, A, foot, claw) {
170
+ S.poly([[31, 42], [37, 42], [38, 49], [39, 55], [35, 55], [33, 49]], SUIT.d);
171
+ S.ell(39, 56.4, 4.4, 1.9, foot.d); S.dots([[43, 57], [41, 58]], claw);
172
+ S.poly([[24, 42], [31, 42], [31, 50], [30, 58], [25, 58], [24, 50]], SUIT.m);
173
+ S.poly([[28, 43], [31, 43], [31, 50], [30, 58], [28, 58], [29, 50]], SUIT.l);
174
+ S.ell(28.5, 59.4, 5.2, 2.1, foot.m); S.rect(25, 58, 31, 58, foot.l);
175
+ S.dots([[33, 60], [32, 61], [29, 61]], claw);
176
+ }
177
+ // the near arm, the body-animal's own, fist on the hip, elbow out
178
+ function nearArm(S, A, skin, claw) {
179
+ S.path([[20, 29], [15, 35], [21, 40]], skin.d, 4.6, 3.8);
180
+ S.path([[20, 28], [15, 34], [21, 39]], skin.m, 3, 2.6);
181
+ S.ell(22.5, 40, 2.2, 2, skin.l); S.dots([[24, 41], [22, 42]], claw);
182
+ S.ell(21, 26, 4.8, 3.4, SUIT.l); S.rect(18, 24, 24, 24, SUIT.hi); S.rect(19, 27, 23, 27, A.c);
183
+ }
184
+ // the far arm, reaching to the tool it carries
185
+ function farArm(S, A, skin, claw, hx) {
186
+ S.path([[40, 28], [44, 34], [hx - 2, 33]], skin.d, 4.2, 3.6);
187
+ S.path([[40, 27], [44, 33], [hx - 2, 32]], skin.m, 2.6, 2.4);
188
+ S.ell(hx, 32.5, 2.6, 2.4, skin.l); S.dots([[hx - 1, 35], [hx + 1, 35], [hx + 3, 34]], claw);
189
+ S.ell(39, 25, 3.6, 2.8, SUIT.l); S.rect(37, 23, 41, 23, A.c);
190
+ }
191
+
192
+ // the visor: a dark glass band over the eyes, rim in the character's colour, a strap
193
+ // running to the back of the head
194
+ function visor(S, A, x0, y0, x1, y1, strapTo) {
195
+ if (strapTo) S.line(x0, y0 + 2, strapTo[0], strapTo[1], A.d, 1.6);
196
+ S.rect(x0, y0, x1, y1, A.c);
197
+ S.rect(x0 + 1, y0 + 1, x1 - 1, y1 - 1, GLASS);
198
+ S.set(x0, y0, null); S.set(x0, y1, null);
199
+ S.rect(x0 + 2, y1 - 1, x1 - 2, y1 - 1, A.d); // the colour, caught along the glass
200
+ S.line(x1 - 3, y1 - 1, x1 - 2, y0 + 1, ICE, 1);
201
+ S.rect(x1 + 1, y0 + 1, x1 + 1, y1 - 1, A.d); // the front of the housing
202
+ }
203
+ // the bare eye: a dark socket, a slit of the character's colour
204
+ function slit(S, A, x, y, dark) { S.rect(x - 2, y - 2, x + 3, y, dark); S.rect(x, y, x + 2, y, A.c); S.set(x + 2, y, A.l); }
205
+
206
+ // ---------- ARTISAN: a ram's head on a lizard's body, the long spanner ----------
207
+ function artisan(withVisor) {
208
+ const S = sheet(), A = COLOUR.artisan;
209
+ const LZ = { m: '#4f7d5f', d: '#33574a', l: '#7fae7c' };
210
+ const WOOL = { m: '#b3a892', d: '#857b68', l: '#d6ccb4' };
211
+ const FACE = { m: '#8b767a', d: '#4a3b42', l: '#b39ea4' };
212
+ const HORN = { m: '#d8c491', d: '#8a7446', l: '#f6ebc4' };
213
+ const FAR = { m: '#a08c5c', d: '#6b5932', l: '#a08c5c' };
214
+ const STEEL = { m: '#93adbd', d: '#5b7687', l: '#d9eaf2' };
215
+
216
+ ground(S, A);
217
+ // the lizard's tail: heavy at the hip, swung out toward the viewer, plated along the top
218
+ S.path([[25, 41], [17, 48], [10, 54], [5, 56], [2, 53]], LZ.d, 6.6, 1.4);
219
+ S.path([[25, 40], [17, 47], [10, 53], [6, 55]], LZ.m, 3.6, 1);
220
+ S.path([[24, 39], [17, 45], [11, 50]], LZ.l, 1.4, 1); // light along the top of the tail
221
+ S.rect(20, 42, 21, 43, A.c); S.rect(13, 48, 14, 49, A.c);
222
+
223
+ legs(S, A, LZ, WOOL.l);
224
+ S.ell(21, 28, 5.6, 5, SUIT.s); S.ell(40, 27, 4.6, 4.4, SUIT.m); // broad round shoulders under the plate
225
+ torso(S, A);
226
+ S.poly([[26, 25], [40, 24], [41, 29], [38, 35], [27, 35]], SUIT.l); // a deeper breastplate
227
+ S.path([[36, 25], [40, 25], [41, 28]], SUIT.hi, 1);
228
+ S.rect(30, 28, 36, 30, A.c); S.rect(32, 29, 34, 29, A.l); // the lit core in the chest
229
+ S.rect(24, 43, 27, 46, SUIT.l); S.rect(34, 43, 36, 45, SUIT.l); // tool pouches on the belt
230
+
231
+ // neck: short dense wool
232
+ S.rect(28, 17, 35, 21, WOOL.d);
233
+ // the far horn first: its curl rising behind the skull
234
+ horn(S, 31, 10, 7.5, 6, -160, -20, 3.4, 2, FAR, 5);
235
+ // skull, close wool
236
+ S.ell(28, 11, 7, 6.5, WOOL.m); S.ell(26, 13, 5, 4.4, WOOL.d);
237
+ // the long bony ram's face, nose line bowed out
238
+ S.poly([[31, 5], [37, 5], [42, 8], [46, 13], [48, 18], [46, 21], [38, 21], [32, 19], [30, 11]], FACE.m);
239
+ S.path([[33, 6], [37, 6], [42, 9], [46, 14]], FACE.l, 1.4);
240
+ S.poly([[32, 15], [39, 17], [38, 20], [33, 19]], '#73616a'); // the cheek, in shade
241
+ S.path([[37, 20], [46, 20]], FACE.d, 1.4);
242
+ S.set(47, 16, FACE.d); S.path([[42, 19], [47, 18]], FACE.d, 1); // nostril, mouth
243
+ S.poly([[38, 21], [41, 21], [40, 25]], WOOL.d); // beard
244
+ // ear, inside the curl
245
+ S.poly([[26, 12], [20, 15], [22, 17], [27, 15]], FACE.m); S.path([[25, 14], [22, 15]], FACE.d, 1);
246
+ // the near horn: up off the brow, back, down, and forward to a point at the cheek
247
+ horn(S, 24, 12, 8.6, 5.4, -62, -352, 5.2, 1.6, HORN, 10);
248
+
249
+ if (withVisor) visor(S, A, 33, 7, 44, 13, null);
250
+ else slit(S, A, 37, 11, FACE.d);
251
+
252
+ // the spanner, carried like a staff: a long shaft, an open jaw at the head
253
+ S.rect(51, 18, 52, 58, STEEL.d); S.rect(52, 19, 52, 57, STEEL.m);
254
+ S.ell(51.5, 14, 4.6, 4.6, STEEL.d); S.ell(51.5, 14, 3.2, 3.2, STEEL.m);
255
+ S.rect(50, 8, 53, 14, null); // the jaw
256
+ S.rect(51, 24, 52, 25, A.c); S.rect(50, 57, 53, 58, STEEL.d);
257
+ farArm(S, A, LZ, WOOL.l, 51);
258
+ nearArm(S, A, LZ, WOOL.l);
259
+ S.ell(20, 25.5, 6, 4.2, SUIT.l); S.ell(21.5, 23.6, 4, 1.4, SUIT.hi); S.rect(17, 28, 23, 28, A.c); // heavy round pauldrons
260
+ S.ell(40, 24.5, 4.6, 3.2, SUIT.l); S.ell(41, 23.2, 3, 1.1, SUIT.hi); S.rect(38, 26, 42, 26, A.c);
261
+
262
+ S.shade(lit(A));
263
+ S.outline();
264
+ return S;
265
+ }
266
+
267
+ // ---------- ARTIST: an ibis's head on an octopus's body, the light-brush ----------
268
+ function artist(withVisor) {
269
+ const S = sheet(), A = COLOUR.artist;
270
+ const FE = { m: '#e6e3ee', d: '#a59fbb', l: '#ffffff' };
271
+ const BEAK = { m: '#b3a6dd', d: '#6f609f', l: '#e3dbf7', face: '#3a3058' };
272
+ const OC = { m: '#6657aa', d: '#453a80', l: '#8f80d0', suck: '#cdbcf5' };
273
+
274
+ ground(S, A);
275
+ // the far arms under the hem, higher and darker
276
+ S.path([[24, 52], [16, 53], [10, 50], [8, 46]], OC.d, 4.2, 1.2);
277
+ S.path([[39, 52], [47, 53], [53, 50], [55, 46]], OC.d, 4.2, 1.2);
278
+ // the near ones, lower and lit
279
+ S.path([[25, 54], [19, 59], [12, 60], [8, 57]], OC.m, 4.6, 1.2);
280
+ S.path([[37, 54], [43, 59], [50, 60], [54, 57]], OC.m, 4.6, 1.2);
281
+ S.rect(13, 59, 14, 59, OC.suck); S.rect(48, 59, 49, 59, OC.suck);
282
+
283
+ // a long coat to the ground, turned three-quarters, open down the front, a lit hem
284
+ S.poly([[22, 24], [38, 23], [39, 28], [38, 40], [42, 54], [19, 54], [23, 40], [21, 29]], SUIT.m);
285
+ S.poly([[28, 25], [37, 24], [38, 28], [37, 40], [41, 53], [29, 53], [28, 40]], SUIT.l);
286
+ S.poly([[22, 24], [25, 24], [26, 38], [23, 40], [21, 29]], SUIT.s); // the near flank, out of the light
287
+ S.path([[25, 41], [22, 53]], SUIT.d, 1.6); S.path([[31, 42], [31, 53]], SUIT.m, 1.6); // folds
288
+ S.path([[38, 41], [40, 52]], SUIT.hi, 1); // the lit fall of the cloth
289
+ S.path([[33, 25], [35, 53]], A.d, 1);
290
+ S.rect(19, 54, 42, 55, A.c); S.rect(19, 53, 42, 53, A.d);
291
+ S.rect(23, 38, 39, 39, SUIT.d); S.rect(35, 38, 37, 42, A.c); // sash, its tail
292
+ S.rect(28, 21, 36, 23, SUIT.d); S.rect(28, 21, 36, 21, A.d); // high collar
293
+
294
+ // the ibis: a long white neck
295
+ S.path([[31, 22], [29, 19], [29, 16]], FE.d, 4.4);
296
+ S.path([[32, 22], [30, 19], [30, 16]], FE.m, 2.2);
297
+ // head, two plumes off the back of it
298
+ S.ell(29, 11, 7, 7, FE.m); S.ell(26, 13, 4.4, 4.4, FE.d); S.ell(28, 11, 5.4, 5.4, FE.m);
299
+ S.path([[23, 9], [18, 11], [16, 15]], FE.m, 2, 1); S.path([[23, 12], [19, 15], [18, 19]], FE.d, 1.6, 1);
300
+ // the bare dark face and the long down-curved beak
301
+ S.ell(34, 11, 3.4, 3.8, BEAK.face);
302
+ S.path([[36, 11], [41, 12], [45, 15], [47, 19], [48, 24]], BEAK.d, 4.8, 1.2);
303
+ S.path([[36, 10], [41, 11], [45, 14], [47, 18], [48, 23]], BEAK.m, 3, 1);
304
+ S.path([[37, 9], [41, 10], [44, 12]], BEAK.l, 1);
305
+
306
+ if (withVisor) visor(S, A, 28, 7, 37, 12, [22, 9]);
307
+ else slit(S, A, 32, 10, BEAK.face);
308
+
309
+ // the light-brush: a long thin rod, the head burning in her colour
310
+ S.path([[44, 44], [55, 22]], '#5b7687', 1.8); S.path([[45, 42], [55, 22]], '#93adbd', 1);
311
+ S.poly([[54, 22], [57, 22], [59, 17], [58, 12], [55, 17]], A.c);
312
+ S.path([[56, 21], [57, 15]], A.l, 1); S.set(58, 11, ICE);
313
+ // the far arm, wound once round the rod
314
+ S.path([[38, 27], [43, 33], [48, 35], [51, 32], [49, 30]], OC.d, 4, 1.6);
315
+ S.path([[38, 27], [43, 32], [48, 34], [50, 32]], OC.m, 2.4, 1.2);
316
+ // the near arm, curled at the hip
317
+ S.path([[22, 29], [18, 37], [19, 44], [16, 48]], OC.d, 4.4, 1.6);
318
+ S.path([[22, 28], [18, 36], [19, 43]], OC.m, 2.6, 1.4);
319
+ // a shawl over both shoulders, a lit edge: cloth, where the others wear plate
320
+ S.poly([[22, 23], [38, 22], [41, 27], [37, 30], [30, 32], [24, 30], [19, 28]], '#2a2350');
321
+ S.poly([[27, 23], [37, 22], [39, 26], [33, 27], [28, 26]], '#3d3378'); // light across the shawl
322
+ S.path([[19, 28], [24, 30], [30, 32], [37, 30], [41, 27]], A.c, 1.4);
323
+ S.rect(28, 21, 36, 22, '#2a2350'); S.rect(28, 21, 36, 21, A.d);
324
+
325
+ S.shade(lit(A));
326
+ S.outline();
327
+ return S;
328
+ }
329
+
330
+ // ---------- PATRON: a falcon's head on a lion's body, the staff of office ----------
331
+ function patron(withVisor) {
332
+ const S = sheet(), A = COLOUR.patron;
333
+ const FE = { m: '#e9e5db', d: '#aaa392', l: '#ffffff' };
334
+ const CAP = { m: '#44516c', d: '#28324a', l: '#62749a' };
335
+ const LI = { m: '#b98d55', d: '#85602f', l: '#dcb682', tuft: '#573820' };
336
+ const BEAK = { m: '#8090aa', d: '#262e40', cere: '#e0b040' };
337
+
338
+ ground(S, A);
339
+ // the lion's tail: thin, swung out, a dark tuft, a ring of office on it
340
+ S.path([[18, 50], [11, 57], [6, 55], [4, 49], [6, 44]], LI.d, 3, 2.2);
341
+ S.path([[18, 49], [11, 56], [6, 54]], LI.m, 1.4, 1.2);
342
+ S.ell(7, 40, 2.6, 3.8, LI.tuft); S.set(7, 36, LI.tuft); S.rect(5, 44, 8, 44, A.c);
343
+
344
+ legs(S, A, LI, LI.tuft);
345
+ torso(S, A);
346
+ S.path([[24, 25], [36, 40]], A.c, 2); S.path([[25, 25], [37, 39]], A.d, 1); // the sash of office
347
+ S.ell(32, 41.5, 1.8, 1.8, A.c); S.set(32, 41, A.l);
348
+ S.rect(28, 23, 36, 23, A.c); // gold at the collar
349
+ // the cape of office: over the near shoulder, to the ground, a gold edge
350
+ S.poly([[17, 25], [27, 24], [27, 34], [25, 45], [25, 56], [12, 54], [14, 40]], '#27386a');
351
+ S.poly([[17, 25], [21, 25], [18, 40], [16, 54], [12, 54], [14, 40]], '#1a2748');
352
+ S.path([[23, 28], [21, 42], [20, 55]], '#3d5290', 1.6); // a lit fold
353
+ S.path([[25, 34], [23, 46], [23, 55]], '#1a2748', 1); // a deep one
354
+ S.ell(21, 26, 4, 1.6, '#3d5290'); // light on the shoulder
355
+ S.path([[27, 24], [27, 34], [25, 45], [25, 56]], A.c, 1.6);
356
+ S.path([[12, 54], [25, 56]], A.c, 1.6);
357
+ S.ell(26, 25, 2, 2, A.c); S.set(26, 24, A.l);
358
+
359
+ // neck: white feathers
360
+ S.rect(28, 18, 35, 21, FE.d); S.rect(31, 18, 35, 21, FE.m);
361
+ // head: the falcon. Slate cap and nape, white cheek and throat
362
+ S.poly([[23, 8], [17, 10], [22, 12]], CAP.d); S.poly([[22, 12], [17, 15], [23, 16]], CAP.d); // nape feathers
363
+ S.ell(29, 11, 7.6, 7.2, CAP.m); S.ell(28, 6, 5, 2, CAP.l); S.ell(23, 13, 2.6, 4.4, CAP.d);
364
+ S.ell(33, 15, 5, 4.4, FE.m); S.ell(31, 17, 4.4, 2.4, FE.m);
365
+ S.path([[34, 13], [33, 18]], CAP.d, 1.6); // the falcon's tear
366
+ // the hooked beak: yellow cere, blue-grey bill, dark tip turned down
367
+ S.poly([[37, 8], [43, 9], [47, 12], [48, 16], [47, 22], [45, 19], [43, 16], [38, 15]], BEAK.m);
368
+ S.poly([[46, 15], [48, 16], [47, 22], [45, 19]], BEAK.d);
369
+ S.ell(38, 12, 1.8, 2.2, BEAK.cere); S.path([[38, 16], [43, 16]], BEAK.d, 1);
370
+ S.path([[29, 8], [37, 9]], CAP.d, 2); // the heavy brow
371
+
372
+ if (withVisor) visor(S, A, 28, 7, 37, 12, [21, 9]);
373
+ else slit(S, A, 33, 11, CAP.d);
374
+
375
+ // the staff: a tall rod, a ring of light at its head
376
+ S.rect(52, 11, 53, 58, '#5b7687'); S.rect(53, 12, 53, 57, '#93adbd');
377
+ S.ell(52.5, 6, 4.6, 4.6, A.c); S.ell(52.5, 6, 2.6, 2.6, null); S.ell(52.5, 6, 1, 1, ICE);
378
+ S.rect(51, 11, 54, 12, A.d); S.rect(51, 57, 54, 58, A.d);
379
+ farArm(S, A, LI, LI.tuft, 52);
380
+ S.ell(39, 24.5, 4.2, 2.8, A.c); S.rect(36, 26, 42, 26, A.d); // the epaulette of office
381
+
382
+ S.shade(lit(A));
383
+ S.outline();
384
+ return S;
385
+ }
386
+
387
+ // ---------- PNG ----------
388
+ function hex(c) { return [parseInt(c.slice(1, 3), 16), parseInt(c.slice(3, 5), 16), parseInt(c.slice(5, 7), 16)]; }
389
+ function png(w, h, rgba) {
390
+ const raw = Buffer.alloc((w * 4 + 1) * h);
391
+ for (let y = 0; y < h; y++) { raw[y * (w * 4 + 1)] = 0; rgba.copy(raw, y * (w * 4 + 1) + 1, y * w * 4, (y + 1) * w * 4); }
392
+ const chunk = (type, data) => {
393
+ const len = Buffer.alloc(4); len.writeUInt32BE(data.length);
394
+ const body = Buffer.concat([Buffer.from(type), data]);
395
+ const crc = Buffer.alloc(4); crc.writeUInt32BE(zlib.crc32(body) >>> 0);
396
+ return Buffer.concat([len, body, crc]);
397
+ };
398
+ const head = Buffer.alloc(13); head.writeUInt32BE(w, 0); head.writeUInt32BE(h, 4); head[8] = 8; head[9] = 6;
399
+ return Buffer.concat([Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]), chunk('IHDR', head), chunk('IDAT', zlib.deflateSync(raw, { level: 9 })), chunk('IEND', Buffer.alloc(0))]);
400
+ }
401
+ function spritePng(S) {
402
+ const b = Buffer.alloc(N * N * 4);
403
+ S.px.forEach((c, i) => { if (!c) return; const [r, g, bl] = hex(c); b[i * 4] = r; b[i * 4 + 1] = g; b[i * 4 + 2] = bl; b[i * 4 + 3] = 255; });
404
+ return png(N, N, b);
405
+ }
406
+ // the head: a 32x32 window on the top of the figure, where every head sits
407
+ const FACE = { x: 16, y: 0, size: 32 };
408
+ function facePng(S) {
409
+ const n = FACE.size, b = Buffer.alloc(n * n * 4);
410
+ for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) {
411
+ const c = S.get(FACE.x + x, FACE.y + y); if (!c) continue;
412
+ const [r, g, bl] = hex(c), i = (y * n + x) * 4;
413
+ b[i] = r; b[i + 1] = g; b[i + 2] = bl; b[i + 3] = 255;
414
+ }
415
+ return png(n, n, b);
416
+ }
417
+ function contact(list, k) {
418
+ const cols = 3, rows = Math.ceil(list.length / cols), pad = 2, cell = N + pad * 2;
419
+ const w = cols * cell * k, h = rows * cell * k, b = Buffer.alloc(w * h * 4);
420
+ const [br, bg, bb] = hex('#050d16');
421
+ for (let i = 0; i < w * h; i++) { b[i * 4] = br; b[i * 4 + 1] = bg; b[i * 4 + 2] = bb; b[i * 4 + 3] = 255; }
422
+ list.forEach((S, n) => {
423
+ const ox = (n % cols) * cell + pad, oy = Math.floor(n / cols) * cell + pad;
424
+ for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) {
425
+ const c = S.get(x, y); if (!c) continue;
426
+ const [r, g, bl] = hex(c);
427
+ for (let dy = 0; dy < k; dy++) for (let dx = 0; dx < k; dx++) {
428
+ const i = (((oy + y) * k + dy) * w + (ox + x) * k + dx) * 4;
429
+ b[i] = r; b[i + 1] = g; b[i + 2] = bl;
430
+ }
431
+ }
432
+ });
433
+ return png(w, h, b);
434
+ }
435
+
436
+ const DRAW = { artisan, artist, patron };
437
+ // the nine files the hall serves, by name: what --hall writes, and what the test holds the
438
+ // checked-in pictures against
439
+ function hallFiles() {
440
+ const out = {};
441
+ for (const id of Object.keys(DRAW)) {
442
+ out[id + '.png'] = spritePng(DRAW[id](false));
443
+ out[id + '-face.png'] = facePng(DRAW[id](false));
444
+ out[id + '-visor.png'] = facePng(DRAW[id](true));
445
+ }
446
+ return out;
447
+ }
448
+ if (require.main === module) {
449
+ const all = [];
450
+ for (const id of Object.keys(DRAW)) {
451
+ const bare = DRAW[id](false), worn = DRAW[id](true);
452
+ fs.writeFileSync(path.join(__dirname, id + '.png'), spritePng(bare));
453
+ fs.writeFileSync(path.join(__dirname, id + '-visor.png'), spritePng(worn));
454
+ all.push(bare);
455
+ }
456
+ for (const id of Object.keys(DRAW)) all.push(DRAW[id](true));
457
+ fs.writeFileSync(path.join(__dirname, 'contact.png'), contact(all, 6));
458
+ if (process.argv.includes('--hall')) {
459
+ const hall = path.join(__dirname, '..', '..', '..', '..', 'modules', 'hall-ui', 'public', 'characters');
460
+ for (const [name, buf] of Object.entries(hallFiles())) fs.writeFileSync(path.join(hall, name), buf);
461
+ console.log('wrote the hall pictures to ' + path.relative(process.cwd(), hall));
462
+ }
463
+ console.log('drew ' + Object.keys(DRAW).join(', ') + ' (plain and visor) and contact.png');
464
+ }
465
+ module.exports = { DRAW, COLOUR, N, FACE, hallFiles };
@@ -0,0 +1,114 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Characters, second look</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@400;500;600&family=Barlow:wght@400;500&display=swap">
10
+ <style>
11
+ /* A mock-up, not a hall page: the hall's night, ice and salmon are typed in here so the
12
+ sheet opens from a file. The pictures are drawn by draw-characters.js. */
13
+ :root { --bg: #050d16; --card: #0a1622; --ink: #fff; --ice: #c8f1f7; --rule: rgba(200, 241, 247, .2); --night: rgba(5, 13, 22, .72); }
14
+ * { box-sizing: border-box; }
15
+ body { margin: 0; padding: 40px; background: var(--bg); color: var(--ink); font: 400 17px/1.45 Barlow, system-ui, sans-serif; width: 1280px; }
16
+ h1, h2, .name, .cap, .place, .btn, .hint, .unit { font-family: "Barlow Condensed", sans-serif; font-weight: 500; text-transform: uppercase; letter-spacing: .08em; }
17
+ h1 { font-size: 34px; margin: 0 0 4px; }
18
+ .lede { color: var(--ice); margin: 0 0 32px; max-width: 60em; }
19
+ h2 { font-size: 20px; color: var(--ice); margin: 40px 0 14px; padding-top: 14px; border-top: 1px solid var(--rule); }
20
+ img { image-rendering: pixelated; display: block; }
21
+
22
+ .trio { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; }
23
+ .door { background: var(--card); padding: 20px; border-top: 2px solid var(--c); clip-path: polygon(0 0, 100% 0, 100% calc(100% - 18px), calc(100% - 18px) 100%, 0 100%); }
24
+ .door .pair { display: flex; align-items: flex-end; justify-content: center; gap: 8px; }
25
+ .door .pair img { width: 192px; height: 192px; }
26
+ .door .pair img.was { width: 120px; height: auto; opacity: .85; image-rendering: pixelated; }
27
+ .door .arrow { color: var(--ice); font-size: 22px; padding-bottom: 80px; }
28
+ .name { font-size: 30px; margin-top: 12px; color: var(--c); }
29
+ .mix { color: var(--ice); margin: 2px 0 8px; }
30
+ .swatch { display: inline-flex; align-items: center; gap: 8px; color: var(--ice); font-size: 15px; }
31
+ .swatch i { width: 16px; height: 16px; background: var(--c); display: inline-block; }
32
+ .sizes { display: flex; align-items: flex-end; gap: 18px; margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--rule); }
33
+ .sizes span { color: var(--ice); font-size: 13px; display: block; margin-top: 4px; text-align: center; }
34
+
35
+ /* the visor, tinted */
36
+ .glass { position: relative; height: 340px; margin-bottom: 20px; overflow: hidden;
37
+ background: linear-gradient(to bottom, var(--night), transparent 110px, transparent 55%, var(--night)),
38
+ radial-gradient(120% 90% at 50% 120%, #f0b08a 0, #7f6a8f 30%, #25405f 60%, #0b1c30 100%); }
39
+ .glass svg { position: absolute; left: 0; width: 100%; overflow: visible; }
40
+ .glass svg path { fill: none; stroke: var(--c); stroke-width: 1.5; vector-effect: non-scaling-stroke; opacity: .9; }
41
+ .glass svg.top { top: 14px; height: 42px; } .glass svg.foot { bottom: 22px; height: 18px; }
42
+ .place { position: absolute; left: 50%; top: 38px; transform: translateX(-50%); font-size: 22px; letter-spacing: .24em; color: var(--ice); text-shadow: 0 0 6px var(--bg); }
43
+ .sight { position: absolute; left: 50%; top: 50%; width: 24px; height: 24px; margin: -12px; border: 1.5px solid var(--c); border-radius: 50%; }
44
+ .sight::after { content: ""; position: absolute; left: 8px; top: 8px; width: 5px; height: 5px; border-radius: 50%; background: var(--c); }
45
+ .foot { position: absolute; left: 48px; right: 48px; bottom: 64px; display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; }
46
+ .me { display: flex; align-items: center; gap: 14px; transform: perspective(900px) rotateY(16deg); transform-origin: left center; }
47
+ .face { width: 76px; height: 76px; flex: none; background-color: var(--card); background-repeat: no-repeat; background-size: 192px 192px; image-rendering: pixelated;
48
+ border: 2px solid var(--c); clip-path: polygon(0 0, 100% 0, 100% calc(100% - 12px), calc(100% - 12px) 100%, 0 100%); }
49
+ .who .cap { font-size: 14px; color: var(--c); line-height: 1; }
50
+ .who .num { font: 400 34px/1 "Barlow Condensed", sans-serif; }
51
+ .unit { font-size: 16px; color: var(--ice); }
52
+ .btn { justify-self: center; display: inline-flex; align-items: center; gap: 14px; min-height: 56px; padding: 6px 14px 6px 24px; background: var(--c); color: #1c1206; font-size: 26px; font-weight: 600; letter-spacing: .04em;
53
+ clip-path: polygon(0 0, 100% 0, 100% calc(100% - 14px), calc(100% - 14px) 100%, 0 100%); }
54
+ kbd { display: inline-flex; align-items: center; justify-content: center; min-width: 24px; height: 24px; padding: 0 5px; border: 1.5px solid currentColor; font: 500 14px/1 "Barlow Condensed", sans-serif; }
55
+ .hints { justify-self: end; display: flex; gap: 20px; transform: perspective(900px) rotateY(-16deg); transform-origin: right center; }
56
+ .hint { display: inline-flex; align-items: center; gap: 6px; font-size: 18px; letter-spacing: .04em; color: var(--ice); text-shadow: 0 0 6px var(--bg); }
57
+ .hint kbd { background: var(--bg); }
58
+ .note { color: var(--ice); font-size: 15px; margin: 6px 0 0; }
59
+ </style>
60
+ </head>
61
+ <body>
62
+ <h1>Characters, second look</h1>
63
+ <p class="lede">Still two animals in one body: one gives the head, the other the body. They stand three-quarters to you on a lit disc, in armour and long coats, each drawn at 64 by 64 with one colour of its own, carrying the tool of its role, and with a second picture wearing the visor.</p>
64
+
65
+ <h2>Who they are</h2>
66
+ <div class="trio">
67
+ <div class="door" style="--c:#fb9569">
68
+ <div class="pair"><img src="artisan.png" alt=""></div>
69
+ <div class="name">Artisan</div><div class="mix">A ram's head on a lizard's body. Carries the long spanner: the one who builds.</div>
70
+ <span class="swatch"><i></i>Ember</span>
71
+ <div class="sizes"><div><img src="artisan.png" width="64" height="64" alt=""><span>64</span></div><div><img src="artisan.png" width="128" height="128" alt=""><span>128</span></div><div><img src="artisan-visor.png" width="128" height="128" alt=""><span>visor on</span></div></div>
72
+ </div>
73
+ <div class="door" style="--c:#c59bff">
74
+ <div class="pair"><img src="artist.png" alt=""></div>
75
+ <div class="name">Artist</div><div class="mix">An ibis's head on an octopus's body. Carries the light-brush: the one who shapes the look.</div>
76
+ <span class="swatch"><i></i>Violet</span>
77
+ <div class="sizes"><div><img src="artist.png" width="64" height="64" alt=""><span>64</span></div><div><img src="artist.png" width="128" height="128" alt=""><span>128</span></div><div><img src="artist-visor.png" width="128" height="128" alt=""><span>visor on</span></div></div>
78
+ </div>
79
+ <div class="door" style="--c:#ffd166">
80
+ <div class="pair"><img src="patron.png" alt=""></div>
81
+ <div class="name">Patron</div><div class="mix">A falcon's head on a lion's body. Carries the staff of office: the one who decides.</div>
82
+ <span class="swatch"><i></i>Gold</span>
83
+ <div class="sizes"><div><img src="patron.png" width="64" height="64" alt=""><span>64</span></div><div><img src="patron.png" width="128" height="128" alt=""><span>128</span></div><div><img src="patron-visor.png" width="128" height="128" alt=""><span>visor on</span></div></div>
84
+ </div>
85
+ </div>
86
+
87
+ <h2>The visor takes your colour, and you wear it</h2>
88
+ <div class="glass" style="--c:#fb9569">
89
+ <svg class="top" viewBox="0 0 1000 100" preserveAspectRatio="none"><path d="M76 100Q500 -100 924 100"/></svg>
90
+ <svg class="foot" viewBox="0 0 1000 100" preserveAspectRatio="none"><path d="M76 0Q500 200 924 0"/></svg>
91
+ <p class="place" style="margin:0">Cloud city</p><span class="sight"></span>
92
+ <div class="foot"><div class="me"><span class="face" style="background-image:url(artisan-visor.png);background-position:-58px 2px"></span><span class="who"><span class="cap">Artisan</span><br><span class="num">1,240</span> <span class="unit">credits</span></span></div>
93
+ <span class="btn">Claim a task <kbd>C</kbd></span>
94
+ <div class="hints"><span class="hint"><kbd>A</kbd>Actions</span><span class="hint"><kbd>Esc</kbd>Options</span></div></div>
95
+ </div>
96
+ <div class="glass" style="--c:#c59bff">
97
+ <svg class="top" viewBox="0 0 1000 100" preserveAspectRatio="none"><path d="M76 100Q500 -100 924 100"/></svg>
98
+ <svg class="foot" viewBox="0 0 1000 100" preserveAspectRatio="none"><path d="M76 0Q500 200 924 0"/></svg>
99
+ <p class="place" style="margin:0">Cloud city</p><span class="sight"></span>
100
+ <div class="foot"><div class="me"><span class="face" style="background-image:url(artist-visor.png);background-position:-58px 2px"></span><span class="who"><span class="cap">Artist</span><br><span class="num">860</span> <span class="unit">credits</span></span></div>
101
+ <span class="btn">Review this page <kbd>R</kbd></span>
102
+ <div class="hints"><span class="hint"><kbd>A</kbd>Actions</span><span class="hint"><kbd>Esc</kbd>Options</span></div></div>
103
+ </div>
104
+ <div class="glass" style="--c:#ffd166">
105
+ <svg class="top" viewBox="0 0 1000 100" preserveAspectRatio="none"><path d="M76 100Q500 -100 924 100"/></svg>
106
+ <svg class="foot" viewBox="0 0 1000 100" preserveAspectRatio="none"><path d="M76 0Q500 200 924 0"/></svg>
107
+ <p class="place" style="margin:0">Cloud city</p><span class="sight"></span>
108
+ <div class="foot"><div class="me"><span class="face" style="background-image:url(patron-visor.png);background-position:-62px 2px"></span><span class="who"><span class="cap">Patron</span><br><span class="num">2,015</span> <span class="unit">credits</span></span></div>
109
+ <span class="btn">Governor city <kbd>V</kbd></span>
110
+ <div class="hints"><span class="hint"><kbd>A</kbd>Actions</span><span class="hint"><kbd>Esc</kbd>Options</span></div></div>
111
+ </div>
112
+ <p class="note">A mock-up: nothing in the hall has changed yet. The numbers are made up.</p>
113
+ </body>
114
+ </html>
@@ -3436,5 +3436,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
3436
3436
  landed since 1.24.92 with no explicit bump. run 43299203682. (task 1002620)
3437
3437
  1.24.94 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3438
3438
  landed since 1.24.93 with no explicit bump. run 43302410261. (task 1002620)
3439
+ 1.24.95 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3440
+ landed since 1.24.94 with no explicit bump. run 43304959846. (task 1002620)
3439
3441
  ---------------------------------------------------------------------------
3440
3442
  ```