dsh-plugin-vrmpet3d 0.1.0 → 0.1.1
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 +45 -3
- package/desktop/preload.js +6 -2
- package/lib/client.core.js +209 -13
- package/lib/client.js +278 -42
- package/lib/config-page.html +71 -45
- package/lib/config-page.js +1120 -229
- package/lib/core.js +209 -13
- package/lib/desktop-page.js +144 -8
- package/lib/global-page.html +140 -99
- package/lib/index.js +150 -33
- package/lib/model-picker.js +139 -49
- package/lib/picker-page.html +34 -8
- package/lib/picker-page.js +192 -11
- package/lib/setup-page.html +68 -49
- package/lib/setup-page.js +395 -42
- package/lib/whale.js +191 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -76,6 +76,98 @@
|
|
|
76
76
|
try { return JSON.stringify(error); } catch (e) { return String(error); }
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/**
|
|
80
|
+
* The DSH locale service's own answer, or null when it has none to give.
|
|
81
|
+
*
|
|
82
|
+
* `ctx.locale` is a cordis service whose snapshot is a plain object, and which field carries the language has
|
|
83
|
+
* moved between builds (`language`, then `locale`, then `current`), so all three are asked for in that order. A
|
|
84
|
+
* service that is absent, shaped differently or throws is NOT an error here: it means this deployment has no app
|
|
85
|
+
* locale to consult and the browser's own hints answer instead, which is what the try/catch is for.
|
|
86
|
+
*/
|
|
87
|
+
function localeHintOf(locale) {
|
|
88
|
+
try {
|
|
89
|
+
var snapshot = locale && typeof locale.getSnapshot === 'function' ? locale.getSnapshot() : undefined;
|
|
90
|
+
var value = snapshot && (snapshot.language || snapshot.locale || snapshot.current);
|
|
91
|
+
return typeof value === 'string' && value ? value : null;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Which of the two languages this surface draws in — the ONE rule, asked every time it is needed.
|
|
99
|
+
*
|
|
100
|
+
* `core.resolveLanguage` owns the rule (an explicit 'zh'/'en' wins; 'auto' takes the FIRST hint that says
|
|
101
|
+
* anything, else English), and the hints are gathered here in their order of authority: the DSH locale first,
|
|
102
|
+
* because DSH is the application the pet lives inside, then the browser's own two (`navigator.language`, then the
|
|
103
|
+
* document's `lang`). English is what nothing-at-all answers, which is also the plugin's own fallback.
|
|
104
|
+
*
|
|
105
|
+
* Deliberately NOT cached in a module-level variable: the language can change while the app runs (the 界面语言
|
|
106
|
+
* preference, or DSH's own locale), and a captured answer is a surface stuck in the language it started in.
|
|
107
|
+
*/
|
|
108
|
+
function readLanguage(locale, preference) {
|
|
109
|
+
var core = globalThis.__VRMPET3D_CORE__;
|
|
110
|
+
var hints = [localeHintOf(locale)];
|
|
111
|
+
if (core && typeof core.browserLanguageHints === 'function') {
|
|
112
|
+
hints = hints.concat(core.browserLanguageHints());
|
|
113
|
+
} else {
|
|
114
|
+
/**
|
|
115
|
+
* A core chunk that predates the rule — the stale-delivery case this repository has eaten more than once.
|
|
116
|
+
* The same two hints, in the same order, from the page itself, so an older host costs the SHARING of the rule
|
|
117
|
+
* rather than the language.
|
|
118
|
+
*/
|
|
119
|
+
try {
|
|
120
|
+
hints.push(globalThis.navigator && globalThis.navigator.language);
|
|
121
|
+
hints.push(globalThis.document && globalThis.document.documentElement && globalThis.document.documentElement.lang);
|
|
122
|
+
} catch (error) { /* no window: the hints stay empty */ }
|
|
123
|
+
}
|
|
124
|
+
if (core && typeof core.resolveLanguage === 'function') return core.resolveLanguage(preference, hints);
|
|
125
|
+
if (preference === 'zh' || preference === 'en') return preference;
|
|
126
|
+
for (var i = 0; i < hints.length; i += 1) {
|
|
127
|
+
var hint = hints[i];
|
|
128
|
+
if (typeof hint !== 'string' || !hint) continue;
|
|
129
|
+
return /^(zh|cn|chinese)/i.test(hint.trim()) ? 'zh' : 'en';
|
|
130
|
+
}
|
|
131
|
+
return 'en';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The language a message with NO widget behind it is written in.
|
|
136
|
+
*
|
|
137
|
+
* The chunk loader runs before any widget exists — and before `ctx.locale` is reachable at all — so the browser's
|
|
138
|
+
* own two hints are the whole of what there is, and English is what nothing-at-all answers. Every message that
|
|
139
|
+
* HAS a widget reads that widget's own resolved language instead.
|
|
140
|
+
*/
|
|
141
|
+
function ambientLanguage() {
|
|
142
|
+
return readLanguage(null, null);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* One language's half of {@link STRINGS}, read at CALL time.
|
|
147
|
+
*
|
|
148
|
+
* 'zh' is the only value that selects the Chinese half: a widget whose language is missing (a stand-in built
|
|
149
|
+
* from the prototype) or anything unexpected reads the English half, which every entry is required to have.
|
|
150
|
+
*/
|
|
151
|
+
function words(language) {
|
|
152
|
+
return STRINGS[language === 'zh' ? 'zh' : 'en'] || STRINGS.en;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* One entry out of {@link STRINGS}, with `{name}` placeholders filled from `values`.
|
|
157
|
+
*
|
|
158
|
+
* A key missing from BOTH halves returns the key, so a gap is visible rather than silently blank — the rule
|
|
159
|
+
* `core.textOf` follows, which this cannot call because it is also used before a core chunk arrives.
|
|
160
|
+
*/
|
|
161
|
+
function word(language, key, values) {
|
|
162
|
+
var text = words(language)[key];
|
|
163
|
+
if (typeof text !== 'string') text = STRINGS.en[key];
|
|
164
|
+
if (typeof text !== 'string') return key;
|
|
165
|
+
if (!values) return text;
|
|
166
|
+
return text.replace(/\{(\w+)\}/g, function (match, name) {
|
|
167
|
+
return Object.prototype.hasOwnProperty.call(values, name) ? String(values[name]) : match;
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
79
171
|
/**
|
|
80
172
|
* Every chunk request this half makes, resolved through the module system.
|
|
81
173
|
*
|
|
@@ -108,7 +200,12 @@
|
|
|
108
200
|
}
|
|
109
201
|
return require.async(chunk.file).then(function (exports) {
|
|
110
202
|
var value = exports || globalThis[chunk.global];
|
|
111
|
-
if (!value)
|
|
203
|
+
if (!value) {
|
|
204
|
+
// This failure has no widget to ask, so `ambientLanguage` answers: the page's own hints, else English.
|
|
205
|
+
throw new Error('vrmpet3d: ' + word(ambientLanguage(), 'chunkNoPublish', {
|
|
206
|
+
file: chunk.file, global: chunk.global,
|
|
207
|
+
}));
|
|
208
|
+
}
|
|
112
209
|
loaded[chunk.global] = value;
|
|
113
210
|
return publish(chunk, value);
|
|
114
211
|
}, function (error) {
|
|
@@ -122,7 +219,7 @@
|
|
|
122
219
|
|
|
123
220
|
return pending.then(function () {
|
|
124
221
|
if (missing.length) {
|
|
125
|
-
throw new Error('vrmpet3d:
|
|
222
|
+
throw new Error('vrmpet3d: ' + word(ambientLanguage(), 'chunksMissing', { list: missing.join('; ') }));
|
|
126
223
|
}
|
|
127
224
|
return loaded;
|
|
128
225
|
});
|
|
@@ -367,7 +464,12 @@
|
|
|
367
464
|
*/
|
|
368
465
|
function Placement(preferences, now, core) {
|
|
369
466
|
if (!core || typeof core.stepFall !== 'function') {
|
|
370
|
-
|
|
467
|
+
/**
|
|
468
|
+
* Said in the language the preferences name, because a Placement is built before any widget exists to be
|
|
469
|
+
* asked — the first one is constructed by the widget itself, and the floating window's by its own page.
|
|
470
|
+
*/
|
|
471
|
+
throw new Error('vrmpet3d: ' + word(
|
|
472
|
+
readLanguage(null, preferences && preferences.language), 'placementNoCore'));
|
|
371
473
|
}
|
|
372
474
|
this.core = core;
|
|
373
475
|
this.preferences = preferences;
|
|
@@ -673,22 +775,40 @@
|
|
|
673
775
|
|
|
674
776
|
/**
|
|
675
777
|
* @param {HTMLElement} host the element the slot gave us
|
|
676
|
-
* @param {object} options {
|
|
778
|
+
* @param {object} options { locale, language, onScopeChange, onModelChange }
|
|
677
779
|
*/
|
|
678
780
|
function VrmPet3DWidget(host, options) {
|
|
679
781
|
options = options || {};
|
|
680
782
|
this.host = host;
|
|
681
783
|
this.core = globalThis.__VRMPET3D_CORE__;
|
|
682
784
|
this.sceneApi = globalThis.__VRMPET3D_SCENE__;
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
785
|
+
/**
|
|
786
|
+
* The DSH locale SERVICE itself, kept rather than a snapshot of it.
|
|
787
|
+
*
|
|
788
|
+
* The language has to be re-resolvable while the pet runs (`adoptLanguage`), because
|
|
789
|
+
* `preferences.language === 'auto'` means "whatever DSH says" and DSH's own locale can change under it.
|
|
790
|
+
*/
|
|
791
|
+
this.locale = options.locale || null;
|
|
792
|
+
/**
|
|
793
|
+
* An EXPLICIT language is an override, and that is what the probes and the suites inject (`{language:'zh'}`);
|
|
794
|
+
* the real mount passes `locale` and lets the user's preference decide.
|
|
795
|
+
*/
|
|
796
|
+
this.injectedLanguage = options.language === 'zh' || options.language === 'en' ? options.language : null;
|
|
797
|
+
/**
|
|
798
|
+
* Resolved BEFORE the two failures below, because those messages are shown to the user in a badge and have to
|
|
799
|
+
* be said in the same language as everything else. The preference is not readable yet (`readStorage` needs
|
|
800
|
+
* `this.document`, and that needs a core), so this pass has DSH's locale and the browser's hints only; the
|
|
801
|
+
* line after `readStorage()` re-resolves with the user's own answer.
|
|
802
|
+
*/
|
|
803
|
+
this.language = this.injectedLanguage || readLanguage(this.locale, null);
|
|
804
|
+
if (!this.core) throw new Error('vrmpet3d: ' + word(this.language, 'mountNoCore'));
|
|
805
|
+
if (!this.sceneApi) throw new Error('vrmpet3d: ' + word(this.language, 'mountNoScene'));
|
|
687
806
|
this.onScopeChange = typeof options.onScopeChange === 'function' ? options.onScopeChange : function () {};
|
|
688
807
|
this.onModelChange = typeof options.onModelChange === 'function' ? options.onModelChange : function () {};
|
|
689
808
|
this.document = host.ownerDocument || document;
|
|
690
809
|
|
|
691
810
|
this.preferences = this.readStorage();
|
|
811
|
+
this.language = this.injectedLanguage || readLanguage(this.locale, this.preferences.language);
|
|
692
812
|
this.placement = new Placement(this.preferences, function () { return Date.now(); }, this.core);
|
|
693
813
|
|
|
694
814
|
this.machine = new this.core.PetMachine(function () { return Date.now(); }, this.preferences.sleepAfterMs);
|
|
@@ -985,6 +1105,13 @@
|
|
|
985
1105
|
// The model is ONE setting for both surfaces, so a change made in the window or in the settings
|
|
986
1106
|
// window moves this pet too - there is no per-surface model to keep in step.
|
|
987
1107
|
self.preferences = next;
|
|
1108
|
+
/**
|
|
1109
|
+
* 界面语言 follows the snapshot like every other preference, and only NOW is the answer readable:
|
|
1110
|
+
* `preferences.language` may be an explicit 中文/English or `auto` (what DSH's own locale says). A language
|
|
1111
|
+
* that actually CHANGED re-says this surface's own words and rebuilds the shared menu; nothing else about
|
|
1112
|
+
* the pet is touched.
|
|
1113
|
+
*/
|
|
1114
|
+
self.adoptLanguage();
|
|
988
1115
|
if (self.scene) self.scene.applyPreferences(Object.assign({}, next, { motions: self.effectiveMotions() }));
|
|
989
1116
|
self.applyMotions();
|
|
990
1117
|
self.applyPlacement();
|
|
@@ -1052,6 +1179,8 @@
|
|
|
1052
1179
|
if (event && event.key && !known) return;
|
|
1053
1180
|
var next = self.readStorage();
|
|
1054
1181
|
self.preferences = next;
|
|
1182
|
+
// The other document may have been the settings window's 界面语言 field: same rule as the host snapshot.
|
|
1183
|
+
self.adoptLanguage();
|
|
1055
1184
|
if (self.scene) self.scene.applyPreferences(Object.assign({}, next, { motions: self.effectiveMotions() }));
|
|
1056
1185
|
self.applyMotions();
|
|
1057
1186
|
self.applyPlacement();
|
|
@@ -1198,7 +1327,7 @@
|
|
|
1198
1327
|
};
|
|
1199
1328
|
|
|
1200
1329
|
VrmPet3DWidget.prototype.applyStrings = function () {
|
|
1201
|
-
var text =
|
|
1330
|
+
var text = words(this.language);
|
|
1202
1331
|
/**
|
|
1203
1332
|
* The ONE label this surface still writes by hand.
|
|
1204
1333
|
*
|
|
@@ -1210,6 +1339,37 @@
|
|
|
1210
1339
|
this.pet.setAttribute('aria-label', text.ariaPet);
|
|
1211
1340
|
};
|
|
1212
1341
|
|
|
1342
|
+
/**
|
|
1343
|
+
* Follow 界面语言 — the preference, DSH's locale, and this surface's own words.
|
|
1344
|
+
*
|
|
1345
|
+
* Called from every path that can change the preference (a host snapshot, a storage write from another
|
|
1346
|
+
* document) because 'auto' means "whatever DSH says" and DSH's locale can change under a running pet. Only a
|
|
1347
|
+
* CHANGE does anything: this rebuilds the menu, so running it on every poll would throw away and rebuild a live
|
|
1348
|
+
* element every few seconds for nothing.
|
|
1349
|
+
*
|
|
1350
|
+
* What is re-said, and why exactly these three:
|
|
1351
|
+
* * the restore label and the aria name are the only strings this surface CAPTURES — everything else is read
|
|
1352
|
+
* out of the table when it is drawn, which is the rule that makes a live language change possible at all;
|
|
1353
|
+
* * the shared menu carries its words from `createMenu` and cannot be re-worded in place, so it is rebuilt
|
|
1354
|
+
* (the old element is destroyed by `buildMenu`, not left in the document);
|
|
1355
|
+
* * the prompt is drawn from `core.statusFor(..., this.language)` on the next frame, and its cached key is
|
|
1356
|
+
* dropped so "next frame" is when the new words arrive rather than "the next time the line differs";
|
|
1357
|
+
* * the 碎碎念 already on screen is the machine's own held line and is left alone — it expires on its own
|
|
1358
|
+
* clock, and the remark after it is picked in the new language. Clearing the machine here would also throw
|
|
1359
|
+
* away its throttle memory, which is a change nobody asked for.
|
|
1360
|
+
*/
|
|
1361
|
+
VrmPet3DWidget.prototype.adoptLanguage = function () {
|
|
1362
|
+
// A stand-in built from the prototype (a probe's own widget) has no language to change and no DOM to re-say.
|
|
1363
|
+
if (!this.language) return;
|
|
1364
|
+
var wanted = this.injectedLanguage
|
|
1365
|
+
|| readLanguage(this.locale, this.preferences && this.preferences.language);
|
|
1366
|
+
if (wanted === this.language) return;
|
|
1367
|
+
this.language = wanted;
|
|
1368
|
+
this.applyStrings();
|
|
1369
|
+
this.buildMenu();
|
|
1370
|
+
this.headStatusKey = null;
|
|
1371
|
+
};
|
|
1372
|
+
|
|
1213
1373
|
VrmPet3DWidget.prototype.bindMenu = function () {
|
|
1214
1374
|
var self = this;
|
|
1215
1375
|
|
|
@@ -1510,7 +1670,7 @@
|
|
|
1510
1670
|
var fallback = function (reason) {
|
|
1511
1671
|
var opened = null;
|
|
1512
1672
|
try { opened = globalThis.open(url, '_blank'); } catch (error) { opened = null; }
|
|
1513
|
-
if (!opened) self.showStatus((
|
|
1673
|
+
if (!opened) self.showStatus((words(self.language).configNote || '') + ' ' + (reason || ''));
|
|
1514
1674
|
};
|
|
1515
1675
|
try {
|
|
1516
1676
|
fetch('/vrmpet3d/desktop', {
|
|
@@ -1523,9 +1683,11 @@
|
|
|
1523
1683
|
// placeholder while a VRM was being shown.
|
|
1524
1684
|
body: JSON.stringify({ action: which, modelId: self.currentModelId() }),
|
|
1525
1685
|
}).then(function (response) {
|
|
1526
|
-
|
|
1686
|
+
// Both reasons are APPENDED to `configNote` and shown in the badge, so they are user-visible sentences of
|
|
1687
|
+
// this surface's own and come out of the table like the rest.
|
|
1688
|
+
if (!response.ok) fallback(words(self.language).settingsNoWindow);
|
|
1527
1689
|
}).catch(function () {
|
|
1528
|
-
fallback(
|
|
1690
|
+
fallback(words(self.language).settingsUnreachable);
|
|
1529
1691
|
});
|
|
1530
1692
|
} catch (error) {
|
|
1531
1693
|
fallback(error && error.message);
|
|
@@ -1577,7 +1739,7 @@
|
|
|
1577
1739
|
*/
|
|
1578
1740
|
var opened = null;
|
|
1579
1741
|
try { opened = globalThis.open('/vrmpet3d/setup', '_blank'); } catch (error) { opened = null; }
|
|
1580
|
-
if (!opened) self.showStatus((
|
|
1742
|
+
if (!opened) self.showStatus((words(self.language).setupNote || ''));
|
|
1581
1743
|
return null;
|
|
1582
1744
|
});
|
|
1583
1745
|
})
|
|
@@ -1655,6 +1817,14 @@
|
|
|
1655
1817
|
*/
|
|
1656
1818
|
VrmPet3DWidget.prototype.buildMenu = function () {
|
|
1657
1819
|
var api = globalThis.__VRMPET3D_MENU__;
|
|
1820
|
+
/**
|
|
1821
|
+
* A REBUILD replaces the element rather than leaving the old one in the document — the menu is DOM, and a
|
|
1822
|
+
* second copy would keep its own document listeners and its own words. `adoptLanguage` is the one caller that
|
|
1823
|
+
* rebuilds (the menu carries its labels from `createMenu` and cannot be re-worded in place), and the first
|
|
1824
|
+
* call finds `this.menu` null.
|
|
1825
|
+
*/
|
|
1826
|
+
if (this.menu && typeof this.menu.destroy === 'function') this.menu.destroy();
|
|
1827
|
+
this.menu = null;
|
|
1658
1828
|
if (!api || typeof api.createMenu !== 'function') return null;
|
|
1659
1829
|
this.menuAvailable = true;
|
|
1660
1830
|
api.injectStyle(this.document, 'vrmpet3d-menu-style');
|
|
@@ -2000,7 +2170,7 @@
|
|
|
2000
2170
|
*/
|
|
2001
2171
|
VrmPet3DWidget.prototype.applySurface = function (attempt) {
|
|
2002
2172
|
var self = this;
|
|
2003
|
-
var strings =
|
|
2173
|
+
var strings = words(this.language);
|
|
2004
2174
|
var surface = this.preferences.petSurface || 'auto';
|
|
2005
2175
|
/**
|
|
2006
2176
|
* What was last ASKED for, which is not the same as what is running: the window can fail to start. It is
|
|
@@ -2066,7 +2236,7 @@
|
|
|
2066
2236
|
|
|
2067
2237
|
VrmPet3DWidget.prototype.setDesktopWindow = function (enabled) {
|
|
2068
2238
|
var self = this;
|
|
2069
|
-
var strings =
|
|
2239
|
+
var strings = words(this.language);
|
|
2070
2240
|
this.syncDesktop(null, strings.desktopStarting);
|
|
2071
2241
|
return fetch('/vrmpet3d/desktop', {
|
|
2072
2242
|
method: 'POST',
|
|
@@ -2099,7 +2269,7 @@
|
|
|
2099
2269
|
.catch(function () {
|
|
2100
2270
|
// A session without the route (an older host half) is RECORDED rather than thrown: `desktopNote` is kept
|
|
2101
2271
|
// as data (see `syncDesktop`), so "the window could not start" stays answerable.
|
|
2102
|
-
var strings =
|
|
2272
|
+
var strings = words(self.language);
|
|
2103
2273
|
self.syncDesktop(null, strings.desktopUnavailable);
|
|
2104
2274
|
return null;
|
|
2105
2275
|
});
|
|
@@ -2115,7 +2285,7 @@
|
|
|
2115
2285
|
* is the whole reason the host reports the page's own verdict.
|
|
2116
2286
|
*/
|
|
2117
2287
|
VrmPet3DWidget.prototype.syncDesktop = function (status, note) {
|
|
2118
|
-
var strings =
|
|
2288
|
+
var strings = words(this.language);
|
|
2119
2289
|
this.desktopStatus = status || null;
|
|
2120
2290
|
if (note) {
|
|
2121
2291
|
this.desktopNote = note;
|
|
@@ -2796,7 +2966,7 @@
|
|
|
2796
2966
|
var self = this;
|
|
2797
2967
|
return fetch('/vrmpet3d/catalog', { credentials: 'omit' })
|
|
2798
2968
|
.then(function (response) {
|
|
2799
|
-
if (!response.ok) throw new Error('
|
|
2969
|
+
if (!response.ok) throw new Error(word(self.language, 'catalogRoute', { status: response.status }));
|
|
2800
2970
|
return response.json();
|
|
2801
2971
|
})
|
|
2802
2972
|
.then(function (body) {
|
|
@@ -2886,15 +3056,23 @@
|
|
|
2886
3056
|
if (!wanted) {
|
|
2887
3057
|
var ids = this.danceClipIds();
|
|
2888
3058
|
if (!ids.length) {
|
|
2889
|
-
|
|
3059
|
+
// The same sentence the floating window's 跳支舞 puts in its bubble (see `lib/desktop-page.js`).
|
|
3060
|
+
this.showStatus(words(this.language).danceEmpty);
|
|
2890
3061
|
return false;
|
|
2891
3062
|
}
|
|
3063
|
+
var current = this.machine.dancing && this.machine.dancing.motionId;
|
|
3064
|
+
var alternatives = ids.filter(function (id) { return id !== current; });
|
|
3065
|
+
if (alternatives.length) ids = alternatives;
|
|
2892
3066
|
wanted = ids[Math.floor(Math.random() * ids.length)];
|
|
2893
3067
|
}
|
|
2894
3068
|
// A generous deadline: the clip's own end is what normally stops this, and a dance that outstays its welcome is worse
|
|
2895
3069
|
// than one that is cut short at three minutes.
|
|
2896
3070
|
if (!this.machine.setDancing(wanted, 180000)) return false;
|
|
2897
|
-
if (this.scene)
|
|
3071
|
+
if (this.scene) {
|
|
3072
|
+
this.scene.setPreview(null);
|
|
3073
|
+
// A previous run may already be finished, even with no preview left to clear.
|
|
3074
|
+
if (this.scene.playback && this.scene.playback.state === 'dancing') this.scene.stopPlayback(null);
|
|
3075
|
+
}
|
|
2898
3076
|
var self = this;
|
|
2899
3077
|
var dance = this.machine.dancing;
|
|
2900
3078
|
this.ensureMotion(wanted, false).then(function () {
|
|
@@ -2908,7 +3086,10 @@
|
|
|
2908
3086
|
/** End a dance early — the double-click gesture, and the clip's own end. */
|
|
2909
3087
|
VrmPet3DWidget.prototype.stopDance = function () {
|
|
2910
3088
|
if (!this.machine || typeof this.machine.stopDancing !== 'function') return null;
|
|
2911
|
-
|
|
3089
|
+
var stopped = this.machine.stopDancing();
|
|
3090
|
+
if (this.scene && this.scene.preview && this.scene.preview.state === 'dancing') this.scene.setPreview(null);
|
|
3091
|
+
if (stopped && !this.disposed) this.pump();
|
|
3092
|
+
return stopped;
|
|
2912
3093
|
};
|
|
2913
3094
|
|
|
2914
3095
|
VrmPet3DWidget.prototype.reloadModel = function () { var self = this;
|
|
@@ -2919,10 +3100,15 @@
|
|
|
2919
3100
|
* answered from the HTTP cache — and "reload" that returns the cached bytes is a button that does nothing.
|
|
2920
3101
|
*/
|
|
2921
3102
|
if (typeof this.scene.bustModelCache === 'function') this.scene.bustModelCache();
|
|
2922
|
-
|
|
3103
|
+
// The badge's three lines of 重载模型, read out of the table at each moment it is said — the language can change
|
|
3104
|
+
// while the load is in flight — and the same three the floating window says in its bubble.
|
|
3105
|
+
this.showStatus(words(this.language).reloading);
|
|
2923
3106
|
return this.loadModel(id).then(function (status) {
|
|
2924
3107
|
// Say what happened, because the visible difference between a good reload and a cached one is nothing at all.
|
|
2925
|
-
if (self.statusNode)
|
|
3108
|
+
if (self.statusNode) {
|
|
3109
|
+
var done = words(self.language);
|
|
3110
|
+
self.showStatus(status && status.ok ? done.reloaded : ((status && status.error) || done.reloadFailed));
|
|
3111
|
+
}
|
|
2926
3112
|
return status;
|
|
2927
3113
|
});
|
|
2928
3114
|
};
|
|
@@ -3097,7 +3283,7 @@
|
|
|
3097
3283
|
* where the clips are chosen.
|
|
3098
3284
|
*/
|
|
3099
3285
|
VrmPet3DWidget.prototype.refreshMotionNotice = function () {
|
|
3100
|
-
var text =
|
|
3286
|
+
var text = words(this.language);
|
|
3101
3287
|
var message = null;
|
|
3102
3288
|
|
|
3103
3289
|
if (!this.motionCatalog) {
|
|
@@ -3455,7 +3641,7 @@
|
|
|
3455
3641
|
VrmPet3DWidget.prototype.fetchMotionBytes = function (motionId) {
|
|
3456
3642
|
var url = '/vrmpet3d/motion?m=' + encodeURIComponent(motionId);
|
|
3457
3643
|
return fetch(url, { credentials: 'omit' }).then(function (response) {
|
|
3458
|
-
if (!response.ok) throw new Error('
|
|
3644
|
+
if (!response.ok) throw new Error(word(this.language, 'motionRoute', { status: response.status }));
|
|
3459
3645
|
return response.arrayBuffer();
|
|
3460
3646
|
});
|
|
3461
3647
|
};
|
|
@@ -3516,7 +3702,10 @@
|
|
|
3516
3702
|
self.motionLoaded[motionId] = status;
|
|
3517
3703
|
self.motionLoadedLoop[motionId] = declaredLoop;
|
|
3518
3704
|
} else {
|
|
3519
|
-
|
|
3705
|
+
// A scene that gave a reason has already said it in its own words; this is the widget's own sentence, and
|
|
3706
|
+
// `vrmpet3d:` stays in front of it because that prefix is what a report greps for.
|
|
3707
|
+
var message = (status && status.reason)
|
|
3708
|
+
|| ('vrmpet3d: ' + word(self.language, 'motionLoad', { id: motionId }));
|
|
3520
3709
|
self.showStatus(message);
|
|
3521
3710
|
self.motionStatusMessage = message;
|
|
3522
3711
|
}
|
|
@@ -3747,6 +3936,9 @@
|
|
|
3747
3936
|
var now = Date.now();
|
|
3748
3937
|
if (now - (this.focusCheckedAt || 0) >= FOCUS_POLL_MS) {
|
|
3749
3938
|
this.focusCheckedAt = now;
|
|
3939
|
+
// Locale changes do not advance the plugin settings version. Re-read alongside focus,
|
|
3940
|
+
// including background/wake pumps, so auto follows DSH without another settings write.
|
|
3941
|
+
this.adoptLanguage();
|
|
3750
3942
|
var doc = this.document;
|
|
3751
3943
|
var focused = true;
|
|
3752
3944
|
try {
|
|
@@ -3827,7 +4019,7 @@
|
|
|
3827
4019
|
// The TURN clock, not the state's own age: 用时 is a claim about DSH's turn (`PetMachine.statusClock`), and the
|
|
3828
4020
|
// token is dropped entirely while no turn is open — DSH's own token-free 「深度求索中」.
|
|
3829
4021
|
var clock = this.machine && typeof this.machine.statusClock === 'function' ? this.machine.statusClock() : undefined;
|
|
3830
|
-
var line = this.core.statusFor(this.preferences, state, this.machine.stateSince, Date.now(), clock);
|
|
4022
|
+
var line = this.core.statusFor(this.preferences, state, this.machine.stateSince, Date.now(), clock, this.language);
|
|
3831
4023
|
/**
|
|
3832
4024
|
* A state with no line clears the prompt rather than leaving the previous one up: a caption that
|
|
3833
4025
|
* outlives its state is how a pet ends up saying 深度求索中 over a finished turn.
|
|
@@ -4180,7 +4372,8 @@
|
|
|
4180
4372
|
if (this.machine && typeof this.machine.dancingNow === 'function' && this.machine.dancing) {
|
|
4181
4373
|
var dancing = this.machine.dancingNow();
|
|
4182
4374
|
var playing = this.scene.playback;
|
|
4183
|
-
if (dancing && playing && playing.state === 'dancing' && playing.
|
|
4375
|
+
if (dancing && playing && playing.state === 'dancing' && playing.id === dancing.motionId && playing.record
|
|
4376
|
+
&& this.scene.motionFinished(playing.record)) {
|
|
4184
4377
|
this.machine.stopDancing();
|
|
4185
4378
|
}
|
|
4186
4379
|
}
|
|
@@ -4598,7 +4791,14 @@
|
|
|
4598
4791
|
return out;
|
|
4599
4792
|
}
|
|
4600
4793
|
|
|
4601
|
-
/**
|
|
4794
|
+
/**
|
|
4795
|
+
* This surface's own words, in the two languages.
|
|
4796
|
+
*
|
|
4797
|
+
* One table per surface, `{ zh: {...}, en: {...} }` with IDENTICAL key sets — the repository's shape (see
|
|
4798
|
+
* `lib/menu.js`'s `LABELS`), so a language half that is missing an entry is a line that reads in the other
|
|
4799
|
+
* language rather than a line that is blank. Read through `words`/`word` at CALL time, never captured into a
|
|
4800
|
+
* variable: the language can change while the pet runs.
|
|
4801
|
+
*/
|
|
4602
4802
|
var STRINGS = {
|
|
4603
4803
|
en: {
|
|
4604
4804
|
/* The floating window's own state, kept as data by `syncDesktop`. */
|
|
@@ -4625,6 +4825,32 @@
|
|
|
4625
4825
|
vrm: 'This VRM model is driven by humanoid bones, so only .vrma clips work here — no .vrma clip was found.',
|
|
4626
4826
|
other: 'The loaded avatar has no skeleton, so no motion clip can drive it. Load a VRM model first.',
|
|
4627
4827
|
},
|
|
4828
|
+
/*
|
|
4829
|
+
* The pet's own OPERATIONS, said in the badge: 跳支舞 with nothing to dance to, and the three moments of
|
|
4830
|
+
* 重载模型. These are the SAME four events the floating window puts in its bubble, so the two surfaces'
|
|
4831
|
+
* wording is kept in step by hand — a drift here is the pet telling the user two different things about one
|
|
4832
|
+
* gesture. `assets/motions/dance` is a path, not words, and is written the same way in both languages.
|
|
4833
|
+
*/
|
|
4834
|
+
danceEmpty: 'No dance clip is available (assets/motions/dance is empty).',
|
|
4835
|
+
reloading: 'Reloading the model…',
|
|
4836
|
+
reloaded: 'Model reloaded',
|
|
4837
|
+
reloadFailed: 'Could not reload the model',
|
|
4838
|
+
/*
|
|
4839
|
+
* DIAGNOSTICS, shown in the badge (a chunk that never arrived, a constructor that threw). `vrmpet3d:` stays
|
|
4840
|
+
* in front of every one of them because it is a greppable marker in a report and a bug report quotes it; the
|
|
4841
|
+
* sentence AFTER it is what a user reads. The names in braces are data — a file, a global, an id, a number.
|
|
4842
|
+
*/
|
|
4843
|
+
mountNoCore: 'lib/core.js is not loaded.',
|
|
4844
|
+
mountNoScene: 'lib/scene.js is not loaded.',
|
|
4845
|
+
placementNoCore: 'Placement needs the core module (stepFall).',
|
|
4846
|
+
chunkNoPublish: '{file} loaded without publishing {global}',
|
|
4847
|
+
chunksMissing: 'DSH did not deliver {list}',
|
|
4848
|
+
catalogRoute: 'catalog route returned HTTP {status}',
|
|
4849
|
+
motionRoute: 'motion route returned HTTP {status}',
|
|
4850
|
+
motionLoad: 'could not load motion {id}',
|
|
4851
|
+
// The two reasons 打开设置 can fail, appended to `configNote` and shown in the badge.
|
|
4852
|
+
settingsNoWindow: 'the launcher could not open a window',
|
|
4853
|
+
settingsUnreachable: 'the host is not reachable',
|
|
4628
4854
|
},
|
|
4629
4855
|
zh: {
|
|
4630
4856
|
/* 悬浮窗自身的状态,由 `syncDesktop` 作为数据保存。 */
|
|
@@ -4648,6 +4874,23 @@
|
|
|
4648
4874
|
vrm: 'VRM 模型使用人形骨骼,因此只能用 .vrma 动作文件——未找到 .vrma 文件。',
|
|
4649
4875
|
other: '当前替身没有骨骼,任何动作文件都无法驱动它。请先加载 VRM 模型。',
|
|
4650
4876
|
},
|
|
4877
|
+
/* 宠物自身的操作提示,写在角标里;与外置悬浮窗气泡里的同一件事保持同一套措辞。路径不翻译。 */
|
|
4878
|
+
danceEmpty: '没有可用的舞蹈动作(assets/motions/dance 是空的)',
|
|
4879
|
+
reloading: '正在重载模型…',
|
|
4880
|
+
reloaded: '模型已重载',
|
|
4881
|
+
reloadFailed: '模型重载失败',
|
|
4882
|
+
/* 诊断信息:角标里给用户看的。`vrmpet3d:` 前缀保留,它是报告里可检索的标记;花括号里是数据。 */
|
|
4883
|
+
mountNoCore: '未加载 lib/core.js。',
|
|
4884
|
+
mountNoScene: '未加载 lib/scene.js。',
|
|
4885
|
+
placementNoCore: 'Placement 需要 core 模块(stepFall)。',
|
|
4886
|
+
chunkNoPublish: '{file} 加载后没有发布 {global}',
|
|
4887
|
+
chunksMissing: 'DSH 没有下发 {list}',
|
|
4888
|
+
catalogRoute: '模型目录路由返回 HTTP {status}',
|
|
4889
|
+
motionRoute: '动作路由返回 HTTP {status}',
|
|
4890
|
+
motionLoad: '无法加载动作 {id}',
|
|
4891
|
+
/* 打开设置失败的两种原因,接在 `configNote` 后面显示在角标里。 */
|
|
4892
|
+
settingsNoWindow: '启动器无法打开窗口',
|
|
4893
|
+
settingsUnreachable: '无法连接到宿主',
|
|
4651
4894
|
}
|
|
4652
4895
|
};
|
|
4653
4896
|
|
|
@@ -4655,18 +4898,6 @@
|
|
|
4655
4898
|
// Plugin
|
|
4656
4899
|
// ===========================================================================
|
|
4657
4900
|
|
|
4658
|
-
function readLanguage(locale) {
|
|
4659
|
-
try {
|
|
4660
|
-
var snapshot = locale && typeof locale.getSnapshot === 'function' ? locale.getSnapshot() : undefined;
|
|
4661
|
-
var value = snapshot && (snapshot.language || snapshot.locale || snapshot.current);
|
|
4662
|
-
if (typeof value === 'string' && value.toLowerCase().indexOf('zh') === 0) return 'zh';
|
|
4663
|
-
var lang = document.documentElement.lang || '';
|
|
4664
|
-
return lang.toLowerCase().indexOf('zh') === 0 ? 'zh' : 'en';
|
|
4665
|
-
} catch (error) {
|
|
4666
|
-
return 'en';
|
|
4667
|
-
}
|
|
4668
|
-
}
|
|
4669
|
-
|
|
4670
4901
|
/**
|
|
4671
4902
|
* Subscribe to live turn boundaries over the plugin's own host service.
|
|
4672
4903
|
*
|
|
@@ -4819,7 +5050,12 @@
|
|
|
4819
5050
|
if (disposed) return;
|
|
4820
5051
|
try {
|
|
4821
5052
|
widget = new VrmPet3DWidget(host, {
|
|
4822
|
-
|
|
5053
|
+
/**
|
|
5054
|
+
* The DSH locale SERVICE, not a snapshot of it: the widget resolves 界面语言 itself — from the
|
|
5055
|
+
* user's `preferences.language`, this locale and the browser's hints — and re-resolves it
|
|
5056
|
+
* whenever the settings change (`adoptLanguage`), because `auto` means "whatever DSH says".
|
|
5057
|
+
*/
|
|
5058
|
+
locale: ctx.locale,
|
|
4823
5059
|
onScopeChange: function (scope) { if (changeScope.current) changeScope.current(scope); },
|
|
4824
5060
|
/**
|
|
4825
5061
|
* Report the state to the host, so the floating window can show the same prompt.
|