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/client.core.js
CHANGED
|
@@ -79,16 +79,32 @@
|
|
|
79
79
|
* The `{duration}` token and its machinery stay — a line the user writes may still carry it (see
|
|
80
80
|
* {@link STATUS_DURATION_TOKEN}) — but the shipped line is DSH's token-free 「深度求索中 ···」, which is also what the
|
|
81
81
|
* app shows while the latest turn is not open.
|
|
82
|
+
*
|
|
83
|
+
* ONE TABLE PER LANGUAGE, keyed state → language, the same shape `SPEECH_LINES` uses for its pools and for the
|
|
84
|
+
* same reason: a status line is TEXT THE USER READS, so an interface in English that still said 「任务达成」 would
|
|
85
|
+
* be a half-translated one. The two halves are written rather than translated word for word, and the Chinese is
|
|
86
|
+
* the operator's own wording, unchanged since 2026-10-13.
|
|
87
|
+
*
|
|
88
|
+
* An EDIT wins over both: `statusEntryFor` only reaches for these while the user's own box is empty, so a line
|
|
89
|
+
* typed by hand is one line in one language and stays exactly that, in either interface language. That is the
|
|
90
|
+
* whole reason this is a per-language DEFAULT rather than a second field in the settings.
|
|
82
91
|
*/
|
|
83
92
|
var DEFAULT_STATUS_TEXT = {
|
|
84
|
-
resting: '',
|
|
85
|
-
working: '深度求索中 ···',
|
|
86
|
-
waiting: '我有问题要问你',
|
|
87
|
-
done: '任务达成',
|
|
88
|
-
error: '遇到问题了...',
|
|
89
|
-
offline: '',
|
|
93
|
+
resting: { zh: '', en: '' },
|
|
94
|
+
working: { zh: '深度求索中 ···', en: 'Working ···' },
|
|
95
|
+
waiting: { zh: '我有问题要问你', en: 'I have a question for you' },
|
|
96
|
+
done: { zh: '任务达成', en: 'Task complete' },
|
|
97
|
+
error: { zh: '遇到问题了...', en: 'Something went wrong...' },
|
|
98
|
+
offline: { zh: '', en: '' },
|
|
90
99
|
};
|
|
91
100
|
|
|
101
|
+
/** One state's shipped line in one language; `''` for a state that says nothing by default. */
|
|
102
|
+
function defaultStatusText(state, language) {
|
|
103
|
+
var entry = DEFAULT_STATUS_TEXT[state];
|
|
104
|
+
if (!entry || typeof entry !== 'object') return '';
|
|
105
|
+
return typeof entry[language === 'en' ? 'en' : 'zh'] === 'string' ? entry[language === 'en' ? 'en' : 'zh'] : '';
|
|
106
|
+
}
|
|
107
|
+
|
|
92
108
|
/**
|
|
93
109
|
* The token a status line may carry where the live elapsed time goes, and the units it is written with.
|
|
94
110
|
*
|
|
@@ -104,6 +120,155 @@
|
|
|
104
120
|
en: { hour: 'h ', minute: 'm ', second: 's' },
|
|
105
121
|
};
|
|
106
122
|
|
|
123
|
+
/**
|
|
124
|
+
* The three answers to 「界面语言」, and the ONE rule that turns them into the language a surface draws in.
|
|
125
|
+
*
|
|
126
|
+
* `auto` is the shipped answer and means "whatever the surface is already running in": DSH's own locale inside
|
|
127
|
+
* the app, the browser's on a host-served page, and on a surface that can see neither (a suite, a headless
|
|
128
|
+
* host) English — the plugin's own fallback rather than a guess about the operator.
|
|
129
|
+
*
|
|
130
|
+
* The other two are the user overruling that, and they exist because the two questions are NOT the same one:
|
|
131
|
+
* DSH can be running in English while its owner wants the pet in Chinese, and the first-run guide is read once,
|
|
132
|
+
* before any of that has settled. Operator, 2026-10-15: 「把全部界面做中英双语自动识别,并在通用设置和初始安装
|
|
133
|
+
* 引导页的第一页欢迎页面中加上语言选择」.
|
|
134
|
+
*
|
|
135
|
+
* `auto` is STORED as `auto`, never resolved on write. A profile that stored the resolved 'zh' would freeze the
|
|
136
|
+
* language on the day it was created, and the user who later switched DSH to English would keep a Chinese pet
|
|
137
|
+
* with no way to tell that from a deliberate choice — the same mistake `DEFAULTS.petSurface` documents for
|
|
138
|
+
* 显示位置, and it is why this is a preference the CLEANER knows about rather than a value a page writes.
|
|
139
|
+
*/
|
|
140
|
+
var LANGUAGE_SETTINGS = ['auto', 'zh', 'en'];
|
|
141
|
+
|
|
142
|
+
/** `'auto'` for anything that is not one of the three — an unknown stored value is a reset, not an error. */
|
|
143
|
+
function cleanLanguage(value) {
|
|
144
|
+
return LANGUAGE_SETTINGS.indexOf(value) === -1 ? 'auto' : value;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Which of the two languages this surface draws in, given the user's setting and what the environment says.
|
|
149
|
+
*
|
|
150
|
+
* `hints` is a LIST, most authoritative first — DSH's locale, then the document's own `lang`, then
|
|
151
|
+
* `navigator.language` — and the FIRST hint that says anything decides. That order is the rule and not a detail:
|
|
152
|
+
* DSH is the application the pet lives inside, so its language outranks the browser's, and a page DSH has
|
|
153
|
+
* already marked `lang="zh"` outranks a browser whose own preference is en-US.
|
|
154
|
+
*
|
|
155
|
+
* The first hint decides the ZH-or-not question instead of the chain continuing past it, because this plugin has
|
|
156
|
+
* exactly two languages. A hint of `fr` is not a reason to go looking for a Chinese one further down the chain;
|
|
157
|
+
* it is a reason to fall back to English, which is also what `auto` with nothing to go on means.
|
|
158
|
+
*/
|
|
159
|
+
function resolveLanguage(preference, hints) {
|
|
160
|
+
var wanted = cleanLanguage(preference);
|
|
161
|
+
if (wanted !== 'auto') return wanted;
|
|
162
|
+
var list = Array.isArray(hints) ? hints : [hints];
|
|
163
|
+
for (var i = 0; i < list.length; i += 1) {
|
|
164
|
+
var hint = list[i];
|
|
165
|
+
if (typeof hint !== 'string' || !hint) continue;
|
|
166
|
+
return /^(zh|cn|chinese)/i.test(hint.trim()) ? 'zh' : 'en';
|
|
167
|
+
}
|
|
168
|
+
return 'en';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* The environment's own two hints, in the order they outrank each other — what a surface that has no DSH locale
|
|
173
|
+
* to ask uses. `scope` exists so a suite can hand in a fake window instead of moving the real one.
|
|
174
|
+
*
|
|
175
|
+
* The BROWSER comes first, and the document's own `lang` second, which is the opposite of the order the app's
|
|
176
|
+
* widget uses — deliberately. Every page this plugin serves ships `<html lang="zh">` (the plugin was written in
|
|
177
|
+
* Chinese), so a page that read its own markup first could never be auto-detected as English: the hint would be
|
|
178
|
+
* its own default rather than anything about the user. `navigator.language` is the browser's answer and in the
|
|
179
|
+
* floating window it is Electron's, i.e. the machine's.
|
|
180
|
+
*/
|
|
181
|
+
function browserLanguageHints(scope) {
|
|
182
|
+
var win = scope || (typeof globalThis !== 'undefined' ? globalThis : null);
|
|
183
|
+
var doc = win && win.document;
|
|
184
|
+
var nav = win && win.navigator;
|
|
185
|
+
return [
|
|
186
|
+
nav ? (nav.language || (nav.languages && nav.languages[0])) : null,
|
|
187
|
+
doc && doc.documentElement ? doc.documentElement.lang : null,
|
|
188
|
+
];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* One surface's own wording, in the two languages, chosen by {@link resolveLanguage}.
|
|
193
|
+
*
|
|
194
|
+
* Every surface keeps its OWN table — `lib/menu.js`'s `LABELS`, `lib/model-picker.js`'s `TEXT`, the settings
|
|
195
|
+
* pages' `TEXT` — rather than one global dictionary, because that is already this repository's shape (a surface
|
|
196
|
+
* owns the words it draws) and because a single table for six surfaces would make every one of them edit the
|
|
197
|
+
* same file. What IS shared is the SHAPE: `{ zh: {...}, en: {...} }`, this accessor, and {@link applyI18n}.
|
|
198
|
+
*
|
|
199
|
+
* A language half that is missing or empty falls back to the other one rather than to blank: a half-translated
|
|
200
|
+
* surface reads as Chinese in one spot, which is a bug somebody reports, where a blank label is a page that
|
|
201
|
+
* looks broken. `textOf` below is what makes a GAP visible, and a suite is what keeps one from shipping.
|
|
202
|
+
*/
|
|
203
|
+
function textFor(table, language) {
|
|
204
|
+
if (!table || typeof table !== 'object') return {};
|
|
205
|
+
var wanted = language === 'en' ? table.en : table.zh;
|
|
206
|
+
var other = language === 'en' ? table.zh : table.en;
|
|
207
|
+
if (wanted && typeof wanted === 'object') return wanted;
|
|
208
|
+
return other && typeof other === 'object' ? other : {};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** One entry out of a surface's table. A missing key returns the KEY, so a gap is visible on screen. */
|
|
212
|
+
function textOf(table, language, key) {
|
|
213
|
+
var value = textFor(table, language)[key];
|
|
214
|
+
return typeof value === 'string' ? value : key;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Fill a document's marked-up text from one surface's table.
|
|
219
|
+
*
|
|
220
|
+
* The markup carries the CHINESE text as well as the key, and that is on purpose: this plugin's pages all ship a
|
|
221
|
+
* bootstrap watchdog for the case where a script never ran, so a page whose script DID not run should still say
|
|
222
|
+
* something in one language rather than show nothing. `verify-desktop.mjs` asserts the two agree, which is what
|
|
223
|
+
* stops the markup and the table from drifting apart.
|
|
224
|
+
*
|
|
225
|
+
* Four attributes, one per place text lives: `data-i18n` writes `textContent`, `data-i18n-html` writes
|
|
226
|
+
* `innerHTML` and is for OUR OWN sentence with a `<strong>` in it (never for anything a user or a model wrote),
|
|
227
|
+
* and `data-i18n-placeholder` / `data-i18n-title` / `data-i18n-aria` write those three attributes.
|
|
228
|
+
*
|
|
229
|
+
* A key the table does not have leaves the markup ALONE rather than printing the key: the visible text stays the
|
|
230
|
+
* Chinese fallback, and the missing entry is the suite's business, not the user's.
|
|
231
|
+
*
|
|
232
|
+
* @returns {number} how many places were rewritten, so a suite can tell "applied nothing" from "no markup".
|
|
233
|
+
*/
|
|
234
|
+
function applyI18n(root, table, language) {
|
|
235
|
+
var doc = root && typeof root.querySelectorAll === 'function'
|
|
236
|
+
? root
|
|
237
|
+
: (typeof globalThis !== 'undefined' ? globalThis.document : null);
|
|
238
|
+
if (!doc || typeof doc.querySelectorAll !== 'function') return 0;
|
|
239
|
+
var words = textFor(table, language);
|
|
240
|
+
var selector = '[data-i18n],[data-i18n-html],[data-i18n-placeholder],[data-i18n-title],[data-i18n-aria]';
|
|
241
|
+
var nodes;
|
|
242
|
+
try {
|
|
243
|
+
nodes = doc.querySelectorAll(selector);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
return 0;
|
|
246
|
+
}
|
|
247
|
+
var applied = 0;
|
|
248
|
+
var write = function (node, attribute, key, html) {
|
|
249
|
+
var value = words[key];
|
|
250
|
+
if (typeof value !== 'string') return;
|
|
251
|
+
if (attribute === 'data-i18n') node.textContent = value;
|
|
252
|
+
else if (html) node.innerHTML = value;
|
|
253
|
+
else node.setAttribute(attribute, value);
|
|
254
|
+
applied += 1;
|
|
255
|
+
};
|
|
256
|
+
for (var i = 0; i < nodes.length; i += 1) {
|
|
257
|
+
var node = nodes[i];
|
|
258
|
+
var key = node.getAttribute('data-i18n');
|
|
259
|
+
if (key) write(node, 'data-i18n', key, false);
|
|
260
|
+
key = node.getAttribute('data-i18n-html');
|
|
261
|
+
if (key) write(node, 'data-i18n-html', key, true);
|
|
262
|
+
key = node.getAttribute('data-i18n-placeholder');
|
|
263
|
+
if (key) write(node, 'placeholder', key, false);
|
|
264
|
+
key = node.getAttribute('data-i18n-title');
|
|
265
|
+
if (key) write(node, 'title', key, false);
|
|
266
|
+
key = node.getAttribute('data-i18n-aria');
|
|
267
|
+
if (key) write(node, 'aria-label', key, false);
|
|
268
|
+
}
|
|
269
|
+
return applied;
|
|
270
|
+
}
|
|
271
|
+
|
|
107
272
|
/**
|
|
108
273
|
* The states whose prompt carries DSH's whale mark.
|
|
109
274
|
*
|
|
@@ -1022,9 +1187,8 @@
|
|
|
1022
1187
|
}
|
|
1023
1188
|
|
|
1024
1189
|
/**
|
|
1025
|
-
*
|
|
1026
|
-
*
|
|
1027
|
-
* same thing in the floating window (the screen's width) and in the app overlay (its window's width).
|
|
1190
|
+
* A positive setting overrides automatic distance. Both axes and both surfaces
|
|
1191
|
+
* pass the screen width here; automatic distance is half of that width.
|
|
1028
1192
|
*/
|
|
1029
1193
|
function resolveGazeSpan(span, fallback) {
|
|
1030
1194
|
var wanted = Number(span);
|
|
@@ -1032,7 +1196,7 @@
|
|
|
1032
1196
|
return clamp(wanted, GAZE_SPAN_RANGE.min, GAZE_SPAN_RANGE.max);
|
|
1033
1197
|
}
|
|
1034
1198
|
var base = Number(fallback);
|
|
1035
|
-
return isFiniteNumber(base) && base > 0 ? base : GAZE_SPAN.x;
|
|
1199
|
+
return isFiniteNumber(base) && base > 0 ? base / 2 : GAZE_SPAN.x;
|
|
1036
1200
|
}
|
|
1037
1201
|
|
|
1038
1202
|
/**
|
|
@@ -1054,7 +1218,9 @@
|
|
|
1054
1218
|
* Pure numbers, no DOM, so the logic suite covers the clamping and the signs.
|
|
1055
1219
|
*
|
|
1056
1220
|
* @param {{pointerX:number, pointerY:number, centreX:number, centreY:number,
|
|
1057
|
-
* spanX?:number, spanY?:number, curve?:number
|
|
1221
|
+
* spanX?:number, spanY?:number, curve?:number, curveY?:number,
|
|
1222
|
+
* curveRadial?:number, curveSpatial?:number, distance?:number,
|
|
1223
|
+
* mode?:'axes'|'radial'|'spatial'}} input
|
|
1058
1224
|
* @returns {{x:number, y:number}|null} `null` when the input is unusable; `y` is
|
|
1059
1225
|
* positive BELOW the pet, i.e. in screen coordinates
|
|
1060
1226
|
*/
|
|
@@ -1073,12 +1239,39 @@
|
|
|
1073
1239
|
: GAZE_SPAN.y;
|
|
1074
1240
|
// The settings' curve, unless the caller passes its own (the suite does, to pin a shape).
|
|
1075
1241
|
var edgeSlope = isFiniteNumber(Number(input.curve)) ? Number(input.curve) : DEFAULTS.gazeCurve;
|
|
1242
|
+
var slopeY = isFiniteNumber(Number(input.curveY)) ? Number(input.curveY) : edgeSlope;
|
|
1243
|
+
if (input.mode === 'spatial') {
|
|
1244
|
+
var spatialX = (pointerX - centreX) / spanX;
|
|
1245
|
+
var spatialY = (pointerY - centreY) / spanY;
|
|
1246
|
+
if (!isFiniteNumber(spatialX) || !isFiniteNumber(spatialY)) return null;
|
|
1247
|
+
var depth = isFiniteNumber(Number(input.distance)) && Number(input.distance) > 0
|
|
1248
|
+
? clamp(Number(input.distance), 0.1, 5) : DEFAULTS.gazeDistance;
|
|
1249
|
+
// A virtual target plane in span units: sideways distance reduces pitch
|
|
1250
|
+
// at a fixed pointer height. Curves shape angles, rather than pixel offsets.
|
|
1251
|
+
var yawAngle = Math.atan2(spatialX, depth) / (Math.PI / 2);
|
|
1252
|
+
var pitchAngle = Math.atan2(spatialY, Math.hypot(spatialX, depth)) / (Math.PI / 2);
|
|
1253
|
+
var spatialSlope = isFiniteNumber(Number(input.curveSpatial)) ? Number(input.curveSpatial) : DEFAULTS.gazeCurveSpatial;
|
|
1254
|
+
return {
|
|
1255
|
+
x: (yawAngle < 0 ? -1 : 1) * gazeCurve(Math.abs(yawAngle), spatialSlope),
|
|
1256
|
+
y: (pitchAngle < 0 ? -1 : 1) * gazeCurve(Math.abs(pitchAngle), spatialSlope),
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
if (input.mode === 'radial') {
|
|
1260
|
+
var dx = (pointerX - centreX) / spanX;
|
|
1261
|
+
var dy = (pointerY - centreY) / spanY;
|
|
1262
|
+
var distance = Math.hypot(dx, dy);
|
|
1263
|
+
if (!isFiniteNumber(distance)) return null;
|
|
1264
|
+
if (distance === 0) return { x: 0, y: 0 };
|
|
1265
|
+
var radialSlope = isFiniteNumber(Number(input.curveRadial)) ? Number(input.curveRadial) : edgeSlope;
|
|
1266
|
+
var gain = gazeCurve(Math.min(1, distance), radialSlope) / distance;
|
|
1267
|
+
return { x: dx * gain, y: dy * gain };
|
|
1268
|
+
}
|
|
1076
1269
|
var rawX = clamp((pointerX - centreX) / spanX, -1, 1);
|
|
1077
1270
|
var rawY = clamp((pointerY - centreY) / spanY, -1, 1);
|
|
1078
1271
|
// Mirrored through `gazeCurve` so the sign survives and the response is the curve on both sides.
|
|
1079
1272
|
return {
|
|
1080
1273
|
x: rawX < 0 ? -gazeCurve(-rawX, edgeSlope) : gazeCurve(rawX, edgeSlope),
|
|
1081
|
-
y: rawY < 0 ? -gazeCurve(-rawY,
|
|
1274
|
+
y: rawY < 0 ? -gazeCurve(-rawY, slopeY) : gazeCurve(rawY, slopeY)
|
|
1082
1275
|
};
|
|
1083
1276
|
}
|
|
1084
1277
|
|
|
@@ -1088,6 +1281,12 @@
|
|
|
1088
1281
|
/** Default pose and behaviour knobs. */
|
|
1089
1282
|
var DEFAULTS = {
|
|
1090
1283
|
scope: 'global',
|
|
1284
|
+
/**
|
|
1285
|
+
* 界面语言 — `auto` follows the surface's own environment ({@link resolveLanguage}), and 中文 / English overrule
|
|
1286
|
+
* it. It sits first in the table rather than beside the appearance rows because it is the one preference that
|
|
1287
|
+
* decides how every OTHER row is spelled.
|
|
1288
|
+
*/
|
|
1289
|
+
language: 'auto',
|
|
1091
1290
|
size: 100,
|
|
1092
1291
|
/**
|
|
1093
1292
|
* WHERE the pet is shown, as the cleaner's DEFAULT rather than a string repeated beside it.
|
|
@@ -1185,12 +1384,21 @@
|
|
|
1185
1384
|
* operator's own (2026-10-13: 「视线:曲率设置默认为0.1,提供0.05-1的选择(原先是0.1-1),静止停看默认为2秒」).
|
|
1186
1385
|
* `gazeCurve` is the curve's slope at the frame's edge — 1 is a straight line, smaller is a
|
|
1187
1386
|
* steeper start and a flatter tail — and `gazeSpanX`/`gazeSpanY` are the full-deflection
|
|
1188
|
-
* distances in CSS pixels, where **0 means AUTO**:
|
|
1189
|
-
*
|
|
1387
|
+
* distances in CSS pixels, where **0 means AUTO**: half the screen width on both axes.
|
|
1388
|
+
* `gazeCurve` / `gazeCurveY` remain readable for older delivered clients; the current
|
|
1389
|
+
* settings expose only spatial (default) and radial modes.
|
|
1190
1390
|
*/
|
|
1191
1391
|
gazeCurve: 0.1,
|
|
1392
|
+
gazeCurveMode: 'spatial',
|
|
1393
|
+
gazeCurveY: 0.1,
|
|
1394
|
+
gazeCurveRadial: 0.1,
|
|
1395
|
+
gazeCurveSpatial: 1,
|
|
1396
|
+
gazeDistance: 0.1,
|
|
1192
1397
|
gazeSpanX: 0,
|
|
1193
1398
|
gazeSpanY: 0,
|
|
1399
|
+
// Actual head pitch in degrees, after the renderer's gaze weight.
|
|
1400
|
+
gazePitchUp: 60,
|
|
1401
|
+
gazePitchDown: 60,
|
|
1194
1402
|
/**
|
|
1195
1403
|
* How long the cursor may sit still before the pet stops following it, in seconds.
|
|
1196
1404
|
*
|
|
@@ -1302,6 +1510,7 @@
|
|
|
1302
1510
|
* not silently pin drag and gravity to the numbers the panel happened to be showing.
|
|
1303
1511
|
*/
|
|
1304
1512
|
springs: {},
|
|
1513
|
+
toon: {},
|
|
1305
1514
|
/**
|
|
1306
1515
|
* Per-model size multiplier for BOTH texts: `{ [modelId]: number }`.
|
|
1307
1516
|
*
|
|
@@ -1933,6 +2142,13 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
1933
2142
|
var out = {};
|
|
1934
2143
|
var source = raw && typeof raw === 'object' ? raw : {};
|
|
1935
2144
|
out.scope = source.scope === 'current' ? 'current' : 'global';
|
|
2145
|
+
/**
|
|
2146
|
+
* 界面语言 — cleaned like every other enumerated preference: an unknown value (an older profile, a typo, a page
|
|
2147
|
+
* that sent the wrong thing) is `auto`, which resolves to the surface's own environment anyway. It is NOT
|
|
2148
|
+
* counted as authorship by `authoredWeight` over in `lib/settings.js`: a language is a way of looking at the
|
|
2149
|
+
* pet, not something the user wrote into it.
|
|
2150
|
+
*/
|
|
2151
|
+
out.language = cleanLanguage(source.language);
|
|
1936
2152
|
// An unknown surface falls back to the shipped default, not to a string repeated here.
|
|
1937
2153
|
out.petSurface = PET_SURFACES.indexOf(source.petSurface) === -1 ? DEFAULTS.petSurface : source.petSurface;
|
|
1938
2154
|
out.size = Number.isFinite(Number(source.size)) ? clamp(Number(source.size), SIZE_RANGE.min, SIZE_RANGE.max) : DEFAULTS.size;
|
|
@@ -2029,12 +2245,25 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
2029
2245
|
out.gazeCurve = Number.isFinite(Number(source.gazeCurve))
|
|
2030
2246
|
? clamp(Number(source.gazeCurve), GAZE_CURVE_RANGE.min, GAZE_CURVE_RANGE.max)
|
|
2031
2247
|
: DEFAULTS.gazeCurve;
|
|
2248
|
+
out.gazeCurveMode = source.gazeCurveMode === 'radial' ? 'radial' : 'spatial';
|
|
2249
|
+
['gazeCurveY', 'gazeCurveRadial'].forEach(function (key) {
|
|
2250
|
+
out[key] = typeof source[key] === 'number' && Number.isFinite(source[key])
|
|
2251
|
+
? clamp(source[key], GAZE_CURVE_RANGE.min, GAZE_CURVE_RANGE.max) : DEFAULTS[key];
|
|
2252
|
+
});
|
|
2253
|
+
out.gazeCurveSpatial = typeof source.gazeCurveSpatial === 'number' && Number.isFinite(source.gazeCurveSpatial)
|
|
2254
|
+
? clamp(source.gazeCurveSpatial, GAZE_CURVE_RANGE.min, GAZE_CURVE_RANGE.max) : DEFAULTS.gazeCurveSpatial;
|
|
2255
|
+
out.gazeDistance = typeof source.gazeDistance === 'number' && Number.isFinite(source.gazeDistance)
|
|
2256
|
+
? clamp(source.gazeDistance, 0.1, 5) : DEFAULTS.gazeDistance;
|
|
2032
2257
|
out.gazeSpanX = Number.isFinite(Number(source.gazeSpanX))
|
|
2033
2258
|
? clamp(Number(source.gazeSpanX), GAZE_SPAN_RANGE.min, GAZE_SPAN_RANGE.max)
|
|
2034
2259
|
: DEFAULTS.gazeSpanX;
|
|
2035
2260
|
out.gazeSpanY = Number.isFinite(Number(source.gazeSpanY))
|
|
2036
2261
|
? clamp(Number(source.gazeSpanY), GAZE_SPAN_RANGE.min, GAZE_SPAN_RANGE.max)
|
|
2037
2262
|
: DEFAULTS.gazeSpanY;
|
|
2263
|
+
['gazePitchUp', 'gazePitchDown'].forEach(function (key) {
|
|
2264
|
+
out[key] = typeof source[key] === 'number' && Number.isFinite(source[key])
|
|
2265
|
+
? clamp(source[key], 0, 60) : DEFAULTS[key];
|
|
2266
|
+
});
|
|
2038
2267
|
// 0 is a legitimate value here — it turns the idle rule off — so this cannot fall back to the default the
|
|
2039
2268
|
// way an unparseable value does. A negative number is unparseable for this purpose and becomes 0.
|
|
2040
2269
|
out.gazeIdleSeconds = Number.isFinite(Number(source.gazeIdleSeconds))
|
|
@@ -2089,6 +2318,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
2089
2318
|
? source.expressions.emotion.parts
|
|
2090
2319
|
: null);
|
|
2091
2320
|
out.springs = cleanSprings(source.springs);
|
|
2321
|
+
out.toon = cleanToon(source.toon);
|
|
2092
2322
|
return out;
|
|
2093
2323
|
}
|
|
2094
2324
|
|
|
@@ -2099,6 +2329,25 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
2099
2329
|
* directory's manifest says `loop: false`) has to be OVERRIDABLE back to looping, and "no entry" cannot
|
|
2100
2330
|
* express that — absence means "ask the catalog".
|
|
2101
2331
|
*/
|
|
2332
|
+
function cleanToon(raw) {
|
|
2333
|
+
var out = {};
|
|
2334
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return out;
|
|
2335
|
+
Object.keys(raw).slice(0, 256).forEach(function (id) {
|
|
2336
|
+
if (id === '__proto__' || id === 'constructor' || id === 'prototype') return;
|
|
2337
|
+
var entry = raw[id];
|
|
2338
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return;
|
|
2339
|
+
var next = {};
|
|
2340
|
+
if (typeof entry.enabled === 'boolean') next.enabled = entry.enabled;
|
|
2341
|
+
var ranges = { shadingToonyFactor: [0, 1], shadingShiftFactor: [-1, 1], shadeStrength: [0, 1], outlineScale: [0, 3] };
|
|
2342
|
+
Object.keys(ranges).forEach(function (key) {
|
|
2343
|
+
var value = entry[key];
|
|
2344
|
+
if (typeof value === 'number' && Number.isFinite(value)) next[key] = clamp(value, ranges[key][0], ranges[key][1]);
|
|
2345
|
+
});
|
|
2346
|
+
if (Object.keys(next).length) out[id] = next;
|
|
2347
|
+
});
|
|
2348
|
+
return out;
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2102
2351
|
function cleanMotionLoop(raw) {
|
|
2103
2352
|
var out = {};
|
|
2104
2353
|
var source = raw && typeof raw === 'object' ? raw : {};
|
|
@@ -2488,8 +2737,8 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
2488
2737
|
// ---------------------------------------------------------------------------
|
|
2489
2738
|
|
|
2490
2739
|
/** One state's status line, exactly as the renderer draws it: text, then the tag. */
|
|
2491
|
-
function statusLineFor(preferences, state) {
|
|
2492
|
-
var entry = statusEntryFor(preferences, state);
|
|
2740
|
+
function statusLineFor(preferences, state, language) {
|
|
2741
|
+
var entry = statusEntryFor(preferences, state, language);
|
|
2493
2742
|
if (!entry) return '';
|
|
2494
2743
|
var tag = typeof entry.tag === 'string' ? entry.tag : '';
|
|
2495
2744
|
return (entry.text + tag).slice(0, STATUS_LIMITS.textLength + STATUS_LIMITS.tagLength);
|
|
@@ -2501,11 +2750,15 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
2501
2750
|
* Always an OBJECT, never null: every state has something to say or an explicit reason to say nothing,
|
|
2502
2751
|
* and a caller that had to distinguish "unconfigured" from "configured empty" would get it wrong in the
|
|
2503
2752
|
* one case that matters (a user who cleared a line).
|
|
2753
|
+
*
|
|
2754
|
+
* `language` picks WHICH shipped default fills an empty box — see {@link DEFAULT_STATUS_TEXT}. It is the third
|
|
2755
|
+
* parameter and optional, so a caller that does not know the surface's language keeps the Chinese default the
|
|
2756
|
+
* suites and the older call sites already pin.
|
|
2504
2757
|
*/
|
|
2505
|
-
function statusEntryFor(preferences, state) {
|
|
2758
|
+
function statusEntryFor(preferences, state, language) {
|
|
2506
2759
|
var map = preferences && preferences.status && typeof preferences.status === 'object' ? preferences.status : {};
|
|
2507
2760
|
var stored = map[state] && typeof map[state] === 'object' ? map[state] : {};
|
|
2508
|
-
var fallback =
|
|
2761
|
+
var fallback = defaultStatusText(state, language);
|
|
2509
2762
|
var text = typeof stored.text === 'string' ? stored.text : fallback;
|
|
2510
2763
|
var fallbackAnimation = Object.prototype.hasOwnProperty.call(DEFAULT_STATUS_ANIMATION, state)
|
|
2511
2764
|
? DEFAULT_STATUS_ANIMATION[state]
|
|
@@ -3378,6 +3631,14 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
3378
3631
|
* @returns {string|null} the line to show, or null for silence
|
|
3379
3632
|
*/
|
|
3380
3633
|
SpeechMachine.prototype.observe = function (preferences, state, pendingKind, language, random, motion) {
|
|
3634
|
+
if (state === 'dancing') {
|
|
3635
|
+
this.current = null;
|
|
3636
|
+
this.wantsAi = null;
|
|
3637
|
+
this.lastEvent = null;
|
|
3638
|
+
this.idleAt = null;
|
|
3639
|
+
this.chatterAt = null;
|
|
3640
|
+
return null;
|
|
3641
|
+
}
|
|
3381
3642
|
var speech = cleanSpeech(preferences && preferences.speech);
|
|
3382
3643
|
var t = this.now();
|
|
3383
3644
|
if (!speech.enabled) {
|
|
@@ -3634,11 +3895,17 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
3634
3895
|
* clause is DROPPED — DSH's own token-free 「深度求索中」 rather than a number invented here. Passed per call so the
|
|
3635
3896
|
* two surfaces (the app's pet and the floating window) can only differ by what they are told, never by the rule.
|
|
3636
3897
|
*
|
|
3898
|
+
* `language` is the SIXTH parameter and it reaches two places: which shipped default an empty box falls back to
|
|
3899
|
+
* ({@link DEFAULT_STATUS_TEXT}), and the units a `{duration}` clause is written in — DSH writes 1分30秒 in one
|
|
3900
|
+
* language and 1m 30s in the other, so a line translated without its clock would read as a foreign number.
|
|
3901
|
+
* Optional, like the status-entry language above: a caller that has no language to pass keeps the Chinese answer
|
|
3902
|
+
* every existing suite pins.
|
|
3903
|
+
*
|
|
3637
3904
|
* @returns {{state: string, text: string, tag: string, animation: string, seconds: number}|null}
|
|
3638
3905
|
*/
|
|
3639
|
-
function statusFor(preferences, state, enteredAt, now, clock) {
|
|
3906
|
+
function statusFor(preferences, state, enteredAt, now, clock, language, completedCount) {
|
|
3640
3907
|
if (STATUS_STATES.indexOf(state) === -1) return null;
|
|
3641
|
-
var entry = statusEntryFor(preferences, state);
|
|
3908
|
+
var entry = statusEntryFor(preferences, state, language);
|
|
3642
3909
|
if (!entry.text && !entry.tag) return null;
|
|
3643
3910
|
var at = Number.isFinite(now) ? now : Date.now();
|
|
3644
3911
|
/**
|
|
@@ -3691,7 +3958,10 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
3691
3958
|
}
|
|
3692
3959
|
text = elapsed === null
|
|
3693
3960
|
? durationClauseRemoved(text)
|
|
3694
|
-
: text.split(STATUS_DURATION_TOKEN).join(formatRunDuration(elapsed));
|
|
3961
|
+
: text.split(STATUS_DURATION_TOKEN).join(formatRunDuration(elapsed, language));
|
|
3962
|
+
}
|
|
3963
|
+
if (state === 'done' && Number(completedCount) > 1 && text) {
|
|
3964
|
+
text = language === 'en' ? (text === 'Task complete' ? 'All tasks complete' : 'All ' + text) : '全部' + text;
|
|
3695
3965
|
}
|
|
3696
3966
|
var look = statusTone(state);
|
|
3697
3967
|
return {
|
|
@@ -4726,6 +4996,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
4726
4996
|
* @param {object} options url, intervalMs, fetchImpl, onPreview(preview|null), onUnsupported
|
|
4727
4997
|
*/
|
|
4728
4998
|
function watchHostPreview(options) {
|
|
4999
|
+
var navigationSeen = null;
|
|
4729
5000
|
var settings = options || {};
|
|
4730
5001
|
var url = settings.url || '/vrmpet3d/preview';
|
|
4731
5002
|
// Four hundred milliseconds: this drives a hover preview, so it has to feel immediate, and the route is a
|
|
@@ -4734,11 +5005,14 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
4734
5005
|
var doFetch = settings.fetchImpl || (typeof fetch === 'function' ? fetch : null);
|
|
4735
5006
|
var timer = null;
|
|
4736
5007
|
var seen = -1;
|
|
5008
|
+
var issued = 0;
|
|
5009
|
+
var delivered = 0;
|
|
4737
5010
|
var supported = Boolean(doFetch);
|
|
4738
5011
|
var stopped = false;
|
|
4739
5012
|
|
|
4740
5013
|
function load() {
|
|
4741
5014
|
if (!supported || stopped) return Promise.resolve(null);
|
|
5015
|
+
var request = ++issued;
|
|
4742
5016
|
return doFetch(url, { credentials: 'omit' })
|
|
4743
5017
|
.then(function (response) {
|
|
4744
5018
|
if (response.status === 404 || response.status === 405) {
|
|
@@ -4750,6 +5024,14 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
4750
5024
|
return response.json ? response.json() : null;
|
|
4751
5025
|
})
|
|
4752
5026
|
.then(function (body) {
|
|
5027
|
+
if (request < delivered) return null;
|
|
5028
|
+
if (body && typeof body.seq === 'number') delivered = request;
|
|
5029
|
+
if (body && body.navigation && typeof body.navigation.seq === 'number') {
|
|
5030
|
+
var navigation = body.navigation;
|
|
5031
|
+
if (navigationSeen !== null && navigationSeen !== navigation.seq && typeof navigation.sessionId === 'string'
|
|
5032
|
+
&& typeof settings.onNavigate === 'function') settings.onNavigate(navigation.sessionId);
|
|
5033
|
+
navigationSeen = navigation.seq;
|
|
5034
|
+
}
|
|
4753
5035
|
if (!body || typeof body.seq !== 'number' || body.seq === seen) return null;
|
|
4754
5036
|
seen = body.seq;
|
|
4755
5037
|
var preview = cleanPreview(body.preview);
|
|
@@ -4887,11 +5169,14 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
4887
5169
|
var doFetch = settings.fetchImpl || (typeof fetch === 'function' ? fetch : null);
|
|
4888
5170
|
var timer = null;
|
|
4889
5171
|
var seen = -1;
|
|
5172
|
+
var issued = 0;
|
|
5173
|
+
var delivered = 0;
|
|
4890
5174
|
var supported = Boolean(doFetch);
|
|
4891
5175
|
var stopped = false;
|
|
4892
5176
|
|
|
4893
5177
|
function load() {
|
|
4894
5178
|
if (!supported || stopped) return Promise.resolve(null);
|
|
5179
|
+
var request = ++issued;
|
|
4895
5180
|
return doFetch(url, { credentials: 'omit' })
|
|
4896
5181
|
.then(function (response) {
|
|
4897
5182
|
if (response.status === 404 || response.status === 405) {
|
|
@@ -4903,6 +5188,8 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
4903
5188
|
return response.json ? response.json() : null;
|
|
4904
5189
|
})
|
|
4905
5190
|
.then(function (body) {
|
|
5191
|
+
if (request < delivered) return null;
|
|
5192
|
+
if (body && typeof body.seq === 'number') delivered = request;
|
|
4906
5193
|
if (!body || typeof body.seq !== 'number' || body.seq === seen) return null;
|
|
4907
5194
|
seen = body.seq;
|
|
4908
5195
|
var state = STATES.indexOf(body.state) === -1 ? null : body.state;
|
|
@@ -5012,6 +5299,12 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5012
5299
|
}
|
|
5013
5300
|
if (typeof why.scope === 'string' && why.scope) out.scope = why.scope.slice(0, 16);
|
|
5014
5301
|
if (typeof why.session === 'string' && why.session) out.session = why.session.slice(0, 64);
|
|
5302
|
+
if (typeof why.statusSessionId === 'string' && why.statusSessionId) out.statusSessionId = why.statusSessionId.slice(0, 64);
|
|
5303
|
+
if (typeof why.completedSessionId === 'string' && why.completedSessionId) out.completedSessionId = why.completedSessionId.slice(0, 64);
|
|
5304
|
+
['waitingSessionIds', 'completedSessionIds'].forEach(function (key) {
|
|
5305
|
+
if (Array.isArray(why[key])) out[key] = why[key].filter(function (id) { return typeof id === 'string' && id; })
|
|
5306
|
+
.slice(0, 256).map(function (id) { return id.slice(0, 64); });
|
|
5307
|
+
});
|
|
5015
5308
|
if (typeof why.interaction === 'string' && why.interaction) out.interaction = why.interaction.slice(0, 16);
|
|
5016
5309
|
if (Array.isArray(why.runningIds)) {
|
|
5017
5310
|
out.runningIds = why.runningIds
|
|
@@ -5169,15 +5462,19 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5169
5462
|
* its 进行中 line has to travel with the state. A whole number, or nothing — an older host that does not send it
|
|
5170
5463
|
* leaves the window's dot as it was rather than inventing a count.
|
|
5171
5464
|
*/
|
|
5172
|
-
var nextUnread = Number.isFinite(
|
|
5465
|
+
var nextUnread = typeof unread === 'number' && Number.isFinite(unread) && unread >= 0 ? Math.floor(unread) : null;
|
|
5173
5466
|
/**
|
|
5174
5467
|
* The diagnostic, kept OUT of the change key on purpose: it carries counts and clock readings that move
|
|
5175
5468
|
* constantly, and letting them force a report would turn "the pet announces a change" into a 1 Hz feed. It
|
|
5176
5469
|
* rides along with the reports the state and the line already earn, and the heartbeat re-states the last one.
|
|
5177
5470
|
*/
|
|
5178
5471
|
var nextWhy = cleanWhy(why);
|
|
5472
|
+
var navigationKey = function (value) {
|
|
5473
|
+
return JSON.stringify(value ? [value.waitingSessionIds || [], value.completedSessionIds || [],
|
|
5474
|
+
value.statusSessionId || null, value.completedSessionId || null] : [[], [], null, null]);
|
|
5475
|
+
};
|
|
5179
5476
|
if (force !== true && next === sent && nextLine === sentLine && nextTouchKey === sentTouchKey
|
|
5180
|
-
&& nextClockKey === sentClockKey && nextUnread === sentUnread) {
|
|
5477
|
+
&& nextClockKey === sentClockKey && nextUnread === sentUnread && navigationKey(nextWhy) === navigationKey(sentWhy)) {
|
|
5181
5478
|
return Promise.resolve(null);
|
|
5182
5479
|
}
|
|
5183
5480
|
sent = next;
|
|
@@ -5302,10 +5599,15 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5302
5599
|
var doFetch = settings.fetchImpl || (typeof fetch === 'function' ? fetch : null);
|
|
5303
5600
|
var timer = null;
|
|
5304
5601
|
var seen = -1;
|
|
5602
|
+
var issued = 0;
|
|
5603
|
+
var delivered = 0;
|
|
5305
5604
|
var supported = Boolean(doFetch);
|
|
5306
5605
|
var stopped = false;
|
|
5307
5606
|
|
|
5308
|
-
function deliver(snapshot) {
|
|
5607
|
+
function deliver(snapshot, request) {
|
|
5608
|
+
// A slow poll/write must not roll a live gesture back to an older reply.
|
|
5609
|
+
if (request < delivered) return null;
|
|
5610
|
+
if (snapshot && typeof snapshot.version === 'number') delivered = request;
|
|
5309
5611
|
if (!snapshot || typeof snapshot.version !== 'number' || snapshot.version === seen) return null;
|
|
5310
5612
|
seen = snapshot.version;
|
|
5311
5613
|
if (typeof settings.onSnapshot === 'function') settings.onSnapshot(snapshot);
|
|
@@ -5315,6 +5617,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5315
5617
|
/** One look at the host. Returns a promise for the snapshot it delivered, if any. */
|
|
5316
5618
|
function load() {
|
|
5317
5619
|
if (!supported || stopped) return Promise.resolve(null);
|
|
5620
|
+
var request = ++issued;
|
|
5318
5621
|
return doFetch(url, { credentials: 'omit' })
|
|
5319
5622
|
.then(function (response) {
|
|
5320
5623
|
if (response.status === 404 || response.status === 405) {
|
|
@@ -5326,7 +5629,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5326
5629
|
if (!response.ok) return null;
|
|
5327
5630
|
return response.json ? response.json() : null;
|
|
5328
5631
|
})
|
|
5329
|
-
.then(deliver)
|
|
5632
|
+
.then(function (snapshot) { return deliver(snapshot, request); })
|
|
5330
5633
|
.catch(function () { return null; });
|
|
5331
5634
|
}
|
|
5332
5635
|
|
|
@@ -5338,6 +5641,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5338
5641
|
*/
|
|
5339
5642
|
function push(body) {
|
|
5340
5643
|
if (!supported || stopped) return Promise.resolve(null);
|
|
5644
|
+
var request = ++issued;
|
|
5341
5645
|
return doFetch(url, {
|
|
5342
5646
|
method: 'POST',
|
|
5343
5647
|
credentials: 'omit',
|
|
@@ -5345,7 +5649,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5345
5649
|
body: JSON.stringify(body || {}),
|
|
5346
5650
|
})
|
|
5347
5651
|
.then(function (response) { return response.ok ? response.json() : null; })
|
|
5348
|
-
.then(deliver)
|
|
5652
|
+
.then(function (snapshot) { return deliver(snapshot, request); })
|
|
5349
5653
|
.catch(function () { return null; });
|
|
5350
5654
|
}
|
|
5351
5655
|
|
|
@@ -5558,6 +5862,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5558
5862
|
* sits above the watermark and is adopted as usual.
|
|
5559
5863
|
*/
|
|
5560
5864
|
this.dismissedNoticeAt = 0;
|
|
5865
|
+
this.dismissedSessions = new Map();
|
|
5561
5866
|
/**
|
|
5562
5867
|
* The TURN the pet is currently describing, and the one that just ended.
|
|
5563
5868
|
*
|
|
@@ -5840,6 +6145,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5840
6145
|
PetMachine.prototype.dismissNotice = function (keepDebt) {
|
|
5841
6146
|
if (this.notice && Number.isFinite(this.notice.at)) {
|
|
5842
6147
|
this.dismissedNoticeAt = Math.max(this.dismissedNoticeAt, this.notice.at);
|
|
6148
|
+
if (this.notice.sessionId) this.dismissedSessions.set(this.notice.sessionId, this.notice.at);
|
|
5843
6149
|
}
|
|
5844
6150
|
if (keepDebt !== true && this.notice && this.notice.sessionId) {
|
|
5845
6151
|
delete this.retractions[this.notice.sessionId];
|
|
@@ -5962,7 +6268,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
5962
6268
|
* well as in the acknowledgement rule in `resolve`. (The floating window never takes focus, which is why the same
|
|
5963
6269
|
* drag could not do this there.)
|
|
5964
6270
|
*/
|
|
5965
|
-
if (this.notice &&
|
|
6271
|
+
if (this.notice && !this.notice.sessionId && !presenceKnown && this.interaction === null) {
|
|
5966
6272
|
this.noteDismissal('focus-return');
|
|
5967
6273
|
this.dismissNotice();
|
|
5968
6274
|
}
|
|
@@ -6074,6 +6380,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6074
6380
|
// reports as its own `at`, so the minimum hold starts when the user can first see it.
|
|
6075
6381
|
endedAt: at,
|
|
6076
6382
|
settledAt: this.now(),
|
|
6383
|
+
confirmed: payload.confirmed === true,
|
|
6077
6384
|
sessionId: sessionId,
|
|
6078
6385
|
};
|
|
6079
6386
|
}
|
|
@@ -6099,8 +6406,14 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6099
6406
|
* than a startup that has not finished.
|
|
6100
6407
|
*/
|
|
6101
6408
|
if (snap.connected === true) this.everConnected = true;
|
|
6102
|
-
var busy = snap.
|
|
6409
|
+
var busy = snap.pending === true || (snap.available === true && snap.running === true);
|
|
6103
6410
|
if (busy) this.lastBusyAt = t;
|
|
6411
|
+
var viewedCompletions = Array.isArray(snap.viewedCompletions) ? snap.viewedCompletions : [];
|
|
6412
|
+
if (this.pendingNotice && viewedCompletions.indexOf(this.pendingNotice.sessionId) !== -1) this.pendingNotice = undefined;
|
|
6413
|
+
if (this.notice && viewedCompletions.indexOf(this.notice.sessionId) !== -1) {
|
|
6414
|
+
this.noteDismissal('acknowledged');
|
|
6415
|
+
this.dismissNotice();
|
|
6416
|
+
}
|
|
6104
6417
|
|
|
6105
6418
|
/**
|
|
6106
6419
|
* Adopt the SNAPSHOT's own notice when it is newer than this machine's.
|
|
@@ -6119,9 +6432,11 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6119
6432
|
* what the aggregate watches, and it falls between the rounds of one task exactly like a turn does.
|
|
6120
6433
|
*/
|
|
6121
6434
|
if (snap.notice && typeof snap.notice.at === 'number'
|
|
6122
|
-
&& snap.notice.
|
|
6123
|
-
|
|
6124
|
-
|
|
6435
|
+
&& (snap.notice.queued === true ? (!this.notice && !this.pendingNotice
|
|
6436
|
+
&& (!this.dismissedSessions.has(snap.notice.sessionId) || snap.notice.at > this.dismissedSessions.get(snap.notice.sessionId)))
|
|
6437
|
+
: (snap.notice.at > this.dismissedNoticeAt
|
|
6438
|
+
&& (!this.notice || snap.notice.at > this.notice.at)
|
|
6439
|
+
&& (!this.pendingNotice || snap.notice.at > this.pendingNotice.endedAt)))) {
|
|
6125
6440
|
var snapSession = typeof snap.notice.sessionId === 'string' && snap.notice.sessionId ? snap.notice.sessionId : null;
|
|
6126
6441
|
if (snap.notice.reason === 'error') {
|
|
6127
6442
|
// A failure is not a completion and is never settled: it is shown at once, exactly as a `turn/end` failure is.
|
|
@@ -6131,7 +6446,8 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6131
6446
|
this.pendingNotice = {
|
|
6132
6447
|
reason: snap.notice.reason,
|
|
6133
6448
|
endedAt: snap.notice.at,
|
|
6134
|
-
settledAt: t,
|
|
6449
|
+
settledAt: snap.notice.queued === true ? snap.notice.at : t,
|
|
6450
|
+
confirmed: snap.notice.confirmed === true,
|
|
6135
6451
|
// The session the notice is ABOUT. Without it there is nothing to come back to, and the notice falls
|
|
6136
6452
|
// back to the older focus/timer rules below.
|
|
6137
6453
|
sessionId: snapSession,
|
|
@@ -6147,6 +6463,9 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6147
6463
|
* (a `turn/end` frame and the store's falling edge) are decided together.
|
|
6148
6464
|
*/
|
|
6149
6465
|
if (this.pendingNotice) {
|
|
6466
|
+
if (snap.notice && snap.notice.sessionId === this.pendingNotice.sessionId && snap.notice.confirmed === true) {
|
|
6467
|
+
this.pendingNotice.confirmed = true;
|
|
6468
|
+
}
|
|
6150
6469
|
var pendingSession = this.pendingNotice.sessionId;
|
|
6151
6470
|
/**
|
|
6152
6471
|
* Cancelled by WORK RESUMING, and only that — the ordering is the whole point.
|
|
@@ -6169,7 +6488,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6169
6488
|
* never produced one.
|
|
6170
6489
|
*/
|
|
6171
6490
|
this.pendingNotice = undefined;
|
|
6172
|
-
} else if (t - this.pendingNotice.settledAt >= this.settleFor(this.pendingNotice.sessionId)) {
|
|
6491
|
+
} else if (this.pendingNotice.confirmed || t - this.pendingNotice.settledAt >= this.settleFor(this.pendingNotice.sessionId)) {
|
|
6173
6492
|
this.notice = {
|
|
6174
6493
|
reason: this.pendingNotice.reason,
|
|
6175
6494
|
// Visible from NOW: the settle window was spent waiting, and a hold measured from the turn's end would
|
|
@@ -6245,8 +6564,10 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6245
6564
|
*/
|
|
6246
6565
|
var unreadDropped = isFiniteNumber(this.unreadAtNotice) && this.unreadCount < this.unreadAtNotice;
|
|
6247
6566
|
var neverUnread = this.unreadAtNotice === 0 && this.unreadCount === 0;
|
|
6248
|
-
|
|
6249
|
-
|
|
6567
|
+
var viewed = Array.isArray(snap.viewedCompletions) && snap.viewedCompletions.indexOf(notice.sessionId) !== -1;
|
|
6568
|
+
acknowledged = notice.sessionId
|
|
6569
|
+
? (viewed || ((!this.requireViewedAcknowledgement || notice.reason !== 'completed')
|
|
6570
|
+
&& presentId === notice.sessionId && this.focused === true))
|
|
6250
6571
|
: (notice.reason !== 'error' && this.focused === true
|
|
6251
6572
|
&& (unreadDropped || (neverUnread && this.interaction === null)));
|
|
6252
6573
|
this.pendingDismissalKind = notice.sessionId && presentId ? 'acknowledged' : 'no-session';
|
|
@@ -6268,8 +6589,8 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6268
6589
|
* still on screen would flip the pet to 出错 and hide the very prompt the user has to answer, and a
|
|
6269
6590
|
* turn that completes would show 任务达成 over an unanswered approval.
|
|
6270
6591
|
*
|
|
6271
|
-
*
|
|
6272
|
-
*
|
|
6592
|
+
* While the session reports a pending interaction, EVERY other state is skipped, including gestures
|
|
6593
|
+
* and greetings. When the interaction is answered the session
|
|
6273
6594
|
* stops reporting it, this latch releases, and the same frame's cascade decides where the pet goes.
|
|
6274
6595
|
*/
|
|
6275
6596
|
var pending = snap.pending === true;
|
|
@@ -6296,7 +6617,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6296
6617
|
/**
|
|
6297
6618
|
* 跳舞 — the user's own state, and the two rules that make it a good guest (operator, 2026-10-14).
|
|
6298
6619
|
*
|
|
6299
|
-
*
|
|
6620
|
+
* Attention comes first, followed by drag states, dance, greeting and offline.
|
|
6300
6621
|
* and it is dropped the moment the session is BUSY, which is the operator's 「跳舞也会被进入工作状态打断」 without a rule of its
|
|
6301
6622
|
* own: work outranks it because the busy branch below is reached first once this field is cleared. A dance also ends when the
|
|
6302
6623
|
* clip ends (the client watches the clip and calls `stopDancing`) or when the user double-clicks the pet, and both of those
|
|
@@ -6307,14 +6628,14 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6307
6628
|
this.stopDancing();
|
|
6308
6629
|
dance = null;
|
|
6309
6630
|
}
|
|
6310
|
-
if (
|
|
6631
|
+
if (pending) {
|
|
6632
|
+
state = 'waiting';
|
|
6633
|
+
} else if (this.interaction) {
|
|
6311
6634
|
state = this.interaction;
|
|
6312
6635
|
} else if (dance) {
|
|
6313
6636
|
state = 'dancing';
|
|
6314
6637
|
} else if (t < this.greetingUntil) {
|
|
6315
6638
|
state = 'greeting';
|
|
6316
|
-
} else if (pending) {
|
|
6317
|
-
state = 'waiting';
|
|
6318
6639
|
} else if (snap.connected === false && this.everConnected) {
|
|
6319
6640
|
/**
|
|
6320
6641
|
* 离线 means the host is GONE — not that it has nothing to say yet.
|
|
@@ -6414,6 +6735,11 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6414
6735
|
this.records = new Map();
|
|
6415
6736
|
/** Each session's own `running` flag as the store last reported it (see `observeStoreEdge`). */
|
|
6416
6737
|
this.storeRunning = new Map();
|
|
6738
|
+
this.completionChecks = new Map();
|
|
6739
|
+
this.attentionSessions = new Map();
|
|
6740
|
+
this.completionQueue = new Map();
|
|
6741
|
+
this.completionSeen = new Set();
|
|
6742
|
+
this.viewedCompletions = new Set();
|
|
6417
6743
|
/**
|
|
6418
6744
|
* When this aggregate's `running` answer last became true, and what it was last published as.
|
|
6419
6745
|
*
|
|
@@ -6473,6 +6799,8 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6473
6799
|
var status = statuses && statuses.get ? statuses.get(id) : undefined;
|
|
6474
6800
|
var running = status && typeof status.running === 'boolean' ? status.running : undefined;
|
|
6475
6801
|
if (running === undefined) return;
|
|
6802
|
+
if (running || status.pendingInteraction != null) self.completionChecks.delete(id);
|
|
6803
|
+
if (running) self.forgetCompletion(id);
|
|
6476
6804
|
var previous = self.storeRunning.get(id);
|
|
6477
6805
|
self.storeRunning.set(id, running);
|
|
6478
6806
|
if (previous === running) return;
|
|
@@ -6496,13 +6824,18 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6496
6824
|
* stream covers that case when it is wired, and a false 任务完成 on every boot is worse than a missed one.
|
|
6497
6825
|
*/
|
|
6498
6826
|
if (previous !== true) return;
|
|
6827
|
+
var boundaryRecord = self.records.get(id);
|
|
6828
|
+
if (boundaryRecord && boundaryRecord.lastEndReason && boundaryRecord.lastEndReason !== 'completed') return;
|
|
6499
6829
|
// Stopped: a completion. A failure already latched outranks it, the same rule the boundary path uses.
|
|
6830
|
+
var completion = { id: id + ':store', reason: 'completed', sessionId: id, at: self.now() };
|
|
6831
|
+
self.rememberCompletion(completion);
|
|
6500
6832
|
if (self.notice && self.notice.reason === 'error') return;
|
|
6501
|
-
self.notice =
|
|
6833
|
+
self.notice = completion;
|
|
6502
6834
|
});
|
|
6503
6835
|
};
|
|
6504
6836
|
|
|
6505
6837
|
SessionAggregate.prototype.reset = function (identities) {
|
|
6838
|
+
this.completionChecks.clear();
|
|
6506
6839
|
this.records.clear();
|
|
6507
6840
|
this.notice = undefined;
|
|
6508
6841
|
if (Array.isArray(identities)) {
|
|
@@ -6516,6 +6849,34 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6516
6849
|
}
|
|
6517
6850
|
};
|
|
6518
6851
|
|
|
6852
|
+
SessionAggregate.prototype.rememberCompletion = function (notice, announce) {
|
|
6853
|
+
var id = notice && notice.sessionId;
|
|
6854
|
+
if (!id || this.completionSeen.has(id)) return;
|
|
6855
|
+
this.completionSeen.add(id);
|
|
6856
|
+
this.completionQueue.set(id, Object.assign({}, notice, {
|
|
6857
|
+
announce: announce !== false, queued: true, confirmed: announce !== false,
|
|
6858
|
+
}));
|
|
6859
|
+
this.completionChecks.set(id, this.now());
|
|
6860
|
+
};
|
|
6861
|
+
|
|
6862
|
+
SessionAggregate.prototype.forgetCompletion = function (id) {
|
|
6863
|
+
this.completionQueue.delete(id);
|
|
6864
|
+
this.completionChecks.delete(id);
|
|
6865
|
+
this.completionSeen.delete(id);
|
|
6866
|
+
this.viewedCompletions.delete(id);
|
|
6867
|
+
};
|
|
6868
|
+
|
|
6869
|
+
SessionAggregate.prototype.acknowledgeViewed = function (id) {
|
|
6870
|
+
if (!id || this.running(id) !== false) return;
|
|
6871
|
+
var status = this.input.statuses.get ? this.input.statuses.get(id) : null;
|
|
6872
|
+
if (!this.completionQueue.has(id) && !(status && status.completionUnread === true)) return;
|
|
6873
|
+
this.completionSeen.add(id);
|
|
6874
|
+
this.viewedCompletions.add(id);
|
|
6875
|
+
this.completionQueue.delete(id);
|
|
6876
|
+
this.completionChecks.delete(id);
|
|
6877
|
+
if (this.notice && this.notice.sessionId === id && this.notice.reason === 'completed') this.notice = undefined;
|
|
6878
|
+
};
|
|
6879
|
+
|
|
6519
6880
|
SessionAggregate.prototype.boundary = function (event) {
|
|
6520
6881
|
if (!event || typeof event !== 'object') return this.snapshot();
|
|
6521
6882
|
var id = event.sessionId;
|
|
@@ -6542,12 +6903,16 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6542
6903
|
// The host's live answer for a session it vouches for: every root it classified arrives as `false`.
|
|
6543
6904
|
if (event.isSubagent === false) record.isSubagent = false;
|
|
6544
6905
|
if (event.type === 'turn/start') {
|
|
6906
|
+
this.forgetCompletion(id);
|
|
6545
6907
|
record.running = true;
|
|
6908
|
+
record.lastEndReason = null;
|
|
6909
|
+
this.completionChecks.delete(id);
|
|
6546
6910
|
record.candidate = undefined;
|
|
6547
6911
|
// A new turn supersedes whatever the last one reported, error included.
|
|
6548
6912
|
this.notice = undefined;
|
|
6549
6913
|
} else if (event.type === 'turn/end') {
|
|
6550
6914
|
record.running = false;
|
|
6915
|
+
record.lastEndReason = event.reason;
|
|
6551
6916
|
record.candidate = { id: id + ':' + event.seq, reason: event.reason, at: this.now() };
|
|
6552
6917
|
}
|
|
6553
6918
|
this.records.set(id, record);
|
|
@@ -6557,7 +6922,8 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6557
6922
|
* a frame for a session nobody can vouch for — not classified, not listed by the app — is silence rather than a
|
|
6558
6923
|
* 任务达成, because the pet has no way to tell it from a helper's turn ending (2026-10-14).
|
|
6559
6924
|
*/
|
|
6560
|
-
if (candidate && (candidate.reason === 'completed' ||
|
|
6925
|
+
if (candidate && (candidate.reason === 'completed' || candidate.reason === 'error') && this.mainSession(id)) {
|
|
6926
|
+
if (candidate.reason === 'completed') this.rememberCompletion({ id: candidate.id, reason: candidate.reason, sessionId: id, at: candidate.at });
|
|
6561
6927
|
record.candidate = undefined;
|
|
6562
6928
|
// A failure latches: a success arriving moments later must not erase the
|
|
6563
6929
|
// fact that a turn just died. The machine owns the same rule, so the two
|
|
@@ -6578,6 +6944,19 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6578
6944
|
};
|
|
6579
6945
|
|
|
6580
6946
|
SessionAggregate.prototype.subagent = function (id) {
|
|
6947
|
+
var list = this.input.catalog || {};
|
|
6948
|
+
var row = list.byId && list.byId[id];
|
|
6949
|
+
// The current DSH store publishes origin and its parents' subagent catalogs, not isSubagent.
|
|
6950
|
+
// These explicit identities outrank an older boundary baseline that called the agent a root.
|
|
6951
|
+
if (row && (row.origin === 'subagent' || row.isSubagent === true)) return true;
|
|
6952
|
+
var parents = Object.keys(list.byId || {});
|
|
6953
|
+
for (var p = 0; p < parents.length; p += 1) {
|
|
6954
|
+
var parent = list.byId[parents[p]];
|
|
6955
|
+
var projection = list.projectionsBySession && list.projectionsBySession[parents[p]];
|
|
6956
|
+
var values = (projection && projection.values) || (parent && parent.projectionValues);
|
|
6957
|
+
var children = values && values.subagentCatalog;
|
|
6958
|
+
if (Array.isArray(children) && children.some(function (child) { return child && child.id === id; })) return true;
|
|
6959
|
+
}
|
|
6581
6960
|
var record = this.records.get(id);
|
|
6582
6961
|
if (record && record.isSubagent !== undefined) return record.isSubagent === true;
|
|
6583
6962
|
var catalog = this.input.catalog && this.input.catalog.byId ? this.input.catalog.byId[id] : undefined;
|
|
@@ -6659,7 +7038,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6659
7038
|
* only ever make the answer STRICTER on a store that has the list.
|
|
6660
7039
|
*/
|
|
6661
7040
|
var listed = catalog.ids;
|
|
6662
|
-
if (listed && typeof listed.length === 'number'
|
|
7041
|
+
if (listed && typeof listed.length === 'number') {
|
|
6663
7042
|
for (var i = 0; i < listed.length; i += 1) {
|
|
6664
7043
|
if (listed[i] === id) return true;
|
|
6665
7044
|
}
|
|
@@ -6677,6 +7056,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6677
7056
|
|
|
6678
7057
|
var workingCount = 0;
|
|
6679
7058
|
var waitingCount = 0;
|
|
7059
|
+
var waitingSessionId = null;
|
|
6680
7060
|
/**
|
|
6681
7061
|
* How many sessions have a completion the user has NOT been back to.
|
|
6682
7062
|
*
|
|
@@ -6693,6 +7073,9 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6693
7073
|
* contributes nothing.
|
|
6694
7074
|
*/
|
|
6695
7075
|
var unreadCount = 0;
|
|
7076
|
+
var completedUnreadCount = 0;
|
|
7077
|
+
var completedSessionId = null;
|
|
7078
|
+
var completedSessionIds = [];
|
|
6696
7079
|
/**
|
|
6697
7080
|
* WHY that count is what it is, as counts rather than ids or titles — added with the 绿点 fix of 2026-10-14, so
|
|
6698
7081
|
* "the dot is lit and no session finished" is answerable from `/vrmpet3d/state` alone instead of by guesswork.
|
|
@@ -6708,9 +7091,32 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6708
7091
|
/** Which pending interaction wins when several sessions are waiting at once. Approval is the rarer. */
|
|
6709
7092
|
var waitingKind = null;
|
|
6710
7093
|
var self = this;
|
|
7094
|
+
this.attentionSessions.forEach(function (_kind, id) {
|
|
7095
|
+
var status = statuses.get ? statuses.get(id) : undefined;
|
|
7096
|
+
// Navigating away, a missing report, or losing focus is not an answer to the problem.
|
|
7097
|
+
if (status) {
|
|
7098
|
+
if (status.pendingInteraction == null) self.attentionSessions.delete(id);
|
|
7099
|
+
else self.attentionSessions.set(id, pendingInteractionKind(status.pendingInteraction));
|
|
7100
|
+
}
|
|
7101
|
+
});
|
|
6711
7102
|
ids.forEach(function (id) {
|
|
6712
7103
|
if (!self.inScope(id)) return;
|
|
6713
7104
|
var status = statuses.get ? statuses.get(id) : undefined;
|
|
7105
|
+
var record = self.records.get(id);
|
|
7106
|
+
if (status && status.completionUnread === true && self.mainSession(id) && !self.completionSeen.has(id)
|
|
7107
|
+
&& self.running(id) === false && status.pendingInteraction == null) {
|
|
7108
|
+
self.rememberCompletion({ sessionId: id, reason: 'completed', at: self.now() }, false);
|
|
7109
|
+
}
|
|
7110
|
+
var completed = self.completionQueue.has(id) && !self.viewedCompletions.has(id) && self.mainSession(id)
|
|
7111
|
+
&& self.running(id) === false && (!status || status.pendingInteraction == null)
|
|
7112
|
+
&& !(record && record.lastEndReason && record.lastEndReason !== 'completed');
|
|
7113
|
+
if (!completed) self.completionChecks.delete(id);
|
|
7114
|
+
else {
|
|
7115
|
+
if (!self.completionChecks.has(id)) self.completionChecks.set(id, self.now());
|
|
7116
|
+
if (self.completionQueue.get(id).confirmed || self.now() - self.completionChecks.get(id) >= COMPLETION_SETTLE_MS) {
|
|
7117
|
+
completedSessionIds.push(id);
|
|
7118
|
+
}
|
|
7119
|
+
}
|
|
6714
7120
|
if (status && status.completionUnread === true) {
|
|
6715
7121
|
unreadDetail.candidates += 1;
|
|
6716
7122
|
var entry = (catalog.byId || {})[id];
|
|
@@ -6733,14 +7139,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6733
7139
|
}
|
|
6734
7140
|
var pending = status ? status.pendingInteraction != null : false;
|
|
6735
7141
|
if (pending) {
|
|
6736
|
-
|
|
6737
|
-
var kind = pendingInteractionKind(status.pendingInteraction);
|
|
6738
|
-
// Ranked rather than first-wins: a global-scope pet watching ten sessions must not report
|
|
6739
|
-
// "a question" just because that session happens to come out of the map first when another one
|
|
6740
|
-
// is blocked on a permission prompt.
|
|
6741
|
-
if (waitingKind === null || PENDING_KIND_RANK.indexOf(kind) < PENDING_KIND_RANK.indexOf(waitingKind)) {
|
|
6742
|
-
waitingKind = kind;
|
|
6743
|
-
}
|
|
7142
|
+
self.attentionSessions.set(id, pendingInteractionKind(status.pendingInteraction));
|
|
6744
7143
|
}
|
|
6745
7144
|
// One rule for "this is the user's own session", shared with the two completion paths (`mainSession`).
|
|
6746
7145
|
if (self.mainSession(id) && self.running(id) === true) {
|
|
@@ -6751,7 +7150,23 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6751
7150
|
}
|
|
6752
7151
|
});
|
|
6753
7152
|
|
|
7153
|
+
this.attentionSessions.forEach(function (kind, id) {
|
|
7154
|
+
waitingCount++;
|
|
7155
|
+
if (waitingKind === null || PENDING_KIND_RANK.indexOf(kind) < PENDING_KIND_RANK.indexOf(waitingKind)) {
|
|
7156
|
+
waitingKind = kind;
|
|
7157
|
+
waitingSessionId = id;
|
|
7158
|
+
}
|
|
7159
|
+
});
|
|
7160
|
+
completedSessionIds.sort(function (a, b) { return self.completionQueue.get(a).at - self.completionQueue.get(b).at; });
|
|
7161
|
+
completedUnreadCount = completedSessionIds.length;
|
|
7162
|
+
completedSessionId = completedSessionIds[0] || null;
|
|
7163
|
+
var waitingSessionIds = Array.from(this.attentionSessions.keys()).sort(function (a, b) {
|
|
7164
|
+
return PENDING_KIND_RANK.indexOf(self.attentionSessions.get(a)) - PENDING_KIND_RANK.indexOf(self.attentionSessions.get(b));
|
|
7165
|
+
});
|
|
6754
7166
|
var ready = catalog.phase === 'ready';
|
|
7167
|
+
self.completionChecks.forEach(function (_at, id) {
|
|
7168
|
+
if (!ids.has(id) || !self.inScope(id)) self.completionChecks.delete(id);
|
|
7169
|
+
});
|
|
6755
7170
|
|
|
6756
7171
|
// Availability is "there is a session whose activity this pet could show".
|
|
6757
7172
|
// In `current` scope that must be proved by the selected session actually
|
|
@@ -6764,7 +7179,12 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6764
7179
|
|
|
6765
7180
|
var hasAnything = ready || workingCount > 0 || waitingCount > 0 || Boolean(this.notice);
|
|
6766
7181
|
var available = this.input.connected === true && hasAnything && hasSelectedSession;
|
|
6767
|
-
var
|
|
7182
|
+
var queuedNotice;
|
|
7183
|
+
this.completionQueue.forEach(function (entry, id) {
|
|
7184
|
+
if (!queuedNotice && entry.announce && self.mainSession(id) && self.inScope(id) && self.running(id) === false) queuedNotice = entry;
|
|
7185
|
+
});
|
|
7186
|
+
var notice = available ? (this.notice && this.notice.reason === 'error' ? this.notice
|
|
7187
|
+
: (queuedNotice || (this.notice && !this.viewedCompletions.has(this.notice.sessionId) ? this.notice : undefined))) : undefined;
|
|
6768
7188
|
/**
|
|
6769
7189
|
* `connected` rides along, because 离线 is decided by the CONNECTION and not by `available`.
|
|
6770
7190
|
*
|
|
@@ -6820,6 +7240,8 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6820
7240
|
pendingKind: waitingCount > 0 ? waitingKind : null,
|
|
6821
7241
|
workingCount: workingCount,
|
|
6822
7242
|
waitingCount: waitingCount,
|
|
7243
|
+
waitingSessionId: waitingSessionId,
|
|
7244
|
+
waitingSessionIds: waitingSessionIds,
|
|
6823
7245
|
/**
|
|
6824
7246
|
* Sessions whose completion is still UNREAD (see above), and whether there are any.
|
|
6825
7247
|
*
|
|
@@ -6827,6 +7249,10 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
6827
7249
|
* surface could say more later without another plumbing round.
|
|
6828
7250
|
*/
|
|
6829
7251
|
unreadCount: unreadCount,
|
|
7252
|
+
completedUnreadCount: completedUnreadCount,
|
|
7253
|
+
completedSessionId: completedSessionId,
|
|
7254
|
+
completedSessionIds: completedSessionIds,
|
|
7255
|
+
viewedCompletions: Array.from(this.viewedCompletions),
|
|
6830
7256
|
hasUnread: unreadCount > 0,
|
|
6831
7257
|
/** The breakdown behind that count — see `unreadDetail` where it is built. */
|
|
6832
7258
|
unreadDetail: unreadDetail,
|
|
@@ -7307,6 +7733,14 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
7307
7733
|
STATES: STATES,
|
|
7308
7734
|
INTERACTION_STATES: INTERACTION_STATES,
|
|
7309
7735
|
STATUS_STATES: STATUS_STATES,
|
|
7736
|
+
LANGUAGE_SETTINGS: LANGUAGE_SETTINGS,
|
|
7737
|
+
cleanLanguage: cleanLanguage,
|
|
7738
|
+
resolveLanguage: resolveLanguage,
|
|
7739
|
+
browserLanguageHints: browserLanguageHints,
|
|
7740
|
+
textFor: textFor,
|
|
7741
|
+
textOf: textOf,
|
|
7742
|
+
applyI18n: applyI18n,
|
|
7743
|
+
defaultStatusText: defaultStatusText,
|
|
7310
7744
|
DEFAULT_STATUS_TEXT: DEFAULT_STATUS_TEXT,
|
|
7311
7745
|
STATUS_TAGS: STATUS_TAGS,
|
|
7312
7746
|
STATUS_ANIMATIONS: STATUS_ANIMATIONS,
|