@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
@@ -153,7 +153,7 @@ test('a held character is the only one offered, and the top bar wears its sprite
153
153
  const sw = t.C.switchHtml();
154
154
  assert.match(sw, /craft-switch--character/);
155
155
  assert.match(sw, /data-craft-me/);
156
- assert.match(sw, /\/builders\/characters\/artisan\.png/);
156
+ assert.match(sw, /\/builders\/characters\/artisan-face\.png/, 'the chip is the head, its own picture (task 1005218)');
157
157
  assert.ok(!/data-craft-opt=/.test(sw), 'the three-way switch is gone');
158
158
  // A session's stored mode from before the character cannot override it.
159
159
  t.session['hall-craft'] = 'patron';
@@ -162,7 +162,7 @@ test('a held character is the only one offered, and the top bar wears its sprite
162
162
 
163
163
  test('the sprite is drawn on first paint from the last held character, before /me answers', () => {
164
164
  const t = boot({ store: { 'hall-character': 'patron' } });
165
- assert.match(t.C.switchHtml(), /\/builders\/characters\/patron\.png/);
165
+ assert.match(t.C.switchHtml(), /\/builders\/characters\/patron-face\.png/);
166
166
  });
167
167
 
168
168
  test('an older core that says nothing about characters changes nothing', () => {
@@ -187,15 +187,52 @@ test('the character section never writes `hidden` onto the nav: a character only
187
187
  assert.ok(!/nav-item|app-sidebar/.test(section), 'the character code does not touch the nav itself; applyNav does');
188
188
  });
189
189
 
190
- test('the owner\'s three sprites are on disk, one per character', () => {
190
+ // task 1005218: the second look. Three pictures per character, drawn by
191
+ // docs/design/characters/v2/draw-characters.js --hall: the whole figure (64), the head for
192
+ // the chip (32) and the head wearing the visor for the visor's corner (32).
193
+ test('each character\'s three pictures are on disk, at their sizes', () => {
191
194
  for (const id of ['artisan', 'artist', 'patron']) {
192
- const f = path.join(HALL, 'characters', id + '.png');
193
- assert.ok(fs.existsSync(f), `${id}.png`);
194
- const head = fs.readFileSync(f).subarray(0, 8);
195
- assert.deepEqual([...head], [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a], `${id}.png is a PNG`);
195
+ for (const [part, size] of [['', 64], ['-face', 32], ['-visor', 32]]) {
196
+ const f = path.join(HALL, 'characters', id + part + '.png');
197
+ assert.ok(fs.existsSync(f), `${id}${part}.png`);
198
+ const buf = fs.readFileSync(f);
199
+ assert.deepEqual([...buf.subarray(0, 8)], [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a], `${id}${part}.png is a PNG`);
200
+ assert.deepEqual([buf.readUInt32BE(16), buf.readUInt32BE(20)], [size, size], `${id}${part}.png is ${size} by ${size}`);
201
+ }
196
202
  }
197
203
  });
198
204
 
205
+ test('the hall\'s pictures are what the drawing script draws: nobody edited one by hand', async () => {
206
+ const { createRequire } = await import('node:module');
207
+ const draw = createRequire(import.meta.url)(path.join(ROOT, 'docs/design/characters/v2/draw-characters.js'));
208
+ const drawn = draw.hallFiles();
209
+ const onDisk = fs.readdirSync(path.join(HALL, 'characters')).sort();
210
+ assert.deepEqual(onDisk, Object.keys(drawn).sort(), 'nine files, three per character, and nothing else');
211
+ // Compared pixel for pixel, not byte for byte: two zlib builds may pack the same picture differently.
212
+ const zlib = await import('node:zlib');
213
+ const pixels = (png) => {
214
+ const parts = [];
215
+ for (let at = 8; at < png.length;) {
216
+ const len = png.readUInt32BE(at), type = png.toString('latin1', at + 4, at + 8);
217
+ if (type === 'IDAT') parts.push(png.subarray(at + 8, at + 8 + len));
218
+ at += 12 + len;
219
+ }
220
+ return zlib.inflateSync(Buffer.concat(parts));
221
+ };
222
+ for (const [name, buf] of Object.entries(drawn)) {
223
+ assert.ok(pixels(buf).equals(pixels(fs.readFileSync(path.join(HALL, 'characters', name)))), name + ' is stale or hand-edited: run node docs/design/characters/v2/draw-characters.js --hall');
224
+ }
225
+ });
226
+
227
+ test('the three characters\' colours are mixed from the hall\'s tokens, never typed in (ADR 0405)', () => {
228
+ const css = fs.readFileSync(path.join(HALL, 'style.css'), 'utf8');
229
+ const block = css.slice(css.indexOf('--char-artisan:'), css.indexOf('--char-patron:') + 120);
230
+ assert.match(block, /--char-artisan: var\(--accent\);/);
231
+ assert.match(block, /--char-artist: color-mix\(/);
232
+ assert.match(block, /--char-patron: color-mix\(/);
233
+ assert.ok(!/#[0-9a-fA-F]{3,8}/.test(block), 'no literal colour');
234
+ });
235
+
199
236
  // ---------- the server half ----------
200
237
  test('core_297 adds the account column, and the CHECK names exactly the three characters', () => {
201
238
  const sql = fs.readFileSync(path.join(ROOT, 'migrations/core_297_builder_character.sql'), 'utf8');
@@ -179,7 +179,7 @@ const SCENE = `<script>
179
179
  cv.addEventListener('pointerup', () => { drag = null; });
180
180
  addEventListener('keydown', (e) => { if (e.key === 'Escape' && window.picked) { e.preventDefault(); window.picked = false; } });
181
181
  window.OTBCraftMode = { held: 'artisan', character() { return this.held; }, current() { return null; },
182
- me() { return { builder: { total_credits: 56021 } }; }, spriteSrc(id) { return '/builders/characters/' + id + '.png'; }, openTool(id) { window.toolOpened = id; } };
182
+ me() { return { builder: { total_credits: 56021 } }; }, spriteSrc(id) { return '/builders/characters/' + id + '.png'; }, portraitSrc(id) { return '/builders/characters/' + id + '-visor.png'; }, openTool(id) { window.toolOpened = id; } };
183
183
  window.__visorScene = { name: 'test', place: 'Test city', actions: [{ label: 'Back to centre', key: '0', run() { window.centred = true; } }], keys: [{ label: 'Look around', key: 'drag' }] };
184
184
  </script>`;
185
185
  // `helm` links the hall's real sheet (the token tier) instead of the stand-in colours.
@@ -240,7 +240,7 @@ test('in game mode the glass shows the place, the main action, the key hints, th
240
240
  // task 1005195: every other control is a quiet key hint. Actions kept its key and its sheet; nothing was dropped.
241
241
  assert.deepEqual(g.hints, ['TYour thinking', 'AActions', 'EscOptions']);
242
242
  assert.equal(g.arcs, 2, 'two arcs frame the glass');
243
- assert.equal(g.sprite, '/builders/characters/artisan.png');
243
+ assert.equal(g.sprite, '/builders/characters/artisan-visor.png', 'your picture on the glass is you wearing the visor (task 1005218)');
244
244
  assert.deepEqual(errors, []);
245
245
  await ctx.close();
246
246
  });
@@ -366,6 +366,32 @@ for (const [width, height] of [[1440, 900], [390, 844], [320, 640]]) {
366
366
  });
367
367
  }
368
368
 
369
+ // task 1005218 (ADR 0405): each character has one colour, and the glass wears the held one.
370
+ test('the glass wears the held character\'s colour: the Artisan\'s is the accent, the Artist\'s and the Patron\'s are their own', { skip }, async () => {
371
+ const { ctx, page } = await open({ helm: true });
372
+ const g = await page.evaluate(() => {
373
+ const probe = (v) => { const d = document.createElement('i'); d.style.color = v; document.body.appendChild(d); const c = getComputedStyle(d).color; d.remove(); return c; };
374
+ const out = { accent: probe('var(--accent)') };
375
+ for (const id of ['artisan', 'artist', 'patron']) {
376
+ window.OTBCraftMode.held = id; window.OTBVisor.render();
377
+ const cs = (s) => getComputedStyle(document.querySelector(s));
378
+ out[id] = { char: probe('var(--char-' + id + ')'), main: cs('.visor__main').backgroundColor, name: cs('.visor__name').color,
379
+ line: cs('.visor__line:not(.visor__line--under)').stroke, rim: cs('.visor__sprite').borderTopColor, src: document.querySelector('.visor__sprite').getAttribute('src') };
380
+ }
381
+ return out;
382
+ });
383
+ assert.equal(g.artisan.main, g.accent, 'an Artisan\'s glass is the accent it always was');
384
+ for (const id of ['artisan', 'artist', 'patron']) {
385
+ assert.equal(g[id].main, g[id].char, id + ': the one button is the character\'s colour');
386
+ assert.equal(g[id].name, g[id].char, id + ': so is the name');
387
+ assert.equal(g[id].line, g[id].char, id + ': and the frame\'s lines');
388
+ assert.equal(g[id].rim, g[id].char, id + ': and the rim of the picture');
389
+ assert.equal(g[id].src, '/builders/characters/' + id + '-visor.png');
390
+ }
391
+ assert.equal(new Set([g.artisan.char, g.artist.char, g.patron.char]).size, 3, 'three characters, three colours');
392
+ await ctx.close();
393
+ });
394
+
369
395
  test('the glass is the kit\'s: the cut accent button, ice hints with no box, the name voice, the inset focus ring', { skip }, async () => {
370
396
  const { ctx, page } = await open({ helm: true, width: 1440, height: 900 });
371
397
  const g = await page.evaluate(() => {