dsh-plugin-vrmpet3d 0.1.0 → 0.1.2
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/README.md +2 -0
- package/desktop/main.js +47 -4
- package/desktop/preload.js +7 -2
- package/lib/client.core.js +487 -53
- package/lib/client.js +391 -57
- package/lib/client.menu.js +14 -4
- package/lib/client.scene.js +134 -7
- package/lib/config-page.html +82 -45
- package/lib/config-page.js +1271 -252
- package/lib/core.js +487 -53
- package/lib/desktop-page.js +298 -44
- package/lib/desktop.js +18 -1
- package/lib/global-page.html +172 -101
- package/lib/index.js +196 -58
- package/lib/menu.js +14 -4
- package/lib/model-picker.js +139 -49
- package/lib/picker-page.html +34 -8
- package/lib/picker-page.js +192 -11
- package/lib/scene.js +134 -7
- package/lib/setup-page.html +68 -49
- package/lib/setup-page.js +395 -42
- package/lib/vrm-info.js +16 -1
- package/lib/whale.js +191 -0
- package/package.json +1 -1
package/lib/core.js
CHANGED
|
@@ -71,16 +71,32 @@
|
|
|
71
71
|
* The `{duration}` token and its machinery stay — a line the user writes may still carry it (see
|
|
72
72
|
* {@link STATUS_DURATION_TOKEN}) — but the shipped line is DSH's token-free 「深度求索中 ···」, which is also what the
|
|
73
73
|
* app shows while the latest turn is not open.
|
|
74
|
+
*
|
|
75
|
+
* ONE TABLE PER LANGUAGE, keyed state → language, the same shape `SPEECH_LINES` uses for its pools and for the
|
|
76
|
+
* same reason: a status line is TEXT THE USER READS, so an interface in English that still said 「任务达成」 would
|
|
77
|
+
* be a half-translated one. The two halves are written rather than translated word for word, and the Chinese is
|
|
78
|
+
* the operator's own wording, unchanged since 2026-10-13.
|
|
79
|
+
*
|
|
80
|
+
* An EDIT wins over both: `statusEntryFor` only reaches for these while the user's own box is empty, so a line
|
|
81
|
+
* typed by hand is one line in one language and stays exactly that, in either interface language. That is the
|
|
82
|
+
* whole reason this is a per-language DEFAULT rather than a second field in the settings.
|
|
74
83
|
*/
|
|
75
84
|
var DEFAULT_STATUS_TEXT = {
|
|
76
|
-
resting: '',
|
|
77
|
-
working: '深度求索中 ···',
|
|
78
|
-
waiting: '我有问题要问你',
|
|
79
|
-
done: '任务达成',
|
|
80
|
-
error: '遇到问题了...',
|
|
81
|
-
offline: '',
|
|
85
|
+
resting: { zh: '', en: '' },
|
|
86
|
+
working: { zh: '深度求索中 ···', en: 'Working ···' },
|
|
87
|
+
waiting: { zh: '我有问题要问你', en: 'I have a question for you' },
|
|
88
|
+
done: { zh: '任务达成', en: 'Task complete' },
|
|
89
|
+
error: { zh: '遇到问题了...', en: 'Something went wrong...' },
|
|
90
|
+
offline: { zh: '', en: '' },
|
|
82
91
|
};
|
|
83
92
|
|
|
93
|
+
/** One state's shipped line in one language; `''` for a state that says nothing by default. */
|
|
94
|
+
function defaultStatusText(state, language) {
|
|
95
|
+
var entry = DEFAULT_STATUS_TEXT[state];
|
|
96
|
+
if (!entry || typeof entry !== 'object') return '';
|
|
97
|
+
return typeof entry[language === 'en' ? 'en' : 'zh'] === 'string' ? entry[language === 'en' ? 'en' : 'zh'] : '';
|
|
98
|
+
}
|
|
99
|
+
|
|
84
100
|
/**
|
|
85
101
|
* The token a status line may carry where the live elapsed time goes, and the units it is written with.
|
|
86
102
|
*
|
|
@@ -96,6 +112,155 @@
|
|
|
96
112
|
en: { hour: 'h ', minute: 'm ', second: 's' },
|
|
97
113
|
};
|
|
98
114
|
|
|
115
|
+
/**
|
|
116
|
+
* The three answers to 「界面语言」, and the ONE rule that turns them into the language a surface draws in.
|
|
117
|
+
*
|
|
118
|
+
* `auto` is the shipped answer and means "whatever the surface is already running in": DSH's own locale inside
|
|
119
|
+
* the app, the browser's on a host-served page, and on a surface that can see neither (a suite, a headless
|
|
120
|
+
* host) English — the plugin's own fallback rather than a guess about the operator.
|
|
121
|
+
*
|
|
122
|
+
* The other two are the user overruling that, and they exist because the two questions are NOT the same one:
|
|
123
|
+
* DSH can be running in English while its owner wants the pet in Chinese, and the first-run guide is read once,
|
|
124
|
+
* before any of that has settled. Operator, 2026-10-15: 「把全部界面做中英双语自动识别,并在通用设置和初始安装
|
|
125
|
+
* 引导页的第一页欢迎页面中加上语言选择」.
|
|
126
|
+
*
|
|
127
|
+
* `auto` is STORED as `auto`, never resolved on write. A profile that stored the resolved 'zh' would freeze the
|
|
128
|
+
* language on the day it was created, and the user who later switched DSH to English would keep a Chinese pet
|
|
129
|
+
* with no way to tell that from a deliberate choice — the same mistake `DEFAULTS.petSurface` documents for
|
|
130
|
+
* 显示位置, and it is why this is a preference the CLEANER knows about rather than a value a page writes.
|
|
131
|
+
*/
|
|
132
|
+
var LANGUAGE_SETTINGS = ['auto', 'zh', 'en'];
|
|
133
|
+
|
|
134
|
+
/** `'auto'` for anything that is not one of the three — an unknown stored value is a reset, not an error. */
|
|
135
|
+
function cleanLanguage(value) {
|
|
136
|
+
return LANGUAGE_SETTINGS.indexOf(value) === -1 ? 'auto' : value;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Which of the two languages this surface draws in, given the user's setting and what the environment says.
|
|
141
|
+
*
|
|
142
|
+
* `hints` is a LIST, most authoritative first — DSH's locale, then the document's own `lang`, then
|
|
143
|
+
* `navigator.language` — and the FIRST hint that says anything decides. That order is the rule and not a detail:
|
|
144
|
+
* DSH is the application the pet lives inside, so its language outranks the browser's, and a page DSH has
|
|
145
|
+
* already marked `lang="zh"` outranks a browser whose own preference is en-US.
|
|
146
|
+
*
|
|
147
|
+
* The first hint decides the ZH-or-not question instead of the chain continuing past it, because this plugin has
|
|
148
|
+
* exactly two languages. A hint of `fr` is not a reason to go looking for a Chinese one further down the chain;
|
|
149
|
+
* it is a reason to fall back to English, which is also what `auto` with nothing to go on means.
|
|
150
|
+
*/
|
|
151
|
+
function resolveLanguage(preference, hints) {
|
|
152
|
+
var wanted = cleanLanguage(preference);
|
|
153
|
+
if (wanted !== 'auto') return wanted;
|
|
154
|
+
var list = Array.isArray(hints) ? hints : [hints];
|
|
155
|
+
for (var i = 0; i < list.length; i += 1) {
|
|
156
|
+
var hint = list[i];
|
|
157
|
+
if (typeof hint !== 'string' || !hint) continue;
|
|
158
|
+
return /^(zh|cn|chinese)/i.test(hint.trim()) ? 'zh' : 'en';
|
|
159
|
+
}
|
|
160
|
+
return 'en';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* The environment's own two hints, in the order they outrank each other — what a surface that has no DSH locale
|
|
165
|
+
* to ask uses. `scope` exists so a suite can hand in a fake window instead of moving the real one.
|
|
166
|
+
*
|
|
167
|
+
* The BROWSER comes first, and the document's own `lang` second, which is the opposite of the order the app's
|
|
168
|
+
* widget uses — deliberately. Every page this plugin serves ships `<html lang="zh">` (the plugin was written in
|
|
169
|
+
* Chinese), so a page that read its own markup first could never be auto-detected as English: the hint would be
|
|
170
|
+
* its own default rather than anything about the user. `navigator.language` is the browser's answer and in the
|
|
171
|
+
* floating window it is Electron's, i.e. the machine's.
|
|
172
|
+
*/
|
|
173
|
+
function browserLanguageHints(scope) {
|
|
174
|
+
var win = scope || (typeof globalThis !== 'undefined' ? globalThis : null);
|
|
175
|
+
var doc = win && win.document;
|
|
176
|
+
var nav = win && win.navigator;
|
|
177
|
+
return [
|
|
178
|
+
nav ? (nav.language || (nav.languages && nav.languages[0])) : null,
|
|
179
|
+
doc && doc.documentElement ? doc.documentElement.lang : null,
|
|
180
|
+
];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* One surface's own wording, in the two languages, chosen by {@link resolveLanguage}.
|
|
185
|
+
*
|
|
186
|
+
* Every surface keeps its OWN table — `lib/menu.js`'s `LABELS`, `lib/model-picker.js`'s `TEXT`, the settings
|
|
187
|
+
* pages' `TEXT` — rather than one global dictionary, because that is already this repository's shape (a surface
|
|
188
|
+
* owns the words it draws) and because a single table for six surfaces would make every one of them edit the
|
|
189
|
+
* same file. What IS shared is the SHAPE: `{ zh: {...}, en: {...} }`, this accessor, and {@link applyI18n}.
|
|
190
|
+
*
|
|
191
|
+
* A language half that is missing or empty falls back to the other one rather than to blank: a half-translated
|
|
192
|
+
* surface reads as Chinese in one spot, which is a bug somebody reports, where a blank label is a page that
|
|
193
|
+
* looks broken. `textOf` below is what makes a GAP visible, and a suite is what keeps one from shipping.
|
|
194
|
+
*/
|
|
195
|
+
function textFor(table, language) {
|
|
196
|
+
if (!table || typeof table !== 'object') return {};
|
|
197
|
+
var wanted = language === 'en' ? table.en : table.zh;
|
|
198
|
+
var other = language === 'en' ? table.zh : table.en;
|
|
199
|
+
if (wanted && typeof wanted === 'object') return wanted;
|
|
200
|
+
return other && typeof other === 'object' ? other : {};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** One entry out of a surface's table. A missing key returns the KEY, so a gap is visible on screen. */
|
|
204
|
+
function textOf(table, language, key) {
|
|
205
|
+
var value = textFor(table, language)[key];
|
|
206
|
+
return typeof value === 'string' ? value : key;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Fill a document's marked-up text from one surface's table.
|
|
211
|
+
*
|
|
212
|
+
* The markup carries the CHINESE text as well as the key, and that is on purpose: this plugin's pages all ship a
|
|
213
|
+
* bootstrap watchdog for the case where a script never ran, so a page whose script DID not run should still say
|
|
214
|
+
* something in one language rather than show nothing. `verify-desktop.mjs` asserts the two agree, which is what
|
|
215
|
+
* stops the markup and the table from drifting apart.
|
|
216
|
+
*
|
|
217
|
+
* Four attributes, one per place text lives: `data-i18n` writes `textContent`, `data-i18n-html` writes
|
|
218
|
+
* `innerHTML` and is for OUR OWN sentence with a `<strong>` in it (never for anything a user or a model wrote),
|
|
219
|
+
* and `data-i18n-placeholder` / `data-i18n-title` / `data-i18n-aria` write those three attributes.
|
|
220
|
+
*
|
|
221
|
+
* A key the table does not have leaves the markup ALONE rather than printing the key: the visible text stays the
|
|
222
|
+
* Chinese fallback, and the missing entry is the suite's business, not the user's.
|
|
223
|
+
*
|
|
224
|
+
* @returns {number} how many places were rewritten, so a suite can tell "applied nothing" from "no markup".
|
|
225
|
+
*/
|
|
226
|
+
function applyI18n(root, table, language) {
|
|
227
|
+
var doc = root && typeof root.querySelectorAll === 'function'
|
|
228
|
+
? root
|
|
229
|
+
: (typeof globalThis !== 'undefined' ? globalThis.document : null);
|
|
230
|
+
if (!doc || typeof doc.querySelectorAll !== 'function') return 0;
|
|
231
|
+
var words = textFor(table, language);
|
|
232
|
+
var selector = '[data-i18n],[data-i18n-html],[data-i18n-placeholder],[data-i18n-title],[data-i18n-aria]';
|
|
233
|
+
var nodes;
|
|
234
|
+
try {
|
|
235
|
+
nodes = doc.querySelectorAll(selector);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
return 0;
|
|
238
|
+
}
|
|
239
|
+
var applied = 0;
|
|
240
|
+
var write = function (node, attribute, key, html) {
|
|
241
|
+
var value = words[key];
|
|
242
|
+
if (typeof value !== 'string') return;
|
|
243
|
+
if (attribute === 'data-i18n') node.textContent = value;
|
|
244
|
+
else if (html) node.innerHTML = value;
|
|
245
|
+
else node.setAttribute(attribute, value);
|
|
246
|
+
applied += 1;
|
|
247
|
+
};
|
|
248
|
+
for (var i = 0; i < nodes.length; i += 1) {
|
|
249
|
+
var node = nodes[i];
|
|
250
|
+
var key = node.getAttribute('data-i18n');
|
|
251
|
+
if (key) write(node, 'data-i18n', key, false);
|
|
252
|
+
key = node.getAttribute('data-i18n-html');
|
|
253
|
+
if (key) write(node, 'data-i18n-html', key, true);
|
|
254
|
+
key = node.getAttribute('data-i18n-placeholder');
|
|
255
|
+
if (key) write(node, 'placeholder', key, false);
|
|
256
|
+
key = node.getAttribute('data-i18n-title');
|
|
257
|
+
if (key) write(node, 'title', key, false);
|
|
258
|
+
key = node.getAttribute('data-i18n-aria');
|
|
259
|
+
if (key) write(node, 'aria-label', key, false);
|
|
260
|
+
}
|
|
261
|
+
return applied;
|
|
262
|
+
}
|
|
263
|
+
|
|
99
264
|
/**
|
|
100
265
|
* The states whose prompt carries DSH's whale mark.
|
|
101
266
|
*
|
|
@@ -1014,9 +1179,8 @@
|
|
|
1014
1179
|
}
|
|
1015
1180
|
|
|
1016
1181
|
/**
|
|
1017
|
-
*
|
|
1018
|
-
*
|
|
1019
|
-
* same thing in the floating window (the screen's width) and in the app overlay (its window's width).
|
|
1182
|
+
* A positive setting overrides automatic distance. Both axes and both surfaces
|
|
1183
|
+
* pass the screen width here; automatic distance is half of that width.
|
|
1020
1184
|
*/
|
|
1021
1185
|
function resolveGazeSpan(span, fallback) {
|
|
1022
1186
|
var wanted = Number(span);
|
|
@@ -1024,7 +1188,7 @@
|
|
|
1024
1188
|
return clamp(wanted, GAZE_SPAN_RANGE.min, GAZE_SPAN_RANGE.max);
|
|
1025
1189
|
}
|
|
1026
1190
|
var base = Number(fallback);
|
|
1027
|
-
return isFiniteNumber(base) && base > 0 ? base : GAZE_SPAN.x;
|
|
1191
|
+
return isFiniteNumber(base) && base > 0 ? base / 2 : GAZE_SPAN.x;
|
|
1028
1192
|
}
|
|
1029
1193
|
|
|
1030
1194
|
/**
|
|
@@ -1046,7 +1210,9 @@
|
|
|
1046
1210
|
* Pure numbers, no DOM, so the logic suite covers the clamping and the signs.
|
|
1047
1211
|
*
|
|
1048
1212
|
* @param {{pointerX:number, pointerY:number, centreX:number, centreY:number,
|
|
1049
|
-
* spanX?:number, spanY?:number, curve?:number
|
|
1213
|
+
* spanX?:number, spanY?:number, curve?:number, curveY?:number,
|
|
1214
|
+
* curveRadial?:number, curveSpatial?:number, distance?:number,
|
|
1215
|
+
* mode?:'axes'|'radial'|'spatial'}} input
|
|
1050
1216
|
* @returns {{x:number, y:number}|null} `null` when the input is unusable; `y` is
|
|
1051
1217
|
* positive BELOW the pet, i.e. in screen coordinates
|
|
1052
1218
|
*/
|
|
@@ -1065,12 +1231,39 @@
|
|
|
1065
1231
|
: GAZE_SPAN.y;
|
|
1066
1232
|
// The settings' curve, unless the caller passes its own (the suite does, to pin a shape).
|
|
1067
1233
|
var edgeSlope = isFiniteNumber(Number(input.curve)) ? Number(input.curve) : DEFAULTS.gazeCurve;
|
|
1234
|
+
var slopeY = isFiniteNumber(Number(input.curveY)) ? Number(input.curveY) : edgeSlope;
|
|
1235
|
+
if (input.mode === 'spatial') {
|
|
1236
|
+
var spatialX = (pointerX - centreX) / spanX;
|
|
1237
|
+
var spatialY = (pointerY - centreY) / spanY;
|
|
1238
|
+
if (!isFiniteNumber(spatialX) || !isFiniteNumber(spatialY)) return null;
|
|
1239
|
+
var depth = isFiniteNumber(Number(input.distance)) && Number(input.distance) > 0
|
|
1240
|
+
? clamp(Number(input.distance), 0.1, 5) : DEFAULTS.gazeDistance;
|
|
1241
|
+
// A virtual target plane in span units: sideways distance reduces pitch
|
|
1242
|
+
// at a fixed pointer height. Curves shape angles, rather than pixel offsets.
|
|
1243
|
+
var yawAngle = Math.atan2(spatialX, depth) / (Math.PI / 2);
|
|
1244
|
+
var pitchAngle = Math.atan2(spatialY, Math.hypot(spatialX, depth)) / (Math.PI / 2);
|
|
1245
|
+
var spatialSlope = isFiniteNumber(Number(input.curveSpatial)) ? Number(input.curveSpatial) : DEFAULTS.gazeCurveSpatial;
|
|
1246
|
+
return {
|
|
1247
|
+
x: (yawAngle < 0 ? -1 : 1) * gazeCurve(Math.abs(yawAngle), spatialSlope),
|
|
1248
|
+
y: (pitchAngle < 0 ? -1 : 1) * gazeCurve(Math.abs(pitchAngle), spatialSlope),
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
if (input.mode === 'radial') {
|
|
1252
|
+
var dx = (pointerX - centreX) / spanX;
|
|
1253
|
+
var dy = (pointerY - centreY) / spanY;
|
|
1254
|
+
var distance = Math.hypot(dx, dy);
|
|
1255
|
+
if (!isFiniteNumber(distance)) return null;
|
|
1256
|
+
if (distance === 0) return { x: 0, y: 0 };
|
|
1257
|
+
var radialSlope = isFiniteNumber(Number(input.curveRadial)) ? Number(input.curveRadial) : edgeSlope;
|
|
1258
|
+
var gain = gazeCurve(Math.min(1, distance), radialSlope) / distance;
|
|
1259
|
+
return { x: dx * gain, y: dy * gain };
|
|
1260
|
+
}
|
|
1068
1261
|
var rawX = clamp((pointerX - centreX) / spanX, -1, 1);
|
|
1069
1262
|
var rawY = clamp((pointerY - centreY) / spanY, -1, 1);
|
|
1070
1263
|
// Mirrored through `gazeCurve` so the sign survives and the response is the curve on both sides.
|
|
1071
1264
|
return {
|
|
1072
1265
|
x: rawX < 0 ? -gazeCurve(-rawX, edgeSlope) : gazeCurve(rawX, edgeSlope),
|
|
1073
|
-
y: rawY < 0 ? -gazeCurve(-rawY,
|
|
1266
|
+
y: rawY < 0 ? -gazeCurve(-rawY, slopeY) : gazeCurve(rawY, slopeY)
|
|
1074
1267
|
};
|
|
1075
1268
|
}
|
|
1076
1269
|
|
|
@@ -1080,6 +1273,12 @@
|
|
|
1080
1273
|
/** Default pose and behaviour knobs. */
|
|
1081
1274
|
var DEFAULTS = {
|
|
1082
1275
|
scope: 'global',
|
|
1276
|
+
/**
|
|
1277
|
+
* 界面语言 — `auto` follows the surface's own environment ({@link resolveLanguage}), and 中文 / English overrule
|
|
1278
|
+
* it. It sits first in the table rather than beside the appearance rows because it is the one preference that
|
|
1279
|
+
* decides how every OTHER row is spelled.
|
|
1280
|
+
*/
|
|
1281
|
+
language: 'auto',
|
|
1083
1282
|
size: 100,
|
|
1084
1283
|
/**
|
|
1085
1284
|
* WHERE the pet is shown, as the cleaner's DEFAULT rather than a string repeated beside it.
|
|
@@ -1177,12 +1376,21 @@
|
|
|
1177
1376
|
* operator's own (2026-10-13: 「视线:曲率设置默认为0.1,提供0.05-1的选择(原先是0.1-1),静止停看默认为2秒」).
|
|
1178
1377
|
* `gazeCurve` is the curve's slope at the frame's edge — 1 is a straight line, smaller is a
|
|
1179
1378
|
* steeper start and a flatter tail — and `gazeSpanX`/`gazeSpanY` are the full-deflection
|
|
1180
|
-
* distances in CSS pixels, where **0 means AUTO**:
|
|
1181
|
-
*
|
|
1379
|
+
* distances in CSS pixels, where **0 means AUTO**: half the screen width on both axes.
|
|
1380
|
+
* `gazeCurve` / `gazeCurveY` remain readable for older delivered clients; the current
|
|
1381
|
+
* settings expose only spatial (default) and radial modes.
|
|
1182
1382
|
*/
|
|
1183
1383
|
gazeCurve: 0.1,
|
|
1384
|
+
gazeCurveMode: 'spatial',
|
|
1385
|
+
gazeCurveY: 0.1,
|
|
1386
|
+
gazeCurveRadial: 0.1,
|
|
1387
|
+
gazeCurveSpatial: 1,
|
|
1388
|
+
gazeDistance: 0.1,
|
|
1184
1389
|
gazeSpanX: 0,
|
|
1185
1390
|
gazeSpanY: 0,
|
|
1391
|
+
// Actual head pitch in degrees, after the renderer's gaze weight.
|
|
1392
|
+
gazePitchUp: 60,
|
|
1393
|
+
gazePitchDown: 60,
|
|
1186
1394
|
/**
|
|
1187
1395
|
* How long the cursor may sit still before the pet stops following it, in seconds.
|
|
1188
1396
|
*
|
|
@@ -1294,6 +1502,7 @@
|
|
|
1294
1502
|
* not silently pin drag and gravity to the numbers the panel happened to be showing.
|
|
1295
1503
|
*/
|
|
1296
1504
|
springs: {},
|
|
1505
|
+
toon: {},
|
|
1297
1506
|
/**
|
|
1298
1507
|
* Per-model size multiplier for BOTH texts: `{ [modelId]: number }`.
|
|
1299
1508
|
*
|
|
@@ -1925,6 +2134,13 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
1925
2134
|
var out = {};
|
|
1926
2135
|
var source = raw && typeof raw === 'object' ? raw : {};
|
|
1927
2136
|
out.scope = source.scope === 'current' ? 'current' : 'global';
|
|
2137
|
+
/**
|
|
2138
|
+
* 界面语言 — cleaned like every other enumerated preference: an unknown value (an older profile, a typo, a page
|
|
2139
|
+
* that sent the wrong thing) is `auto`, which resolves to the surface's own environment anyway. It is NOT
|
|
2140
|
+
* counted as authorship by `authoredWeight` over in `lib/settings.js`: a language is a way of looking at the
|
|
2141
|
+
* pet, not something the user wrote into it.
|
|
2142
|
+
*/
|
|
2143
|
+
out.language = cleanLanguage(source.language);
|
|
1928
2144
|
// An unknown surface falls back to the shipped default, not to a string repeated here.
|
|
1929
2145
|
out.petSurface = PET_SURFACES.indexOf(source.petSurface) === -1 ? DEFAULTS.petSurface : source.petSurface;
|
|
1930
2146
|
out.size = Number.isFinite(Number(source.size)) ? clamp(Number(source.size), SIZE_RANGE.min, SIZE_RANGE.max) : DEFAULTS.size;
|
|
@@ -2021,12 +2237,25 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
2021
2237
|
out.gazeCurve = Number.isFinite(Number(source.gazeCurve))
|
|
2022
2238
|
? clamp(Number(source.gazeCurve), GAZE_CURVE_RANGE.min, GAZE_CURVE_RANGE.max)
|
|
2023
2239
|
: DEFAULTS.gazeCurve;
|
|
2240
|
+
out.gazeCurveMode = source.gazeCurveMode === 'radial' ? 'radial' : 'spatial';
|
|
2241
|
+
['gazeCurveY', 'gazeCurveRadial'].forEach(function (key) {
|
|
2242
|
+
out[key] = typeof source[key] === 'number' && Number.isFinite(source[key])
|
|
2243
|
+
? clamp(source[key], GAZE_CURVE_RANGE.min, GAZE_CURVE_RANGE.max) : DEFAULTS[key];
|
|
2244
|
+
});
|
|
2245
|
+
out.gazeCurveSpatial = typeof source.gazeCurveSpatial === 'number' && Number.isFinite(source.gazeCurveSpatial)
|
|
2246
|
+
? clamp(source.gazeCurveSpatial, GAZE_CURVE_RANGE.min, GAZE_CURVE_RANGE.max) : DEFAULTS.gazeCurveSpatial;
|
|
2247
|
+
out.gazeDistance = typeof source.gazeDistance === 'number' && Number.isFinite(source.gazeDistance)
|
|
2248
|
+
? clamp(source.gazeDistance, 0.1, 5) : DEFAULTS.gazeDistance;
|
|
2024
2249
|
out.gazeSpanX = Number.isFinite(Number(source.gazeSpanX))
|
|
2025
2250
|
? clamp(Number(source.gazeSpanX), GAZE_SPAN_RANGE.min, GAZE_SPAN_RANGE.max)
|
|
2026
2251
|
: DEFAULTS.gazeSpanX;
|
|
2027
2252
|
out.gazeSpanY = Number.isFinite(Number(source.gazeSpanY))
|
|
2028
2253
|
? clamp(Number(source.gazeSpanY), GAZE_SPAN_RANGE.min, GAZE_SPAN_RANGE.max)
|
|
2029
2254
|
: DEFAULTS.gazeSpanY;
|
|
2255
|
+
['gazePitchUp', 'gazePitchDown'].forEach(function (key) {
|
|
2256
|
+
out[key] = typeof source[key] === 'number' && Number.isFinite(source[key])
|
|
2257
|
+
? clamp(source[key], 0, 60) : DEFAULTS[key];
|
|
2258
|
+
});
|
|
2030
2259
|
// 0 is a legitimate value here — it turns the idle rule off — so this cannot fall back to the default the
|
|
2031
2260
|
// way an unparseable value does. A negative number is unparseable for this purpose and becomes 0.
|
|
2032
2261
|
out.gazeIdleSeconds = Number.isFinite(Number(source.gazeIdleSeconds))
|
|
@@ -2081,6 +2310,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
2081
2310
|
? source.expressions.emotion.parts
|
|
2082
2311
|
: null);
|
|
2083
2312
|
out.springs = cleanSprings(source.springs);
|
|
2313
|
+
out.toon = cleanToon(source.toon);
|
|
2084
2314
|
return out;
|
|
2085
2315
|
}
|
|
2086
2316
|
|
|
@@ -2091,6 +2321,25 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
2091
2321
|
* directory's manifest says `loop: false`) has to be OVERRIDABLE back to looping, and "no entry" cannot
|
|
2092
2322
|
* express that — absence means "ask the catalog".
|
|
2093
2323
|
*/
|
|
2324
|
+
function cleanToon(raw) {
|
|
2325
|
+
var out = {};
|
|
2326
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return out;
|
|
2327
|
+
Object.keys(raw).slice(0, 256).forEach(function (id) {
|
|
2328
|
+
if (id === '__proto__' || id === 'constructor' || id === 'prototype') return;
|
|
2329
|
+
var entry = raw[id];
|
|
2330
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return;
|
|
2331
|
+
var next = {};
|
|
2332
|
+
if (typeof entry.enabled === 'boolean') next.enabled = entry.enabled;
|
|
2333
|
+
var ranges = { shadingToonyFactor: [0, 1], shadingShiftFactor: [-1, 1], shadeStrength: [0, 1], outlineScale: [0, 3] };
|
|
2334
|
+
Object.keys(ranges).forEach(function (key) {
|
|
2335
|
+
var value = entry[key];
|
|
2336
|
+
if (typeof value === 'number' && Number.isFinite(value)) next[key] = clamp(value, ranges[key][0], ranges[key][1]);
|
|
2337
|
+
});
|
|
2338
|
+
if (Object.keys(next).length) out[id] = next;
|
|
2339
|
+
});
|
|
2340
|
+
return out;
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2094
2343
|
function cleanMotionLoop(raw) {
|
|
2095
2344
|
var out = {};
|
|
2096
2345
|
var source = raw && typeof raw === 'object' ? raw : {};
|
|
@@ -2480,8 +2729,8 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
2480
2729
|
// ---------------------------------------------------------------------------
|
|
2481
2730
|
|
|
2482
2731
|
/** One state's status line, exactly as the renderer draws it: text, then the tag. */
|
|
2483
|
-
function statusLineFor(preferences, state) {
|
|
2484
|
-
var entry = statusEntryFor(preferences, state);
|
|
2732
|
+
function statusLineFor(preferences, state, language) {
|
|
2733
|
+
var entry = statusEntryFor(preferences, state, language);
|
|
2485
2734
|
if (!entry) return '';
|
|
2486
2735
|
var tag = typeof entry.tag === 'string' ? entry.tag : '';
|
|
2487
2736
|
return (entry.text + tag).slice(0, STATUS_LIMITS.textLength + STATUS_LIMITS.tagLength);
|
|
@@ -2493,11 +2742,15 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
2493
2742
|
* Always an OBJECT, never null: every state has something to say or an explicit reason to say nothing,
|
|
2494
2743
|
* and a caller that had to distinguish "unconfigured" from "configured empty" would get it wrong in the
|
|
2495
2744
|
* one case that matters (a user who cleared a line).
|
|
2745
|
+
*
|
|
2746
|
+
* `language` picks WHICH shipped default fills an empty box — see {@link DEFAULT_STATUS_TEXT}. It is the third
|
|
2747
|
+
* parameter and optional, so a caller that does not know the surface's language keeps the Chinese default the
|
|
2748
|
+
* suites and the older call sites already pin.
|
|
2496
2749
|
*/
|
|
2497
|
-
function statusEntryFor(preferences, state) {
|
|
2750
|
+
function statusEntryFor(preferences, state, language) {
|
|
2498
2751
|
var map = preferences && preferences.status && typeof preferences.status === 'object' ? preferences.status : {};
|
|
2499
2752
|
var stored = map[state] && typeof map[state] === 'object' ? map[state] : {};
|
|
2500
|
-
var fallback =
|
|
2753
|
+
var fallback = defaultStatusText(state, language);
|
|
2501
2754
|
var text = typeof stored.text === 'string' ? stored.text : fallback;
|
|
2502
2755
|
var fallbackAnimation = Object.prototype.hasOwnProperty.call(DEFAULT_STATUS_ANIMATION, state)
|
|
2503
2756
|
? DEFAULT_STATUS_ANIMATION[state]
|
|
@@ -3370,6 +3623,14 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
3370
3623
|
* @returns {string|null} the line to show, or null for silence
|
|
3371
3624
|
*/
|
|
3372
3625
|
SpeechMachine.prototype.observe = function (preferences, state, pendingKind, language, random, motion) {
|
|
3626
|
+
if (state === 'dancing') {
|
|
3627
|
+
this.current = null;
|
|
3628
|
+
this.wantsAi = null;
|
|
3629
|
+
this.lastEvent = null;
|
|
3630
|
+
this.idleAt = null;
|
|
3631
|
+
this.chatterAt = null;
|
|
3632
|
+
return null;
|
|
3633
|
+
}
|
|
3373
3634
|
var speech = cleanSpeech(preferences && preferences.speech);
|
|
3374
3635
|
var t = this.now();
|
|
3375
3636
|
if (!speech.enabled) {
|
|
@@ -3626,11 +3887,17 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
3626
3887
|
* clause is DROPPED — DSH's own token-free 「深度求索中」 rather than a number invented here. Passed per call so the
|
|
3627
3888
|
* two surfaces (the app's pet and the floating window) can only differ by what they are told, never by the rule.
|
|
3628
3889
|
*
|
|
3890
|
+
* `language` is the SIXTH parameter and it reaches two places: which shipped default an empty box falls back to
|
|
3891
|
+
* ({@link DEFAULT_STATUS_TEXT}), and the units a `{duration}` clause is written in — DSH writes 1分30秒 in one
|
|
3892
|
+
* language and 1m 30s in the other, so a line translated without its clock would read as a foreign number.
|
|
3893
|
+
* Optional, like the status-entry language above: a caller that has no language to pass keeps the Chinese answer
|
|
3894
|
+
* every existing suite pins.
|
|
3895
|
+
*
|
|
3629
3896
|
* @returns {{state: string, text: string, tag: string, animation: string, seconds: number}|null}
|
|
3630
3897
|
*/
|
|
3631
|
-
function statusFor(preferences, state, enteredAt, now, clock) {
|
|
3898
|
+
function statusFor(preferences, state, enteredAt, now, clock, language, completedCount) {
|
|
3632
3899
|
if (STATUS_STATES.indexOf(state) === -1) return null;
|
|
3633
|
-
var entry = statusEntryFor(preferences, state);
|
|
3900
|
+
var entry = statusEntryFor(preferences, state, language);
|
|
3634
3901
|
if (!entry.text && !entry.tag) return null;
|
|
3635
3902
|
var at = Number.isFinite(now) ? now : Date.now();
|
|
3636
3903
|
/**
|
|
@@ -3683,7 +3950,10 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
3683
3950
|
}
|
|
3684
3951
|
text = elapsed === null
|
|
3685
3952
|
? durationClauseRemoved(text)
|
|
3686
|
-
: text.split(STATUS_DURATION_TOKEN).join(formatRunDuration(elapsed));
|
|
3953
|
+
: text.split(STATUS_DURATION_TOKEN).join(formatRunDuration(elapsed, language));
|
|
3954
|
+
}
|
|
3955
|
+
if (state === 'done' && Number(completedCount) > 1 && text) {
|
|
3956
|
+
text = language === 'en' ? (text === 'Task complete' ? 'All tasks complete' : 'All ' + text) : '全部' + text;
|
|
3687
3957
|
}
|
|
3688
3958
|
var look = statusTone(state);
|
|
3689
3959
|
return {
|
|
@@ -4718,6 +4988,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
4718
4988
|
* @param {object} options url, intervalMs, fetchImpl, onPreview(preview|null), onUnsupported
|
|
4719
4989
|
*/
|
|
4720
4990
|
function watchHostPreview(options) {
|
|
4991
|
+
var navigationSeen = null;
|
|
4721
4992
|
var settings = options || {};
|
|
4722
4993
|
var url = settings.url || '/vrmpet3d/preview';
|
|
4723
4994
|
// Four hundred milliseconds: this drives a hover preview, so it has to feel immediate, and the route is a
|
|
@@ -4726,11 +4997,14 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
4726
4997
|
var doFetch = settings.fetchImpl || (typeof fetch === 'function' ? fetch : null);
|
|
4727
4998
|
var timer = null;
|
|
4728
4999
|
var seen = -1;
|
|
5000
|
+
var issued = 0;
|
|
5001
|
+
var delivered = 0;
|
|
4729
5002
|
var supported = Boolean(doFetch);
|
|
4730
5003
|
var stopped = false;
|
|
4731
5004
|
|
|
4732
5005
|
function load() {
|
|
4733
5006
|
if (!supported || stopped) return Promise.resolve(null);
|
|
5007
|
+
var request = ++issued;
|
|
4734
5008
|
return doFetch(url, { credentials: 'omit' })
|
|
4735
5009
|
.then(function (response) {
|
|
4736
5010
|
if (response.status === 404 || response.status === 405) {
|
|
@@ -4742,6 +5016,14 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
4742
5016
|
return response.json ? response.json() : null;
|
|
4743
5017
|
})
|
|
4744
5018
|
.then(function (body) {
|
|
5019
|
+
if (request < delivered) return null;
|
|
5020
|
+
if (body && typeof body.seq === 'number') delivered = request;
|
|
5021
|
+
if (body && body.navigation && typeof body.navigation.seq === 'number') {
|
|
5022
|
+
var navigation = body.navigation;
|
|
5023
|
+
if (navigationSeen !== null && navigationSeen !== navigation.seq && typeof navigation.sessionId === 'string'
|
|
5024
|
+
&& typeof settings.onNavigate === 'function') settings.onNavigate(navigation.sessionId);
|
|
5025
|
+
navigationSeen = navigation.seq;
|
|
5026
|
+
}
|
|
4745
5027
|
if (!body || typeof body.seq !== 'number' || body.seq === seen) return null;
|
|
4746
5028
|
seen = body.seq;
|
|
4747
5029
|
var preview = cleanPreview(body.preview);
|
|
@@ -4879,11 +5161,14 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
4879
5161
|
var doFetch = settings.fetchImpl || (typeof fetch === 'function' ? fetch : null);
|
|
4880
5162
|
var timer = null;
|
|
4881
5163
|
var seen = -1;
|
|
5164
|
+
var issued = 0;
|
|
5165
|
+
var delivered = 0;
|
|
4882
5166
|
var supported = Boolean(doFetch);
|
|
4883
5167
|
var stopped = false;
|
|
4884
5168
|
|
|
4885
5169
|
function load() {
|
|
4886
5170
|
if (!supported || stopped) return Promise.resolve(null);
|
|
5171
|
+
var request = ++issued;
|
|
4887
5172
|
return doFetch(url, { credentials: 'omit' })
|
|
4888
5173
|
.then(function (response) {
|
|
4889
5174
|
if (response.status === 404 || response.status === 405) {
|
|
@@ -4895,6 +5180,8 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
4895
5180
|
return response.json ? response.json() : null;
|
|
4896
5181
|
})
|
|
4897
5182
|
.then(function (body) {
|
|
5183
|
+
if (request < delivered) return null;
|
|
5184
|
+
if (body && typeof body.seq === 'number') delivered = request;
|
|
4898
5185
|
if (!body || typeof body.seq !== 'number' || body.seq === seen) return null;
|
|
4899
5186
|
seen = body.seq;
|
|
4900
5187
|
var state = STATES.indexOf(body.state) === -1 ? null : body.state;
|
|
@@ -5004,6 +5291,12 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5004
5291
|
}
|
|
5005
5292
|
if (typeof why.scope === 'string' && why.scope) out.scope = why.scope.slice(0, 16);
|
|
5006
5293
|
if (typeof why.session === 'string' && why.session) out.session = why.session.slice(0, 64);
|
|
5294
|
+
if (typeof why.statusSessionId === 'string' && why.statusSessionId) out.statusSessionId = why.statusSessionId.slice(0, 64);
|
|
5295
|
+
if (typeof why.completedSessionId === 'string' && why.completedSessionId) out.completedSessionId = why.completedSessionId.slice(0, 64);
|
|
5296
|
+
['waitingSessionIds', 'completedSessionIds'].forEach(function (key) {
|
|
5297
|
+
if (Array.isArray(why[key])) out[key] = why[key].filter(function (id) { return typeof id === 'string' && id; })
|
|
5298
|
+
.slice(0, 256).map(function (id) { return id.slice(0, 64); });
|
|
5299
|
+
});
|
|
5007
5300
|
if (typeof why.interaction === 'string' && why.interaction) out.interaction = why.interaction.slice(0, 16);
|
|
5008
5301
|
if (Array.isArray(why.runningIds)) {
|
|
5009
5302
|
out.runningIds = why.runningIds
|
|
@@ -5161,15 +5454,19 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5161
5454
|
* its 进行中 line has to travel with the state. A whole number, or nothing — an older host that does not send it
|
|
5162
5455
|
* leaves the window's dot as it was rather than inventing a count.
|
|
5163
5456
|
*/
|
|
5164
|
-
var nextUnread = Number.isFinite(
|
|
5457
|
+
var nextUnread = typeof unread === 'number' && Number.isFinite(unread) && unread >= 0 ? Math.floor(unread) : null;
|
|
5165
5458
|
/**
|
|
5166
5459
|
* The diagnostic, kept OUT of the change key on purpose: it carries counts and clock readings that move
|
|
5167
5460
|
* constantly, and letting them force a report would turn "the pet announces a change" into a 1 Hz feed. It
|
|
5168
5461
|
* rides along with the reports the state and the line already earn, and the heartbeat re-states the last one.
|
|
5169
5462
|
*/
|
|
5170
5463
|
var nextWhy = cleanWhy(why);
|
|
5464
|
+
var navigationKey = function (value) {
|
|
5465
|
+
return JSON.stringify(value ? [value.waitingSessionIds || [], value.completedSessionIds || [],
|
|
5466
|
+
value.statusSessionId || null, value.completedSessionId || null] : [[], [], null, null]);
|
|
5467
|
+
};
|
|
5171
5468
|
if (force !== true && next === sent && nextLine === sentLine && nextTouchKey === sentTouchKey
|
|
5172
|
-
&& nextClockKey === sentClockKey && nextUnread === sentUnread) {
|
|
5469
|
+
&& nextClockKey === sentClockKey && nextUnread === sentUnread && navigationKey(nextWhy) === navigationKey(sentWhy)) {
|
|
5173
5470
|
return Promise.resolve(null);
|
|
5174
5471
|
}
|
|
5175
5472
|
sent = next;
|
|
@@ -5294,10 +5591,15 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5294
5591
|
var doFetch = settings.fetchImpl || (typeof fetch === 'function' ? fetch : null);
|
|
5295
5592
|
var timer = null;
|
|
5296
5593
|
var seen = -1;
|
|
5594
|
+
var issued = 0;
|
|
5595
|
+
var delivered = 0;
|
|
5297
5596
|
var supported = Boolean(doFetch);
|
|
5298
5597
|
var stopped = false;
|
|
5299
5598
|
|
|
5300
|
-
function deliver(snapshot) {
|
|
5599
|
+
function deliver(snapshot, request) {
|
|
5600
|
+
// A slow poll/write must not roll a live gesture back to an older reply.
|
|
5601
|
+
if (request < delivered) return null;
|
|
5602
|
+
if (snapshot && typeof snapshot.version === 'number') delivered = request;
|
|
5301
5603
|
if (!snapshot || typeof snapshot.version !== 'number' || snapshot.version === seen) return null;
|
|
5302
5604
|
seen = snapshot.version;
|
|
5303
5605
|
if (typeof settings.onSnapshot === 'function') settings.onSnapshot(snapshot);
|
|
@@ -5307,6 +5609,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5307
5609
|
/** One look at the host. Returns a promise for the snapshot it delivered, if any. */
|
|
5308
5610
|
function load() {
|
|
5309
5611
|
if (!supported || stopped) return Promise.resolve(null);
|
|
5612
|
+
var request = ++issued;
|
|
5310
5613
|
return doFetch(url, { credentials: 'omit' })
|
|
5311
5614
|
.then(function (response) {
|
|
5312
5615
|
if (response.status === 404 || response.status === 405) {
|
|
@@ -5318,7 +5621,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5318
5621
|
if (!response.ok) return null;
|
|
5319
5622
|
return response.json ? response.json() : null;
|
|
5320
5623
|
})
|
|
5321
|
-
.then(deliver)
|
|
5624
|
+
.then(function (snapshot) { return deliver(snapshot, request); })
|
|
5322
5625
|
.catch(function () { return null; });
|
|
5323
5626
|
}
|
|
5324
5627
|
|
|
@@ -5330,6 +5633,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5330
5633
|
*/
|
|
5331
5634
|
function push(body) {
|
|
5332
5635
|
if (!supported || stopped) return Promise.resolve(null);
|
|
5636
|
+
var request = ++issued;
|
|
5333
5637
|
return doFetch(url, {
|
|
5334
5638
|
method: 'POST',
|
|
5335
5639
|
credentials: 'omit',
|
|
@@ -5337,7 +5641,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5337
5641
|
body: JSON.stringify(body || {}),
|
|
5338
5642
|
})
|
|
5339
5643
|
.then(function (response) { return response.ok ? response.json() : null; })
|
|
5340
|
-
.then(deliver)
|
|
5644
|
+
.then(function (snapshot) { return deliver(snapshot, request); })
|
|
5341
5645
|
.catch(function () { return null; });
|
|
5342
5646
|
}
|
|
5343
5647
|
|
|
@@ -5550,6 +5854,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5550
5854
|
* sits above the watermark and is adopted as usual.
|
|
5551
5855
|
*/
|
|
5552
5856
|
this.dismissedNoticeAt = 0;
|
|
5857
|
+
this.dismissedSessions = new Map();
|
|
5553
5858
|
/**
|
|
5554
5859
|
* The TURN the pet is currently describing, and the one that just ended.
|
|
5555
5860
|
*
|
|
@@ -5832,6 +6137,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5832
6137
|
PetMachine.prototype.dismissNotice = function (keepDebt) {
|
|
5833
6138
|
if (this.notice && Number.isFinite(this.notice.at)) {
|
|
5834
6139
|
this.dismissedNoticeAt = Math.max(this.dismissedNoticeAt, this.notice.at);
|
|
6140
|
+
if (this.notice.sessionId) this.dismissedSessions.set(this.notice.sessionId, this.notice.at);
|
|
5835
6141
|
}
|
|
5836
6142
|
if (keepDebt !== true && this.notice && this.notice.sessionId) {
|
|
5837
6143
|
delete this.retractions[this.notice.sessionId];
|
|
@@ -5954,7 +6260,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5954
6260
|
* well as in the acknowledgement rule in `resolve`. (The floating window never takes focus, which is why the same
|
|
5955
6261
|
* drag could not do this there.)
|
|
5956
6262
|
*/
|
|
5957
|
-
if (this.notice &&
|
|
6263
|
+
if (this.notice && !this.notice.sessionId && !presenceKnown && this.interaction === null) {
|
|
5958
6264
|
this.noteDismissal('focus-return');
|
|
5959
6265
|
this.dismissNotice();
|
|
5960
6266
|
}
|
|
@@ -6066,6 +6372,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6066
6372
|
// reports as its own `at`, so the minimum hold starts when the user can first see it.
|
|
6067
6373
|
endedAt: at,
|
|
6068
6374
|
settledAt: this.now(),
|
|
6375
|
+
confirmed: payload.confirmed === true,
|
|
6069
6376
|
sessionId: sessionId,
|
|
6070
6377
|
};
|
|
6071
6378
|
}
|
|
@@ -6091,8 +6398,14 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6091
6398
|
* than a startup that has not finished.
|
|
6092
6399
|
*/
|
|
6093
6400
|
if (snap.connected === true) this.everConnected = true;
|
|
6094
|
-
var busy = snap.
|
|
6401
|
+
var busy = snap.pending === true || (snap.available === true && snap.running === true);
|
|
6095
6402
|
if (busy) this.lastBusyAt = t;
|
|
6403
|
+
var viewedCompletions = Array.isArray(snap.viewedCompletions) ? snap.viewedCompletions : [];
|
|
6404
|
+
if (this.pendingNotice && viewedCompletions.indexOf(this.pendingNotice.sessionId) !== -1) this.pendingNotice = undefined;
|
|
6405
|
+
if (this.notice && viewedCompletions.indexOf(this.notice.sessionId) !== -1) {
|
|
6406
|
+
this.noteDismissal('acknowledged');
|
|
6407
|
+
this.dismissNotice();
|
|
6408
|
+
}
|
|
6096
6409
|
|
|
6097
6410
|
/**
|
|
6098
6411
|
* Adopt the SNAPSHOT's own notice when it is newer than this machine's.
|
|
@@ -6111,9 +6424,11 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6111
6424
|
* what the aggregate watches, and it falls between the rounds of one task exactly like a turn does.
|
|
6112
6425
|
*/
|
|
6113
6426
|
if (snap.notice && typeof snap.notice.at === 'number'
|
|
6114
|
-
&& snap.notice.
|
|
6115
|
-
|
|
6116
|
-
|
|
6427
|
+
&& (snap.notice.queued === true ? (!this.notice && !this.pendingNotice
|
|
6428
|
+
&& (!this.dismissedSessions.has(snap.notice.sessionId) || snap.notice.at > this.dismissedSessions.get(snap.notice.sessionId)))
|
|
6429
|
+
: (snap.notice.at > this.dismissedNoticeAt
|
|
6430
|
+
&& (!this.notice || snap.notice.at > this.notice.at)
|
|
6431
|
+
&& (!this.pendingNotice || snap.notice.at > this.pendingNotice.endedAt)))) {
|
|
6117
6432
|
var snapSession = typeof snap.notice.sessionId === 'string' && snap.notice.sessionId ? snap.notice.sessionId : null;
|
|
6118
6433
|
if (snap.notice.reason === 'error') {
|
|
6119
6434
|
// A failure is not a completion and is never settled: it is shown at once, exactly as a `turn/end` failure is.
|
|
@@ -6123,7 +6438,8 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6123
6438
|
this.pendingNotice = {
|
|
6124
6439
|
reason: snap.notice.reason,
|
|
6125
6440
|
endedAt: snap.notice.at,
|
|
6126
|
-
settledAt: t,
|
|
6441
|
+
settledAt: snap.notice.queued === true ? snap.notice.at : t,
|
|
6442
|
+
confirmed: snap.notice.confirmed === true,
|
|
6127
6443
|
// The session the notice is ABOUT. Without it there is nothing to come back to, and the notice falls
|
|
6128
6444
|
// back to the older focus/timer rules below.
|
|
6129
6445
|
sessionId: snapSession,
|
|
@@ -6139,6 +6455,9 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6139
6455
|
* (a `turn/end` frame and the store's falling edge) are decided together.
|
|
6140
6456
|
*/
|
|
6141
6457
|
if (this.pendingNotice) {
|
|
6458
|
+
if (snap.notice && snap.notice.sessionId === this.pendingNotice.sessionId && snap.notice.confirmed === true) {
|
|
6459
|
+
this.pendingNotice.confirmed = true;
|
|
6460
|
+
}
|
|
6142
6461
|
var pendingSession = this.pendingNotice.sessionId;
|
|
6143
6462
|
/**
|
|
6144
6463
|
* Cancelled by WORK RESUMING, and only that — the ordering is the whole point.
|
|
@@ -6161,7 +6480,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6161
6480
|
* never produced one.
|
|
6162
6481
|
*/
|
|
6163
6482
|
this.pendingNotice = undefined;
|
|
6164
|
-
} else if (t - this.pendingNotice.settledAt >= this.settleFor(this.pendingNotice.sessionId)) {
|
|
6483
|
+
} else if (this.pendingNotice.confirmed || t - this.pendingNotice.settledAt >= this.settleFor(this.pendingNotice.sessionId)) {
|
|
6165
6484
|
this.notice = {
|
|
6166
6485
|
reason: this.pendingNotice.reason,
|
|
6167
6486
|
// Visible from NOW: the settle window was spent waiting, and a hold measured from the turn's end would
|
|
@@ -6237,8 +6556,10 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6237
6556
|
*/
|
|
6238
6557
|
var unreadDropped = isFiniteNumber(this.unreadAtNotice) && this.unreadCount < this.unreadAtNotice;
|
|
6239
6558
|
var neverUnread = this.unreadAtNotice === 0 && this.unreadCount === 0;
|
|
6240
|
-
|
|
6241
|
-
|
|
6559
|
+
var viewed = Array.isArray(snap.viewedCompletions) && snap.viewedCompletions.indexOf(notice.sessionId) !== -1;
|
|
6560
|
+
acknowledged = notice.sessionId
|
|
6561
|
+
? (viewed || ((!this.requireViewedAcknowledgement || notice.reason !== 'completed')
|
|
6562
|
+
&& presentId === notice.sessionId && this.focused === true))
|
|
6242
6563
|
: (notice.reason !== 'error' && this.focused === true
|
|
6243
6564
|
&& (unreadDropped || (neverUnread && this.interaction === null)));
|
|
6244
6565
|
this.pendingDismissalKind = notice.sessionId && presentId ? 'acknowledged' : 'no-session';
|
|
@@ -6260,8 +6581,8 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6260
6581
|
* still on screen would flip the pet to 出错 and hide the very prompt the user has to answer, and a
|
|
6261
6582
|
* turn that completes would show 任务达成 over an unanswered approval.
|
|
6262
6583
|
*
|
|
6263
|
-
*
|
|
6264
|
-
*
|
|
6584
|
+
* While the session reports a pending interaction, EVERY other state is skipped, including gestures
|
|
6585
|
+
* and greetings. When the interaction is answered the session
|
|
6265
6586
|
* stops reporting it, this latch releases, and the same frame's cascade decides where the pet goes.
|
|
6266
6587
|
*/
|
|
6267
6588
|
var pending = snap.pending === true;
|
|
@@ -6288,7 +6609,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6288
6609
|
/**
|
|
6289
6610
|
* 跳舞 — the user's own state, and the two rules that make it a good guest (operator, 2026-10-14).
|
|
6290
6611
|
*
|
|
6291
|
-
*
|
|
6612
|
+
* Attention comes first, followed by drag states, dance, greeting and offline.
|
|
6292
6613
|
* and it is dropped the moment the session is BUSY, which is the operator's 「跳舞也会被进入工作状态打断」 without a rule of its
|
|
6293
6614
|
* own: work outranks it because the busy branch below is reached first once this field is cleared. A dance also ends when the
|
|
6294
6615
|
* clip ends (the client watches the clip and calls `stopDancing`) or when the user double-clicks the pet, and both of those
|
|
@@ -6299,14 +6620,14 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6299
6620
|
this.stopDancing();
|
|
6300
6621
|
dance = null;
|
|
6301
6622
|
}
|
|
6302
|
-
if (
|
|
6623
|
+
if (pending) {
|
|
6624
|
+
state = 'waiting';
|
|
6625
|
+
} else if (this.interaction) {
|
|
6303
6626
|
state = this.interaction;
|
|
6304
6627
|
} else if (dance) {
|
|
6305
6628
|
state = 'dancing';
|
|
6306
6629
|
} else if (t < this.greetingUntil) {
|
|
6307
6630
|
state = 'greeting';
|
|
6308
|
-
} else if (pending) {
|
|
6309
|
-
state = 'waiting';
|
|
6310
6631
|
} else if (snap.connected === false && this.everConnected) {
|
|
6311
6632
|
/**
|
|
6312
6633
|
* 离线 means the host is GONE — not that it has nothing to say yet.
|
|
@@ -6406,6 +6727,11 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6406
6727
|
this.records = new Map();
|
|
6407
6728
|
/** Each session's own `running` flag as the store last reported it (see `observeStoreEdge`). */
|
|
6408
6729
|
this.storeRunning = new Map();
|
|
6730
|
+
this.completionChecks = new Map();
|
|
6731
|
+
this.attentionSessions = new Map();
|
|
6732
|
+
this.completionQueue = new Map();
|
|
6733
|
+
this.completionSeen = new Set();
|
|
6734
|
+
this.viewedCompletions = new Set();
|
|
6409
6735
|
/**
|
|
6410
6736
|
* When this aggregate's `running` answer last became true, and what it was last published as.
|
|
6411
6737
|
*
|
|
@@ -6465,6 +6791,8 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6465
6791
|
var status = statuses && statuses.get ? statuses.get(id) : undefined;
|
|
6466
6792
|
var running = status && typeof status.running === 'boolean' ? status.running : undefined;
|
|
6467
6793
|
if (running === undefined) return;
|
|
6794
|
+
if (running || status.pendingInteraction != null) self.completionChecks.delete(id);
|
|
6795
|
+
if (running) self.forgetCompletion(id);
|
|
6468
6796
|
var previous = self.storeRunning.get(id);
|
|
6469
6797
|
self.storeRunning.set(id, running);
|
|
6470
6798
|
if (previous === running) return;
|
|
@@ -6488,13 +6816,18 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6488
6816
|
* stream covers that case when it is wired, and a false 任务完成 on every boot is worse than a missed one.
|
|
6489
6817
|
*/
|
|
6490
6818
|
if (previous !== true) return;
|
|
6819
|
+
var boundaryRecord = self.records.get(id);
|
|
6820
|
+
if (boundaryRecord && boundaryRecord.lastEndReason && boundaryRecord.lastEndReason !== 'completed') return;
|
|
6491
6821
|
// Stopped: a completion. A failure already latched outranks it, the same rule the boundary path uses.
|
|
6822
|
+
var completion = { id: id + ':store', reason: 'completed', sessionId: id, at: self.now() };
|
|
6823
|
+
self.rememberCompletion(completion);
|
|
6492
6824
|
if (self.notice && self.notice.reason === 'error') return;
|
|
6493
|
-
self.notice =
|
|
6825
|
+
self.notice = completion;
|
|
6494
6826
|
});
|
|
6495
6827
|
};
|
|
6496
6828
|
|
|
6497
6829
|
SessionAggregate.prototype.reset = function (identities) {
|
|
6830
|
+
this.completionChecks.clear();
|
|
6498
6831
|
this.records.clear();
|
|
6499
6832
|
this.notice = undefined;
|
|
6500
6833
|
if (Array.isArray(identities)) {
|
|
@@ -6508,6 +6841,34 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6508
6841
|
}
|
|
6509
6842
|
};
|
|
6510
6843
|
|
|
6844
|
+
SessionAggregate.prototype.rememberCompletion = function (notice, announce) {
|
|
6845
|
+
var id = notice && notice.sessionId;
|
|
6846
|
+
if (!id || this.completionSeen.has(id)) return;
|
|
6847
|
+
this.completionSeen.add(id);
|
|
6848
|
+
this.completionQueue.set(id, Object.assign({}, notice, {
|
|
6849
|
+
announce: announce !== false, queued: true, confirmed: announce !== false,
|
|
6850
|
+
}));
|
|
6851
|
+
this.completionChecks.set(id, this.now());
|
|
6852
|
+
};
|
|
6853
|
+
|
|
6854
|
+
SessionAggregate.prototype.forgetCompletion = function (id) {
|
|
6855
|
+
this.completionQueue.delete(id);
|
|
6856
|
+
this.completionChecks.delete(id);
|
|
6857
|
+
this.completionSeen.delete(id);
|
|
6858
|
+
this.viewedCompletions.delete(id);
|
|
6859
|
+
};
|
|
6860
|
+
|
|
6861
|
+
SessionAggregate.prototype.acknowledgeViewed = function (id) {
|
|
6862
|
+
if (!id || this.running(id) !== false) return;
|
|
6863
|
+
var status = this.input.statuses.get ? this.input.statuses.get(id) : null;
|
|
6864
|
+
if (!this.completionQueue.has(id) && !(status && status.completionUnread === true)) return;
|
|
6865
|
+
this.completionSeen.add(id);
|
|
6866
|
+
this.viewedCompletions.add(id);
|
|
6867
|
+
this.completionQueue.delete(id);
|
|
6868
|
+
this.completionChecks.delete(id);
|
|
6869
|
+
if (this.notice && this.notice.sessionId === id && this.notice.reason === 'completed') this.notice = undefined;
|
|
6870
|
+
};
|
|
6871
|
+
|
|
6511
6872
|
SessionAggregate.prototype.boundary = function (event) {
|
|
6512
6873
|
if (!event || typeof event !== 'object') return this.snapshot();
|
|
6513
6874
|
var id = event.sessionId;
|
|
@@ -6534,12 +6895,16 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6534
6895
|
// The host's live answer for a session it vouches for: every root it classified arrives as `false`.
|
|
6535
6896
|
if (event.isSubagent === false) record.isSubagent = false;
|
|
6536
6897
|
if (event.type === 'turn/start') {
|
|
6898
|
+
this.forgetCompletion(id);
|
|
6537
6899
|
record.running = true;
|
|
6900
|
+
record.lastEndReason = null;
|
|
6901
|
+
this.completionChecks.delete(id);
|
|
6538
6902
|
record.candidate = undefined;
|
|
6539
6903
|
// A new turn supersedes whatever the last one reported, error included.
|
|
6540
6904
|
this.notice = undefined;
|
|
6541
6905
|
} else if (event.type === 'turn/end') {
|
|
6542
6906
|
record.running = false;
|
|
6907
|
+
record.lastEndReason = event.reason;
|
|
6543
6908
|
record.candidate = { id: id + ':' + event.seq, reason: event.reason, at: this.now() };
|
|
6544
6909
|
}
|
|
6545
6910
|
this.records.set(id, record);
|
|
@@ -6549,7 +6914,8 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6549
6914
|
* a frame for a session nobody can vouch for — not classified, not listed by the app — is silence rather than a
|
|
6550
6915
|
* 任务达成, because the pet has no way to tell it from a helper's turn ending (2026-10-14).
|
|
6551
6916
|
*/
|
|
6552
|
-
if (candidate && (candidate.reason === 'completed' ||
|
|
6917
|
+
if (candidate && (candidate.reason === 'completed' || candidate.reason === 'error') && this.mainSession(id)) {
|
|
6918
|
+
if (candidate.reason === 'completed') this.rememberCompletion({ id: candidate.id, reason: candidate.reason, sessionId: id, at: candidate.at });
|
|
6553
6919
|
record.candidate = undefined;
|
|
6554
6920
|
// A failure latches: a success arriving moments later must not erase the
|
|
6555
6921
|
// fact that a turn just died. The machine owns the same rule, so the two
|
|
@@ -6570,6 +6936,19 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6570
6936
|
};
|
|
6571
6937
|
|
|
6572
6938
|
SessionAggregate.prototype.subagent = function (id) {
|
|
6939
|
+
var list = this.input.catalog || {};
|
|
6940
|
+
var row = list.byId && list.byId[id];
|
|
6941
|
+
// The current DSH store publishes origin and its parents' subagent catalogs, not isSubagent.
|
|
6942
|
+
// These explicit identities outrank an older boundary baseline that called the agent a root.
|
|
6943
|
+
if (row && (row.origin === 'subagent' || row.isSubagent === true)) return true;
|
|
6944
|
+
var parents = Object.keys(list.byId || {});
|
|
6945
|
+
for (var p = 0; p < parents.length; p += 1) {
|
|
6946
|
+
var parent = list.byId[parents[p]];
|
|
6947
|
+
var projection = list.projectionsBySession && list.projectionsBySession[parents[p]];
|
|
6948
|
+
var values = (projection && projection.values) || (parent && parent.projectionValues);
|
|
6949
|
+
var children = values && values.subagentCatalog;
|
|
6950
|
+
if (Array.isArray(children) && children.some(function (child) { return child && child.id === id; })) return true;
|
|
6951
|
+
}
|
|
6573
6952
|
var record = this.records.get(id);
|
|
6574
6953
|
if (record && record.isSubagent !== undefined) return record.isSubagent === true;
|
|
6575
6954
|
var catalog = this.input.catalog && this.input.catalog.byId ? this.input.catalog.byId[id] : undefined;
|
|
@@ -6651,7 +7030,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6651
7030
|
* only ever make the answer STRICTER on a store that has the list.
|
|
6652
7031
|
*/
|
|
6653
7032
|
var listed = catalog.ids;
|
|
6654
|
-
if (listed && typeof listed.length === 'number'
|
|
7033
|
+
if (listed && typeof listed.length === 'number') {
|
|
6655
7034
|
for (var i = 0; i < listed.length; i += 1) {
|
|
6656
7035
|
if (listed[i] === id) return true;
|
|
6657
7036
|
}
|
|
@@ -6669,6 +7048,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6669
7048
|
|
|
6670
7049
|
var workingCount = 0;
|
|
6671
7050
|
var waitingCount = 0;
|
|
7051
|
+
var waitingSessionId = null;
|
|
6672
7052
|
/**
|
|
6673
7053
|
* How many sessions have a completion the user has NOT been back to.
|
|
6674
7054
|
*
|
|
@@ -6685,6 +7065,9 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6685
7065
|
* contributes nothing.
|
|
6686
7066
|
*/
|
|
6687
7067
|
var unreadCount = 0;
|
|
7068
|
+
var completedUnreadCount = 0;
|
|
7069
|
+
var completedSessionId = null;
|
|
7070
|
+
var completedSessionIds = [];
|
|
6688
7071
|
/**
|
|
6689
7072
|
* WHY that count is what it is, as counts rather than ids or titles — added with the 绿点 fix of 2026-10-14, so
|
|
6690
7073
|
* "the dot is lit and no session finished" is answerable from `/vrmpet3d/state` alone instead of by guesswork.
|
|
@@ -6700,9 +7083,32 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6700
7083
|
/** Which pending interaction wins when several sessions are waiting at once. Approval is the rarer. */
|
|
6701
7084
|
var waitingKind = null;
|
|
6702
7085
|
var self = this;
|
|
7086
|
+
this.attentionSessions.forEach(function (_kind, id) {
|
|
7087
|
+
var status = statuses.get ? statuses.get(id) : undefined;
|
|
7088
|
+
// Navigating away, a missing report, or losing focus is not an answer to the problem.
|
|
7089
|
+
if (status) {
|
|
7090
|
+
if (status.pendingInteraction == null) self.attentionSessions.delete(id);
|
|
7091
|
+
else self.attentionSessions.set(id, pendingInteractionKind(status.pendingInteraction));
|
|
7092
|
+
}
|
|
7093
|
+
});
|
|
6703
7094
|
ids.forEach(function (id) {
|
|
6704
7095
|
if (!self.inScope(id)) return;
|
|
6705
7096
|
var status = statuses.get ? statuses.get(id) : undefined;
|
|
7097
|
+
var record = self.records.get(id);
|
|
7098
|
+
if (status && status.completionUnread === true && self.mainSession(id) && !self.completionSeen.has(id)
|
|
7099
|
+
&& self.running(id) === false && status.pendingInteraction == null) {
|
|
7100
|
+
self.rememberCompletion({ sessionId: id, reason: 'completed', at: self.now() }, false);
|
|
7101
|
+
}
|
|
7102
|
+
var completed = self.completionQueue.has(id) && !self.viewedCompletions.has(id) && self.mainSession(id)
|
|
7103
|
+
&& self.running(id) === false && (!status || status.pendingInteraction == null)
|
|
7104
|
+
&& !(record && record.lastEndReason && record.lastEndReason !== 'completed');
|
|
7105
|
+
if (!completed) self.completionChecks.delete(id);
|
|
7106
|
+
else {
|
|
7107
|
+
if (!self.completionChecks.has(id)) self.completionChecks.set(id, self.now());
|
|
7108
|
+
if (self.completionQueue.get(id).confirmed || self.now() - self.completionChecks.get(id) >= COMPLETION_SETTLE_MS) {
|
|
7109
|
+
completedSessionIds.push(id);
|
|
7110
|
+
}
|
|
7111
|
+
}
|
|
6706
7112
|
if (status && status.completionUnread === true) {
|
|
6707
7113
|
unreadDetail.candidates += 1;
|
|
6708
7114
|
var entry = (catalog.byId || {})[id];
|
|
@@ -6725,14 +7131,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6725
7131
|
}
|
|
6726
7132
|
var pending = status ? status.pendingInteraction != null : false;
|
|
6727
7133
|
if (pending) {
|
|
6728
|
-
|
|
6729
|
-
var kind = pendingInteractionKind(status.pendingInteraction);
|
|
6730
|
-
// Ranked rather than first-wins: a global-scope pet watching ten sessions must not report
|
|
6731
|
-
// "a question" just because that session happens to come out of the map first when another one
|
|
6732
|
-
// is blocked on a permission prompt.
|
|
6733
|
-
if (waitingKind === null || PENDING_KIND_RANK.indexOf(kind) < PENDING_KIND_RANK.indexOf(waitingKind)) {
|
|
6734
|
-
waitingKind = kind;
|
|
6735
|
-
}
|
|
7134
|
+
self.attentionSessions.set(id, pendingInteractionKind(status.pendingInteraction));
|
|
6736
7135
|
}
|
|
6737
7136
|
// One rule for "this is the user's own session", shared with the two completion paths (`mainSession`).
|
|
6738
7137
|
if (self.mainSession(id) && self.running(id) === true) {
|
|
@@ -6743,7 +7142,23 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6743
7142
|
}
|
|
6744
7143
|
});
|
|
6745
7144
|
|
|
7145
|
+
this.attentionSessions.forEach(function (kind, id) {
|
|
7146
|
+
waitingCount++;
|
|
7147
|
+
if (waitingKind === null || PENDING_KIND_RANK.indexOf(kind) < PENDING_KIND_RANK.indexOf(waitingKind)) {
|
|
7148
|
+
waitingKind = kind;
|
|
7149
|
+
waitingSessionId = id;
|
|
7150
|
+
}
|
|
7151
|
+
});
|
|
7152
|
+
completedSessionIds.sort(function (a, b) { return self.completionQueue.get(a).at - self.completionQueue.get(b).at; });
|
|
7153
|
+
completedUnreadCount = completedSessionIds.length;
|
|
7154
|
+
completedSessionId = completedSessionIds[0] || null;
|
|
7155
|
+
var waitingSessionIds = Array.from(this.attentionSessions.keys()).sort(function (a, b) {
|
|
7156
|
+
return PENDING_KIND_RANK.indexOf(self.attentionSessions.get(a)) - PENDING_KIND_RANK.indexOf(self.attentionSessions.get(b));
|
|
7157
|
+
});
|
|
6746
7158
|
var ready = catalog.phase === 'ready';
|
|
7159
|
+
self.completionChecks.forEach(function (_at, id) {
|
|
7160
|
+
if (!ids.has(id) || !self.inScope(id)) self.completionChecks.delete(id);
|
|
7161
|
+
});
|
|
6747
7162
|
|
|
6748
7163
|
// Availability is "there is a session whose activity this pet could show".
|
|
6749
7164
|
// In `current` scope that must be proved by the selected session actually
|
|
@@ -6756,7 +7171,12 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6756
7171
|
|
|
6757
7172
|
var hasAnything = ready || workingCount > 0 || waitingCount > 0 || Boolean(this.notice);
|
|
6758
7173
|
var available = this.input.connected === true && hasAnything && hasSelectedSession;
|
|
6759
|
-
var
|
|
7174
|
+
var queuedNotice;
|
|
7175
|
+
this.completionQueue.forEach(function (entry, id) {
|
|
7176
|
+
if (!queuedNotice && entry.announce && self.mainSession(id) && self.inScope(id) && self.running(id) === false) queuedNotice = entry;
|
|
7177
|
+
});
|
|
7178
|
+
var notice = available ? (this.notice && this.notice.reason === 'error' ? this.notice
|
|
7179
|
+
: (queuedNotice || (this.notice && !this.viewedCompletions.has(this.notice.sessionId) ? this.notice : undefined))) : undefined;
|
|
6760
7180
|
/**
|
|
6761
7181
|
* `connected` rides along, because 离线 is decided by the CONNECTION and not by `available`.
|
|
6762
7182
|
*
|
|
@@ -6812,6 +7232,8 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6812
7232
|
pendingKind: waitingCount > 0 ? waitingKind : null,
|
|
6813
7233
|
workingCount: workingCount,
|
|
6814
7234
|
waitingCount: waitingCount,
|
|
7235
|
+
waitingSessionId: waitingSessionId,
|
|
7236
|
+
waitingSessionIds: waitingSessionIds,
|
|
6815
7237
|
/**
|
|
6816
7238
|
* Sessions whose completion is still UNREAD (see above), and whether there are any.
|
|
6817
7239
|
*
|
|
@@ -6819,6 +7241,10 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6819
7241
|
* surface could say more later without another plumbing round.
|
|
6820
7242
|
*/
|
|
6821
7243
|
unreadCount: unreadCount,
|
|
7244
|
+
completedUnreadCount: completedUnreadCount,
|
|
7245
|
+
completedSessionId: completedSessionId,
|
|
7246
|
+
completedSessionIds: completedSessionIds,
|
|
7247
|
+
viewedCompletions: Array.from(this.viewedCompletions),
|
|
6822
7248
|
hasUnread: unreadCount > 0,
|
|
6823
7249
|
/** The breakdown behind that count — see `unreadDetail` where it is built. */
|
|
6824
7250
|
unreadDetail: unreadDetail,
|
|
@@ -7299,6 +7725,14 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
7299
7725
|
STATES: STATES,
|
|
7300
7726
|
INTERACTION_STATES: INTERACTION_STATES,
|
|
7301
7727
|
STATUS_STATES: STATUS_STATES,
|
|
7728
|
+
LANGUAGE_SETTINGS: LANGUAGE_SETTINGS,
|
|
7729
|
+
cleanLanguage: cleanLanguage,
|
|
7730
|
+
resolveLanguage: resolveLanguage,
|
|
7731
|
+
browserLanguageHints: browserLanguageHints,
|
|
7732
|
+
textFor: textFor,
|
|
7733
|
+
textOf: textOf,
|
|
7734
|
+
applyI18n: applyI18n,
|
|
7735
|
+
defaultStatusText: defaultStatusText,
|
|
7302
7736
|
DEFAULT_STATUS_TEXT: DEFAULT_STATUS_TEXT,
|
|
7303
7737
|
STATUS_TAGS: STATUS_TAGS,
|
|
7304
7738
|
STATUS_ANIMATIONS: STATUS_ANIMATIONS,
|