@softov/ahpc 0.1.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 (111) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +296 -0
  3. package/dist/src/ahp/channels.d.ts +146 -0
  4. package/dist/src/ahp/channels.js +423 -0
  5. package/dist/src/ahp/connection.d.ts +656 -0
  6. package/dist/src/ahp/connection.js +1 -0
  7. package/dist/src/ahp/fake.d.ts +69 -0
  8. package/dist/src/ahp/fake.js +1996 -0
  9. package/dist/src/ahp/live.d.ts +160 -0
  10. package/dist/src/ahp/live.js +2641 -0
  11. package/dist/src/ahp/operate.d.ts +36 -0
  12. package/dist/src/ahp/operate.js +56 -0
  13. package/dist/src/ahp/publish.d.ts +39 -0
  14. package/dist/src/ahp/publish.js +370 -0
  15. package/dist/src/ahp/status.d.ts +23 -0
  16. package/dist/src/ahp/status.js +23 -0
  17. package/dist/src/ahp/types.d.ts +745 -0
  18. package/dist/src/ahp/types.js +27 -0
  19. package/dist/src/app.d.ts +54 -0
  20. package/dist/src/app.js +369 -0
  21. package/dist/src/blocks.d.ts +88 -0
  22. package/dist/src/blocks.js +102 -0
  23. package/dist/src/cli/main.d.ts +13 -0
  24. package/dist/src/cli/main.js +1537 -0
  25. package/dist/src/cli/render.d.ts +43 -0
  26. package/dist/src/cli/render.js +91 -0
  27. package/dist/src/config.d.ts +63 -0
  28. package/dist/src/config.js +45 -0
  29. package/dist/src/connect.d.ts +53 -0
  30. package/dist/src/connect.js +69 -0
  31. package/dist/src/control.d.ts +189 -0
  32. package/dist/src/control.js +2241 -0
  33. package/dist/src/diff.d.ts +44 -0
  34. package/dist/src/diff.js +111 -0
  35. package/dist/src/flags.d.ts +28 -0
  36. package/dist/src/flags.js +76 -0
  37. package/dist/src/main.d.ts +10 -0
  38. package/dist/src/main.js +54 -0
  39. package/dist/src/schedule.d.ts +46 -0
  40. package/dist/src/schedule.js +140 -0
  41. package/dist/src/screens.d.ts +79 -0
  42. package/dist/src/screens.js +1322 -0
  43. package/dist/src/state.d.ts +355 -0
  44. package/dist/src/state.js +531 -0
  45. package/dist/src/terminal.d.ts +44 -0
  46. package/dist/src/terminal.js +101 -0
  47. package/dist/src/tui.d.ts +92 -0
  48. package/dist/src/tui.js +372 -0
  49. package/dist/src/view/automations.d.ts +31 -0
  50. package/dist/src/view/automations.js +76 -0
  51. package/dist/src/view/bood/art.d.ts +90 -0
  52. package/dist/src/view/bood/art.js +152 -0
  53. package/dist/src/view/bood/beetle.d.ts +7 -0
  54. package/dist/src/view/bood/beetle.js +123 -0
  55. package/dist/src/view/bood/bunny.d.ts +8 -0
  56. package/dist/src/view/bood/bunny.js +228 -0
  57. package/dist/src/view/bood/cat.d.ts +9 -0
  58. package/dist/src/view/bood/cat.js +358 -0
  59. package/dist/src/view/bood/crab.d.ts +7 -0
  60. package/dist/src/view/bood/crab.js +121 -0
  61. package/dist/src/view/bood/index.d.ts +29 -0
  62. package/dist/src/view/bood/index.js +25 -0
  63. package/dist/src/view/bood/motion.d.ts +178 -0
  64. package/dist/src/view/bood/motion.js +298 -0
  65. package/dist/src/view/bood/owl.d.ts +9 -0
  66. package/dist/src/view/bood/owl.js +285 -0
  67. package/dist/src/view/bood/registry.d.ts +36 -0
  68. package/dist/src/view/bood/registry.js +218 -0
  69. package/dist/src/view/bood/render.d.ts +31 -0
  70. package/dist/src/view/bood/render.js +196 -0
  71. package/dist/src/view/bood/sprite.d.ts +18 -0
  72. package/dist/src/view/bood/sprite.js +130 -0
  73. package/dist/src/view/bood/sprout.d.ts +8 -0
  74. package/dist/src/view/bood/sprout.js +121 -0
  75. package/dist/src/view/bood/types.d.ts +158 -0
  76. package/dist/src/view/bood/types.js +57 -0
  77. package/dist/src/view/bubble.d.ts +75 -0
  78. package/dist/src/view/bubble.js +86 -0
  79. package/dist/src/view/changes.d.ts +35 -0
  80. package/dist/src/view/changes.js +22 -0
  81. package/dist/src/view/composer.d.ts +64 -0
  82. package/dist/src/view/composer.js +192 -0
  83. package/dist/src/view/controls.d.ts +44 -0
  84. package/dist/src/view/controls.js +49 -0
  85. package/dist/src/view/creature.d.ts +69 -0
  86. package/dist/src/view/creature.js +83 -0
  87. package/dist/src/view/customizations.d.ts +28 -0
  88. package/dist/src/view/customizations.js +53 -0
  89. package/dist/src/view/details.d.ts +65 -0
  90. package/dist/src/view/details.js +65 -0
  91. package/dist/src/view/filediff.d.ts +29 -0
  92. package/dist/src/view/filediff.js +24 -0
  93. package/dist/src/view/files.d.ts +31 -0
  94. package/dist/src/view/files.js +39 -0
  95. package/dist/src/view/hitl.d.ts +43 -0
  96. package/dist/src/view/hitl.js +171 -0
  97. package/dist/src/view/icons.d.ts +13 -0
  98. package/dist/src/view/icons.js +71 -0
  99. package/dist/src/view/picker.d.ts +42 -0
  100. package/dist/src/view/picker.js +71 -0
  101. package/dist/src/view/sessionhead.d.ts +41 -0
  102. package/dist/src/view/sessionhead.js +60 -0
  103. package/dist/src/view/sessions.d.ts +34 -0
  104. package/dist/src/view/sessions.js +61 -0
  105. package/dist/src/view/terminal.d.ts +30 -0
  106. package/dist/src/view/terminal.js +32 -0
  107. package/dist/src/view/toolcall.d.ts +27 -0
  108. package/dist/src/view/toolcall.js +48 -0
  109. package/dist/src/view/transcript.d.ts +50 -0
  110. package/dist/src/view/transcript.js +60 -0
  111. package/package.json +72 -0
@@ -0,0 +1,196 @@
1
+ import { jsx as _jsx } from "@textui/core/jsx-runtime";
2
+ import { defineComponent, useFrame, useMeasure, useMemo, useRef, useState } from '@textui/core';
3
+ import { Column } from '@textui/widgets';
4
+ import { anchorOf, blit, fill, metric } from './art.js';
5
+ import { carry, createBody, faceOf, frameOf, grab, poseOf, release, slipped, stepBody, WORLD, } from './motion.js';
6
+ import { creatureFrames, creatureMotion, creatureNames, poseFrames } from './registry.js';
7
+ /**
8
+ * A mood per tone, rather than a colour per creature.
9
+ *
10
+ * Which creature you got says nothing; what it is doing is the thing worth
11
+ * reading from across the room. Semantic names, so a theme decides the colour
12
+ * and the figure is legible on paper as well as on a dark terminal.
13
+ *
14
+ * The two that also appear in the session list borrow that list's tones on
15
+ * purpose: `executing` is accent because that is what a running session is,
16
+ * and `error` is danger because that is what a failed one is. A mascot that
17
+ * disagreed with the status column about what red means would be worse than
18
+ * no mascot.
19
+ */
20
+ const TONE = {
21
+ happy: 'success',
22
+ sad: 'muted',
23
+ thinking: 'info',
24
+ executing: 'accent',
25
+ error: 'danger',
26
+ };
27
+ /**
28
+ * How fast the cycle turns.
29
+ *
30
+ * One rate for every form and every mood, with the holds written into the art
31
+ * instead - `blink` and `alternate` in `art.ts`. A component that chose its own
32
+ * rate per mood would put the timing in two places, and the drawing would lose.
33
+ */
34
+ const FPS = 2;
35
+ /**
36
+ * The second clock, for the half that moves.
37
+ *
38
+ * Twelve rather than two, because a body under gravity at two frames a second
39
+ * is a body teleporting; and twelve rather than thirty, because every tick is
40
+ * a repaint of the whole subtree and a mascot is not what the frame budget is
41
+ * for. Position is a float and rendering rounds, so what twelve costs is the
42
+ * top speed - past about a cell a tick the figure starts to skip.
43
+ */
44
+ const MOTION_FPS = 12;
45
+ /** How much of a stall it will catch up on, rather than leaping across the box. */
46
+ const MAX_CATCHUP = 4;
47
+ const clamp = (value, low, high) => (value < low ? low : value > high ? high : value);
48
+ export const Creature = defineComponent('Creature', (props) => {
49
+ const { name, mood = 'happy', form = 'draw', animated = true, tone, lively = false, fieldWidth = 48, fieldHeight = 10, ...rest } = props;
50
+ // Picked once and kept: a figure that changed on every keystroke would be
51
+ // a flicker rather than a mascot. `useMemo` with no deps is the mount.
52
+ const names = creatureNames();
53
+ const chosen = useMemo(() => name ?? names[Math.floor(Math.random() * names.length)], [name]);
54
+ // Unconditional, because it is a hook. What `animated` decides is whether
55
+ // the number is used, not whether the ticker exists - and a ticker that
56
+ // appeared and vanished with a prop would be a hook order bug the first
57
+ // time a mood turned animation off.
58
+ const frame = useFrame(FPS);
59
+ // Also unconditional, and also gated: a ticker is a standing invalidation,
60
+ // so the one that runs at twelve a second has to be off for every figure
61
+ // that is not moving rather than merely ignored.
62
+ const motion = creatureMotion(chosen);
63
+ const running = lively && motion !== undefined && animated;
64
+ const beat = useFrame(MOTION_FPS, { enabled: running });
65
+ const body = useRef(createBody());
66
+ const seen = useRef(0);
67
+ const placed = useRef(false);
68
+ // Where the field is on the terminal, so an absolute pointer can be asked
69
+ // which cell of the field it is over.
70
+ const rect = useMeasure();
71
+ const drag = useRef(null);
72
+ /**
73
+ * What tells the runtime the body moved.
74
+ *
75
+ * The body lives in a ref, because physics wants somewhere to accumulate
76
+ * that is not a render. The cost is that mutating it is invisible: a
77
+ * pointer that carries the creature across the screen changes nothing the
78
+ * runtime can see, so the component is never marked dirty and the frame on
79
+ * screen is the one from before the gesture. The ticker hides this for the
80
+ * walking, which arrives on its own state change - it is only the pointer,
81
+ * which moves the body between ticks, that has to say so.
82
+ */
83
+ const [, moved] = useState(0);
84
+ const repaint = () => moved((count) => count + 1);
85
+ if (running && motion) {
86
+ // Walls from the creature's widest drawing, settled at registration, so
87
+ // they hold still while the pose changes underneath them.
88
+ const world = {
89
+ ...WORLD,
90
+ floor: fieldHeight - 1,
91
+ left: motion.reach.left,
92
+ right: Math.max(motion.reach.left, fieldWidth - 1 - motion.reach.right),
93
+ };
94
+ // Once, and a flag rather than `now === 0`, which is only false after the
95
+ // first tick: until then every render put the body back where it started,
96
+ // so a pointer that carried it somewhere was undone by its own repaint.
97
+ if (!placed.current) {
98
+ placed.current = true;
99
+ body.current.y = world.floor;
100
+ body.current.x = Math.max(world.left, Math.min(world.right, Math.floor(fieldWidth / 3)));
101
+ }
102
+ // `useFrame` answers zero while animation is off, so a beat that went
103
+ // backwards is the runtime saying stop rather than a dropped frame.
104
+ const ticks = beat < seen.current ? 0 : Math.min(beat - seen.current, MAX_CATCHUP);
105
+ seen.current = beat;
106
+ for (let tick = 0; tick < ticks; tick += 1)
107
+ stepBody(body.current, motion, mood, world, 1 / MOTION_FPS);
108
+ // The body's clock runs while it is held, so a grip nothing has touched
109
+ // for a while is a gesture that ended where this handler could not see -
110
+ // a release outside the terminal, which is never reported at all.
111
+ if (drag.current && slipped(body.current, drag.current)) {
112
+ release(body.current);
113
+ drag.current = null;
114
+ }
115
+ const pose = poseOf(body.current, motion, mood);
116
+ const at = poseFrames(chosen, pose, body.current.facing, mood);
117
+ const shape = at ? metric(at[0]) : { width: 1, height: 1, anchor: 0 };
118
+ const anchor = at ? anchorOf(at) : 0;
119
+ /**
120
+ * The pointer, which is the one input this has that a terminal figure
121
+ * has never had.
122
+ *
123
+ * Returning `true` on the way down claims the gesture: every `drag` and
124
+ * the `up` that ends it come back here wherever the pointer has got to,
125
+ * which is what lets a creature be carried off the field and dropped
126
+ * somewhere else rather than being lost the moment it leaves its own box.
127
+ *
128
+ * Picking it up is not the same as calling it. A press on the figure
129
+ * carries it; a press anywhere else in the field is somewhere to go, and
130
+ * the brain is told rather than the body moved - so it walks over, in
131
+ * its own gait, and a bunny hops there.
132
+ */
133
+ const onMouse = (event) => {
134
+ const held = body.current;
135
+ const x = event.x - rect.x;
136
+ const y = event.y - rect.y;
137
+ if (event.action === 'down' && event.button === 'left') {
138
+ const over = x >= held.x - anchor - 1 && x <= held.x - anchor + shape.width
139
+ && y >= held.y - shape.height + 1 && y <= held.y + 1;
140
+ if (over) {
141
+ drag.current = grab(held, x, y);
142
+ repaint();
143
+ return true;
144
+ }
145
+ held.intent = { kind: 'goto', x: clamp(x, world.left, world.right), until: held.now + 10 };
146
+ held.sitting = false;
147
+ held.stillFor = 0;
148
+ // The same question the brain asks itself, asked of where the finger
149
+ // landed: far enough away, or up in the air, is worth taking off for.
150
+ held.wantsFlight = motion.flies
151
+ && (Math.abs(held.intent.x - held.x) > 14 || y < world.floor - 4);
152
+ repaint();
153
+ return true;
154
+ }
155
+ if (!drag.current)
156
+ return undefined;
157
+ // Only a press takes hold. A drag reaches this handler either because
158
+ // the press did - the runtime gives the rest of a gesture to whoever
159
+ // claimed it - or because the pointer passed over the figure during
160
+ // somebody else's unclaimed drag, and the second must not snatch the
161
+ // creature into a hand that was doing something else entirely.
162
+ if (event.action === 'drag') {
163
+ carry(held, drag.current, x, y, world, event.at);
164
+ repaint();
165
+ return true;
166
+ }
167
+ // A pointer moving with no button down is a pointer that was released
168
+ // while this handler was not being told.
169
+ if (event.action === 'move') {
170
+ release(held);
171
+ drag.current = null;
172
+ repaint();
173
+ return undefined;
174
+ }
175
+ if (event.action === 'up') {
176
+ release(held, drag.current);
177
+ drag.current = null;
178
+ repaint();
179
+ return true;
180
+ }
181
+ return undefined;
182
+ };
183
+ const field = Array.from({ length: fieldHeight }, () => ' '.repeat(fieldWidth));
184
+ const painted = at
185
+ ? blit(field, fill(at[frameOf(pose, body.current, at.length)], faceOf(pose, body.current, motion, mood), motion.tells[mood]), body.current.x, body.current.y, anchor)
186
+ : field;
187
+ return (_jsx(Column, { align: "start", onMouse: onMouse, ...rest, children: painted.map((row, at2) => (_jsx("text", { content: row, fg: tone ?? TONE[mood] }, `${chosen}-live-${at2}`))) }));
188
+ }
189
+ const frames = creatureFrames(chosen, mood, form);
190
+ const rows = frames[(animated ? frame : 0) % frames.length];
191
+ // No `unicode` check, which is the point of the alphabet: there is no
192
+ // ASCII fallback to swap in because there is nothing here to fall back
193
+ // from. The figure that renders on a terminal that can draw anything is
194
+ // the same figure that renders on one that can draw nothing.
195
+ return (_jsx(Column, { align: "center", ...rest, children: rows.map((row, at) => (_jsx("text", { content: row, fg: tone ?? TONE[mood] }, `${chosen}-${at}`))) }));
196
+ });
@@ -0,0 +1,18 @@
1
+ import type { RenderOutput } from '@textui/core';
2
+ import type { Mood } from './types.js';
3
+ export interface BoodSpriteProps {
4
+ /** Which creature. One is chosen per run and kept in the store. */
5
+ name?: string;
6
+ mood?: Mood;
7
+ /**
8
+ * The row to stand on, when something on the screen has said which.
9
+ *
10
+ * The composer says so, and so does the block that appears when a tool asks
11
+ * for permission. Left out - a screen with nothing at the bottom - the floor
12
+ * is the bottom of the terminal less `inset`.
13
+ */
14
+ floor?: number;
15
+ /** Rows kept clear at the bottom of the terminal when nothing has said. */
16
+ inset?: number;
17
+ }
18
+ export declare const BoodSprite: (props: BoodSpriteProps) => RenderOutput;
@@ -0,0 +1,130 @@
1
+ import { jsx as _jsx } from "@textui/core/jsx-runtime";
2
+ import { defineComponent, useFrame, useRef, useSize, useState } from '@textui/core';
3
+ import { Column } from '@textui/widgets';
4
+ import { anchorOf, fill } from './art.js';
5
+ import { carry, createBody, faceOf, frameOf, grab, poseOf, release, slipped, stepBody, WORLD, } from './motion.js';
6
+ import { creatureMotion, poseFrames } from './registry.js';
7
+ /**
8
+ * The creature, on the whole application rather than on one screen of it.
9
+ *
10
+ * It lives on the `floating` layer, which is a plane over every screen: it is
11
+ * not remounted by navigating, so pressing escape moves the screen out from
12
+ * under it and leaves the creature where it was standing. A figure that only
13
+ * exists on the screen that drew it is a decoration; one that is still there
14
+ * when the screen changes is an inhabitant, and that is the whole difference.
15
+ *
16
+ * Nothing here draws a field. The wrapper is a box that states no background,
17
+ * and a box that states nothing paints nothing - so the only cells this writes
18
+ * are the ones the animal's own rows occupy. That is the constraint the whole
19
+ * design turns on: a terminal has no per-cell transparency, so a rectangle of
20
+ * padding would erase a rectangle of the application. A creature-shaped hole
21
+ * is the most a sprite can cost, and it is what this costs.
22
+ */
23
+ const MOTION_FPS = 12;
24
+ const MAX_CATCHUP = 4;
25
+ const clamp = (value, low, high) => (value < low ? low : value > high ? high : value);
26
+ export const BoodSprite = defineComponent('BoodSprite', (props) => {
27
+ const { name, mood = 'happy', floor, inset = 3 } = props;
28
+ const size = useSize();
29
+ const motion = name ? creatureMotion(name) : undefined;
30
+ const body = useRef(createBody());
31
+ const placed = useRef(false);
32
+ const seen = useRef(0);
33
+ const drag = useRef(null);
34
+ // The body is a ref, so moving it by hand changes nothing the runtime can
35
+ // see. The ticker covers the walking; the pointer has to say so itself.
36
+ const [, moved] = useState(0);
37
+ const beat = useFrame(MOTION_FPS, { enabled: motion !== undefined });
38
+ if (!motion || !name)
39
+ return null;
40
+ const world = {
41
+ ...WORLD,
42
+ floor: Math.max(1, Math.min(floor ?? Infinity, size.height - inset - 1)),
43
+ left: motion.reach.left,
44
+ right: Math.max(motion.reach.left, size.width - 1 - motion.reach.right),
45
+ };
46
+ if (!placed.current) {
47
+ placed.current = true;
48
+ body.current.y = world.floor;
49
+ body.current.x = clamp(Math.floor(size.width / 3), world.left, world.right);
50
+ }
51
+ // The floor is not the same height on every screen - a composer keeps a
52
+ // lot more of the bottom than a status bar - so moving between them moves
53
+ // the ground. Above it, the creature falls to the new one rather than
54
+ // standing on the height the last screen had.
55
+ body.current.y = Math.min(body.current.y, world.floor);
56
+ if (body.current.y < world.floor && body.current.grounded)
57
+ body.current.grounded = false;
58
+ const ticks = beat < seen.current ? 0 : Math.min(beat - seen.current, MAX_CATCHUP);
59
+ seen.current = beat;
60
+ for (let tick = 0; tick < ticks; tick += 1)
61
+ stepBody(body.current, motion, mood, world, 1 / MOTION_FPS);
62
+ // The clock runs while it is held, so a grip nothing has touched for a
63
+ // while is a gesture that ended somewhere this handler could not see.
64
+ if (drag.current && slipped(body.current, drag.current)) {
65
+ release(body.current);
66
+ drag.current = null;
67
+ }
68
+ const pose = poseOf(body.current, motion, mood);
69
+ const frames = poseFrames(name, pose, body.current.facing, mood);
70
+ if (!frames)
71
+ return null;
72
+ const raw = frames[frameOf(pose, body.current, frames.length)];
73
+ const anchor = anchorOf(frames);
74
+ const rows = fill(raw, faceOf(pose, body.current, motion, mood), motion.tells[mood]);
75
+ const left = Math.max(0, Math.round(body.current.x) - anchor);
76
+ const top = Math.max(0, Math.round(body.current.y) - rows.length + 1);
77
+ /**
78
+ * Only what is actually on the animal.
79
+ *
80
+ * There is no full-screen catcher: the node is the creature's own box, so
81
+ * the runtime's hit test has already decided this press was on the figure
82
+ * and every other press in the application never reaches here. An overlay
83
+ * that covered the terminal to listen would be one that broke every
84
+ * control it happened to be standing over.
85
+ */
86
+ const onMouse = (event) => {
87
+ const held = body.current;
88
+ if (event.action === 'down' && event.button === 'left') {
89
+ drag.current = grab(held, event.x, event.y);
90
+ moved((count) => count + 1);
91
+ return true;
92
+ }
93
+ if (!drag.current)
94
+ return false;
95
+ // Only a press takes hold. A drag reaches this handler either because
96
+ // the press did - the runtime gives the rest of a gesture to whoever
97
+ // claimed it - or because the pointer happened to pass over the figure
98
+ // during somebody else's unclaimed drag, and the second must not snatch
99
+ // the creature into a hand that was doing something else entirely.
100
+ if (event.action === 'drag') {
101
+ carry(held, drag.current, event.x, event.y, world, event.at);
102
+ moved((count) => count + 1);
103
+ return true;
104
+ }
105
+ // A pointer moving with no button down is a pointer that was released
106
+ // while this handler was not being told: the terminal reports the
107
+ // motion but never reported the release.
108
+ if (event.action === 'move') {
109
+ release(held);
110
+ drag.current = null;
111
+ moved((count) => count + 1);
112
+ return false;
113
+ }
114
+ if (event.action === 'up') {
115
+ release(held, drag.current);
116
+ drag.current = null;
117
+ moved((count) => count + 1);
118
+ return true;
119
+ }
120
+ return false;
121
+ };
122
+ // The layer is pinned at the top-left of the terminal and states no
123
+ // background, so the margin is the position and the only cells written are
124
+ // the ones the animal's own rows occupy.
125
+ return (_jsx(Column, { margin: [top, 0, 0, left], onMouse: onMouse, children: rows.map((row, at) => (_jsx("text", { content: row, fg: TONE[mood] }, `bood-${at}`))) }));
126
+ });
127
+ /** The same tones the still figure uses, so the two never disagree. */
128
+ const TONE = {
129
+ happy: 'success', sad: 'muted', thinking: 'info', executing: 'accent', error: 'danger',
130
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * A sprout. Not an animal, and the only one here with a face that is two eyes
3
+ * and no mouth - which is what lets the leaves do the talking.
4
+ *
5
+ * Leaves up and open, drooping when things are not going well, spread when
6
+ * they are going badly. The pot never changes; everything above it does.
7
+ */
8
+ export declare const sprout: import("./types.js").CreatureSpec;
@@ -0,0 +1,121 @@
1
+ import { alternate, art, blink } from './art.js';
2
+ import { defineCreature } from './types.js';
3
+ /**
4
+ * A sprout. Not an animal, and the only one here with a face that is two eyes
5
+ * and no mouth - which is what lets the leaves do the talking.
6
+ *
7
+ * Leaves up and open, drooping when things are not going well, spread when
8
+ * they are going badly. The pot never changes; everything above it does.
9
+ */
10
+ export const sprout = defineCreature({
11
+ name: 'sprout',
12
+ label: 'Sprout',
13
+ about: 'Newer than the rest of this, and growing',
14
+ draw: {
15
+ happy: blink(art `
16
+ \|/
17
+ _|_
18
+ (^ ^)
19
+ (\v/)
20
+ | |
21
+ `, art `
22
+ \|/
23
+ _|_
24
+ (- -)
25
+ (\V/)
26
+ | |
27
+ `),
28
+ sad: art `
29
+ ,|,
30
+ _|_
31
+ (T T)
32
+ (\v/)
33
+ | |
34
+ `,
35
+ thinking: alternate(art `
36
+ \|/ o
37
+ _|_
38
+ (o -)
39
+ ( v )
40
+ | |
41
+ `, art `
42
+ \|/ .
43
+ _|_
44
+ (o -)
45
+ ( V )
46
+ | |
47
+ `),
48
+ executing: [art `
49
+ \|/
50
+ _|_
51
+ (o o)
52
+ /( V )\
53
+ | |
54
+ `, art `
55
+ \|/
56
+ _|_
57
+ _(o -)_
58
+ ( v )
59
+ | |
60
+ `, art `
61
+ \|/
62
+ _|_
63
+ (- o)
64
+ /( V )\
65
+ | |
66
+ `],
67
+ error: art `
68
+ /|\
69
+ _|_
70
+ (> <)
71
+ (/W\)
72
+ | |
73
+ `,
74
+ },
75
+ block: {
76
+ happy: blink(art `
77
+ \|/
78
+ (^ ^)
79
+ |V|
80
+ `, art `
81
+ \|/
82
+ (- -)
83
+ |V|
84
+ `),
85
+ sad: art `
86
+ ,|,
87
+ (T T)
88
+ |V|
89
+ `,
90
+ thinking: art `
91
+ \|/
92
+ (- o)
93
+ |V|
94
+ `,
95
+ executing: [art `
96
+ \|/
97
+ (o o)
98
+ |V|
99
+ `, art `
100
+ \|/
101
+ (o -)
102
+ |V|
103
+ `, art `
104
+ \|/
105
+ (- o)
106
+ |V|
107
+ `],
108
+ error: art `
109
+ /|\
110
+ (> <)
111
+ |V|
112
+ `,
113
+ },
114
+ inline: {
115
+ happy: blink(art `\^v^/`, art `\-,-/`),
116
+ sad: art `,TvT,`,
117
+ thinking: art `\-,o/`,
118
+ executing: [art `\o,o/`, art `\o,-/`, art `\-,o/`],
119
+ error: art `/>.</`,
120
+ },
121
+ });
@@ -0,0 +1,158 @@
1
+ /**
2
+ * What a creature is, before anyone draws one.
3
+ *
4
+ * A creature file writes art and nothing else - no measuring, no padding, no
5
+ * component. The registry does the arithmetic once, at registration, so that
6
+ * a drawing is checked the moment it joins the bood rather than the first
7
+ * time somebody renders it on a narrow terminal.
8
+ */
9
+ /** What the figure is doing. The one thing worth reading from across the room. */
10
+ export declare const MOODS: readonly ["happy", "sad", "thinking", "executing", "error"];
11
+ export type Mood = typeof MOODS[number];
12
+ /**
13
+ * How much room the figure gets.
14
+ *
15
+ * Three sizes rather than one scaled one, because art does not scale: a cat
16
+ * shrunk to five cells is a smudge, and a cat *redrawn* at five cells is a
17
+ * cat. Each form is drawn by hand and each one is allowed to look different.
18
+ */
19
+ export declare const FORMS: readonly ["draw", "block", "inline"];
20
+ export type Form = typeof FORMS[number];
21
+ /**
22
+ * The ceiling each form is held to.
23
+ *
24
+ * `block` and `inline` exist to be placed next to something else - a header, a
25
+ * status row, a list item - and a caller who has budgeted seven cells cannot
26
+ * have that budget decided by whichever creature came up. So the bound is the
27
+ * contract, and `registerCreature` refuses art that breaks it.
28
+ *
29
+ * `draw` has no width bound on purpose. The figures run from seven cells wide
30
+ * to seventeen and the outline *is* the animal; a creature that fits a
31
+ * template is a template wearing a hat.
32
+ */
33
+ export declare const BOUNDS: Record<Form, {
34
+ rows: number;
35
+ cols: number;
36
+ }>;
37
+ /**
38
+ * One still, as rows. `art` produces these.
39
+ *
40
+ * A cell is one of them, or several to cycle through. Frame zero is the
41
+ * still: it is what shows with animation off, on a terminal that has said no,
42
+ * and in a snapshot test - so it is the frame worth getting right.
43
+ */
44
+ export type Cell = string[] | string[][];
45
+ /**
46
+ * What the body is doing, as distinct from what the mood is.
47
+ *
48
+ * Nothing sets one of these. A pose is derived from the physics every tick -
49
+ * `poseOf` in `motion.ts` - because a body that is told what it is doing and a
50
+ * body that is falling are two authorities on one fact, and the visible half
51
+ * of that disagreement is a figure walking through the air.
52
+ */
53
+ export declare const POSES: readonly ["idle", "sit", "walk", "jump", "fall", "fly", "land", "held", "sleep", "alarm"];
54
+ export type Pose = typeof POSES[number];
55
+ /**
56
+ * Which way it is pointed, which is latched rather than read.
57
+ *
58
+ * `front` is the one a creature returns to: after a couple of seconds standing
59
+ * still it turns and looks at you. It is also the fallback, so a species that
60
+ * has no left view of a pose falls through to its front one and still reads
61
+ * right rather than drawing nothing.
62
+ */
63
+ export declare const FACINGS: readonly ["front", "right", "left"];
64
+ export type Facing = typeof FACINGS[number];
65
+ /** How motion art is keyed: a pose, or a pose at one facing. */
66
+ export type PoseKey = Pose | `${Pose}.${Facing}`;
67
+ /**
68
+ * The moving half of a creature, which is optional.
69
+ *
70
+ * A creature without this is drawn exactly as it always was; `lively` simply
71
+ * has nothing to run. That is the whole reason it is a separate block rather
72
+ * than more fields on the spec - three of the six here have side views drawn
73
+ * for them and three do not, and the three that do not are not broken.
74
+ *
75
+ * The art here is keyed on `pose`, never on mood, because mood arrives through
76
+ * the slots instead: a run of `%` is the face and a run of `#` is the tell -
77
+ * ears, tail, crest. The run is exactly as wide as the token that fills it, so
78
+ * substitution cannot move a column, and `registerCreature` checks that rather
79
+ * than trusting it. Without that seam the matrix is poses x facings x moods
80
+ * and nobody draws five hundred pictures of a rabbit.
81
+ */
82
+ export interface Motion {
83
+ /** `hop` cannot travel without leaving the ground. `walk` can. */
84
+ gait: 'walk' | 'hop';
85
+ /** Whether gravity can be suspended for this one while it has somewhere to be. */
86
+ flies: boolean;
87
+ sits: boolean;
88
+ /** How much of an impact comes back up. Zero for a cat, which does not bounce. */
89
+ bounce: number;
90
+ /** Gravity multiplier. Below one is a creature the air holds up. */
91
+ fallSpeed: number;
92
+ /** One gait hop, as a fraction of the standard impulse. Ignored by a walker. */
93
+ hop: number;
94
+ /** What a restful pose blinks with, whatever mood it is in. */
95
+ blink: string;
96
+ /** The mood token, three cells, dropped into every `%` run. */
97
+ faces: Record<Mood, string>;
98
+ /** The second carrier of the same meaning, dropped into every `#` run. */
99
+ tells: Record<Mood, string>;
100
+ poses: Partial<Record<PoseKey, Cell>>;
101
+ /**
102
+ * A mood that changes the shape rather than the expression.
103
+ *
104
+ * The escape hatch the face slot cannot cover: a cat that is working sits at
105
+ * a keyboard, which is a different animal outline and not a different face.
106
+ */
107
+ overrides?: Partial<Record<Mood, Partial<Record<PoseKey, Cell>>>>;
108
+ }
109
+ /** What a creature file exports: art, in three sizes, in five moods. */
110
+ export interface CreatureSpec {
111
+ /** The registry key. Lowercase, one word. */
112
+ name: string;
113
+ /** For a picker, or a roster. */
114
+ label: string;
115
+ /** One line about what it is, where a picker has room for one. */
116
+ about?: string;
117
+ draw: Record<Mood, Cell>;
118
+ block: Record<Mood, Cell>;
119
+ inline: Record<Mood, Cell>;
120
+ /** Present on the creatures that have been drawn moving. */
121
+ motion?: Motion;
122
+ }
123
+ /** Motion as the registry keeps it: the same art, framed and slot-checked. */
124
+ export interface RegisteredMotion extends Omit<Motion, 'poses' | 'overrides'> {
125
+ poses: Partial<Record<PoseKey, string[][]>>;
126
+ overrides: Partial<Record<Mood, Partial<Record<PoseKey, string[][]>>>>;
127
+ /**
128
+ * How far the widest drawing reaches either side of the anchor.
129
+ *
130
+ * The walls are set from this rather than from whichever frame is up, so
131
+ * they do not move. A bound taken from the current pose shrinks the moment
132
+ * a cat turns side-on and doubles in width, and the creature - already
133
+ * standing legally where it was - is shoved several cells inward on the
134
+ * next tick. Constant walls cost it half a body length at each end and
135
+ * nothing else.
136
+ */
137
+ reach: {
138
+ left: number;
139
+ right: number;
140
+ };
141
+ }
142
+ /** What the registry hands back: the same art, measured and squared off. */
143
+ export interface Creature {
144
+ name: string;
145
+ label: string;
146
+ about: string;
147
+ /** form to mood to frames to rows. Every row of a form is one width. */
148
+ art: Record<Form, Record<Mood, string[][]>>;
149
+ /** What a form costs, whatever mood it is in. Settled at registration. */
150
+ size: Record<Form, {
151
+ width: number;
152
+ height: number;
153
+ }>;
154
+ /** Undefined on a creature nobody has drawn a walk cycle for. */
155
+ motion?: RegisteredMotion;
156
+ }
157
+ /** Identity, for the type inference. A creature file is data, and stays data. */
158
+ export declare const defineCreature: (spec: CreatureSpec) => CreatureSpec;