dsh-folia 4.3.0 → 4.3.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/lib/client.js +157 -13
- package/package.json +3 -2
package/lib/client.js
CHANGED
|
@@ -17345,10 +17345,20 @@ window.__ModuleLoader__.load({
|
|
|
17345
17345
|
actions.push(btn(isActive ? weT("使用中") : weT("使用"), () => act("config", { enabled: true, petId: p.dirId }, p.name), isActive || Boolean(busy)));
|
|
17346
17346
|
actions.push(btn(weT("卸载"), () => act("remove", { id: p.dirId }, p.name), Boolean(busy)));
|
|
17347
17347
|
return React.createElement("div", { key, style: PET_STYLE.card },
|
|
17348
|
-
|
|
17349
|
-
|
|
17350
|
-
|
|
17351
|
-
|
|
17348
|
+
// 预览只显示**第一帧**:雪碧图是 8×9 网格,直接贴图会看到一整排小人(用户实测反馈"看着很乱")。
|
|
17349
|
+
// 用 background-size = 列数/行数 × 100% + position 0 0 裁出左上角那一格。
|
|
17350
|
+
React.createElement("div", {
|
|
17351
|
+
style: {
|
|
17352
|
+
width: "100%",
|
|
17353
|
+
aspectRatio: (p.cell ? p.cell.width + " / " + p.cell.height : "192 / 208"),
|
|
17354
|
+
maxHeight: "96px",
|
|
17355
|
+
backgroundImage: "url(" + apiUrl(p.sheet) + ")",
|
|
17356
|
+
backgroundSize: (Number(p.columns) || 8) * 100 + "% " + (Array.isArray(p.frames) ? p.frames.length : 9) * 100 + "%",
|
|
17357
|
+
backgroundPosition: "0 0",
|
|
17358
|
+
backgroundRepeat: "no-repeat",
|
|
17359
|
+
backgroundColor: "rgba(0,0,0,0.25)",
|
|
17360
|
+
borderRadius: "6px",
|
|
17361
|
+
},
|
|
17352
17362
|
}),
|
|
17353
17363
|
React.createElement("div", { style: PET_STYLE.title }, p.name),
|
|
17354
17364
|
React.createElement("div", { style: PET_STYLE.meta },
|
|
@@ -17437,6 +17447,36 @@ window.__ModuleLoader__.load({
|
|
|
17437
17447
|
const PET_ACTIVITY_POLL_MS = 500;
|
|
17438
17448
|
/** 光标静止后多久切回 idle(毫秒)。 */
|
|
17439
17449
|
const PET_WAVE_IDLE_MS = 9000;
|
|
17450
|
+
/**
|
|
17451
|
+
* 日常动作调度:没在干活时,每隔 5~11 秒随机演一个。
|
|
17452
|
+
*
|
|
17453
|
+
* 为什么需要它:只播 idle 的宠物看起来是"一张会呼吸的图",不像活物。这里把 manifest 的
|
|
17454
|
+
* 9 条轨道都用上 —— 走一段(左右跑)、挥手、跳一下、发会儿呆 —— 用**随机间隔**而不是固定节拍,
|
|
17455
|
+
* 避免机械感。干活(work/done/fail)优先级最高,调度器在那些状态下不抢行。
|
|
17456
|
+
*/
|
|
17457
|
+
const PET_IDLE_ACTIONS = [
|
|
17458
|
+
{ row: "wave", weight: 3, ms: 0 },
|
|
17459
|
+
{ row: "jump", weight: 2, ms: 0 },
|
|
17460
|
+
{ row: "wait", weight: 2, ms: 1800 },
|
|
17461
|
+
{ row: "walkRight", weight: 3, ms: 1600 },
|
|
17462
|
+
{ row: "walkLeft", weight: 3, ms: 1600 },
|
|
17463
|
+
];
|
|
17464
|
+
/**
|
|
17465
|
+
* 统一时钟。
|
|
17466
|
+
*
|
|
17467
|
+
* ⚠️ `requestAnimationFrame(fn)` 交给 fn 的是**页面相对时间**(与 performance.now 同源),
|
|
17468
|
+
* 不是 petNow()。两者混用会出现「holdUntil = petNow() + 1400,而 now 只有几千」——
|
|
17469
|
+
* 比较永远不成立,于是宠物切到 done/fail/wave 之后**再也回不到 idle**(实测复现)。
|
|
17470
|
+
* 所有时间比较一律走这一个函数。
|
|
17471
|
+
*/
|
|
17472
|
+
function petNow() {
|
|
17473
|
+
try {
|
|
17474
|
+
if (typeof performance !== "undefined" && typeof performance.now === "function") return performance.now();
|
|
17475
|
+
} catch { /* 退回 Date.now */ }
|
|
17476
|
+
return petNow();
|
|
17477
|
+
}
|
|
17478
|
+
/** 两次随机动作之间的间隔范围(毫秒)。 */
|
|
17479
|
+
const PET_IDLE_GAP_MS = [5000, 11000];
|
|
17440
17480
|
|
|
17441
17481
|
/**
|
|
17442
17482
|
* 声音:包内 `sounds/<事件>.ogg|mp3|wav` 优先(manifest.sounds 显式声明),
|
|
@@ -17533,7 +17573,7 @@ window.__ModuleLoader__.load({
|
|
|
17533
17573
|
let frame = 0;
|
|
17534
17574
|
let row = PET_ROW.idle;
|
|
17535
17575
|
let acc = 0;
|
|
17536
|
-
let last = 0;
|
|
17576
|
+
let last = 0; // rAF 时间戳(页面相对时间),不是 Date.now()
|
|
17537
17577
|
let raf = 0;
|
|
17538
17578
|
let dead = false;
|
|
17539
17579
|
let idleSince = 0;
|
|
@@ -17544,6 +17584,12 @@ window.__ModuleLoader__.load({
|
|
|
17544
17584
|
let activityTimer = null;
|
|
17545
17585
|
/** 一次性动作(done/fail)播完自动回 idle 的时间戳。 */
|
|
17546
17586
|
let holdUntil = 0;
|
|
17587
|
+
/** 下一次随机动作的时间点;0 = 立即重排。 */
|
|
17588
|
+
let nextActionAt = 0;
|
|
17589
|
+
/** 走动中的速度(px/帧,正=向右)。0 = 不走。 */
|
|
17590
|
+
let walkSpeed = 0;
|
|
17591
|
+
/** 走动剩余时长(毫秒)。 */
|
|
17592
|
+
let walkLeft = 0;
|
|
17547
17593
|
|
|
17548
17594
|
const cw = Math.max(1, Number(pet.cell && pet.cell.width) || 192);
|
|
17549
17595
|
const ch = Math.max(1, Number(pet.cell && pet.cell.height) || 208);
|
|
@@ -17589,8 +17635,35 @@ window.__ModuleLoader__.load({
|
|
|
17589
17635
|
function tick(now) {
|
|
17590
17636
|
if (dead) return;
|
|
17591
17637
|
if (!last) last = now;
|
|
17592
|
-
|
|
17638
|
+
const dt = Math.max(0, Math.min(100, now - last)); // 切后台回来可能是几百毫秒,钳住
|
|
17639
|
+
acc += dt;
|
|
17593
17640
|
last = now;
|
|
17641
|
+
// ── 走动:每帧挪一点,走到头或时间到就停下回 idle ──
|
|
17642
|
+
if (walkSpeed !== 0 && walkLeft > 0) {
|
|
17643
|
+
const p = pos();
|
|
17644
|
+
const maxX = Math.max(0, (window.innerWidth || 1920) - layerEl.offsetWidth);
|
|
17645
|
+
const nx = p.left + walkSpeed;
|
|
17646
|
+
if (nx <= 0 || nx >= maxX) {
|
|
17647
|
+
walkSpeed = -walkSpeed;
|
|
17648
|
+
setRow(walkSpeed > 0 ? PET_ROW.runRight : PET_ROW.runLeft);
|
|
17649
|
+
moveTo(Math.max(0, Math.min(maxX, nx)), p.top);
|
|
17650
|
+
} else {
|
|
17651
|
+
moveTo(nx, p.top);
|
|
17652
|
+
}
|
|
17653
|
+
walkLeft -= dt;
|
|
17654
|
+
if (walkLeft <= 0) {
|
|
17655
|
+
walkSpeed = 0;
|
|
17656
|
+
setRow(PET_ROW.idle);
|
|
17657
|
+
nextActionAt = petNow() + PET_IDLE_GAP_MS[0] + Math.random() * (PET_IDLE_GAP_MS[1] - PET_IDLE_GAP_MS[0]);
|
|
17658
|
+
}
|
|
17659
|
+
}
|
|
17660
|
+
// ── 日常动作调度:只在"真的闲着"时才演 ──
|
|
17661
|
+
if (!activity && walkSpeed === 0 && row === PET_ROW.idle && !holdUntil && now >= nextActionAt) {
|
|
17662
|
+
playIdleAction();
|
|
17663
|
+
const gap = PET_IDLE_GAP_MS[0] + Math.random() * (PET_IDLE_GAP_MS[1] - PET_IDLE_GAP_MS[0]);
|
|
17664
|
+
nextActionAt = petNow() + gap;
|
|
17665
|
+
}
|
|
17666
|
+
|
|
17594
17667
|
// 活动状态变化时先切行(work/done/fail 优先于 idle/wave)。
|
|
17595
17668
|
if (activityTimer === null && typeof setInterval === "function") {
|
|
17596
17669
|
activityTimer = setInterval(() => {
|
|
@@ -17599,11 +17672,13 @@ window.__ModuleLoader__.load({
|
|
|
17599
17672
|
activity = nowActive;
|
|
17600
17673
|
if (activity) {
|
|
17601
17674
|
holdUntil = 0;
|
|
17675
|
+
walkSpeed = 0;
|
|
17676
|
+
walkLeft = 0;
|
|
17602
17677
|
if (row !== PET_ROW.work) setRow(PET_ROW.work);
|
|
17603
17678
|
} else if (row === PET_ROW.work) {
|
|
17604
17679
|
// 干完活 → 完成行一小会儿,再回 idle("被看见"的反馈)
|
|
17605
17680
|
setRow(PET_ROW.done);
|
|
17606
|
-
holdUntil =
|
|
17681
|
+
holdUntil = petNow() + 1600;
|
|
17607
17682
|
voice.play("done");
|
|
17608
17683
|
}
|
|
17609
17684
|
}, PET_ACTIVITY_POLL_MS);
|
|
@@ -17623,7 +17698,54 @@ window.__ModuleLoader__.load({
|
|
|
17623
17698
|
draw();
|
|
17624
17699
|
raf = (typeof requestAnimationFrame === "function")
|
|
17625
17700
|
? requestAnimationFrame(tick)
|
|
17626
|
-
: setTimeout(() => tick(
|
|
17701
|
+
: setTimeout(() => tick(petNow()), 60);
|
|
17702
|
+
}
|
|
17703
|
+
|
|
17704
|
+
/** 当前左上角(拖动/走动都读写这一对值)。 */
|
|
17705
|
+
function pos() {
|
|
17706
|
+
const left = parseFloat(layerEl.style.left);
|
|
17707
|
+
const top = parseFloat(layerEl.style.top);
|
|
17708
|
+
return {
|
|
17709
|
+
left: Number.isFinite(left) ? left : (window.innerWidth - layerEl.offsetWidth - 24),
|
|
17710
|
+
top: Number.isFinite(top) ? top : (window.innerHeight - layerEl.offsetHeight - 16),
|
|
17711
|
+
};
|
|
17712
|
+
}
|
|
17713
|
+
function moveTo(left, top) {
|
|
17714
|
+
const maxX = Math.max(0, (window.innerWidth || 1920) - layerEl.offsetWidth);
|
|
17715
|
+
const maxY = Math.max(0, (window.innerHeight || 1080) - layerEl.offsetHeight);
|
|
17716
|
+
const x = Math.max(0, Math.min(maxX, left));
|
|
17717
|
+
const y = Math.max(0, Math.min(maxY, top));
|
|
17718
|
+
layerEl.style.left = x + "px";
|
|
17719
|
+
layerEl.style.top = y + "px";
|
|
17720
|
+
layerEl.style.right = "auto";
|
|
17721
|
+
layerEl.style.bottom = "auto";
|
|
17722
|
+
}
|
|
17723
|
+
|
|
17724
|
+
/** 从 idle 里挑一个动作演。走动会真的位移,演完自己停。 */
|
|
17725
|
+
function playIdleAction() {
|
|
17726
|
+
const total = PET_IDLE_ACTIONS.reduce((s, a) => s + a.weight, 0);
|
|
17727
|
+
let pick = Math.random() * total;
|
|
17728
|
+
let action = PET_IDLE_ACTIONS[0];
|
|
17729
|
+
for (const a of PET_IDLE_ACTIONS) { if ((pick -= a.weight) <= 0) { action = a; break; } }
|
|
17730
|
+
if (action.row === "walkRight" || action.row === "walkLeft") {
|
|
17731
|
+
const dir = action.row === "walkRight" ? 1 : -1;
|
|
17732
|
+
const p0 = pos();
|
|
17733
|
+
// 走到边上就掉头:撞墙不停在原地踏步,直接换方向。
|
|
17734
|
+
const maxX = Math.max(0, (window.innerWidth || 1920) - layerEl.offsetWidth);
|
|
17735
|
+
if ((dir > 0 && p0.left >= maxX - 2) || (dir < 0 && p0.left <= 2)) {
|
|
17736
|
+
setRow(dir > 0 ? PET_ROW.runLeft : PET_ROW.runRight);
|
|
17737
|
+
walkSpeed = -dir * (0.9 * scale);
|
|
17738
|
+
} else {
|
|
17739
|
+
setRow(dir > 0 ? PET_ROW.runRight : PET_ROW.runLeft);
|
|
17740
|
+
walkSpeed = dir * (0.9 * scale);
|
|
17741
|
+
}
|
|
17742
|
+
walkLeft = action.ms;
|
|
17743
|
+
return;
|
|
17744
|
+
}
|
|
17745
|
+
walkSpeed = 0;
|
|
17746
|
+
walkLeft = 0;
|
|
17747
|
+
setRow(PET_ROW[action.row] === undefined ? PET_ROW.idle : PET_ROW[action.row]);
|
|
17748
|
+
holdUntil = action.ms ? petNow() + action.ms : petNow() + 1400;
|
|
17627
17749
|
}
|
|
17628
17750
|
|
|
17629
17751
|
function setRow(next) {
|
|
@@ -17659,7 +17781,10 @@ window.__ModuleLoader__.load({
|
|
|
17659
17781
|
dragging = null;
|
|
17660
17782
|
if (!moved) {
|
|
17661
17783
|
// 单击 = 挥手(给用户一个"它有反应"的反馈),之后自动回 idle
|
|
17784
|
+
walkSpeed = 0;
|
|
17785
|
+
walkLeft = 0;
|
|
17662
17786
|
setRow(PET_ROW.wave);
|
|
17787
|
+
holdUntil = petNow() + 1400;
|
|
17663
17788
|
voice.play("wave");
|
|
17664
17789
|
} else {
|
|
17665
17790
|
try { localStorage.setItem("wePetPos", JSON.stringify({ left: layerEl.style.left, top: layerEl.style.top })); } catch { /* ignore */ }
|
|
@@ -17675,8 +17800,8 @@ window.__ModuleLoader__.load({
|
|
|
17675
17800
|
window.addEventListener("pointermove", onPointerMove);
|
|
17676
17801
|
window.addEventListener("pointerup", onPointerUp);
|
|
17677
17802
|
}
|
|
17678
|
-
idleSince =
|
|
17679
|
-
raf = (typeof requestAnimationFrame === "function") ? requestAnimationFrame(tick) : setTimeout(() => tick(
|
|
17803
|
+
idleSince = petNow();
|
|
17804
|
+
raf = (typeof requestAnimationFrame === "function") ? requestAnimationFrame(tick) : setTimeout(() => tick(petNow()), 60);
|
|
17680
17805
|
|
|
17681
17806
|
function destroy() {
|
|
17682
17807
|
dead = true;
|
|
@@ -17694,9 +17819,11 @@ window.__ModuleLoader__.load({
|
|
|
17694
17819
|
return {
|
|
17695
17820
|
destroy, setRow, resize,
|
|
17696
17821
|
/** 外部事件注入(面板操作失败等):一次性播放失败行并出声。 */
|
|
17697
|
-
fail() { setRow(PET_ROW.fail); holdUntil =
|
|
17698
|
-
cheer() { setRow(PET_ROW.done); holdUntil =
|
|
17822
|
+
fail() { setRow(PET_ROW.fail); holdUntil = petNow() + 1600; voice.play("fail"); },
|
|
17823
|
+
cheer() { setRow(PET_ROW.done); holdUntil = petNow() + 1600; voice.play("done"); },
|
|
17699
17824
|
get row() { return row; },
|
|
17825
|
+
/** 让下一次随机动作尽快发生(首次出现时用)。 */
|
|
17826
|
+
scheduleSoon() { nextActionAt = petNow() + 1200 + Math.random() * 1800; },
|
|
17700
17827
|
};
|
|
17701
17828
|
}
|
|
17702
17829
|
|
|
@@ -17706,16 +17833,24 @@ window.__ModuleLoader__.load({
|
|
|
17706
17833
|
* 它自己拉 `/folia/pets/list`(配置 + 已装宠物),所以**不依赖面板是否打开过** ——
|
|
17707
17834
|
* 冷启动就能出宠物。
|
|
17708
17835
|
*/
|
|
17836
|
+
/** 让刚出现的宠物在 1.2~3 秒后演第一个动作(实例内部按需读 nextActionAt)。 */
|
|
17837
|
+
function nextActionSoon(instance) {
|
|
17838
|
+
try { if (instance && typeof instance.scheduleSoon === "function") instance.scheduleSoon(); } catch { /* ignore */ }
|
|
17839
|
+
}
|
|
17840
|
+
|
|
17709
17841
|
function mountPetLayer() {
|
|
17710
17842
|
if (typeof document === "undefined" || !document.body) return null;
|
|
17711
17843
|
let disposed = false;
|
|
17712
17844
|
let instance = null;
|
|
17713
17845
|
let layerEl = null;
|
|
17846
|
+
/** 当前实例对应的「宠物@大小」;变了就要销毁重建。 */
|
|
17847
|
+
let currentKey = "";
|
|
17714
17848
|
|
|
17715
17849
|
function teardownDom() {
|
|
17716
17850
|
if (instance) { try { instance.destroy(); } catch { /* ignore */ } instance = null; }
|
|
17717
17851
|
if (layerEl && layerEl.parentNode) layerEl.parentNode.removeChild(layerEl);
|
|
17718
17852
|
layerEl = null;
|
|
17853
|
+
currentKey = "";
|
|
17719
17854
|
}
|
|
17720
17855
|
|
|
17721
17856
|
async function sync() {
|
|
@@ -17731,7 +17866,12 @@ window.__ModuleLoader__.load({
|
|
|
17731
17866
|
if (!cfg || !cfg.enabled || !pet) { teardownDom(); return; }
|
|
17732
17867
|
|
|
17733
17868
|
const scale = Number(cfg.scale) > 0 ? Number(cfg.scale) : 1;
|
|
17734
|
-
|
|
17869
|
+
const key = String(pet.dirId || pet.id) + "@" + scale;
|
|
17870
|
+
// 换宠物 / 改大小 ⇒ **必须重建**:旧实例的贴图与几何都是上一只的。
|
|
17871
|
+
// (实测踩过:只在首次创建,于是点「使用」换了宠物而屏幕没变 —— 就是这里。)
|
|
17872
|
+
if (layerEl && currentKey === key) { layerEl.style.display = ""; return; }
|
|
17873
|
+
teardownDom();
|
|
17874
|
+
currentKey = key;
|
|
17735
17875
|
|
|
17736
17876
|
layerEl = document.createElement("div");
|
|
17737
17877
|
layerEl.id = PET_LAYER_ID;
|
|
@@ -17753,6 +17893,8 @@ window.__ModuleLoader__.load({
|
|
|
17753
17893
|
layerEl.appendChild(canvas);
|
|
17754
17894
|
document.body.appendChild(layerEl);
|
|
17755
17895
|
instance = createPetLayer(layerEl, canvas, pet, scale, cfg);
|
|
17896
|
+
// 首次出现时排一个随机动作,别一上来就傻站着。
|
|
17897
|
+
nextActionSoon(instance);
|
|
17756
17898
|
}
|
|
17757
17899
|
|
|
17758
17900
|
sync();
|
|
@@ -17770,6 +17912,8 @@ window.__ModuleLoader__.load({
|
|
|
17770
17912
|
};
|
|
17771
17913
|
}
|
|
17772
17914
|
|
|
17915
|
+
// 仅供单测驱动(构建期内联时会剥掉 export,不影响产物)。
|
|
17916
|
+
|
|
17773
17917
|
// ── 内联模块:src/quick-panel.js —— 快捷播放面板(官方侧栏 tab 与低版本抽屉共用同一份内容):当前壁纸 + 轮播 + 列表快切 + 声音 + 设置入口。列表虚拟滚动(spacer + 可视窗口)(构建期注入;勿手改本段)──
|
|
17774
17918
|
/**
|
|
17775
17919
|
* quick-panel.js — 快捷播放面板(侧边栏的**唯一**内容):当前壁纸 + 轮播 + 三档页签
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-folia",
|
|
3
|
-
"version": "4.3.
|
|
3
|
+
"version": "4.3.1",
|
|
4
4
|
"description": "Folia 壁纸引擎:把本机 Wallpaper Engine 壁纸搬进 DSH 界面(场景实时渲染 / 视频 / 网页),内置液态玻璃与 Folia 吉祥物,并可从自建站点拉取壁纸。单包形态(组件已合并)。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -192,7 +192,8 @@
|
|
|
192
192
|
"prepack": "node scripts/prepare.mjs",
|
|
193
193
|
"test:boot": "node test/boot-http.mjs",
|
|
194
194
|
"test:pets": "node test/pets-http.mjs",
|
|
195
|
-
"
|
|
195
|
+
"test:pets-layer": "node test/pets-layer.mjs",
|
|
196
|
+
"gate": "node scripts/build-client.mjs \u0026\u0026 node test/run-guards.mjs smoke \u0026\u0026 node test/boot-http.mjs \u0026\u0026 node test/pets-http.mjs \u0026\u0026 node test/pets-layer.mjs"
|
|
196
197
|
},
|
|
197
198
|
"peerDependencies": {
|
|
198
199
|
"@deepseek-ai/cordis": "^4.0.4",
|