@7ots/cli 0.1.4 → 0.1.5
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/cli/pet/electron/main.cjs +186 -35
- package/cli/pet/pet.html +87 -7
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Desktop mode: the pet page in a transparent, frameless, always-on-top window that strolls
|
|
3
|
-
* along the bottom of the screen. Launched by `7ots pet --mode desktop` with
|
|
3
|
+
* along the bottom of the screen. It resizes to what is in use and lets clicks through its empty parts. Launched by `7ots pet --mode desktop` with
|
|
4
4
|
* ~/.7ots/electron/…/electron cli/pet/electron/main.cjs (installed once by the CLI)
|
|
5
5
|
* and SEVENOTS_PET_URL / SEVENOTS_PET_TOKEN in the environment.
|
|
6
6
|
* Menu: right click on the pet, or the tray icon (Show / Hide / Talk / care / Settings / Quit).
|
|
@@ -16,8 +16,8 @@ const OPAQUE = process.env.SEVENOTS_PET_OPAQUE === '1';
|
|
|
16
16
|
const VIEW3D = process.env.SEVENOTS_PET_3D === '1';
|
|
17
17
|
const LANG = String(process.env.SEVENOTS_PET_LANG || 'en').slice(0, 2);
|
|
18
18
|
const ICON = path.join(__dirname, '..', '..', '..', 'brand', 'icon-512.png');
|
|
19
|
-
const
|
|
20
|
-
const
|
|
19
|
+
const BIG = { w: 240, h: 270 }; // with room for the bubble, the buttons and the talk box
|
|
20
|
+
const SMALL = { w: 160, h: 164 }; // just the ot
|
|
21
21
|
const linux = process.platform === 'linux';
|
|
22
22
|
|
|
23
23
|
const L = {
|
|
@@ -41,8 +41,8 @@ app.whenReady().then(() => setTimeout(start, linux ? 400 : 0)); // Linux: the co
|
|
|
41
41
|
|
|
42
42
|
function start() {
|
|
43
43
|
const area = screen.getPrimaryDisplay().workArea;
|
|
44
|
-
const
|
|
45
|
-
|
|
44
|
+
const right = area.x + area.width;
|
|
45
|
+
const floor = area.y + area.height;
|
|
46
46
|
|
|
47
47
|
// Tray first: if there is one, the window can stay out of the taskbar; if not, it shows there so it can be found.
|
|
48
48
|
let tray = null;
|
|
@@ -54,14 +54,18 @@ function start() {
|
|
|
54
54
|
}
|
|
55
55
|
} catch {}
|
|
56
56
|
|
|
57
|
+
// The ot lives at an anchor (bottom-center of the window). The window is sized to what is in use:
|
|
58
|
+
// SMALL is just the body; BIG (room for the bubble, the buttons and the talk box) only while needed.
|
|
59
|
+
const homeAnchor = () => ({ x: right - BIG.w / 2 - 24, y: floor });
|
|
60
|
+
const pos = homeAnchor();
|
|
61
|
+
let size = BIG; // it says hi on start
|
|
62
|
+
const bounds = (s = size, a = pos, lift = 0) => ({ x: Math.round(a.x - s.w / 2), y: Math.round(a.y - s.h - lift), width: s.w, height: s.h });
|
|
63
|
+
|
|
57
64
|
const win = new BrowserWindow({
|
|
58
|
-
|
|
59
|
-
height: H,
|
|
60
|
-
x,
|
|
61
|
-
y,
|
|
65
|
+
...bounds(),
|
|
62
66
|
transparent: !OPAQUE,
|
|
63
67
|
frame: false,
|
|
64
|
-
resizable:
|
|
68
|
+
resizable: true, // resized by code only; frameless, so no edges to grab
|
|
65
69
|
hasShadow: OPAQUE,
|
|
66
70
|
alwaysOnTop: true,
|
|
67
71
|
skipTaskbar: !!tray,
|
|
@@ -81,11 +85,20 @@ function start() {
|
|
|
81
85
|
return { action: 'deny' };
|
|
82
86
|
});
|
|
83
87
|
win.webContents.on('will-navigate', (e) => e.preventDefault());
|
|
88
|
+
const alive = () => !win.isDestroyed();
|
|
89
|
+
const page = (js) => alive() && win.webContents.executeJavaScript(js).catch(() => {});
|
|
84
90
|
|
|
85
91
|
let still = false;
|
|
92
|
+
let lastBounds = null;
|
|
93
|
+
const place = (lift = 0) => {
|
|
94
|
+
const b = bounds(size, pos, lift);
|
|
95
|
+
if (lastBounds && b.x === lastBounds.x && b.y === lastBounds.y && b.width === lastBounds.width && b.height === lastBounds.height) return;
|
|
96
|
+
lastBounds = b;
|
|
97
|
+
win.setBounds(b);
|
|
98
|
+
};
|
|
86
99
|
const show = () => {
|
|
87
|
-
|
|
88
|
-
|
|
100
|
+
Object.assign(pos, homeAnchor());
|
|
101
|
+
place();
|
|
89
102
|
win.showInactive();
|
|
90
103
|
win.moveTop();
|
|
91
104
|
};
|
|
@@ -93,7 +106,7 @@ function start() {
|
|
|
93
106
|
Menu.buildFromTemplate([
|
|
94
107
|
win.isVisible() ? { label: tr('hide'), click: () => win.hide() } : { label: tr('show'), click: show },
|
|
95
108
|
{ type: 'separator' },
|
|
96
|
-
{ label: tr('talk'), click: () => { show(); win.focus();
|
|
109
|
+
{ label: tr('talk'), click: () => { show(); win.focus(); page('postMessage({ talk: true })'); } },
|
|
97
110
|
{ label: tr('feed'), click: () => post('/event', { type: 'feed' }) },
|
|
98
111
|
{ label: tr('play'), click: () => post('/event', { type: 'play' }) },
|
|
99
112
|
{ label: tr('sleep'), click: () => post('/event', { type: 'sleep' }) },
|
|
@@ -112,37 +125,175 @@ function start() {
|
|
|
112
125
|
tray.on('click', () => (win.isVisible() ? win.hide() : show()));
|
|
113
126
|
}
|
|
114
127
|
|
|
115
|
-
//
|
|
116
|
-
let dir = -1;
|
|
117
|
-
let walking = false;
|
|
128
|
+
// The user dragged it: adopt the new spot as its anchor and let it rest there a while.
|
|
118
129
|
let dragged = 0;
|
|
119
|
-
let lastSet = x;
|
|
120
130
|
win.on('moved', () => {
|
|
121
|
-
if (
|
|
131
|
+
if (!alive() || !lastBounds) return;
|
|
132
|
+
const [wx, wy] = win.getPosition();
|
|
133
|
+
if (Math.abs(wx - lastBounds.x) < 3 && Math.abs(wy - lastBounds.y) < 3) return;
|
|
134
|
+
dragged = Date.now();
|
|
135
|
+
droppedSaid = false;
|
|
136
|
+
pos.x = wx + size.w / 2;
|
|
137
|
+
pos.y = wy + size.h;
|
|
138
|
+
lastBounds = { ...lastBounds, x: wx, y: wy };
|
|
139
|
+
plan = { mode: 'idle', until: Date.now() + 6000 };
|
|
122
140
|
});
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
141
|
+
|
|
142
|
+
// ── What the page is showing (polled): hit areas for click-through, and whether it needs the big window ──
|
|
143
|
+
let rects = [];
|
|
144
|
+
let busy = false; // bubble or talk box on screen
|
|
145
|
+
const HIT = `(() => {
|
|
146
|
+
const out = [];
|
|
147
|
+
for (const e of document.querySelectorAll('#stage, .actions, .ask')) {
|
|
148
|
+
const cs = getComputedStyle(e);
|
|
149
|
+
if (cs.display === 'none' || cs.visibility === 'hidden' || +cs.opacity < 0.3) continue;
|
|
150
|
+
const r = e.getBoundingClientRect();
|
|
151
|
+
if (r.width && r.height) out.push([r.left, r.top, r.right, r.bottom]);
|
|
152
|
+
}
|
|
153
|
+
return { rects: out, busy: document.body.classList.contains('talking') || !!document.querySelector('.bubble.on') };
|
|
154
|
+
})()`;
|
|
155
|
+
const hitTimer = setInterval(() => {
|
|
156
|
+
if (!alive()) return;
|
|
157
|
+
win.webContents.executeJavaScript(HIT).then((r) => ({ rects, busy } = r)).catch(() => {});
|
|
158
|
+
}, 250);
|
|
159
|
+
|
|
160
|
+
// Click-through: only the ot, its buttons and the talk box take the mouse; the transparent rest lets
|
|
161
|
+
// clicks reach the window behind. The cursor is polled because `forward: true` is macOS/Windows only.
|
|
162
|
+
let passing = null;
|
|
163
|
+
let hoverUntil = 0;
|
|
164
|
+
const pass = (p) => {
|
|
165
|
+
if (p === passing || !alive()) return;
|
|
166
|
+
passing = p;
|
|
167
|
+
win.setIgnoreMouseEvents(p, { forward: true });
|
|
168
|
+
};
|
|
169
|
+
let cursorNear = false;
|
|
170
|
+
let lastLook = null;
|
|
171
|
+
let droppedSaid = true;
|
|
172
|
+
const mouseTimer = setInterval(() => {
|
|
173
|
+
if (!alive() || !win.isVisible()) return;
|
|
174
|
+
const c = screen.getCursorScreenPoint();
|
|
175
|
+
const [wx, wy] = win.getPosition();
|
|
176
|
+
const x = c.x - wx;
|
|
177
|
+
const y = c.y - wy;
|
|
178
|
+
const over = rects.some(([l, t, r, b]) => x >= l - 4 && x <= r + 4 && y >= t - 4 && y <= b + 4);
|
|
179
|
+
if (over) hoverUntil = Date.now() + 900;
|
|
180
|
+
// the eyes follow the cursor anywhere on screen (the page only sees it inside the window)
|
|
181
|
+
if (!lastLook || Math.abs(x - lastLook[0]) + Math.abs(y - lastLook[1]) > 6) {
|
|
182
|
+
lastLook = [x, y];
|
|
183
|
+
page(`postMessage({ look: [${x}, ${y}] })`);
|
|
184
|
+
}
|
|
185
|
+
// dropped after a drag: a little dizzy
|
|
186
|
+
if (dragged && !droppedSaid && Date.now() - dragged > 350) {
|
|
187
|
+
droppedSaid = true;
|
|
188
|
+
page('postMessage({ dropped: true })');
|
|
189
|
+
}
|
|
190
|
+
cursorNear = Math.hypot(c.x - pos.x, c.y - (pos.y - SMALL.h / 2)) < 260;
|
|
191
|
+
pass(!(over || busy) && rects.length > 0);
|
|
192
|
+
// Size: big while it talks, you type, or the cursor is on it (its buttons show on hover).
|
|
193
|
+
const want = busy || Date.now() < hoverUntil ? BIG : SMALL;
|
|
194
|
+
if (want !== size && Date.now() - dragged > 400) {
|
|
195
|
+
size = want;
|
|
196
|
+
place();
|
|
197
|
+
}
|
|
198
|
+
}, 60);
|
|
199
|
+
|
|
200
|
+
// ── Movement: steering with ease-in/ease-out, a little gait bounce, hops, pauses, curiosity ──
|
|
201
|
+
const rnd = (a, b) => a + Math.random() * (b - a);
|
|
202
|
+
const BAND = 200; // how far above the taskbar it roams
|
|
203
|
+
const minX = area.x + BIG.w / 2;
|
|
204
|
+
const maxX = right - BIG.w / 2;
|
|
205
|
+
const clampY = (y) => Math.max(floor - BAND, Math.min(floor, y));
|
|
206
|
+
const vel = { x: 0, y: 0 };
|
|
207
|
+
let plan = { mode: 'idle', until: Date.now() + 2500 };
|
|
208
|
+
let gait = 0;
|
|
209
|
+
let facing = 0; // what the page last heard: -1..1 (3D yaw follows it smoothly)
|
|
210
|
+
let lastT = Date.now();
|
|
211
|
+
|
|
212
|
+
function next() {
|
|
213
|
+
const now = Date.now();
|
|
214
|
+
const r = Math.random();
|
|
215
|
+
if (r < 0.38) return { mode: 'idle', until: now + rnd(1800, 7000) };
|
|
216
|
+
if (r < 0.48) return { mode: 'hop', start: now, hops: 1 + Math.floor(Math.random() * 3), then: { mode: 'idle', until: now + 2500 } };
|
|
217
|
+
if (r < 0.6 && cursorNear) return { mode: 'follow', until: now + rnd(3000, 6000), speed: rnd(70, 110) };
|
|
218
|
+
// a stroll: sometimes short and lazy, sometimes across the screen
|
|
219
|
+
const far = Math.random() < 0.3;
|
|
220
|
+
const dx = (far ? rnd(300, 900) : rnd(60, 260)) * (Math.random() < 0.5 ? -1 : 1);
|
|
221
|
+
const tx = Math.max(minX, Math.min(maxX, pos.x + dx));
|
|
222
|
+
return { mode: 'go', tx, ty: clampY(pos.y + rnd(-60, 60)), speed: far ? rnd(110, 170) : rnd(45, 95) };
|
|
223
|
+
}
|
|
224
|
+
|
|
130
225
|
const stepTimer = setInterval(() => {
|
|
131
226
|
// isDestroyed first: once the window is gone, isVisible() throws ("Object has been destroyed").
|
|
132
|
-
if (!alive() || !
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
227
|
+
if (!alive() || !win.isVisible()) return;
|
|
228
|
+
const now = Date.now();
|
|
229
|
+
const dt = Math.min(0.05, (now - lastT) / 1000);
|
|
230
|
+
lastT = now;
|
|
231
|
+
if (still || busy || now - dragged < 6000 || now < hoverUntil) {
|
|
232
|
+
// stop gently
|
|
233
|
+
vel.x *= 0.8;
|
|
234
|
+
vel.y *= 0.8;
|
|
235
|
+
if (Math.abs(vel.x) < 1 && facing !== 0) tellFacing(0);
|
|
138
236
|
return;
|
|
139
237
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
238
|
+
|
|
239
|
+
let lift = 0;
|
|
240
|
+
let tx = pos.x;
|
|
241
|
+
let ty = pos.y;
|
|
242
|
+
let speed = 0;
|
|
243
|
+
if (plan.mode === 'idle' && now > plan.until) {
|
|
244
|
+
plan = next();
|
|
245
|
+
if (plan.mode === 'follow') page('postMessage({ follow: true })');
|
|
246
|
+
}
|
|
247
|
+
if (plan.mode === 'go') ({ tx, ty, speed } = plan);
|
|
248
|
+
else if (plan.mode === 'follow') {
|
|
249
|
+
const c = screen.getCursorScreenPoint();
|
|
250
|
+
// stays a polite distance from the cursor, on the floor band
|
|
251
|
+
tx = Math.max(minX, Math.min(maxX, c.x + (c.x > pos.x ? -90 : 90)));
|
|
252
|
+
ty = clampY(c.y + SMALL.h / 2);
|
|
253
|
+
speed = plan.speed;
|
|
254
|
+
if (now > plan.until) plan = { mode: 'idle', until: now + rnd(1500, 4000) };
|
|
255
|
+
} else if (plan.mode === 'hop') {
|
|
256
|
+
const t = (now - plan.start) / 480;
|
|
257
|
+
if (t >= plan.hops) plan = plan.then;
|
|
258
|
+
else if (Math.floor(t) !== plan.said) (plan.said = Math.floor(t), page('postMessage({ hop: true })'));
|
|
259
|
+
if (plan.mode === 'hop') lift = Math.sin(Math.PI * (t % 1)) * 34 * (1 - Math.floor(t) * 0.25);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// arrive: full speed far away, easing to a stop in the last 140 px; velocity eases too (no jerks)
|
|
263
|
+
const dx = tx - pos.x;
|
|
264
|
+
const dy = ty - pos.y;
|
|
265
|
+
const dist = Math.hypot(dx, dy);
|
|
266
|
+
const want = speed * Math.min(1, dist / 140);
|
|
267
|
+
const vx = dist > 0.5 ? (dx / dist) * want : 0;
|
|
268
|
+
const vy = dist > 0.5 ? (dy / dist) * want : 0;
|
|
269
|
+
const k = Math.min(1, dt * 2.6);
|
|
270
|
+
vel.x += (vx - vel.x) * k;
|
|
271
|
+
vel.y += (vy - vel.y) * k;
|
|
272
|
+
pos.x = Math.max(minX, Math.min(maxX, pos.x + vel.x * dt));
|
|
273
|
+
pos.y = clampY(pos.y + vel.y * dt);
|
|
274
|
+
if (plan.mode === 'go' && dist < 3 && Math.hypot(vel.x, vel.y) < 6) plan = { mode: 'idle', until: now + rnd(1500, 6000) };
|
|
275
|
+
|
|
276
|
+
// gait: a small bounce per step, scaled by speed
|
|
277
|
+
const v = Math.hypot(vel.x, vel.y);
|
|
278
|
+
gait += dt * (4 + v / 18);
|
|
279
|
+
if (v > 4) lift += Math.abs(Math.sin(gait * Math.PI)) * Math.min(5, v / 22);
|
|
280
|
+
|
|
281
|
+
// facing: turn toward where it walks, back to the front when it stops
|
|
282
|
+
const f = v > 8 ? Math.max(-1, Math.min(1, vel.x / 70)) : 0;
|
|
283
|
+
if (Math.abs(f - facing) > 0.15 || (f === 0 && facing !== 0)) tellFacing(Math.round(f * 10) / 10);
|
|
284
|
+
|
|
285
|
+
place(Math.round(lift));
|
|
286
|
+
}, 16);
|
|
287
|
+
|
|
288
|
+
function tellFacing(f) {
|
|
289
|
+
facing = f;
|
|
290
|
+
page(`postMessage({ walk: ${f} })`);
|
|
291
|
+
}
|
|
292
|
+
|
|
143
293
|
win.on('closed', () => {
|
|
144
|
-
clearInterval(walkTimer);
|
|
145
294
|
clearInterval(stepTimer);
|
|
295
|
+
clearInterval(hitTimer);
|
|
296
|
+
clearInterval(mouseTimer);
|
|
146
297
|
});
|
|
147
298
|
}
|
|
148
299
|
|
package/cli/pet/pet.html
CHANGED
|
@@ -246,7 +246,8 @@
|
|
|
246
246
|
}
|
|
247
247
|
ch ||= createCharacter($('#face'), spec, { locale: lang, title: id.name });
|
|
248
248
|
const is3d = !!ch.character;
|
|
249
|
-
|
|
249
|
+
let lookFree = 0; // while > now, the eyes wander on their own (a glance) instead of following the cursor
|
|
250
|
+
addEventListener('pointermove', (e) => Date.now() > lookFree && ch.lookAtPoint(e.clientX, e.clientY));
|
|
250
251
|
$('#face').addEventListener('click', () => ch.gesture('bounce'));
|
|
251
252
|
|
|
252
253
|
const post = (path, body) => fetch(path, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-7ots-Token': BOOT.token }, body: JSON.stringify(body) });
|
|
@@ -386,8 +387,35 @@
|
|
|
386
387
|
clearTimeout(hideT);
|
|
387
388
|
if (ms) hideT = setTimeout(() => bubble.classList.remove('on'), ms);
|
|
388
389
|
}
|
|
389
|
-
|
|
390
|
+
// What it says shows on its face too: a gesture/effect picked from the words (when the server sent none).
|
|
391
|
+
const REACT = [
|
|
392
|
+
[/(ja){2,}|(ha){2,}|jeje|lol|kkk|😂|🤣/i, 'laugh', null],
|
|
393
|
+
[/gracias|thank|obrigad|te quiero|love you|❤|💜|lindo|cute/i, 'bow', 'hearts'],
|
|
394
|
+
[/error|fail|fall[óo]|rompi|broke|oops|ups|ay no|uh.?oh/i, 'shake', 'sweat'],
|
|
395
|
+
[/listo|done|logrado|genial|great|bravo|yay|bien hecho|nice|perfect|✓|✅|🎉/i, 'celebrate', 'confetti'],
|
|
396
|
+
[/hambre|hungry|fome|comida|food|🍎/i, 'point', 'thought'],
|
|
397
|
+
[/sueño|tired|cansad|dormir|sleep|nap|zz/i, 'yawn', null],
|
|
398
|
+
[/miedo|scary|assust|fear|😱/i, 'shiver', 'sweat'],
|
|
399
|
+
[/enojad|angry|bravo con|grr|😠/i, 'stomp', 'anger'],
|
|
400
|
+
[/hola|hello|hi\b|hey|oi\b|buen(os|as) d[ií]as|good morning/i, 'wave', null],
|
|
401
|
+
[/[?¿]\s*$/, 'think', 'question'],
|
|
402
|
+
[/!\s*$/, 'bounce', 'exclaim'],
|
|
403
|
+
];
|
|
404
|
+
function react(text) {
|
|
405
|
+
for (const [re, g, fx] of REACT) {
|
|
406
|
+
if (!re.test(text)) continue;
|
|
407
|
+
if (g) ch.gesture(g);
|
|
408
|
+
if (fx) ch.effect?.(fx);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
ch.gesture(Math.random() < 0.5 ? 'nod' : 'wiggle');
|
|
412
|
+
}
|
|
413
|
+
function say(text, withFace = false) {
|
|
390
414
|
showBubble(document.createTextNode(text), Math.max(4500, text.length * 90));
|
|
415
|
+
talk(true, text); // the mouth moves with the words even without a voice; a real voice re-syncs it
|
|
416
|
+
clearTimeout(say.t);
|
|
417
|
+
say.t = setTimeout(() => talk(false), 500 + text.length * 60);
|
|
418
|
+
if (withFace) react(text);
|
|
391
419
|
speak(text);
|
|
392
420
|
}
|
|
393
421
|
function setThinking(on) {
|
|
@@ -416,7 +444,7 @@
|
|
|
416
444
|
if (m.gesture) ch.gesture(m.gesture);
|
|
417
445
|
if (m.morph) ch.morph?.(m.morph);
|
|
418
446
|
if (m.effect) ch.effect?.(m.effect);
|
|
419
|
-
if (m.say) say(m.say);
|
|
447
|
+
if (m.say) say(m.say, !m.gesture);
|
|
420
448
|
};
|
|
421
449
|
}
|
|
422
450
|
live.textContent = t('off');
|
|
@@ -446,12 +474,64 @@
|
|
|
446
474
|
});
|
|
447
475
|
addEventListener('message', (e) => e.data?.talk && talking(true));
|
|
448
476
|
|
|
449
|
-
// The desktop shell tells us
|
|
477
|
+
// The desktop shell tells us how we move: facing (walk -1..1), hops, being dropped, following, the cursor.
|
|
450
478
|
addEventListener('message', (e) => {
|
|
451
|
-
|
|
452
|
-
if (
|
|
453
|
-
|
|
479
|
+
const d = e.data || {};
|
|
480
|
+
if (d.walk != null) {
|
|
481
|
+
if (is3d) ch.character.yaw = -d.walk * 0.9; // turns toward where it walks, smoothly
|
|
482
|
+
else $('#stage').classList.toggle('flip', d.walk < 0);
|
|
483
|
+
}
|
|
484
|
+
if (d.hop) ch.gesture('bounce');
|
|
485
|
+
if (d.dropped) (ch.gesture('dizzy'), ch.effect?.('dizzy'));
|
|
486
|
+
if (d.follow) ch.effect?.(Math.random() < 0.6 ? 'hearts' : 'question');
|
|
487
|
+
if (d.look && Date.now() > lookFree) ch.lookAtPoint(d.look[0], d.look[1]); // the cursor anywhere on screen
|
|
454
488
|
});
|
|
489
|
+
|
|
490
|
+
// ── personality: small life between events (glances, fidgets, effects, shape changes) ──
|
|
491
|
+
const pick = (a) => a[Math.floor(Math.random() * a.length)];
|
|
492
|
+
const SHAPES = ['round', 'egg', 'pear', 'bean', 'squircle', 'drop', 'ghost', 'cat', 'cloud', 'blob', 'heart', 'mushroom', 'tall', 'wide'].filter((x) => x !== spec.shape);
|
|
493
|
+
const weighted = (list) => {
|
|
494
|
+
let r = Math.random() * list.reduce((a, x) => a + x[0], 0);
|
|
495
|
+
for (const x of list) if ((r -= x[0]) <= 0) return x[1];
|
|
496
|
+
};
|
|
497
|
+
function glance() {
|
|
498
|
+
lookFree = Date.now() + 1400 + Math.random() * 1200;
|
|
499
|
+
ch.lookAt((Math.random() * 2 - 1) * 0.8, (Math.random() * 2 - 1) * 0.5);
|
|
500
|
+
if (Math.random() < 0.4) ch.blink?.();
|
|
501
|
+
}
|
|
502
|
+
function fidget() {
|
|
503
|
+
const s = last || {};
|
|
504
|
+
const tired = s.energy < 35;
|
|
505
|
+
const bored = s.fun < 35;
|
|
506
|
+
const hungry = s.food < 35;
|
|
507
|
+
const happy = s.mood === 'happy';
|
|
508
|
+
weighted([
|
|
509
|
+
[5, glance],
|
|
510
|
+
[2, () => ch.gesture('wiggle')],
|
|
511
|
+
[1.5, () => (ch.gesture('think'), ch.effect?.('question'))],
|
|
512
|
+
[1, () => ch.gesture('shrug')],
|
|
513
|
+
[0.6, () => ch.gesture('hiccup')],
|
|
514
|
+
[0.5, () => (ch.gesture('sneeze'), ch.effect?.('sneeze'))],
|
|
515
|
+
[happy ? 2.5 : 0.6, () => (ch.gesture('dance'), ch.effect?.('notes'))],
|
|
516
|
+
[happy ? 0.8 : 0.2, () => (ch.gesture('celebrate'), ch.effect?.('sparkles'))],
|
|
517
|
+
[0.6, () => ch.gesture('spin')],
|
|
518
|
+
[0.5, () => ch.gesture('clap')],
|
|
519
|
+
[tired ? 3 : 0.4, () => ch.gesture('yawn')],
|
|
520
|
+
[bored ? 1.5 : 0.2, () => (ch.gesture('stomp'), ch.effect?.('steam'))],
|
|
521
|
+
[hungry ? 1.2 : 0, () => (ch.gesture('point'), ch.effect?.('thought'))],
|
|
522
|
+
[0.6, () => ch.effect?.('blush')],
|
|
523
|
+
[0.3, () => ch.gesture('shiver')],
|
|
524
|
+
[1.2, () => ch.morph?.(pick(['puff', 'squish', 'stretch', 'jelly']), { ms: 1600 + Math.random() * 1600 })],
|
|
525
|
+
[0.7, () => ch.morph?.(pick(['wings', 'horns', 'spikes', 'hands']), { ms: 2500 + Math.random() * 2500 })],
|
|
526
|
+
[0.8, () => ch.morph?.(`shape:${pick(SHAPES)}`, { ms: 2500 + Math.random() * 3000 })],
|
|
527
|
+
])?.();
|
|
528
|
+
}
|
|
529
|
+
(function life() {
|
|
530
|
+
setTimeout(() => {
|
|
531
|
+
if (!asleep && !thinking && !bubble.classList.contains('on') && !document.hidden) fidget();
|
|
532
|
+
life();
|
|
533
|
+
}, 4000 + Math.random() * 8000);
|
|
534
|
+
})();
|
|
455
535
|
</script>
|
|
456
536
|
</body>
|
|
457
537
|
</html>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@7ots/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "7ots.com — open-source embeddable web agent: it sees the page, speaks and acts for your visitor. Web Component + self-hosted Node proxy, Claude/OpenAI, 2D/3D avatar with voice, i18n, backoffice — plus a CLI with reproducible identities, a desktop pet and apuchat meet.",
|
|
5
5
|
"homepage": "https://7ots.com",
|
|
6
6
|
"repository": {
|