@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.
- package/LICENSE +21 -0
- package/README.md +296 -0
- package/dist/src/ahp/channels.d.ts +146 -0
- package/dist/src/ahp/channels.js +423 -0
- package/dist/src/ahp/connection.d.ts +656 -0
- package/dist/src/ahp/connection.js +1 -0
- package/dist/src/ahp/fake.d.ts +69 -0
- package/dist/src/ahp/fake.js +1996 -0
- package/dist/src/ahp/live.d.ts +160 -0
- package/dist/src/ahp/live.js +2641 -0
- package/dist/src/ahp/operate.d.ts +36 -0
- package/dist/src/ahp/operate.js +56 -0
- package/dist/src/ahp/publish.d.ts +39 -0
- package/dist/src/ahp/publish.js +370 -0
- package/dist/src/ahp/status.d.ts +23 -0
- package/dist/src/ahp/status.js +23 -0
- package/dist/src/ahp/types.d.ts +745 -0
- package/dist/src/ahp/types.js +27 -0
- package/dist/src/app.d.ts +54 -0
- package/dist/src/app.js +369 -0
- package/dist/src/blocks.d.ts +88 -0
- package/dist/src/blocks.js +102 -0
- package/dist/src/cli/main.d.ts +13 -0
- package/dist/src/cli/main.js +1537 -0
- package/dist/src/cli/render.d.ts +43 -0
- package/dist/src/cli/render.js +91 -0
- package/dist/src/config.d.ts +63 -0
- package/dist/src/config.js +45 -0
- package/dist/src/connect.d.ts +53 -0
- package/dist/src/connect.js +69 -0
- package/dist/src/control.d.ts +189 -0
- package/dist/src/control.js +2241 -0
- package/dist/src/diff.d.ts +44 -0
- package/dist/src/diff.js +111 -0
- package/dist/src/flags.d.ts +28 -0
- package/dist/src/flags.js +76 -0
- package/dist/src/main.d.ts +10 -0
- package/dist/src/main.js +54 -0
- package/dist/src/schedule.d.ts +46 -0
- package/dist/src/schedule.js +140 -0
- package/dist/src/screens.d.ts +79 -0
- package/dist/src/screens.js +1322 -0
- package/dist/src/state.d.ts +355 -0
- package/dist/src/state.js +531 -0
- package/dist/src/terminal.d.ts +44 -0
- package/dist/src/terminal.js +101 -0
- package/dist/src/tui.d.ts +92 -0
- package/dist/src/tui.js +372 -0
- package/dist/src/view/automations.d.ts +31 -0
- package/dist/src/view/automations.js +76 -0
- package/dist/src/view/bood/art.d.ts +90 -0
- package/dist/src/view/bood/art.js +152 -0
- package/dist/src/view/bood/beetle.d.ts +7 -0
- package/dist/src/view/bood/beetle.js +123 -0
- package/dist/src/view/bood/bunny.d.ts +8 -0
- package/dist/src/view/bood/bunny.js +228 -0
- package/dist/src/view/bood/cat.d.ts +9 -0
- package/dist/src/view/bood/cat.js +358 -0
- package/dist/src/view/bood/crab.d.ts +7 -0
- package/dist/src/view/bood/crab.js +121 -0
- package/dist/src/view/bood/index.d.ts +29 -0
- package/dist/src/view/bood/index.js +25 -0
- package/dist/src/view/bood/motion.d.ts +178 -0
- package/dist/src/view/bood/motion.js +298 -0
- package/dist/src/view/bood/owl.d.ts +9 -0
- package/dist/src/view/bood/owl.js +285 -0
- package/dist/src/view/bood/registry.d.ts +36 -0
- package/dist/src/view/bood/registry.js +218 -0
- package/dist/src/view/bood/render.d.ts +31 -0
- package/dist/src/view/bood/render.js +196 -0
- package/dist/src/view/bood/sprite.d.ts +18 -0
- package/dist/src/view/bood/sprite.js +130 -0
- package/dist/src/view/bood/sprout.d.ts +8 -0
- package/dist/src/view/bood/sprout.js +121 -0
- package/dist/src/view/bood/types.d.ts +158 -0
- package/dist/src/view/bood/types.js +57 -0
- package/dist/src/view/bubble.d.ts +75 -0
- package/dist/src/view/bubble.js +86 -0
- package/dist/src/view/changes.d.ts +35 -0
- package/dist/src/view/changes.js +22 -0
- package/dist/src/view/composer.d.ts +64 -0
- package/dist/src/view/composer.js +192 -0
- package/dist/src/view/controls.d.ts +44 -0
- package/dist/src/view/controls.js +49 -0
- package/dist/src/view/creature.d.ts +69 -0
- package/dist/src/view/creature.js +83 -0
- package/dist/src/view/customizations.d.ts +28 -0
- package/dist/src/view/customizations.js +53 -0
- package/dist/src/view/details.d.ts +65 -0
- package/dist/src/view/details.js +65 -0
- package/dist/src/view/filediff.d.ts +29 -0
- package/dist/src/view/filediff.js +24 -0
- package/dist/src/view/files.d.ts +31 -0
- package/dist/src/view/files.js +39 -0
- package/dist/src/view/hitl.d.ts +43 -0
- package/dist/src/view/hitl.js +171 -0
- package/dist/src/view/icons.d.ts +13 -0
- package/dist/src/view/icons.js +71 -0
- package/dist/src/view/picker.d.ts +42 -0
- package/dist/src/view/picker.js +71 -0
- package/dist/src/view/sessionhead.d.ts +41 -0
- package/dist/src/view/sessionhead.js +60 -0
- package/dist/src/view/sessions.d.ts +34 -0
- package/dist/src/view/sessions.js +61 -0
- package/dist/src/view/terminal.d.ts +30 -0
- package/dist/src/view/terminal.js +32 -0
- package/dist/src/view/toolcall.d.ts +27 -0
- package/dist/src/view/toolcall.js +48 -0
- package/dist/src/view/transcript.d.ts +50 -0
- package/dist/src/view/transcript.js +60 -0
- 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;
|