dsh-plugin-vrmpet3d 0.1.1 → 0.1.3
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 +1 -0
- package/assets/game/levels.json +2925 -0
- package/desktop/game-visibility.cjs +50 -0
- package/desktop/main.js +60 -3
- package/desktop/preload.js +11 -0
- package/lib/client.core.js +279 -41
- package/lib/client.js +144 -16
- package/lib/client.menu.js +17 -4
- package/lib/client.scene.js +134 -7
- package/lib/config-page.html +11 -0
- package/lib/config-page.js +153 -25
- package/lib/core.js +279 -41
- package/lib/desktop-page.js +167 -40
- package/lib/desktop.js +18 -1
- package/lib/global-page.html +34 -4
- package/lib/index.js +94 -25
- package/lib/menu.js +17 -4
- package/lib/mini-game-core.js +329 -0
- package/lib/mini-game-level-store.js +37 -0
- package/lib/mini-game-page.js +615 -0
- package/lib/mini-game.css +19 -0
- package/lib/mini-game.html +50 -0
- package/lib/scene.js +134 -7
- package/lib/vrm-info.js +16 -1
- package/package.json +3 -3
package/lib/client.js
CHANGED
|
@@ -300,6 +300,7 @@
|
|
|
300
300
|
*/
|
|
301
301
|
'.vrmpet3d-status[data-tone="gold"]{color:light-dark(#8a5a00,#ffc94d);--dsw-alias-label-shimmer:light-dark(#ffe0a3,#fff0c9)}',
|
|
302
302
|
'.vrmpet3d-status[data-tone="gold"][data-bold="1"]{font-weight:700}',
|
|
303
|
+
'.vrmpet3d-status[data-attention="1"]{pointer-events:auto;cursor:pointer;text-shadow:0 0 4px currentColor,0 0 10px currentColor,0 0 18px currentColor}',
|
|
303
304
|
'.vrmpet3d-status[data-tone="danger"]{color:light-dark(#b3261e,#ff6b6b);--dsw-alias-label-shimmer:light-dark(#ffc9c4,#ffd0cc)}',
|
|
304
305
|
/**
|
|
305
306
|
* 碎碎念: the pet's own short remark, in a bubble above the status prompt.
|
|
@@ -411,6 +412,7 @@
|
|
|
411
412
|
'.vrmpet3d-status-content{display:flex;align-items:center;min-width:0}',
|
|
412
413
|
/* `nowrap`, always: the operator's "提示文字永不换行". One line, however long the line is. */
|
|
413
414
|
'.vrmpet3d-status-text{color:inherit;font-variant-numeric:tabular-nums;white-space:nowrap}',
|
|
415
|
+
'.vrmpet3d-status-dot{pointer-events:auto;cursor:pointer}',
|
|
414
416
|
/* The words are rendered twice from ONE string: `attr()` feeds both copies, so the two cannot drift. */
|
|
415
417
|
'.vrmpet3d-status-text[data-shimmer-text]::after{content:attr(data-shimmer-text)}',
|
|
416
418
|
'.vrmpet3d-status-decoration{position:absolute;inset:0;overflow:clip;pointer-events:none;user-select:none}',
|
|
@@ -780,6 +782,8 @@
|
|
|
780
782
|
function VrmPet3DWidget(host, options) {
|
|
781
783
|
options = options || {};
|
|
782
784
|
this.host = host;
|
|
785
|
+
this.openSession = options.openSession || function () {};
|
|
786
|
+
this.conversationVisible = options.conversationVisible || function () { return true; };
|
|
783
787
|
this.core = globalThis.__VRMPET3D_CORE__;
|
|
784
788
|
this.sceneApi = globalThis.__VRMPET3D_SCENE__;
|
|
785
789
|
/**
|
|
@@ -812,6 +816,7 @@
|
|
|
812
816
|
this.placement = new Placement(this.preferences, function () { return Date.now(); }, this.core);
|
|
813
817
|
|
|
814
818
|
this.machine = new this.core.PetMachine(function () { return Date.now(); }, this.preferences.sleepAfterMs);
|
|
819
|
+
this.machine.requireViewedAcknowledgement = typeof options.conversationVisible === 'function';
|
|
815
820
|
/**
|
|
816
821
|
* The machine reads the settings it needs through the WIDGET, not from a copy.
|
|
817
822
|
*
|
|
@@ -1135,6 +1140,7 @@
|
|
|
1135
1140
|
*/
|
|
1136
1141
|
if (this.core.watchHostPreview) {
|
|
1137
1142
|
this.previewSync = this.core.watchHostPreview({
|
|
1143
|
+
onNavigate: function (id) { self.openSession(id); },
|
|
1138
1144
|
onPreview: function (preview) {
|
|
1139
1145
|
/**
|
|
1140
1146
|
* A PREVIEW ENDS A DANCE — and a preview of the DANCING state STARTS one.
|
|
@@ -1224,6 +1230,7 @@
|
|
|
1224
1230
|
// ---- DOM -----------------------------------------------------------------
|
|
1225
1231
|
|
|
1226
1232
|
VrmPet3DWidget.prototype.build = function () {
|
|
1233
|
+
var self = this;
|
|
1227
1234
|
var doc = this.document;
|
|
1228
1235
|
var root = doc.createElement('div');
|
|
1229
1236
|
root.className = 'vrmpet3d-root';
|
|
@@ -1288,6 +1295,10 @@
|
|
|
1288
1295
|
this.canvas = root.querySelector('.vrmpet3d-pet canvas');
|
|
1289
1296
|
this.badge = root.querySelector('.vrmpet3d-badge');
|
|
1290
1297
|
this.statusNode = root.querySelector('.vrmpet3d-status');
|
|
1298
|
+
this.statusNode.addEventListener('click', function (event) {
|
|
1299
|
+
event.stopPropagation();
|
|
1300
|
+
self.openStatusSession();
|
|
1301
|
+
});
|
|
1291
1302
|
this.statusIcon = root.querySelector('.vrmpet3d-status-icon');
|
|
1292
1303
|
/**
|
|
1293
1304
|
* The green dot, when a completion is waiting elsewhere.
|
|
@@ -1298,6 +1309,11 @@
|
|
|
1298
1309
|
* puts beside that session in its list, and it disappears when the user goes back there.
|
|
1299
1310
|
*/
|
|
1300
1311
|
this.statusDot = root.querySelector('.vrmpet3d-status-dot');
|
|
1312
|
+
this.statusDot.addEventListener('click', function (event) {
|
|
1313
|
+
event.stopPropagation();
|
|
1314
|
+
self.openQueuedSession('completed');
|
|
1315
|
+
});
|
|
1316
|
+
this.statusDot.title = this.language === 'en' ? 'Open completed conversation' : '打开已完成的会话';
|
|
1301
1317
|
this.statusDecoration = root.querySelector('.vrmpet3d-status-decoration');
|
|
1302
1318
|
/**
|
|
1303
1319
|
* The prompt's two text copies — the real one and the masked highlight — which always render the SAME
|
|
@@ -1651,6 +1667,19 @@
|
|
|
1651
1667
|
*/
|
|
1652
1668
|
VrmPet3DWidget.prototype.openSettingsWindow = function (page) {
|
|
1653
1669
|
var self = this;
|
|
1670
|
+
if (page === 'game') {
|
|
1671
|
+
var gameUrl = '/vrmpet3d/game?model=' + encodeURIComponent(self.currentModelId() || '');
|
|
1672
|
+
if ((!globalThis.dshDesktop && !/Electron\//i.test(globalThis.navigator.userAgent)) || !self.canUseIndependentPet()) {
|
|
1673
|
+
globalThis.open(gameUrl, '_blank', 'noopener'); return;
|
|
1674
|
+
}
|
|
1675
|
+
fetch('/vrmpet3d/desktop', { method: 'POST', credentials: 'omit',
|
|
1676
|
+
headers: { 'content-type': 'application/json' },
|
|
1677
|
+
body: JSON.stringify({ action: 'game', modelId: self.currentModelId() })
|
|
1678
|
+
}).then(function (response) {
|
|
1679
|
+
if (!response.ok) self.showStatus('小游戏窗口打开失败,请重试。');
|
|
1680
|
+
}).catch(function () { self.showStatus('小游戏窗口打开失败,请重试。'); });
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1654
1683
|
/**
|
|
1655
1684
|
* Which window, and the host action that opens it.
|
|
1656
1685
|
*
|
|
@@ -1762,6 +1791,7 @@
|
|
|
1762
1791
|
*/
|
|
1763
1792
|
VrmPet3DWidget.prototype.openMenuAt = function (clientX, clientY) {
|
|
1764
1793
|
if (!this.menu) return false;
|
|
1794
|
+
if (typeof this.menu.setSurfaceVisible === 'function') this.menu.setSurfaceVisible(this.canUseIndependentPet());
|
|
1765
1795
|
var doc = this.document;
|
|
1766
1796
|
var view = doc.defaultView;
|
|
1767
1797
|
var viewportWidth = Number(view && view.innerWidth) || doc.documentElement.clientWidth || 0;
|
|
@@ -1774,6 +1804,7 @@
|
|
|
1774
1804
|
* it is read when the menu is built.
|
|
1775
1805
|
*/
|
|
1776
1806
|
this.menu.setSurfaceAllowed(this.preferences.petSurface !== 'both');
|
|
1807
|
+
if (this.menu.setDanceState) this.menu.setDanceState(this.machine.state);
|
|
1777
1808
|
/**
|
|
1778
1809
|
* 重力's check mark, read at the same moment and for the same reason: a host snapshot or a settings-window
|
|
1779
1810
|
* edit can change the preference while the page keeps running, so a menu that cached it would show the
|
|
@@ -1834,6 +1865,7 @@
|
|
|
1834
1865
|
container: this.root,
|
|
1835
1866
|
language: this.language,
|
|
1836
1867
|
toSurface: 'window',
|
|
1868
|
+
showSurface: this.canUseIndependentPet(),
|
|
1837
1869
|
onChoose: function (id) { self.onMenuChoice(id); },
|
|
1838
1870
|
});
|
|
1839
1871
|
return this.menu;
|
|
@@ -1856,6 +1888,7 @@
|
|
|
1856
1888
|
// 切换模型: the model picker's own page. It decides WHICH model every other page is about, so it is a
|
|
1857
1889
|
// window of its own rather than a row inside 当前模型参数设置 (see ITEMS in lib/menu.js).
|
|
1858
1890
|
if (id === 'switchModel') { this.openSettingsWindow('picker'); return; }
|
|
1891
|
+
if (id === 'miniGame') { this.openSettingsWindow('game'); return; }
|
|
1859
1892
|
/**
|
|
1860
1893
|
* 重载模型 — re-read the model file and rebuild the scene (operator, 2026-10-14: 「给内置和外置宠物的右键菜单新增“重载模型”
|
|
1861
1894
|
* 的选项,用来刷新模型」).
|
|
@@ -2284,6 +2317,12 @@
|
|
|
2284
2317
|
* the widget's own state. "The window is up but the pet is not drawing in it" stays answerable that way, which
|
|
2285
2318
|
* is the whole reason the host reports the page's own verdict.
|
|
2286
2319
|
*/
|
|
2320
|
+
VrmPet3DWidget.prototype.canUseIndependentPet = function () {
|
|
2321
|
+
// The host can borrow Electron even when it is itself a plain Node process.
|
|
2322
|
+
// Its runtime result, rather than the UI bridge or process type, decides this.
|
|
2323
|
+
return Boolean(this.desktopStatus && this.desktopStatus.electronAvailable === true);
|
|
2324
|
+
};
|
|
2325
|
+
|
|
2287
2326
|
VrmPet3DWidget.prototype.syncDesktop = function (status, note) {
|
|
2288
2327
|
var strings = words(this.language);
|
|
2289
2328
|
this.desktopStatus = status || null;
|
|
@@ -2370,6 +2409,7 @@
|
|
|
2370
2409
|
* place that knows it. A caller that omits it gets the click pool, which is what an older cached chunk does.
|
|
2371
2410
|
*/
|
|
2372
2411
|
VrmPet3DWidget.prototype.reactSpeech = function (anatomy, gesture) {
|
|
2412
|
+
if (this.machine && this.machine.state === 'dancing') return null;
|
|
2373
2413
|
if (!this.speech) return null;
|
|
2374
2414
|
var random = this.scene && typeof this.scene.random === 'function' ? this.scene.random : null;
|
|
2375
2415
|
var line = this.speech.react(
|
|
@@ -3052,6 +3092,7 @@
|
|
|
3052
3092
|
*/
|
|
3053
3093
|
VrmPet3DWidget.prototype.startDance = function (motionId) {
|
|
3054
3094
|
if (!this.machine || typeof this.machine.setDancing !== 'function') return false;
|
|
3095
|
+
if (['resting', 'offline', 'dancing'].indexOf(this.machine.resolve(this.aggregate.snapshot())) === -1) return false;
|
|
3055
3096
|
var wanted = typeof motionId === 'string' && motionId ? motionId : null;
|
|
3056
3097
|
if (!wanted) {
|
|
3057
3098
|
var ids = this.danceClipIds();
|
|
@@ -3080,6 +3121,9 @@
|
|
|
3080
3121
|
self.scene.setPreview({ state: 'dancing', motionId: wanted });
|
|
3081
3122
|
}
|
|
3082
3123
|
});
|
|
3124
|
+
// Publish the accepted dance immediately, including when this surface is hidden.
|
|
3125
|
+
// The floating window must not wait for the next background animation frame.
|
|
3126
|
+
if (!this.disposed) this.pump();
|
|
3083
3127
|
return true;
|
|
3084
3128
|
};
|
|
3085
3129
|
|
|
@@ -3347,6 +3391,11 @@
|
|
|
3347
3391
|
running: snap.running === true,
|
|
3348
3392
|
working: Number(snap.workingCount) || 0,
|
|
3349
3393
|
waiting: Number(snap.waitingCount) || 0,
|
|
3394
|
+
statusSessionId: this.machine.state === 'waiting' ? snap.waitingSessionId
|
|
3395
|
+
: (this.machine.notice && this.machine.notice.sessionId) || null,
|
|
3396
|
+
completedSessionId: snap.completedSessionId || null,
|
|
3397
|
+
waitingSessionIds: snap.waitingSessionIds || [],
|
|
3398
|
+
completedSessionIds: snap.completedSessionIds || [],
|
|
3350
3399
|
unread: Number(snap.unreadCount) || 0,
|
|
3351
3400
|
/**
|
|
3352
3401
|
* The breakdown behind that count, as numbers — why the green dot is lit, with no session id or title in it.
|
|
@@ -3952,8 +4001,11 @@
|
|
|
3952
4001
|
}
|
|
3953
4002
|
}
|
|
3954
4003
|
|
|
4004
|
+
this.acknowledgeVisibleConversation();
|
|
3955
4005
|
var snapshot = this.aggregate.snapshot();
|
|
3956
4006
|
var state = this.machine.resolve(snapshot);
|
|
4007
|
+
this.completedUnreadCount = snapshot.completedUnreadCount;
|
|
4008
|
+
if (this.menu && this.menu.setDanceState) this.menu.setDanceState(state);
|
|
3957
4009
|
// Dance previews belong to the dance lifetime, including work interruption.
|
|
3958
4010
|
if (this.scene.preview && this.scene.preview.state === 'dancing' && state !== 'dancing') {
|
|
3959
4011
|
this.scene.setPreview(null);
|
|
@@ -3985,11 +4037,13 @@
|
|
|
3985
4037
|
*/
|
|
3986
4038
|
var clock = this.machine && typeof this.machine.statusClock === 'function' ? this.machine.statusClock() : null;
|
|
3987
4039
|
var clockKey = clock ? clock.startedAt + ':' + (clock.endedAt || '') : '';
|
|
4040
|
+
var completedKey = (snapshot.completedSessionIds || []).join(',') + '|' + (snapshot.waitingSessionIds || []).join(',');
|
|
3988
4041
|
if (state !== this.lastPublishedState || speech !== this.lastPublishedSpeech || touch !== null
|
|
3989
|
-
|| clockKey !== this.lastPublishedClockKey) {
|
|
4042
|
+
|| clockKey !== this.lastPublishedClockKey || completedKey !== this.lastPublishedCompletedKey) {
|
|
3990
4043
|
this.lastPublishedState = state;
|
|
3991
4044
|
this.lastPublishedSpeech = speech;
|
|
3992
4045
|
this.lastPublishedClockKey = clockKey;
|
|
4046
|
+
this.lastPublishedCompletedKey = completedKey;
|
|
3993
4047
|
/**
|
|
3994
4048
|
* `clock` rides along because the window has no turn frames of its own, and the UNREAD count because the window
|
|
3995
4049
|
* has no session store either: its green dot is DSH's `completionUnread`, which only this half can see.
|
|
@@ -3998,7 +4052,7 @@
|
|
|
3998
4052
|
* question with an answer (`/vrmpet3d/state`) instead of a guess. See `decisionWhy`.
|
|
3999
4053
|
*/
|
|
4000
4054
|
if (this.publishState) {
|
|
4001
|
-
this.publishState(state, speech, touch, clock, null,
|
|
4055
|
+
this.publishState(state, speech, touch, clock, null, snapshot.completedUnreadCount || 0, this.decisionWhy());
|
|
4002
4056
|
}
|
|
4003
4057
|
}
|
|
4004
4058
|
|
|
@@ -4019,7 +4073,7 @@
|
|
|
4019
4073
|
// The TURN clock, not the state's own age: 用时 is a claim about DSH's turn (`PetMachine.statusClock`), and the
|
|
4020
4074
|
// token is dropped entirely while no turn is open — DSH's own token-free 「深度求索中」.
|
|
4021
4075
|
var clock = this.machine && typeof this.machine.statusClock === 'function' ? this.machine.statusClock() : undefined;
|
|
4022
|
-
var line = this.core.statusFor(this.preferences, state, this.machine.stateSince, Date.now(), clock, this.language);
|
|
4076
|
+
var line = this.core.statusFor(this.preferences, state, this.machine.stateSince, Date.now(), clock, this.language, this.completedUnreadCount);
|
|
4023
4077
|
/**
|
|
4024
4078
|
* A state with no line clears the prompt rather than leaving the previous one up: a caption that
|
|
4025
4079
|
* outlives its state is how a pet ends up saying 深度求索中 over a finished turn.
|
|
@@ -4047,6 +4101,8 @@
|
|
|
4047
4101
|
* other, and the attribute is cheap. `data-tone` is what the stylesheet reads, so the colours live in CSS
|
|
4048
4102
|
* (where the two surfaces can differ) and the DECISION lives in `core.statusFor` (where they cannot).
|
|
4049
4103
|
*/
|
|
4104
|
+
if (['done', 'waiting', 'error'].indexOf(state) !== -1) node.setAttribute('data-attention', '1');
|
|
4105
|
+
else node.removeAttribute('data-attention');
|
|
4050
4106
|
if (line.tone) node.setAttribute('data-tone', line.tone);
|
|
4051
4107
|
else node.removeAttribute('data-tone');
|
|
4052
4108
|
if (line.bold) node.setAttribute('data-bold', '1');
|
|
@@ -4074,8 +4130,8 @@
|
|
|
4074
4130
|
// The dot is about ANOTHER session's unread completion, so it is drawn from the machine's count and not from
|
|
4075
4131
|
// the line: the line is what is happening here, the dot is what finished elsewhere.
|
|
4076
4132
|
if (this.statusDot) {
|
|
4077
|
-
this.statusDot.hidden = !(line.state
|
|
4078
|
-
&&
|
|
4133
|
+
this.statusDot.hidden = !(['working', 'waiting'].indexOf(line.state) !== -1
|
|
4134
|
+
&& Number(this.completedUnreadCount) > 0);
|
|
4079
4135
|
}
|
|
4080
4136
|
}
|
|
4081
4137
|
// Every frame, not only when the words change: the caption is anchored to the HEAD, so an orbit, a zoom
|
|
@@ -4206,6 +4262,27 @@
|
|
|
4206
4262
|
return line;
|
|
4207
4263
|
};
|
|
4208
4264
|
|
|
4265
|
+
VrmPet3DWidget.prototype.openStatusSession = function () {
|
|
4266
|
+
var state = this.machine.state;
|
|
4267
|
+
if (state === 'waiting' || state === 'done') return this.openQueuedSession(state === 'waiting' ? 'waiting' : 'completed');
|
|
4268
|
+
var snap = this.aggregate.snapshot();
|
|
4269
|
+
var id = state === 'waiting' ? snap.waitingSessionId
|
|
4270
|
+
: ((state === 'done' || state === 'error') && this.machine.notice ? this.machine.notice.sessionId : null);
|
|
4271
|
+
if (id) this.openSession(id);
|
|
4272
|
+
};
|
|
4273
|
+
|
|
4274
|
+
VrmPet3DWidget.prototype.openQueuedSession = function (kind) {
|
|
4275
|
+
var snap = this.aggregate.snapshot();
|
|
4276
|
+
var ids = kind === 'waiting' ? snap.waitingSessionIds : snap.completedSessionIds;
|
|
4277
|
+
if (!ids || !ids.length) return null;
|
|
4278
|
+
this.queueCursor = this.queueCursor || {};
|
|
4279
|
+
var index = ids.indexOf(this.queueCursor[kind]);
|
|
4280
|
+
var id = ids[(index + 1) % ids.length];
|
|
4281
|
+
this.queueCursor[kind] = id;
|
|
4282
|
+
this.openSession(id);
|
|
4283
|
+
return id;
|
|
4284
|
+
};
|
|
4285
|
+
|
|
4209
4286
|
/**
|
|
4210
4287
|
* Ask the host for a generated line, when the machine has asked for one and nobody has yet.
|
|
4211
4288
|
*
|
|
@@ -4436,22 +4513,27 @@
|
|
|
4436
4513
|
// …and a badge the scene wrote is retracted once the scene is happy again (see `retractSceneStatus`).
|
|
4437
4514
|
this.retractSceneStatus();
|
|
4438
4515
|
|
|
4439
|
-
// The
|
|
4440
|
-
// the pet has THIS frame: the cursor can sit anywhere in the window (the listener is
|
|
4441
|
-
// on the document), and the pet can be dragged or falling while it looks. Using the
|
|
4442
|
-
// placement instead of `getBoundingClientRect` keeps this out of layout.
|
|
4516
|
+
// The rendered eyes, rather than the slot's centre, are the zero of the aim.
|
|
4443
4517
|
if (this.preferences.lookAtPointer && this.pointer) {
|
|
4444
4518
|
var pixels = this.placement.pixels();
|
|
4519
|
+
var eye = typeof this.scene.gazeOrigin === 'function' ? this.scene.gazeOrigin() : null;
|
|
4520
|
+
var gazeView = this.document && this.document.defaultView;
|
|
4521
|
+
var gazeScreenWidth = Number(gazeView && gazeView.screen && gazeView.screen.width) || this.placement.viewportWidth;
|
|
4445
4522
|
var aim = this.core.pointerAim({
|
|
4446
4523
|
pointerX: this.pointer.x,
|
|
4447
4524
|
pointerY: this.pointer.y,
|
|
4448
|
-
centreX: pixels.left + this.placement.width / 2,
|
|
4449
|
-
centreY: pixels.top + this.placement.height / 2,
|
|
4525
|
+
centreX: pixels.left + (eye ? eye.x * this.placement.width / this.scene.width : this.placement.width / 2),
|
|
4526
|
+
centreY: pixels.top + (eye ? eye.y * this.placement.height / this.scene.height : this.placement.height / 2),
|
|
4450
4527
|
// The same two settings as the floating window's (the 模型参数 window's 视线 section), with the
|
|
4451
|
-
// same "auto" default: 0 px means
|
|
4452
|
-
spanX: this.core.resolveGazeSpan(this.preferences.gazeSpanX,
|
|
4453
|
-
spanY: this.core.resolveGazeSpan(this.preferences.gazeSpanY,
|
|
4454
|
-
curve: this.preferences.gazeCurve
|
|
4528
|
+
// same "auto" default: 0 px means half the screen width for both axes.
|
|
4529
|
+
spanX: this.core.resolveGazeSpan(this.preferences.gazeSpanX, gazeScreenWidth),
|
|
4530
|
+
spanY: this.core.resolveGazeSpan(this.preferences.gazeSpanY, gazeScreenWidth),
|
|
4531
|
+
curve: this.preferences.gazeCurve,
|
|
4532
|
+
curveY: this.preferences.gazeCurveY,
|
|
4533
|
+
curveRadial: this.preferences.gazeCurveRadial,
|
|
4534
|
+
curveSpatial: this.preferences.gazeCurveSpatial,
|
|
4535
|
+
distance: this.preferences.gazeDistance,
|
|
4536
|
+
mode: this.preferences.gazeCurveMode
|
|
4455
4537
|
});
|
|
4456
4538
|
this.scene.motion.setGaze(this.scene.gazeFromAim(aim));
|
|
4457
4539
|
} else {
|
|
@@ -4463,15 +4545,30 @@
|
|
|
4463
4545
|
|
|
4464
4546
|
// ---- session bridge ------------------------------------------------------
|
|
4465
4547
|
|
|
4548
|
+
VrmPet3DWidget.prototype.acknowledgeVisibleConversation = function () {
|
|
4549
|
+
var focused = typeof this.document.hasFocus === 'function' ? this.document.hasFocus() : this.machine.focused;
|
|
4550
|
+
if (!focused || this.document.visibilityState === 'hidden' || this.machine.interaction || !this.conversationVisible()) return;
|
|
4551
|
+
// Read the visible conversation on every host update and pump, regardless of how it was opened.
|
|
4552
|
+
var input = this.aggregate.input;
|
|
4553
|
+
this.aggregate.acknowledgeViewed(selectedSessionId(input.catalog) || input.currentSessionId);
|
|
4554
|
+
};
|
|
4555
|
+
|
|
4466
4556
|
VrmPet3DWidget.prototype.setInput = function (input) {
|
|
4467
4557
|
this.aggregate.setInput(input);
|
|
4558
|
+
this.acknowledgeVisibleConversation();
|
|
4468
4559
|
var snapshot = this.aggregate.snapshot();
|
|
4469
4560
|
this.machine.resolve(snapshot);
|
|
4470
4561
|
};
|
|
4471
4562
|
|
|
4472
4563
|
VrmPet3DWidget.prototype.boundary = function (event) {
|
|
4473
4564
|
this.aggregate.boundary(event);
|
|
4474
|
-
|
|
4565
|
+
if (event && event.sessionId && !this.aggregate.mainSession(event.sessionId)) {
|
|
4566
|
+
this.machine.resolve(this.aggregate.snapshot());
|
|
4567
|
+
return;
|
|
4568
|
+
}
|
|
4569
|
+
this.machine.boundary(this.aggregate.snapshot(), Object.assign({}, event, {
|
|
4570
|
+
confirmed: event && event.reason === 'completed' && this.aggregate.running(event.sessionId) === false,
|
|
4571
|
+
}));
|
|
4475
4572
|
};
|
|
4476
4573
|
|
|
4477
4574
|
VrmPet3DWidget.prototype.resetAggregate = function (identities) {
|
|
@@ -4721,6 +4818,10 @@
|
|
|
4721
4818
|
|
|
4722
4819
|
function selectedSessionId(catalog) {
|
|
4723
4820
|
if (!catalog) return undefined;
|
|
4821
|
+
var visible = Object.values(catalog.byId || {}).find(function (row) {
|
|
4822
|
+
return row && row.retainedBy && Number(row.retainedBy.mainView) > 0;
|
|
4823
|
+
});
|
|
4824
|
+
if (visible && typeof visible.id === 'string') return visible.id;
|
|
4724
4825
|
if (typeof catalog.currentSessionId === 'string') return catalog.currentSessionId;
|
|
4725
4826
|
if (catalog.selectedSessionId) return catalog.selectedSessionId;
|
|
4726
4827
|
if (catalog.current && typeof catalog.current.id === 'string') return catalog.current.id;
|
|
@@ -5056,6 +5157,18 @@
|
|
|
5056
5157
|
* whenever the settings change (`adoptLanguage`), because `auto` means "whatever DSH says".
|
|
5057
5158
|
*/
|
|
5058
5159
|
locale: ctx.locale,
|
|
5160
|
+
conversationVisible: function () {
|
|
5161
|
+
var layout = typeof ctx.get === 'function' ? ctx.get('layout') : ctx.layout;
|
|
5162
|
+
return !layout || !layout.panelInfo || layout.panelInfo.getSnapshot().activePanelId === null;
|
|
5163
|
+
},
|
|
5164
|
+
openSession: function (id) {
|
|
5165
|
+
var entries = ctx.slots.entries('sidebar.workspaces');
|
|
5166
|
+
for (var i = 0; i < entries.length; i += 1) {
|
|
5167
|
+
var injected = typeof entries[i].inject === 'function' ? entries[i].inject() : entries[i].inject;
|
|
5168
|
+
if (injected && typeof injected.open === 'function') { injected.open(id); return true; }
|
|
5169
|
+
}
|
|
5170
|
+
return false;
|
|
5171
|
+
},
|
|
5059
5172
|
onScopeChange: function (scope) { if (changeScope.current) changeScope.current(scope); },
|
|
5060
5173
|
/**
|
|
5061
5174
|
* Report the state to the host, so the floating window can show the same prompt.
|
|
@@ -5073,6 +5186,21 @@
|
|
|
5073
5186
|
}
|
|
5074
5187
|
});
|
|
5075
5188
|
widgetRef.current = widget;
|
|
5189
|
+
// A browser game opens this explicit query contract in a NEW DSH tab.
|
|
5190
|
+
// Retry until the workspace slot is ready; consume the query only on success.
|
|
5191
|
+
var gameTarget = new URL(globalThis.location.href).searchParams.get('vrmpet3d-session');
|
|
5192
|
+
if (gameTarget && gameTarget.length <= 64) {
|
|
5193
|
+
var gameAttempts = 0;
|
|
5194
|
+
var gameNavigation = setInterval(function () {
|
|
5195
|
+
if (disposed || ++gameAttempts > 60) { clearInterval(gameNavigation); return; }
|
|
5196
|
+
if (widget.openSession(gameTarget)) {
|
|
5197
|
+
clearInterval(gameNavigation);
|
|
5198
|
+
var cleanUrl = new URL(globalThis.location.href);
|
|
5199
|
+
cleanUrl.searchParams.delete('vrmpet3d-session');
|
|
5200
|
+
globalThis.history.replaceState(globalThis.history.state, '', cleanUrl.href);
|
|
5201
|
+
}
|
|
5202
|
+
}, 250);
|
|
5203
|
+
}
|
|
5076
5204
|
widgets.add(widget);
|
|
5077
5205
|
setStatus('ready');
|
|
5078
5206
|
} catch (error) {
|
package/lib/client.menu.js
CHANGED
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
* whose effect ends by itself (the clip finishes, or the user double-clicks the pet, or work starts).
|
|
69
69
|
*/
|
|
70
70
|
'dance',
|
|
71
|
+
'miniGame',
|
|
71
72
|
/**
|
|
72
73
|
* 重载模型 — refresh the model on screen without touching anything else (operator, 2026-10-14: 「给内置和外置宠物的右键菜单
|
|
73
74
|
* 新增“重载模型”的选项,用来刷新模型」).
|
|
@@ -110,10 +111,11 @@
|
|
|
110
111
|
resetSize: 'Reset size',
|
|
111
112
|
globalSettings: 'General settings',
|
|
112
113
|
switchModel: 'Switch model',
|
|
114
|
+
miniGame: 'Pet breakout',
|
|
113
115
|
reloadModel: 'Reload model',
|
|
114
116
|
dance: 'Dance for me',
|
|
115
117
|
modelSettings: "This model's parameters",
|
|
116
|
-
surfaceWindow: '
|
|
118
|
+
surfaceWindow: 'Switch to an independent pet',
|
|
117
119
|
surfaceApp: 'Move back inside the app',
|
|
118
120
|
surfaceOff: 'disabled while debug mode is on',
|
|
119
121
|
gravity: 'Gravity',
|
|
@@ -138,10 +140,11 @@
|
|
|
138
140
|
resetSize: '重置大小',
|
|
139
141
|
globalSettings: '通用设置',
|
|
140
142
|
switchModel: '切换模型',
|
|
143
|
+
miniGame: '小游戏 · 吃白饭',
|
|
141
144
|
reloadModel: '重载模型',
|
|
142
145
|
dance: '跳支舞',
|
|
143
146
|
modelSettings: '当前模型参数设置',
|
|
144
|
-
surfaceWindow: '
|
|
147
|
+
surfaceWindow: '切换为独立宠物',
|
|
145
148
|
surfaceApp: '切换回窗口内',
|
|
146
149
|
surfaceOff: '调试模式下不生效',
|
|
147
150
|
gravity: '重力',
|
|
@@ -236,6 +239,7 @@
|
|
|
236
239
|
var onChoose = typeof options.onChoose === 'function' ? options.onChoose : function () {};
|
|
237
240
|
var toSurface = options.toSurface === 'app' ? 'app' : 'window';
|
|
238
241
|
var surfaceAllowed = options.surfaceAllowed !== false;
|
|
242
|
+
var danceState = 'resting';
|
|
239
243
|
/** Whether this surface offers the 切换 row at all. See the `showSurface` option. */
|
|
240
244
|
var showSurface = options.showSurface !== false;
|
|
241
245
|
|
|
@@ -262,6 +266,8 @@
|
|
|
262
266
|
|
|
263
267
|
/** The label for one id, from the state this menu was built with. */
|
|
264
268
|
function labelFor(id) {
|
|
269
|
+
if (id === 'dance') return danceState === 'dancing' ? (options.language === 'en' ? 'Another dance' : '换一支舞')
|
|
270
|
+
: (['resting', 'offline'].indexOf(danceState) === -1 ? (options.language === 'en' ? 'The pet is busy…' : '宠物目前很忙…') : text.dance);
|
|
265
271
|
if (id === 'surface') return toSurface === 'app' ? text.surfaceApp : text.surfaceWindow;
|
|
266
272
|
if (id === 'closePet') return toSurface === 'app' ? text.closePetWindow : text.closePetApp;
|
|
267
273
|
return text[id] || id;
|
|
@@ -275,7 +281,8 @@
|
|
|
275
281
|
ITEMS.forEach(function (id) {
|
|
276
282
|
// The 切换 row is the one entry a surface may omit — the window's 关闭独立宠物 already does its job.
|
|
277
283
|
if (id === 'surface' && !showSurface) return;
|
|
278
|
-
var disabled = id === 'surface' && surfaceAllowed !== true
|
|
284
|
+
var disabled = (id === 'surface' && surfaceAllowed !== true)
|
|
285
|
+
|| (id === 'dance' && ['resting', 'offline', 'dancing'].indexOf(danceState) === -1);
|
|
279
286
|
var toggle = TOGGLES.indexOf(id) !== -1;
|
|
280
287
|
var checked = toggle && checkedState[id] === true;
|
|
281
288
|
var item = doc.createElement('button');
|
|
@@ -300,7 +307,7 @@
|
|
|
300
307
|
* waiting to happen ("the switch does nothing"). The reason is the operator's own qualifier —
|
|
301
308
|
* 非debug模式生效 — spelled out for the mode that turns it off.
|
|
302
309
|
*/
|
|
303
|
-
item.title = text.surfaceOff;
|
|
310
|
+
item.title = id === 'dance' ? label : text.surfaceOff;
|
|
304
311
|
} else {
|
|
305
312
|
if (toggle && !checked) item.title = text.gravityOff;
|
|
306
313
|
item.addEventListener('click', function (event) {
|
|
@@ -456,6 +463,12 @@
|
|
|
456
463
|
setSurface: function (next) { toSurface = next === 'app' ? 'app' : 'window'; },
|
|
457
464
|
/** Whether that row may be used at all (false in debug mode). */
|
|
458
465
|
setSurfaceAllowed: function (allowed) { surfaceAllowed = allowed !== false; },
|
|
466
|
+
setSurfaceVisible: function (visible) { showSurface = visible === true; },
|
|
467
|
+
setDanceState: function (state) {
|
|
468
|
+
if (danceState === state) return;
|
|
469
|
+
danceState = state;
|
|
470
|
+
if (!element.hidden) render();
|
|
471
|
+
},
|
|
459
472
|
/**
|
|
460
473
|
* Which way a SWITCH row is thrown, read at OPEN time by the surface that owns the preference.
|
|
461
474
|
*
|