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.
@@ -0,0 +1,50 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
5
+ <title>吃白饭 · 桌宠弹球</title>
6
+ <link rel="stylesheet" href="/vrmpet3d/window/lib/mini-game.css">
7
+ <style>.actions{flex-wrap:wrap}#test-controls{position:absolute;left:18px;bottom:64px;display:flex;flex-wrap:wrap;gap:8px;align-items:center;max-width:90%;padding:10px;border-radius:12px;background:#101b24e8;font-size:12px}#test-controls button{padding:6px 10px}</style>
8
+ </head>
9
+ <body>
10
+ <main id="game" aria-label="吃白饭,桌宠弹球游戏">
11
+ <div id="playfield">
12
+ <canvas id="arena" aria-label="鼠标移动横杆,左键发射,Esc 暂停"></canvas>
13
+ <canvas id="pet" aria-hidden="true"></canvas>
14
+ <div id="resume-countdown" hidden role="status" aria-live="polite" aria-atomic="true"><strong id="countdown-number">3</strong><span>准备继续 · Esc 返回暂停</span></div>
15
+ </div>
16
+ <header><div class="brand">吃白饭 <small>PET BREAKOUT</small></div><div></div><div class="controls"><button id="pause">暂停 <kbd>Esc</kbd></button><button id="fullscreen">全屏 ↗</button></div></header>
17
+ <div id="game-hud"><span id="level" aria-live="polite"></span><span id="score" aria-live="polite"></span></div>
18
+ <button id="notice" hidden></button>
19
+ <div id="lives" role="img" aria-label="备用 2 次机会"></div>
20
+ <p id="hint">移动鼠标控制横杆 · 左键发射 · Esc 暂停</p>
21
+ <p id="model-status" role="status">正在准备桌宠模型…</p>
22
+ <section id="overlay" hidden role="dialog" aria-modal="true" aria-labelledby="pause-title">
23
+ <fieldset id="debug-controls" class="model-controls" hidden><legend>DEBUG · 目前开放 2 关</legend>
24
+ <label for="debug-level">选择关卡</label><select id="debug-level"><option value="1">第1关 · DS / BALL</option><option value="2">第2关 · 蓝白大碗</option></select>
25
+ <small>切换关卡会重置所选关卡进度,保留当前机会和游戏设置。</small>
26
+ </fieldset>
27
+ <div class="panel"><span class="eyebrow">TAKE A LITTLE BREAK</span><h1 id="pause-title">休息一下</h1><p id="pause-copy">饭还热着,随时回来继续。</p><div id="sessions"></div><div class="actions"><button id="resume" class="primary">继续吃饭</button><button id="restart">重置当前关卡</button><button id="exit">保存并退出</button></div><p id="save-status" role="status"></p><p id="connection" role="status"></p></div>
28
+ <fieldset class="model-controls" hidden><legend>游戏模型</legend>
29
+ <label for="model-size">模型尺寸 <output id="model-size-value" for="model-size">90</output> px</label>
30
+ <input id="model-size" type="range" min="30" max="150" step="5" value="90">
31
+ <label for="spin-speed">旋转速度 <output id="spin-speed-value" for="spin-speed">7</output> rad/s</label>
32
+ <input id="spin-speed" type="range" min="0" max="30" step="1" value="7">
33
+ <label for="flight-speed">飞行速度 <output id="flight-speed-value" for="flight-speed">1</output> 倍</label>
34
+ <input id="flight-speed" type="range" min="0.5" max="2" step="0.1" value="1">
35
+ <label for="paddle-boost">每次撞杆加速 <output id="paddle-boost-value" for="paddle-boost">1.5</output> %</label>
36
+ <input id="paddle-boost" type="range" min="0" max="20" step="0.5" value="1.5">
37
+ <small>尺寸仅影响外观;自转设为 0 可停止旋转。飞行速度会改变游戏速度。</small>
38
+ </fieldset>
39
+ </section>
40
+ <aside id="test-controls" hidden aria-label="独立测试模式">
41
+ <strong>独立测试模式</strong><button id="test-rice">测试吃饭</button><button id="test-bowl">测试撞碗</button><button id="test-loss">模拟漏球</button><button id="test-clear">模拟通关</button><button id="test-notice">模拟消息</button>
42
+ </aside>
43
+ </main>
44
+ <script id="game-level-data" type="application/json">null</script>
45
+ <script src="/vrmpet3d/window/lib/core.js"></script>
46
+ <script src="/vrmpet3d/window/lib/client.vendor.js"></script>
47
+ <script src="/vrmpet3d/window/lib/scene.js"></script>
48
+ <script src="/vrmpet3d/window/lib/mini-game-core.js"></script>
49
+ <script src="/vrmpet3d/window/lib/mini-game-page.js"></script>
50
+ </body></html>
package/lib/scene.js CHANGED
@@ -67,10 +67,20 @@
67
67
  var GAZE_YAW_LIMIT = ((80 * Math.PI) / 180) / GAZE_WEIGHT;
68
68
 
69
69
  /**
70
- * Pitch is left where it was: a head does not tip 90° forward or back without looking broken, and the
71
- * request was for the turn. Raise this the same way if a bigger nod is ever wanted.
70
+ * Automatic pitch limit, before the gaze weight. Settings expose separate actual
71
+ * upward/downward angles in degrees; both default to 60 degrees.
72
72
  */
73
- var GAZE_PITCH_LIMIT = 0.3;
73
+ var GAZE_PITCH_LIMIT = (60 * Math.PI / 180) / GAZE_WEIGHT;
74
+
75
+ function gazePitchLimits(preferences) {
76
+ var prefs = preferences || {};
77
+ function limit(key) {
78
+ return Number.isFinite(prefs[key])
79
+ ? Math.max(0, Math.min(60, prefs[key])) * Math.PI / 180 / GAZE_WEIGHT
80
+ : GAZE_PITCH_LIMIT;
81
+ }
82
+ return { up: limit('gazePitchUp'), down: limit('gazePitchDown') };
83
+ }
74
84
 
75
85
  /**
76
86
  * How far the head may turn IN TOTAL while an authored clip is driving it, in radians.
@@ -786,7 +796,8 @@
786
796
  }
787
797
 
788
798
  var yaw = Math.max(-GAZE_YAW_LIMIT, Math.min(GAZE_YAW_LIMIT, aim.yaw)) * GAZE_WEIGHT * this.gazeBlend;
789
- var pitch = Math.max(-GAZE_PITCH_LIMIT, Math.min(GAZE_PITCH_LIMIT, aim.pitch)) * GAZE_WEIGHT * this.gazeBlend;
799
+ var pitchLimits = gazePitchLimits(this.scene && this.scene.preferences);
800
+ var pitch = Math.max(-pitchLimits.down, Math.min(pitchLimits.up, aim.pitch)) * GAZE_WEIGHT * this.gazeBlend;
790
801
  var axes = rest ? this.gazeAxes(head) : null;
791
802
 
792
803
  if (layeredOnClip) {
@@ -799,8 +810,7 @@
799
810
  var budget = Math.max(0, HEAD_LAYER_LIMIT - clipTurn);
800
811
  yaw = Math.max(-budget, Math.min(budget, yaw));
801
812
  // Pitch keeps its own (smaller) limit, but never more than what is left of the budget either.
802
- pitch = Math.max(-Math.min(GAZE_PITCH_LIMIT * GAZE_WEIGHT, budget),
803
- Math.min(Math.min(GAZE_PITCH_LIMIT * GAZE_WEIGHT, budget), pitch));
813
+ pitch = Math.max(-budget, Math.min(budget, pitch));
804
814
  if (axes) {
805
815
  this._quat.setFromAxisAngle(axes.yaw, yaw);
806
816
  this._quat2.setFromAxisAngle(axes.pitch, pitch);
@@ -1669,6 +1679,9 @@
1669
1679
  if (this.disposed) return;
1670
1680
  var w = Math.max(1, Math.round(width));
1671
1681
  var h = Math.max(1, Math.round(height));
1682
+ // Layout works in fractional CSS pixels. Reapplying the same rounded size clears
1683
+ // the framebuffer between rAFs when a settings reply arrives during an orbit.
1684
+ if (this.width === w && this.height === h) return;
1672
1685
  this.renderer.setSize(w, h, false);
1673
1686
  this.camera.aspect = w / h;
1674
1687
  this.camera.updateProjectionMatrix();
@@ -1755,6 +1768,8 @@
1755
1768
 
1756
1769
  /** Replace the current avatar with a hardware-ready one. */
1757
1770
  Scene.prototype.installAvatar = function (avatar, root) {
1771
+ (this.toonMaterials || []).forEach(function (entry) { if (entry.plain) entry.plain.dispose(); });
1772
+ this.toonMaterials = null;
1758
1773
  // Stop before the old avatar is replaced: stopping is what undoes the clip's
1759
1774
  // pose, and it has to happen while the old model is still installed. Every
1760
1775
  // clip is then dropped, because a clip is fitted to one model — the cached
@@ -1779,6 +1794,7 @@
1779
1794
  this.frameAvatar(root);
1780
1795
  this.stage.add(root);
1781
1796
  this.avatar = avatar;
1797
+ this.captureToonMaterials(root);
1782
1798
  this.kind = avatar.kind;
1783
1799
  /**
1784
1800
  * …and the collider switch, applied to the avatar that was just adopted.
@@ -1844,6 +1860,7 @@
1844
1860
  * model is loaded, so on a fresh pet that call finds nothing to snapshot and the first sweep would have
1845
1861
  * had no gravity to lean against (measured: `windState().joints === 0` on a real model with two chains).
1846
1862
  */
1863
+ this.applySprings(this.preferences);
1847
1864
  this.captureSpringBase();
1848
1865
  this.expressionBehaviour = { blink: 0, blinkExpression: 'blink', emotion: null, emotionWeight: 0 };
1849
1866
  this.expressionState = { nextBlinkAt: null, blinkUntil: 0, nextIdleEmotionAt: null, emotionUntil: 0 };
@@ -1852,6 +1869,73 @@
1852
1869
  this.motion.setGazeIdle(this.preferences.gazeIdleSeconds);
1853
1870
  this.updateShadowPlane();
1854
1871
  this.refitCamera();
1872
+ this.applyToonPreferences();
1873
+ };
1874
+
1875
+ /** Keep the loaded materials (and expression bindings) intact so the switch is reversible. */
1876
+ Scene.prototype.captureToonMaterials = function (root) {
1877
+ var self = this;
1878
+ this.toonMeshes = [];
1879
+ this.toonMaterials = [];
1880
+ var seen = new Map();
1881
+ root.traverse(function (node) {
1882
+ if (!node.isMesh || !node.material) return;
1883
+ var originals = Array.isArray(node.material) ? node.material : [node.material];
1884
+ var entries = originals.map(function (material) {
1885
+ if (!material.isMToonMaterial) return null;
1886
+ if (seen.has(material)) return seen.get(material);
1887
+ var entry = { material: material, toony: material.shadingToonyFactor, shift: material.shadingShiftFactor,
1888
+ shade: material.shadeColorFactor.clone(), width: material.outlineWidthFactor, plain: null };
1889
+ seen.set(material, entry);
1890
+ self.toonMaterials.push(entry);
1891
+ return entry;
1892
+ });
1893
+ if (entries.some(Boolean)) self.toonMeshes.push({ node: node, original: node.material, entries: entries, originals: originals });
1894
+ });
1895
+ };
1896
+
1897
+ Scene.prototype.applyToonPreferences = function () {
1898
+ var self = this;
1899
+ var id = this.currentModelId || this.preferences.modelId;
1900
+ var prefs = (this.preferences.toon || {})[id] || {};
1901
+ var disabled = prefs.enabled === false;
1902
+ (this.toonMaterials || []).forEach(function (entry) {
1903
+ var material = entry.material;
1904
+ material.shadingToonyFactor = prefs.shadingToonyFactor === undefined ? entry.toony : prefs.shadingToonyFactor;
1905
+ material.shadingShiftFactor = prefs.shadingShiftFactor === undefined ? entry.shift : prefs.shadingShiftFactor;
1906
+ material.shadeColorFactor.copy(entry.shade);
1907
+ if (prefs.shadeStrength !== undefined) material.shadeColorFactor.lerp(material.color, 1 - prefs.shadeStrength);
1908
+ material.outlineWidthFactor = entry.width * (prefs.outlineScale === undefined ? 1 : prefs.outlineScale);
1909
+ if (disabled && !entry.plain) {
1910
+ var plain = new self.THREE.MeshStandardMaterial();
1911
+ ['map', 'alphaMap', 'alphaTest', 'transparent', 'opacity', 'side', 'depthWrite', 'depthTest',
1912
+ 'normalMap', 'normalScale', 'emissive', 'emissiveMap', 'emissiveIntensity', 'color'].forEach(function (key) {
1913
+ if (material[key] === undefined) return;
1914
+ if (plain[key] && typeof plain[key].copy === 'function') plain[key].copy(material[key]);
1915
+ else plain[key] = material[key];
1916
+ });
1917
+ plain.visible = !material.isOutline;
1918
+ entry.plain = plain;
1919
+ }
1920
+ });
1921
+ (this.toonMeshes || []).forEach(function (mesh) {
1922
+ mesh.node.material = disabled
1923
+ ? (Array.isArray(mesh.original) ? mesh.originals.map(function (m, i) { return mesh.entries[i] ? mesh.entries[i].plain : m; })
1924
+ : mesh.entries[0].plain)
1925
+ : mesh.original;
1926
+ });
1927
+ this.toonDisabled = disabled;
1928
+ };
1929
+
1930
+ Scene.prototype.syncPlainMaterials = function () {
1931
+ if (!this.toonDisabled) return;
1932
+ (this.toonMaterials || []).forEach(function (entry) {
1933
+ if (!entry.plain) return;
1934
+ // Expressions still target the authored MToon material, including UV animation.
1935
+ entry.plain.color.copy(entry.material.color);
1936
+ entry.plain.emissive.copy(entry.material.emissive);
1937
+ entry.plain.opacity = entry.material.opacity;
1938
+ });
1855
1939
  };
1856
1940
 
1857
1941
  /** A soft ground blob, so the pet does not appear to float. */
@@ -3380,7 +3464,8 @@
3380
3464
  var x = Math.max(-1, Math.min(1, aim.x));
3381
3465
  var y = Math.max(-1, Math.min(1, aim.y));
3382
3466
  // Screen y grows downward, pitch grows upward.
3383
- return { yaw: x * GAZE_YAW_LIMIT, pitch: -y * GAZE_PITCH_LIMIT };
3467
+ var limits = gazePitchLimits(this.preferences);
3468
+ return { yaw: x * GAZE_YAW_LIMIT, pitch: -y * (y < 0 ? limits.up : limits.down) };
3384
3469
  };
3385
3470
 
3386
3471
  /** Advance one frame. `dt` is in seconds. */
@@ -3437,6 +3522,7 @@
3437
3522
  this.advanceExpressions(dt);
3438
3523
  this.applyExpressions(this.preferences);
3439
3524
 
3525
+ this.syncPlainMaterials();
3440
3526
  this.renderer.render(this.scene, this.camera);
3441
3527
  this.frame += 1;
3442
3528
  };
@@ -3448,6 +3534,7 @@
3448
3534
  Scene.prototype.applyPreferences = function (preferences) {
3449
3535
  var raw = preferences && typeof preferences === 'object' ? preferences : {};
3450
3536
  this.preferences = this.core.cleanPreferences(preferences);
3537
+ this.applyToonPreferences();
3451
3538
  // A preferences push only carries `motions` when the caller meant to change
3452
3539
  // it. cleanPreferences always produces a (possibly empty) key, so adopting it
3453
3540
  // unconditionally would silently wipe a map set through setMotionMap.
@@ -3641,6 +3728,40 @@
3641
3728
  };
3642
3729
  };
3643
3730
 
3731
+ /** The rendered eyes' midpoint, in canvas CSS pixels, independent of framing and camera orbit. */
3732
+ Scene.prototype.gazeOrigin = function () {
3733
+ var avatar = this.avatar;
3734
+ if (!avatar || !this.THREE || !this.camera) return null;
3735
+ var point = this._gazeOriginVec || (this._gazeOriginVec = new this.THREE.Vector3());
3736
+ var other = this._gazeOtherEyeVec || (this._gazeOtherEyeVec = new this.THREE.Vector3());
3737
+ var humanoid = avatar.vrm && avatar.vrm.humanoid;
3738
+ var left = humanoid && typeof humanoid.getRawBoneNode === 'function' ? humanoid.getRawBoneNode('leftEye') : null;
3739
+ var right = humanoid && typeof humanoid.getRawBoneNode === 'function' ? humanoid.getRawBoneNode('rightEye') : null;
3740
+ left = left || (avatar.bones && avatar.bones.leftEye);
3741
+ right = right || (avatar.bones && avatar.bones.rightEye);
3742
+ var source;
3743
+ if (left && right) {
3744
+ left.getWorldPosition(point); right.getWorldPosition(other);
3745
+ point.add(other).multiplyScalar(0.5);
3746
+ source = 'eyes';
3747
+ } else if (avatar.vrm && avatar.vrm.lookAt && typeof avatar.vrm.lookAt.getLookAtWorldPosition === 'function') {
3748
+ // The loader has already converted VRM 0/1 coordinate conventions for this offset.
3749
+ if (avatar.root) avatar.root.updateWorldMatrix(true, true);
3750
+ avatar.vrm.lookAt.getLookAtWorldPosition(point);
3751
+ source = 'lookAt';
3752
+ } else {
3753
+ var head = gazeNodeFor(avatar);
3754
+ if (!head) return null;
3755
+ head.getWorldPosition(point);
3756
+ source = 'head';
3757
+ }
3758
+ this.camera.updateMatrixWorld(true);
3759
+ var projected = this.projectToCanvas(point);
3760
+ if (!projected || projected.z < -1 || projected.z > 1) return null;
3761
+ projected.source = source;
3762
+ return projected;
3763
+ };
3764
+
3644
3765
  /**
3645
3766
  * Apply the draught to the chains, decay it, and keep it LOCAL.
3646
3767
  *
@@ -3865,6 +3986,10 @@
3865
3986
  */
3866
3987
  Scene.prototype.setSpringDefaults = function (defaults) {
3867
3988
  this.springDefaults = defaults && typeof defaults === 'object' ? defaults : null;
3989
+ // Catalog defaults can arrive after the avatar and preferences on either surface.
3990
+ // Apply them now; waiting for another preference edit makes a reload appear to fix physics.
3991
+ this.applySprings(this.preferences);
3992
+ this.captureSpringBase();
3868
3993
  };
3869
3994
 
3870
3995
  /**
@@ -4931,6 +5056,8 @@
4931
5056
 
4932
5057
  Scene.prototype.dispose = function () {
4933
5058
  if (this.disposed) return;
5059
+ (this.toonMeshes || []).forEach(function (mesh) { mesh.node.material = mesh.original; });
5060
+ (this.toonMaterials || []).forEach(function (entry) { if (entry.plain) entry.plain.dispose(); });
4934
5061
  this.disposed = true;
4935
5062
  // Stop and uncache every mixer before the renderer goes: a mixer holds a
4936
5063
  // binding table that references the model's bones, so leaving them behind
package/lib/vrm-info.js CHANGED
@@ -437,7 +437,7 @@ export function describeGlb(glb, label) {
437
437
  (gltf.materials || []).forEach((material, index) => {
438
438
  const pbr = material.pbrMetallicRoughness || {};
439
439
  const extensions = material.extensions || {};
440
- const mtoon = extensions.VRM && extensions.VRM.materialProperties;
440
+ const mtoon = vrm0 && (vrm0.materialProperties || [])[index];
441
441
  const mtoon1 = extensions.VRMC_materials_mtoon || null;
442
442
  report.materials.push({
443
443
  index,
@@ -826,6 +826,21 @@ export function cacheStats() {
826
826
  */
827
827
  export function compactModelInfo(info) {
828
828
  return {
829
+ toon: {
830
+ total: (info.materials || []).length,
831
+ materials: (info.materials || []).filter((m) => m.mtoon1 || (m.mtoon && m.mtoon.shader === 'VRM/MToon')).map((m) => {
832
+ // Same VRM 0 -> MToon 1 conversion as the pinned three-vrm loader.
833
+ const legacy = m.mtoon && m.mtoon.floatProperties || {};
834
+ const shift = legacy._ShadeShift ?? 0;
835
+ const toony = (legacy._ShadeToony ?? 0.9) + (1 - (legacy._ShadeToony ?? 0.9)) * (0.5 + 0.5 * shift);
836
+ return {
837
+ name: m.name,
838
+ shadingToonyFactor: m.mtoon1 ? (m.mtoon1.shadingToonyFactor ?? 0.9) : toony,
839
+ shadingShiftFactor: m.mtoon1 ? (m.mtoon1.shadingShiftFactor ?? 0) : -shift - (1 - toony),
840
+ outline: m.mtoon1 ? Boolean(m.mtoon1.outlineWidthMode && m.mtoon1.outlineWidthMode !== 'none' && m.mtoon1.outlineWidthFactor > 0)
841
+ : Boolean(m.mtoon.floatProperties._OutlineWidthMode && m.mtoon.floatProperties._OutlineWidth > 0),
842
+ }; }),
843
+ },
829
844
  supported: info.supported === true,
830
845
  vrmSpec: info.vrmSpec,
831
846
  generator: info.generator,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-vrmpet3d",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "DeepSeek Harness 的 VRM 3D 桌宠:任务状态联动、独立悬浮窗、模型动作与 AI 碎碎念。",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -36,7 +36,6 @@
36
36
  ]
37
37
  }
38
38
  },
39
- "displayName": "桌宠 3D · VRM Pet 3D",
40
39
  "repository": {
41
40
  "type": "git",
42
41
  "url": "https://github.com/drliuhuan/dsh-plugin-vrmpet3d.git"
@@ -53,5 +52,6 @@
53
52
  "vrma",
54
53
  "threejs",
55
54
  "electron"
56
- ]
55
+ ],
56
+ "displayName": "桌宠 3D · VRM Pet 3D"
57
57
  }