@datamoc/mw_games 0.26.1 → 0.30.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.
Files changed (77) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/README.md +2 -1
  3. package/api-surface.json +564 -0
  4. package/dist/ai/Suggest.d.ts +54 -0
  5. package/dist/ai/Suggest.d.ts.map +1 -0
  6. package/dist/ai/Suggest.js +48 -0
  7. package/dist/ai/Suggest.js.map +1 -0
  8. package/dist/ai/index.d.ts +2 -0
  9. package/dist/ai/index.d.ts.map +1 -1
  10. package/dist/ai/index.js +1 -0
  11. package/dist/ai/index.js.map +1 -1
  12. package/dist/core/Input.d.ts +7 -0
  13. package/dist/core/Input.d.ts.map +1 -1
  14. package/dist/core/Input.js +9 -0
  15. package/dist/core/Input.js.map +1 -1
  16. package/dist/core/Random.d.ts +32 -5
  17. package/dist/core/Random.d.ts.map +1 -1
  18. package/dist/core/Random.js.map +1 -1
  19. package/dist/core/Replay.d.ts +28 -4
  20. package/dist/core/Replay.d.ts.map +1 -1
  21. package/dist/core/Replay.js +38 -9
  22. package/dist/core/Replay.js.map +1 -1
  23. package/dist/core/ReplayFile.d.ts +197 -0
  24. package/dist/core/ReplayFile.d.ts.map +1 -0
  25. package/dist/core/ReplayFile.js +257 -0
  26. package/dist/core/ReplayFile.js.map +1 -0
  27. package/dist/core/index.d.ts +4 -2
  28. package/dist/core/index.d.ts.map +1 -1
  29. package/dist/core/index.js +2 -1
  30. package/dist/core/index.js.map +1 -1
  31. package/dist/mw_games.global.js +113 -113
  32. package/dist/simulation/Training.d.ts +77 -0
  33. package/dist/simulation/Training.d.ts.map +1 -1
  34. package/dist/simulation/Training.js +87 -0
  35. package/dist/simulation/Training.js.map +1 -1
  36. package/dist/simulation/index.d.ts +2 -2
  37. package/dist/simulation/index.d.ts.map +1 -1
  38. package/dist/simulation/index.js +1 -1
  39. package/dist/simulation/index.js.map +1 -1
  40. package/dist/two-d/render/Beam.d.ts +136 -0
  41. package/dist/two-d/render/Beam.d.ts.map +1 -0
  42. package/dist/two-d/render/Beam.js +203 -0
  43. package/dist/two-d/render/Beam.js.map +1 -0
  44. package/dist/two-d/render/Flights.d.ts +90 -0
  45. package/dist/two-d/render/Flights.d.ts.map +1 -0
  46. package/dist/two-d/render/Flights.js +135 -0
  47. package/dist/two-d/render/Flights.js.map +1 -0
  48. package/dist/two-d/render/Particles.d.ts +57 -0
  49. package/dist/two-d/render/Particles.d.ts.map +1 -1
  50. package/dist/two-d/render/Particles.js +68 -1
  51. package/dist/two-d/render/Particles.js.map +1 -1
  52. package/dist/two-d/render/index.d.ts +5 -1
  53. package/dist/two-d/render/index.d.ts.map +1 -1
  54. package/dist/two-d/render/index.js +2 -0
  55. package/dist/two-d/render/index.js.map +1 -1
  56. package/dist/two-d/ui/MessageLog.d.ts +28 -0
  57. package/dist/two-d/ui/MessageLog.d.ts.map +1 -1
  58. package/dist/two-d/ui/MessageLog.js +23 -6
  59. package/dist/two-d/ui/MessageLog.js.map +1 -1
  60. package/dist/two-d/ui/OnScreenKeyboard.d.ts +91 -0
  61. package/dist/two-d/ui/OnScreenKeyboard.d.ts.map +1 -0
  62. package/dist/two-d/ui/OnScreenKeyboard.js +109 -0
  63. package/dist/two-d/ui/OnScreenKeyboard.js.map +1 -0
  64. package/dist/two-d/ui/TextPrompt.d.ts +122 -0
  65. package/dist/two-d/ui/TextPrompt.d.ts.map +1 -0
  66. package/dist/two-d/ui/TextPrompt.js +236 -0
  67. package/dist/two-d/ui/TextPrompt.js.map +1 -0
  68. package/dist/two-d/ui/index.d.ts +6 -2
  69. package/dist/two-d/ui/index.d.ts.map +1 -1
  70. package/dist/two-d/ui/index.js +3 -1
  71. package/dist/two-d/ui/index.js.map +1 -1
  72. package/dist/version.d.ts +1 -1
  73. package/dist/version.js +1 -1
  74. package/package.json +1 -1
  75. package/sbom.cdx.json +6 -6
  76. package/tools/browser-driver.d.mts +30 -0
  77. package/tools/browser-driver.mjs +146 -3
@@ -4,17 +4,22 @@ import { theme } from './theme.js';
4
4
  /** the default per-level text colours; `info` and `highlight` follow the active theme */
5
5
  const LEVEL_COLORS = { positive: 0x55dd77, negative: 0xff5555, warning: 0xffb347 };
6
6
  /**
7
- * How many of the oldest entries to drop so the rest fit in `maxLines`. The budget is in
8
- * wrapped lines, not entries, so one long message costs the room of the several short ones
9
- * it is worth. The newest entry always stays, even when it alone is over budget.
7
+ * The newest `count` entries of a retained log, oldest first. Pure so the readback
8
+ * contract is testable without a renderer: a `count` of 0 (or negative) reads
9
+ * nothing, a `count past what is retained reads everything retained.
10
10
  *
11
11
  * @example
12
12
  * ```ts
13
- * import { linesToDrop } from '@datamoc/mw_games/two-d/ui';
13
+ * import { takeLastEntries } from '@datamoc/mw_games/two-d/ui';
14
14
  *
15
- * console.log(linesToDrop([1, 2, 2], 3)); // 2 - the two oldest go, leaving one 2-line entry
15
+ * console.log(takeLastEntries(['a', 'b', 'c'], 2)); // ['b', 'c']
16
16
  * ```
17
17
  */
18
+ export function takeLastEntries(entries, count) {
19
+ if (!(count > 0))
20
+ return [];
21
+ return entries.slice(Math.max(0, entries.length - Math.floor(count)));
22
+ }
18
23
  export function linesToDrop(lineCounts, maxLines) {
19
24
  let total = 0;
20
25
  for (const lines of lineCounts)
@@ -54,6 +59,18 @@ export class MessageLog extends Container {
54
59
  get contentHeight() {
55
60
  return this.blocks.reduce((height, block) => height + block.label.height, 0);
56
61
  }
62
+ /**
63
+ * The newest `count` entries as plain `{ text, level }` records, oldest first -
64
+ * the readback for automated checks and secondary displays, without scraping
65
+ * rendered children. Entries the wrapped-line budget already dropped are gone, so
66
+ * what "last" means is bounded by `entryCount`: a `count` past that returns
67
+ * everything retained, and a `count` of 0 (or negative) returns `[]`. The
68
+ * records are copies; mutating them cannot corrupt the log. (There is no
69
+ * `snapshot()` on this widget, so there is nothing further to include.)
70
+ */
71
+ lastEntries(count) {
72
+ return takeLastEntries(this.blocks.map((block) => ({ text: block.text, level: block.level })), count);
73
+ }
57
74
  add(text, level = 'info') {
58
75
  const label = new Label({
59
76
  text,
@@ -63,7 +80,7 @@ export class MessageLog extends Container {
63
80
  resolution: this.options.resolution,
64
81
  });
65
82
  this.addChild(label);
66
- this.blocks.push({ label, level });
83
+ this.blocks.push({ label, level, text });
67
84
  this.trim();
68
85
  this.layout();
69
86
  }
@@ -1 +1 @@
1
- {"version":3,"file":"MessageLog.js","sourceRoot":"","sources":["../../../src/two-d/ui/MessageLog.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"MessageLog.js","sourceRoot":"","sources":["../../../src/two-d/ui/MessageLog.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,91 @@
1
+ import { Container } from 'pixi.js';
2
+ import { type Action } from '../../core/Input.ts';
3
+ /** one key: what it is labelled, what it types, or which action it fires */
4
+ export interface KeyboardKey {
5
+ /** the key's own caption */
6
+ label: string;
7
+ /**
8
+ * What the key types through `core.Input`'s `onText`. Defaults to `label`, so an
9
+ * ordinary letter key is just `{ label: 'A' }`; a wider gap or an accented glyph
10
+ * can say `{ label: 'a`', text: 'à' }`.
11
+ */
12
+ text?: string;
13
+ /**
14
+ * The named action the key fires instead of typing - `backspace`/`delete`/`left`/
15
+ * `right`/`confirm`/`cancel`, the same vocabulary a physical key is bound to. A
16
+ * key with an action types nothing.
17
+ */
18
+ action?: Action;
19
+ /** row units wide; a space bar is wide, an ordinary key is 1. Default 1 */
20
+ span?: number;
21
+ }
22
+ /**
23
+ * Rows of keys as plain data - the shape a game authors, never code. Ragged rows are
24
+ * fine: each starts at the left edge and ends where it ends.
25
+ */
26
+ export interface KeyboardLayout {
27
+ /** the rows, top to bottom */
28
+ rows: KeyboardKey[][];
29
+ }
30
+ /**
31
+ * One shipped default: the uppercase alphabet over a digit row, then the editing tail
32
+ * (`Bksp`, caret moves, space, `OK`, `Esc`). A game with its own needs - digits only, a
33
+ * seed code's letters and hyphens - authors its own layout; the default exists so a
34
+ * first game does not start from a blank page.
35
+ */
36
+ export declare const UPPERCASE_KEYBOARD: KeyboardLayout;
37
+ /**
38
+ * What pressing a key does: a typing key feeds `core.Input`'s `onText`, an action key
39
+ * fires its action through `onAction` - the same two paths a physical key travels, so
40
+ * a prompt or menu cannot tell an on-screen tap from a keyboard press. Pure, so the
41
+ * dispatch semantics are testable without a renderer.
42
+ */
43
+ export declare function pressKey(key: KeyboardKey): void;
44
+ /** where one laid-out key sits, and which key it is */
45
+ export interface LaidOutKey {
46
+ key: KeyboardKey;
47
+ x: number;
48
+ y: number;
49
+ width: number;
50
+ height: number;
51
+ }
52
+ /**
53
+ * The rows-to-buttons arithmetic as a pure function: a row's span units share its width
54
+ * minus the gaps, wide keys take their units plus the gaps between them, and rows stack
55
+ * at a fixed height. The class below is the thin Button shell over this.
56
+ */
57
+ export declare function layoutKeys(layout: KeyboardLayout, options: {
58
+ width: number;
59
+ keyHeight: number;
60
+ gap: number;
61
+ }): LaidOutKey[];
62
+ /**
63
+ * An on-screen keyboard: rows of keys as data, rendered through the theme's own
64
+ * `Button`, each tap pressing its key through `pressKey` - `dispatchText` for a typing
65
+ * key, `dispatchAction` for an action key - so a `TextPrompt` (or any listener of
66
+ * `onText`/`onAction`) cannot tell the tap from a physical key press. Pointer-only
67
+ * devices get a way to type; nothing game-specific lives here.
68
+ *
69
+ * @example
70
+ * ```ts
71
+ * import { OnScreenKeyboard, UPPERCASE_KEYBOARD } from '@datamoc/mw_games/two-d/ui';
72
+ * import type { Container2D } from '@datamoc/mw_games/two-d/render';
73
+ *
74
+ * declare const layer: Container2D;
75
+ *
76
+ * const keyboard = new OnScreenKeyboard(UPPERCASE_KEYBOARD, { width: 320 });
77
+ * keyboard.x = 20;
78
+ * keyboard.y = 240;
79
+ * layer.addChild(keyboard);
80
+ * // taps type through core.Input, exactly like a physical key
81
+ * ```
82
+ */
83
+ export declare class OnScreenKeyboard extends Container {
84
+ private readonly laidOutHeight;
85
+ constructor(layout: KeyboardLayout, options: {
86
+ width: number;
87
+ });
88
+ /** the laid-out height, so a host window can size itself to the keyboard */
89
+ get contentHeight(): number;
90
+ }
91
+ //# sourceMappingURL=OnScreenKeyboard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OnScreenKeyboard.d.ts","sourceRoot":"","sources":["../../../src/two-d/ui/OnScreenKeyboard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEpC,OAAO,EAAgC,KAAK,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAIhF,4EAA4E;AAC5E,MAAM,WAAW,WAAW;IAC3B,4BAA4B;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2EAA2E;IAC3E,IAAI,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC9B,8BAA8B;IAC9B,IAAI,EAAE,WAAW,EAAE,EAAE,CAAC;CACtB;AAID;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,EAAE,cAehC,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,WAAW,GAAG,IAAI,CAG/C;AAED,uDAAuD;AACvD,MAAM,WAAW,UAAU;IAC1B,GAAG,EAAE,WAAW,CAAC;IACjB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CACf;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CACzB,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GACxD,UAAU,EAAE,CAkBd;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,gBAAiB,SAAQ,SAAS;IAC9C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IAEvC,YAAY,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,EAkB7D;IAED,4EAA4E;IAC5E,IAAI,aAAa,IAAI,MAAM,CAE1B;CACD"}
@@ -0,0 +1,109 @@
1
+ import { Container } from 'pixi.js';
2
+ import { dispatchAction, dispatchText } from '../../core/Input.js';
3
+ import { Button } from './Button.js';
4
+ import { theme } from './theme.js';
5
+ const letters = (word) => word.split('').map((label) => ({ label }));
6
+ /**
7
+ * One shipped default: the uppercase alphabet over a digit row, then the editing tail
8
+ * (`Bksp`, caret moves, space, `OK`, `Esc`). A game with its own needs - digits only, a
9
+ * seed code's letters and hyphens - authors its own layout; the default exists so a
10
+ * first game does not start from a blank page.
11
+ */
12
+ export const UPPERCASE_KEYBOARD = {
13
+ rows: [
14
+ letters('QWERTYUIOP'),
15
+ letters('ASDFGHJKL'),
16
+ letters('ZXCVBNM'),
17
+ letters('1234567890'),
18
+ [
19
+ { label: 'Bksp', action: 'backspace' },
20
+ { label: 'Left', action: 'left' },
21
+ { label: 'Right', action: 'right' },
22
+ { label: 'Space', text: ' ', span: 3 },
23
+ { label: 'OK', action: 'confirm' },
24
+ { label: 'Esc', action: 'cancel' },
25
+ ],
26
+ ],
27
+ };
28
+ /**
29
+ * What pressing a key does: a typing key feeds `core.Input`'s `onText`, an action key
30
+ * fires its action through `onAction` - the same two paths a physical key travels, so
31
+ * a prompt or menu cannot tell an on-screen tap from a keyboard press. Pure, so the
32
+ * dispatch semantics are testable without a renderer.
33
+ */
34
+ export function pressKey(key) {
35
+ if (key.action !== undefined)
36
+ dispatchAction(key.action);
37
+ else
38
+ dispatchText(key.text ?? key.label);
39
+ }
40
+ /**
41
+ * The rows-to-buttons arithmetic as a pure function: a row's span units share its width
42
+ * minus the gaps, wide keys take their units plus the gaps between them, and rows stack
43
+ * at a fixed height. The class below is the thin Button shell over this.
44
+ */
45
+ export function layoutKeys(layout, options) {
46
+ const laid = [];
47
+ let y = 0;
48
+ for (const row of layout.rows) {
49
+ const units = row.reduce((sum, key) => sum + (key.span ?? 1), 0);
50
+ //a row is span-units wide with a gap between every unit slot, so a span-S key
51
+ //covers S slots plus its internal gaps and the row consumes its width exactly
52
+ const unitWidth = (options.width - options.gap * (units - 1)) / units;
53
+ let x = 0;
54
+ for (const key of row) {
55
+ const span = key.span ?? 1;
56
+ const width = unitWidth * span + options.gap * (span - 1);
57
+ laid.push({ key, x, y, width, height: options.keyHeight });
58
+ x += width + options.gap;
59
+ }
60
+ y += options.keyHeight + options.gap;
61
+ }
62
+ return laid;
63
+ }
64
+ /**
65
+ * An on-screen keyboard: rows of keys as data, rendered through the theme's own
66
+ * `Button`, each tap pressing its key through `pressKey` - `dispatchText` for a typing
67
+ * key, `dispatchAction` for an action key - so a `TextPrompt` (or any listener of
68
+ * `onText`/`onAction`) cannot tell the tap from a physical key press. Pointer-only
69
+ * devices get a way to type; nothing game-specific lives here.
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * import { OnScreenKeyboard, UPPERCASE_KEYBOARD } from '@datamoc/mw_games/two-d/ui';
74
+ * import type { Container2D } from '@datamoc/mw_games/two-d/render';
75
+ *
76
+ * declare const layer: Container2D;
77
+ *
78
+ * const keyboard = new OnScreenKeyboard(UPPERCASE_KEYBOARD, { width: 320 });
79
+ * keyboard.x = 20;
80
+ * keyboard.y = 240;
81
+ * layer.addChild(keyboard);
82
+ * // taps type through core.Input, exactly like a physical key
83
+ * ```
84
+ */
85
+ export class OnScreenKeyboard extends Container {
86
+ constructor(layout, options) {
87
+ super();
88
+ const gap = theme().spacing;
89
+ const keyHeight = Math.max(theme().font.size * 2, 18);
90
+ let maxY = 0;
91
+ for (const laid of layoutKeys(layout, { width: options.width, keyHeight, gap })) {
92
+ const button = new Button({
93
+ width: laid.width,
94
+ height: laid.height,
95
+ text: laid.key.label,
96
+ onClick: () => pressKey(laid.key),
97
+ });
98
+ button.position.set(laid.x, laid.y);
99
+ this.addChild(button);
100
+ maxY = Math.max(maxY, laid.y + laid.height);
101
+ }
102
+ this.laidOutHeight = maxY;
103
+ }
104
+ /** the laid-out height, so a host window can size itself to the keyboard */
105
+ get contentHeight() {
106
+ return this.laidOutHeight;
107
+ }
108
+ }
109
+ //# sourceMappingURL=OnScreenKeyboard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OnScreenKeyboard.js","sourceRoot":"","sources":["../../../src/two-d/ui/OnScreenKeyboard.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,122 @@
1
+ import { type Action } from '../../core/Input.ts';
2
+ import { Window } from './Window.ts';
3
+ import { type ScreenReader } from './a11y.ts';
4
+ import { type KeyboardLayout } from './OnScreenKeyboard.ts';
5
+ export interface TextPromptOptions {
6
+ width: number;
7
+ height: number;
8
+ /** the window title; also announced by `Window` itself */
9
+ title?: string;
10
+ /** the one-line question above the field, announced politely on open */
11
+ message?: string;
12
+ /** the field's starting text; the caret starts after it */
13
+ initialValue?: string;
14
+ /** the most characters the field will hold; unlimited when omitted */
15
+ maxLength?: number;
16
+ /**
17
+ * Rows of keys rendered beneath the field for pointer-only devices, feeding the same
18
+ * `onText`/`onAction` paths a physical key travels. Omit on a keyboard-first platform.
19
+ */
20
+ keyboard?: KeyboardLayout;
21
+ /**
22
+ * Decides whether the current value may be confirmed: return an error message to
23
+ * keep the prompt open, or null (or '') when the value is fine. Runs only on
24
+ * `confirm`, so typing never nags; the error clears on the next edit.
25
+ */
26
+ validate?: (value: string) => string | null;
27
+ /** called with the value, then the prompt closes */
28
+ onConfirm: (value: string) => void;
29
+ /** called on `cancel`, then the prompt closes; omit to just close */
30
+ onCancel?: () => void;
31
+ /** dim the world behind; true for a modal window, which this is */
32
+ dims?: boolean;
33
+ /**
34
+ * Whether a click under the prompt is swallowed rather than reaching the game
35
+ * behind it. On by default: a name not yet given is not something a stray click
36
+ * on the world should be able to act past.
37
+ */
38
+ blocker?: boolean;
39
+ /** where the stack puts the prompt */
40
+ anchor?: 'center' | 'bottom' | 'top';
41
+ /**
42
+ * Announce the message on open and a validation error on `confirm` through an
43
+ * `a11y.ScreenReader`. Default true; the calls are no-ops where there is no DOM.
44
+ */
45
+ announce?: boolean;
46
+ /** the reader announcements go to; the shared one by default, a fake in tests */
47
+ announcer?: Pick<ScreenReader, 'announce'>;
48
+ }
49
+ /**
50
+ * A modal single-line text field: a name entry, a save-slot label, a seed.
51
+ *
52
+ * Typing arrives through `core.Input`'s `onText`, which is the signal a free text field
53
+ * listens to, so this plays no part in key bindings for characters: whatever the
54
+ * platform turns a key press into lands in the field. Editing and movement arrive as
55
+ * actions instead, which a game binds the way it binds everything else: `left`/`right`
56
+ * move the caret, `backspace`/`delete` edit, `confirm` accepts subject to `validate`,
57
+ * `cancel` gives up. The prompt is modal and swallows each of those while open, so
58
+ * the game underneath never walks away mid-name.
59
+ *
60
+ * `onText` wakes the most recently added listener first, so when two prompts overlap
61
+ * the topmost one gets the keystrokes - the same order the window stack shows them in.
62
+ *
63
+ * An in-progress IME composition previews below the field and only becomes value on
64
+ * commit, so CJK, dead keys and emoji pickers type through the same path the DOM gives
65
+ * every other input rather than through a game-specific workaround.
66
+ *
67
+ * @example
68
+ * ```ts
69
+ * import { TextPrompt } from '@datamoc/mw_games/two-d/ui';
70
+ *
71
+ * const prompt = new TextPrompt({
72
+ * width: 320,
73
+ * height: 140,
74
+ * title: 'New hero',
75
+ * message: 'Name your hero (Enter keeps it)',
76
+ * maxLength: 12,
77
+ * validate: (value) => (value.trim() === '' ? 'Give them a name first.' : null),
78
+ * onConfirm: (value) => console.log('named', value),
79
+ * });
80
+ * ```
81
+ */
82
+ export declare class TextPrompt extends Window {
83
+ private readonly model;
84
+ private readonly validate?;
85
+ private readonly onConfirm;
86
+ private readonly onCancel?;
87
+ private readonly announce;
88
+ private readonly announcer;
89
+ private preview;
90
+ private promptError;
91
+ private readonly keyboard;
92
+ private messageLabel;
93
+ private valueLabel;
94
+ private previewLabel;
95
+ private errorLabel;
96
+ private caretBar;
97
+ private readonly measurer;
98
+ private readonly lineHeight;
99
+ private readonly valueY;
100
+ private canMeasureCaret;
101
+ private blinkOn;
102
+ private blinkElapsed;
103
+ private readonly promptThemeListener;
104
+ private readonly textListener;
105
+ private readonly compositionListener;
106
+ constructor(options: TextPromptOptions);
107
+ /** the field's current text */
108
+ get value(): string;
109
+ /** the caret's index into `value` */
110
+ get caretIndex(): number;
111
+ /** the last validation error, or null when the value currently passes */
112
+ get error(): string | null;
113
+ handleAction(action: Action): boolean;
114
+ update(dt: number): void;
115
+ destroy(options?: Parameters<Window['destroy']>[0]): void;
116
+ /** runs an edit, dropping a stale validation error only when the text changed */
117
+ private edit;
118
+ private confirm;
119
+ private restylePrompt;
120
+ private render;
121
+ }
122
+ //# sourceMappingURL=TextPrompt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TextPrompt.d.ts","sourceRoot":"","sources":["../../../src/two-d/ui/TextPrompt.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,236 @@
1
+ import { Graphics } from 'pixi.js';
2
+ import { onComposition, onText } from '../../core/Input.js';
3
+ import { TextModel } from './TextModel.js';
4
+ import { Window } from './Window.js';
5
+ import { Label } from './Label.js';
6
+ import { theme, themeChanged } from './theme.js';
7
+ import { screenReader } from './a11y.js';
8
+ import { OnScreenKeyboard } from './OnScreenKeyboard.js';
9
+ /** seconds per caret blink half-cycle */
10
+ const BLINK_PERIOD = 0.5;
11
+ /**
12
+ * A modal single-line text field: a name entry, a save-slot label, a seed.
13
+ *
14
+ * Typing arrives through `core.Input`'s `onText`, which is the signal a free text field
15
+ * listens to, so this plays no part in key bindings for characters: whatever the
16
+ * platform turns a key press into lands in the field. Editing and movement arrive as
17
+ * actions instead, which a game binds the way it binds everything else: `left`/`right`
18
+ * move the caret, `backspace`/`delete` edit, `confirm` accepts subject to `validate`,
19
+ * `cancel` gives up. The prompt is modal and swallows each of those while open, so
20
+ * the game underneath never walks away mid-name.
21
+ *
22
+ * `onText` wakes the most recently added listener first, so when two prompts overlap
23
+ * the topmost one gets the keystrokes - the same order the window stack shows them in.
24
+ *
25
+ * An in-progress IME composition previews below the field and only becomes value on
26
+ * commit, so CJK, dead keys and emoji pickers type through the same path the DOM gives
27
+ * every other input rather than through a game-specific workaround.
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * import { TextPrompt } from '@datamoc/mw_games/two-d/ui';
32
+ *
33
+ * const prompt = new TextPrompt({
34
+ * width: 320,
35
+ * height: 140,
36
+ * title: 'New hero',
37
+ * message: 'Name your hero (Enter keeps it)',
38
+ * maxLength: 12,
39
+ * validate: (value) => (value.trim() === '' ? 'Give them a name first.' : null),
40
+ * onConfirm: (value) => console.log('named', value),
41
+ * });
42
+ * ```
43
+ */
44
+ export class TextPrompt extends Window {
45
+ constructor(options) {
46
+ super({
47
+ width: options.width,
48
+ height: options.height,
49
+ title: options.title,
50
+ modal: true,
51
+ closable: false,
52
+ dims: options.dims,
53
+ blocker: options.blocker ?? true,
54
+ anchor: options.anchor ?? 'center',
55
+ });
56
+ this.preview = '';
57
+ this.promptError = null;
58
+ this.keyboard = null;
59
+ this.messageLabel = null;
60
+ this.caretBar = new Graphics();
61
+ this.measurer = new Label();
62
+ this.canMeasureCaret = typeof document !== 'undefined';
63
+ this.blinkOn = true;
64
+ this.blinkElapsed = 0;
65
+ this.promptThemeListener = () => this.restylePrompt();
66
+ this.textListener = (text) => {
67
+ if (this.closed)
68
+ return false;
69
+ this.edit(() => this.model.insert(text));
70
+ return true;
71
+ };
72
+ this.compositionListener = (input) => {
73
+ if (this.closed)
74
+ return false;
75
+ if (input.phase === 'end') {
76
+ this.edit(() => {
77
+ this.preview = '';
78
+ this.model.insert(input.text);
79
+ });
80
+ }
81
+ else {
82
+ this.preview = input.phase === 'start' ? '' : input.text;
83
+ this.render();
84
+ }
85
+ return true;
86
+ };
87
+ this.model = new TextModel({ value: options.initialValue ?? '', maxLength: options.maxLength });
88
+ this.validate = options.validate;
89
+ this.onConfirm = options.onConfirm;
90
+ this.onCancel = options.onCancel;
91
+ this.announce = options.announce ?? true;
92
+ this.announcer = options.announcer ?? screenReader;
93
+ const t = theme();
94
+ this.lineHeight = t.font.size * t.font.lineHeight;
95
+ const gap = t.spacing;
96
+ if (options.message !== undefined) {
97
+ this.messageLabel = new Label({ text: options.message });
98
+ this.content.addChild(this.messageLabel);
99
+ }
100
+ this.valueY = this.messageLabel ? this.lineHeight + gap : 0;
101
+ this.valueLabel = new Label();
102
+ this.valueLabel.y = this.valueY;
103
+ this.content.addChild(this.valueLabel);
104
+ this.previewLabel = new Label({ color: t.color.textDim });
105
+ this.previewLabel.y = this.valueY + this.lineHeight + gap;
106
+ this.content.addChild(this.previewLabel);
107
+ this.errorLabel = new Label({ color: t.color.textHighlight });
108
+ this.errorLabel.y = this.previewLabel.y + this.lineHeight + gap;
109
+ this.content.addChild(this.errorLabel);
110
+ this.content.addChild(this.caretBar);
111
+ //the touch half: rows of keys under the field, sized into the window
112
+ if (options.keyboard) {
113
+ this.keyboard = new OnScreenKeyboard(options.keyboard, { width: this.contentWidth });
114
+ this.keyboard.y = this.errorLabel.y + this.lineHeight + gap;
115
+ this.content.addChild(this.keyboard);
116
+ this.resize(options.width, options.height + this.keyboard.contentHeight + gap);
117
+ }
118
+ onText.add(this.textListener);
119
+ onComposition.add(this.compositionListener);
120
+ themeChanged.add(this.promptThemeListener);
121
+ this.render();
122
+ if (this.announce && options.message !== undefined)
123
+ this.announcer.announce(options.message);
124
+ }
125
+ /** the field's current text */
126
+ get value() {
127
+ return this.model.value;
128
+ }
129
+ /** the caret's index into `value` */
130
+ get caretIndex() {
131
+ return this.model.caret;
132
+ }
133
+ /** the last validation error, or null when the value currently passes */
134
+ get error() {
135
+ return this.promptError;
136
+ }
137
+ handleAction(action) {
138
+ if (this.delegate)
139
+ return super.handleAction(action);
140
+ if (this.closed)
141
+ return false;
142
+ switch (action) {
143
+ case 'left':
144
+ this.model.moveCaret(-1);
145
+ this.render();
146
+ return true;
147
+ case 'right':
148
+ this.model.moveCaret(1);
149
+ this.render();
150
+ return true;
151
+ case 'backspace':
152
+ this.edit(() => this.model.backspace());
153
+ return true;
154
+ case 'delete':
155
+ this.edit(() => this.model.deleteForward());
156
+ return true;
157
+ case 'confirm':
158
+ this.confirm();
159
+ return true;
160
+ case 'cancel':
161
+ this.onCancel?.();
162
+ this.close();
163
+ return true;
164
+ default:
165
+ return false;
166
+ }
167
+ }
168
+ update(dt) {
169
+ this.blinkElapsed += dt;
170
+ if (this.blinkElapsed < BLINK_PERIOD)
171
+ return;
172
+ this.blinkElapsed = 0;
173
+ this.blinkOn = !this.blinkOn;
174
+ this.caretBar.visible = this.blinkOn && this.canMeasureCaret;
175
+ }
176
+ destroy(options) {
177
+ onText.remove(this.textListener);
178
+ onComposition.remove(this.compositionListener);
179
+ themeChanged.remove(this.promptThemeListener);
180
+ this.measurer.destroy();
181
+ super.destroy(options);
182
+ }
183
+ /** runs an edit, dropping a stale validation error only when the text changed */
184
+ edit(run) {
185
+ const before = this.model.value;
186
+ run();
187
+ if (this.model.value !== before)
188
+ this.promptError = null;
189
+ this.render();
190
+ }
191
+ confirm() {
192
+ const problem = this.validate?.(this.model.value) ?? null;
193
+ if (problem) {
194
+ this.promptError = problem;
195
+ this.render();
196
+ if (this.announce)
197
+ this.announcer.announce(problem, { assertive: true });
198
+ return;
199
+ }
200
+ this.onConfirm(this.model.value);
201
+ this.close();
202
+ }
203
+ restylePrompt() {
204
+ const t = theme();
205
+ this.previewLabel.setColor(t.color.textDim);
206
+ this.errorLabel.setColor(t.color.textHighlight);
207
+ this.render();
208
+ }
209
+ render() {
210
+ this.valueLabel.setText(this.model.value);
211
+ //the caret sits after the text before it, measured through a label that is never
212
+ //added to the scene: reading any width needs a canvas, which `node --test` has
213
+ //not got, so where there is no document the position is unknowable and the caret
214
+ //stays hidden rather than guessing
215
+ this.measurer.setText(this.model.value.slice(0, this.model.caret));
216
+ let caretX = 0;
217
+ if (this.canMeasureCaret) {
218
+ try {
219
+ caretX = this.measurer.width;
220
+ }
221
+ catch {
222
+ this.canMeasureCaret = false;
223
+ }
224
+ }
225
+ this.caretBar
226
+ .clear()
227
+ .rect(this.valueLabel.x + caretX, this.valueY, 2, this.lineHeight)
228
+ .fill({ color: theme().color.text });
229
+ this.caretBar.visible = this.blinkOn && this.canMeasureCaret;
230
+ this.previewLabel.setText(this.preview);
231
+ this.previewLabel.visible = this.preview.length > 0;
232
+ this.errorLabel.setText(this.promptError ?? '');
233
+ this.errorLabel.visible = this.promptError !== null;
234
+ }
235
+ }
236
+ //# sourceMappingURL=TextPrompt.js.map