dsh-pet 0.1.4 → 0.1.5

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.
Files changed (110) hide show
  1. package/README.en.md +10 -10
  2. package/README.md +11 -11
  3. package/assets/config.jsonc +83 -0
  4. package/assets/thumb//344/270/211/347/220/203/346/212/233/346/216/245.webm +0 -0
  5. package/assets/thumb//344/270/213/344/272/224/345/255/220/346/243/213.webm +0 -0
  6. package/assets/thumb//344/270/234/345/274/240/350/245/277/346/234/233.webm +0 -0
  7. package/assets/thumb//344/270/255/347/247/213/350/265/217/346/234/210/345/220/203/346/234/210/351/245/274.webm +0 -0
  8. package/assets/thumb//344/274/230/351/233/205/345/245/263/344/273/206/350/210/236.webm +0 -0
  9. package/assets/thumb//345/201/267/345/220/203/351/233/266/351/243/237/350/242/253/346/212/223/344/275/217.webm +0 -0
  10. package/assets/thumb//345/206/231/344/273/243/347/240/201.webm +0 -0
  11. package/assets/thumb//345/206/231/347/246/217/345/255/227.webm +0 -0
  12. package/assets/thumb//345/207/255/347/251/272/347/224/237/350/212/261.webm +0 -0
  13. package/assets/thumb//345/212/250/347/211/251/347/216/257/347/273/225.webm +0 -0
  14. package/assets/thumb//345/216/237/345/234/260/344/270/223/345/277/203/347/216/251/351/255/224/346/226/271.webm +0 -0
  15. package/assets/thumb//345/216/237/345/234/260/345/260/217/346/206/251/346/262/211/347/234/240.webm +0 -0
  16. package/assets/thumb//345/216/237/345/234/260/345/267/246/350/275/254/345/245/224/350/267/221.webm +0 -0
  17. package/assets/thumb//345/216/237/345/234/260/346/225/262/345/207/273/346/241/214/351/235/242/344/272/222/345/212/250.webm +0 -0
  18. package/assets/thumb//345/216/237/345/234/260/346/274/202/346/265/256/350/270/217/346/255/245.webm +0 -0
  19. package/assets/thumb//345/216/237/345/234/260/350/267/263/350/267/203/346/212/223/347/242/216/345/244/264/351/241/266/347/211/251/345/223/201.webm +0 -0
  20. package/assets/thumb//345/216/237/345/234/260/350/271/262/344/270/213/347/216/251/347/216/251/345/205/267/346/261/275/350/275/246.webm +0 -0
  21. package/assets/thumb//345/216/237/345/234/260/351/207/215/345/212/233/344/270/213/350/271/262/345/216/213/347/274/251.webm +0 -0
  22. package/assets/thumb//345/217/230/351/270/275/345/255/220.webm +0 -0
  23. package/assets/thumb//345/217/257/347/210/261/345/256/205/350/210/236.webm +0 -0
  24. package/assets/thumb//345/220/203Token.webm +0 -0
  25. package/assets/thumb//345/220/203/345/206/260/346/267/207/346/267/213/350/236/215/345/214/226.webm +0 -0
  26. package/assets/thumb//345/220/203/345/215/210/351/244/220.webm +0 -0
  27. package/assets/thumb//345/220/203/345/244/247/351/227/270/350/237/271.webm +0 -0
  28. package/assets/thumb//345/220/203/345/271/264/347/263/225.webm +0 -0
  29. package/assets/thumb//345/220/203/346/227/251/351/244/220.webm +0 -0
  30. package/assets/thumb//345/220/203/346/231/232/351/244/220.webm +0 -0
  31. package/assets/thumb//345/220/203/346/261/244/345/234/206.webm +0 -0
  32. package/assets/thumb//345/220/203/347/231/275/351/245/255.webm +0 -0
  33. package/assets/thumb//345/220/203/347/262/275/345/255/220.webm +0 -0
  34. package/assets/thumb//345/220/203/347/263/226/350/221/253/350/212/246.webm +0 -0
  35. package/assets/thumb//345/220/203/350/205/212/345/205/253/347/262/245.webm +0 -0
  36. package/assets/thumb//345/220/203/350/245/277/347/223/234.webm +0 -0
  37. package/assets/thumb//345/220/203/351/207/215/351/230/263/347/263/225.webm +0 -0
  38. package/assets/thumb//345/220/203/351/225/277/345/257/277/351/235/242.webm +0 -0
  39. package/assets/thumb//345/220/203/351/235/222/345/233/242.webm +0 -0
  40. package/assets/thumb//345/220/203/351/245/272/345/255/220.webm +0 -0
  41. package/assets/thumb//345/220/271/346/260/224/347/220/203.webm +0 -0
  42. package/assets/thumb//345/220/271/347/254/233/345/255/220.webm +0 -0
  43. package/assets/thumb//345/223/210/346/254/240/350/277/236/345/244/251.webm +0 -0
  44. package/assets/thumb//345/240/206/351/233/252/344/272/272.webm +0 -0
  45. package/assets/thumb//345/244/247/345/217/243/345/220/203/351/233/266/351/243/237.webm +0 -0
  46. package/assets/thumb//345/245/263/344/273/206/345/261/210/350/206/235/347/244/274/344/273/252.webm +0 -0
  47. package/assets/thumb//345/260/217/345/271/205/345/272/246/345/216/237/345/234/260360/345/272/246/346/227/213/350/275/254/345/261/225/347/244/272.webm +0 -0
  48. package/assets/thumb//345/260/217/346/217/220/347/220/264/346/274/224/345/245/217.webm +0 -0
  49. package/assets/thumb//345/276/205/346/234/272/345/221/274/345/220/270/344/274/221/351/227/262.webm +0 -0
  50. package/assets/thumb//346/202/240/351/227/262/345/223/274/346/255/214.webm +0 -0
  51. package/assets/thumb//346/211/221/345/205/213/351/255/224/346/234/257.webm +0 -0
  52. package/assets/thumb//346/211/223/347/236/214/347/235/241/350/242/253/346/203/212/351/206/222.webm +0 -0
  53. package/assets/thumb//346/212/275/351/231/200/350/236/272.webm +0 -0
  54. package/assets/thumb//346/213/206/347/244/274/347/211/251.webm +0 -0
  55. package/assets/thumb//346/217/222/350/214/261/350/220/270/350/265/217/350/217/212.webm +0 -0
  56. package/assets/thumb//346/221/207/346/211/207/347/272/263/345/207/211.webm +0 -0
  57. package/assets/thumb//346/222/270/347/214/253.webm +0 -0
  58. package/assets/thumb//346/224/266/347/272/242/345/214/205.webm +0 -0
  59. package/assets/thumb//346/224/276/345/255/224/346/230/216/347/201/257.webm +0 -0
  60. package/assets/thumb//346/224/276/346/262/263/347/201/257.webm +0 -0
  61. package/assets/thumb//346/224/276/347/203/237/350/212/261.webm +0 -0
  62. package/assets/thumb//346/224/276/351/243/216/347/255/235.webm +0 -0
  63. package/assets/thumb//346/225/264/344/275/223/346/215/242/350/243/205/350/257/225/350/211/262.webm +0 -0
  64. package/assets/thumb//346/230/257/345/225/212/357/274/214/345/220/203/344/273/200/344/271/210.webm +0 -0
  65. package/assets/thumb//346/231/250/351/227/264/345/210/267/347/211/231.webm +0 -0
  66. package/assets/thumb//346/266/256/347/201/253/351/224/205.webm +0 -0
  67. package/assets/thumb//346/267/261/345/272/246/346/200/235/350/200/203/347/242/216/347/242/216/345/277/265.webm +0 -0
  68. package/assets/thumb//347/202/271/345/207/273/345/233/236/345/272/224-/345/202/262/345/250/207/347/224/237/346/260/224.webm +0 -0
  69. package/assets/thumb//347/202/271/345/207/273/345/233/236/345/272/224-/345/205/203/346/260/224/346/214/245/346/211/213.webm +0 -0
  70. package/assets/thumb//347/202/271/345/207/273/345/233/236/345/272/224-/345/256/263/347/276/236/346/203/212/350/256/266.webm +0 -0
  71. package/assets/thumb//347/202/271/345/207/273/345/233/236/345/272/224-/345/274/200/345/277/203/350/267/203/345/212/250.webm +0 -0
  72. package/assets/thumb//347/202/271/345/207/273/345/233/236/345/272/224-/346/214/240/347/227/222/345/222/257/345/222/257/347/254/221.webm +0 -0
  73. package/assets/thumb//347/205/247/351/225/234/345/255/220.webm +0 -0
  74. package/assets/thumb//347/216/251/346/260/264/346/236/252.webm +0 -0
  75. package/assets/thumb//347/216/251/346/270/270/346/210/217/346/260/224/346/200/245/350/264/245/345/235/217.webm +0 -0
  76. package/assets/thumb//347/224/250/351/262/270/351/261/274/345/260/276/345/267/264/346/213/215/346/211/223/345/234/260/351/235/242.webm +0 -0
  77. package/assets/thumb//347/251/277/351/222/210/344/271/236/345/267/247.webm +0 -0
  78. package/assets/thumb//350/210/236/347/213/256/345/244/264.webm +0 -0
  79. package/assets/thumb//350/215/241/347/247/213/345/215/203.webm +0 -0
  80. package/assets/thumb//350/220/214/345/214/226/345/260/217/345/271/275/347/201/265.webm +0 -0
  81. package/assets/thumb//350/223/235/351/262/270/347/216/260/344/270/226.webm +0 -0
  82. package/assets/thumb//350/235/264/350/235/266/350/234/234/350/234/202/347/216/257/347/273/225/345/244/264/351/241/266/345/274/200/350/212/261.webm +0 -0
  83. package/assets/thumb//350/236/203/350/237/271/350/265/260/350/267/257.webm +0 -0
  84. package/assets/thumb//350/242/253/345/220/223/344/270/200/350/267/263.webm +0 -0
  85. package/assets/thumb//350/242/253/350/220/275/345/217/266/346/267/271/346/262/241.webm +0 -0
  86. package/assets/thumb//350/242/253/351/274/240/346/240/207/346/213/226/346/213/275/346/202/254/347/251/272/345/217/215/351/246/210.webm +0 -0
  87. package/assets/thumb//350/243/205/347/202/271/345/234/243/350/257/236/346/240/221.webm +0 -0
  88. package/assets/thumb//350/256/250/347/263/226/345/215/227/347/223/234/347/201/257.webm +0 -0
  89. package/assets/thumb//350/266/205/345/244/247/344/274/270/346/207/222/350/205/260.webm +0 -0
  90. package/assets/thumb//350/270/242/346/257/275/345/255/220.webm +0 -0
  91. package/assets/thumb//350/275/273/345/277/253/346/221/207/346/221/206/350/210/236.webm +0 -0
  92. package/assets/thumb//350/275/273/345/277/253/350/256/260/345/275/225.webm +0 -0
  93. package/assets/thumb//351/252/221/346/234/250/351/251/254.webm +0 -0
  94. package/assets/thumb//351/262/270/351/261/274/345/220/220/346/263/241/346/263/241/347/211/271/346/225/210.webm +0 -0
  95. package/lib/client.js +568 -693
  96. package/lib/index.js +80 -125
  97. package/package.json +20 -6
  98. package/src/client/app.ts +452 -0
  99. package/src/client/config.ts +77 -0
  100. package/src/client/constants.ts +9 -0
  101. package/src/client/index.ts +15 -0
  102. package/src/client/pickers.ts +30 -0
  103. package/src/client/types.ts +37 -0
  104. package/src/env.d.ts +6 -0
  105. package/src/host/index.ts +119 -0
  106. package/assets/thumb//345/260/217/345/271/205/345/272/246/345/216/237/345/234/260 360 /345/272/246/346/227/213/350/275/254/345/261/225/347/244/272.webm +0 -0
  107. package/assets/thumb//347/202/271/345/207/273/345/233/236/345/272/224 - /345/202/262/345/250/207/347/224/237/346/260/224/357/274/210/344/276/247/350/272/253/345/261/225/347/244/272/357/274/211.webm +0 -0
  108. package/assets/thumb//347/202/271/345/207/273/345/233/236/345/272/224 - /345/256/263/347/276/236/346/203/212/350/256/266.webm +0 -0
  109. package/assets/thumb//347/202/271/345/207/273/345/233/236/345/272/224 - /345/274/200/345/277/203/350/267/203/345/212/250.webm +0 -0
  110. package/assets/thumb//350/242/253/345/220/223/344/270/200/350/267/263/357/274/210/347/202/270/346/257/233/357/274/211.webm +0 -0
@@ -0,0 +1,452 @@
1
+ // client 半侧「应用逻辑」:由 tsdown 内联进 lib/client.js 的 __ModuleLoader__ factory。
2
+ // 设计:makeFactory() 返回 DSH 的 factory 函数(f(require) => module),
3
+ // react 通过 factory 的 require 取得(不走 ESM 顶部 import),
4
+ // 以匹配 DSH 的 window.__ModuleLoader__.load({ id, factory }) 加载方式。
5
+ import { pick, randomBetween, pickWeightedCategory } from './pickers';
6
+ import { stripJsonc, buildAnim, buildMinimalAnim } from './config';
7
+ import { CANVAS_H, FEET_Y, HIT_BOX, DRAG_THRESHOLD } from './constants';
8
+ import type { AnimConfig, Corner } from './types';
9
+ import type * as ReactNS from 'react';
10
+
11
+ /** 支持的角落白名单 */
12
+ const CORNERS: Corner[] = ['top-left', 'top-right', 'bottom-left', 'bottom-right'];
13
+
14
+ /**
15
+ * 返回 DSH 插件 factory:`(require) => module`。
16
+ * 插件三件套(name / inject / apply)都在其返回的 module 上。
17
+ */
18
+ export function makeFactory(): (require: (mod: string) => any) => any {
19
+ return (require) => {
20
+ var module = { exports: {} as any };
21
+
22
+ const react: typeof ReactNS = require('react');
23
+ const { useEffect, useRef, useState } = react;
24
+ const { jsx: h } = require('react/jsx-runtime');
25
+
26
+ // ============================================================================
27
+ // 内联 CSS —— 注入一次(官方插件标准做法)
28
+ // ============================================================================
29
+ const css = [
30
+ '.dsh-pet-root{position:fixed;z-index:40;pointer-events:none;user-select:none}',
31
+ '.dsh-pet-root[data-corner="bottom-right"]{right:var(--dsh-pet-mx,24px);bottom:var(--dsh-pet-my,0)}',
32
+ '.dsh-pet-root[data-corner="bottom-left"]{left:var(--dsh-pet-mx,24px);bottom:var(--dsh-pet-my,0)}',
33
+ '.dsh-pet-root[data-corner="top-right"]{right:var(--dsh-pet-mx,24px);top:var(--dsh-pet-my,0)}',
34
+ '.dsh-pet-root[data-corner="top-left"]{left:var(--dsh-pet-mx,24px);top:var(--dsh-pet-my,0)}',
35
+ '.dsh-pet-stage{position:relative;width:var(--dsh-pet-size,462px);height:calc(var(--dsh-pet-size,462px)*9/16);pointer-events:none}',
36
+ '.dsh-pet-video{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;pointer-events:none;opacity:0;transition:opacity .18s ease;transform-origin:center}',
37
+ '.dsh-pet-video.is-front{opacity:1}',
38
+ '.dsh-pet-hit{position:absolute;pointer-events:auto;cursor:default;z-index:1}',
39
+ '.dsh-pet-hit.dragging{cursor:grabbing}',
40
+ '@media (prefers-reduced-motion: reduce){.dsh-pet-video{transition:none}}',
41
+ ].join('\n');
42
+ const cssTag = 'dsh-pet/style.css';
43
+ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin-css="' + cssTag + '"]') === null) {
44
+ const tag = document.createElement('style');
45
+ tag.dataset.plugin = 'dsh-pet';
46
+ tag.dataset.pluginCss = cssTag;
47
+ tag.textContent = css;
48
+ document.head.appendChild(tag);
49
+ }
50
+
51
+ // ============================================================================
52
+ // 动作配置(唯一事实来源 = config.jsonc)
53
+ // ============================================================================
54
+ let ANIM: AnimConfig = buildMinimalAnim();
55
+
56
+ // ============================================================================
57
+ // Pet 组件 —— 宠物本体
58
+ // ============================================================================
59
+ function Pet() {
60
+ // ---- 尺寸(默认 462px 宽;可由 config.jsonc 的 size 覆盖)----
61
+ const [size, setSize] = useState(462);
62
+ const halfW = size / 2;
63
+ const halfH = (size * 9) / 16 / 2;
64
+
65
+ // ---- React 状态 ----
66
+ const [anim, setAnim] = useState(ANIM.idle[0] ?? '');
67
+ const [once, setOnce] = useState(true);
68
+ const [facing, setFacing] = useState('left' as 'left' | 'right');
69
+ const [dragging, setDragging] = useState(false);
70
+ const [customPos, setCustomPos] = useState<null | { rx: number; ry: number }>(null);
71
+ // 初始角落与边距(默认右下 right24/bottom0;可被 config.jsonc 覆盖)
72
+ const [corner, setCorner] = useState<Corner>('bottom-right');
73
+ const [margin, setMargin] = useState({ x: 24, y: 0 });
74
+ // 拉取 config.jsonc 应用尺寸/位置/动作配置;失败/缺失/写错均静默沿用默认/兜底
75
+ useEffect(() => {
76
+ fetch('/pet/config.jsonc')
77
+ .then((r) => {
78
+ if (!r.ok) throw new Error('config');
79
+ return r.text();
80
+ })
81
+ .then((src) => {
82
+ const obj = JSON.parse(stripJsonc(src));
83
+ const sz = Number(obj && obj.size);
84
+ if (Number.isFinite(sz) && sz > 0) setSize(sz);
85
+ ANIM = buildAnim(obj);
86
+ const p = obj && obj.position;
87
+ if (p) {
88
+ if (CORNERS.indexOf(p.corner) !== -1) setCorner(p.corner as Corner);
89
+ const mx = Number(p.marginX);
90
+ const my = Number(p.marginY);
91
+ if (!Number.isNaN(mx)) setMargin((m) => ({ ...m, x: mx }));
92
+ if (!Number.isNaN(my)) setMargin((m) => ({ ...m, y: my }));
93
+ }
94
+ // 配置就绪后用待机动画启动(ANIM 由 buildAnim 兜底,必有 idle)
95
+ if (ANIM.idle && ANIM.idle.length) {
96
+ setAnim(ANIM.idle[0]);
97
+ setSeq((s) => s + 1);
98
+ }
99
+ })
100
+ .catch(() => {});
101
+ // eslint-disable-next-line react-hooks/exhaustive-deps
102
+ }, []);
103
+ const [seq, setSeq] = useState(0);
104
+
105
+ // ---- DOM / 状态 refs ----
106
+ const rootRef = useRef<HTMLDivElement | null>(null);
107
+ const stageRef = useRef<HTMLDivElement | null>(null);
108
+ const videoARef = useRef<HTMLVideoElement | null>(null);
109
+ const videoBRef = useRef<HTMLVideoElement | null>(null);
110
+ const frontRef = useRef(0);
111
+ const pendingRef = useRef<null | { anim: string; once: boolean; gen: number }>(null);
112
+ const genRef = useRef(0);
113
+ const dragRef = useRef({ active: false, dragging: false, sx: 0, sy: 0, offX: 0, offY: 0 });
114
+ const justDraggedRef = useRef(false);
115
+ const animRef = useRef(anim);
116
+ animRef.current = anim;
117
+
118
+ const switchTo = (next: string, nextOnce: boolean) => {
119
+ if (!next) return;
120
+ const pending = pendingRef.current;
121
+ if (pending && pending.anim === next && pending.once === nextOnce) return;
122
+ const gen = ++genRef.current;
123
+ pendingRef.current = { anim: next, once: nextOnce, gen };
124
+ const target = frontRef.current === 0 ? videoBRef : videoARef;
125
+ const el = target.current;
126
+ if (!el) return;
127
+ el.src = '/pet/thumb/' + encodeURIComponent(next) + '.webm';
128
+ el.loop = !nextOnce;
129
+ el.muted = true;
130
+ el.autoplay = true;
131
+ el.playsInline = true;
132
+ el.onended = nextOnce ? handleEnded : null;
133
+ el.load();
134
+ const onReady = () => {
135
+ el.removeEventListener('loadeddata', onReady);
136
+ if (pendingRef.current?.gen !== gen) return;
137
+ const old = frontRef.current === 0 ? videoARef : videoBRef;
138
+ el.classList.add('is-front');
139
+ if (old.current && old.current !== el) old.current.classList.remove('is-front');
140
+ frontRef.current = frontRef.current === 0 ? 1 : 0;
141
+ pendingRef.current = null;
142
+ el.style.transform = facingRef.current === 'right' ? 'scaleX(-1)' : '';
143
+ el.play().catch(() => {});
144
+ if (pendingMoveRef.current) startMoveDrive(el);
145
+ };
146
+ el.addEventListener('loadeddata', onReady);
147
+ if (el.readyState >= 2) onReady();
148
+ };
149
+
150
+ // ---- 状态驱动播放 ----
151
+ useEffect(() => {
152
+ switchTo(anim, once);
153
+ // eslint-disable-next-line react-hooks/exhaustive-deps
154
+ }, [anim, once, seq]);
155
+ useEffect(() => () => stopMove(), []);
156
+ useEffect(() => {
157
+ const onResize = () => setCustomPos((prev) => (prev ? { ...prev } : prev));
158
+ window.addEventListener('resize', onResize);
159
+ return () => window.removeEventListener('resize', onResize);
160
+ // eslint-disable-next-line react-hooks/exhaustive-deps
161
+ }, []);
162
+
163
+ // ---- 动画链:播完按权重选下一个 ----
164
+ const pickNext = () => {
165
+ const roll = Math.random();
166
+ const wI = ANIM.weights.idle;
167
+ const wT = ANIM.weights.turn;
168
+ const wM = ANIM.weights.move;
169
+ const topEnd = (wI + wT + wM) / 100;
170
+ let kind = '';
171
+ let next = '';
172
+ if (roll < wI / 100) {
173
+ kind = 'IDLE';
174
+ next = pick(ANIM.idle, animRef.current);
175
+ setAnim(next);
176
+ } else if (roll < (wI + wT) / 100) {
177
+ kind = 'TURN';
178
+ next = pick(ANIM.turn, animRef.current);
179
+ setAnim(next);
180
+ } else if (roll < topEnd) {
181
+ if (!tryMove()) {
182
+ const cat = pickWeightedCategory(ANIM.categories, facingRef.current);
183
+ kind = cat ? cat.id : 'FALLBACK';
184
+ next = cat ? pick(cat.actions, animRef.current) : pick(ANIM.idle, animRef.current);
185
+ setAnim(next);
186
+ } else {
187
+ kind = 'MOVES';
188
+ next = '移动(池内随机)';
189
+ }
190
+ } else {
191
+ const cat = pickWeightedCategory(ANIM.categories, facingRef.current);
192
+ kind = cat ? cat.id : 'FALLBACK';
193
+ next = cat ? pick(cat.actions, animRef.current) : pick(ANIM.idle, animRef.current);
194
+ setAnim(next);
195
+ }
196
+ console.log('[dsh-pet] roll=' + roll.toFixed(4) + ' -> [' + kind + '] ' + next);
197
+ setOnce(true);
198
+ setSeq((s) => s + 1);
199
+ };
200
+
201
+ const handleEnded = () => {
202
+ if (dragRef.current.active) return;
203
+ if (ANIM.turn.includes(animRef.current)) {
204
+ setFacing((f) => (f === 'left' ? 'right' : 'left'));
205
+ }
206
+ if (ANIM.drag.includes(animRef.current) || ANIM.clicks.includes(animRef.current)) {
207
+ if (ANIM.idle.length) setAnim(pick(ANIM.idle, animRef.current));
208
+ setOnce(true);
209
+ setSeq((s) => s + 1);
210
+ return;
211
+ }
212
+ pickNext();
213
+ };
214
+
215
+ // ---- 移动系统 ----
216
+ const moveRef = useRef<number | null>(null);
217
+ const moveTokenRef = useRef(0);
218
+ const pendingMoveRef = useRef<null | { startRatio: number; startYRatio: number; targetRatio: number; dir: number; totalRatio: number; leadSec: number; tailSec: number }>(null);
219
+ const customPosRef = useRef(customPos);
220
+ customPosRef.current = customPos;
221
+
222
+ const currentCenterX = () => {
223
+ const cp = customPosRef.current;
224
+ if (cp) return cp.rx * window.innerWidth;
225
+ const rootEl = rootRef.current;
226
+ if (rootEl) return rootEl.getBoundingClientRect().left + halfW;
227
+ return window.innerWidth - 24 - halfW;
228
+ };
229
+ const currentCenterY = () => {
230
+ const cp = customPosRef.current;
231
+ if (cp) return cp.ry * window.innerHeight;
232
+ const rootEl = rootRef.current;
233
+ if (rootEl) return rootEl.getBoundingClientRect().top + halfH;
234
+ return window.innerHeight - 20 - halfH;
235
+ };
236
+
237
+ const startMoveDrive = (el: HTMLVideoElement) => {
238
+ const pm = pendingMoveRef.current;
239
+ if (!pm || moveRef.current !== null) return;
240
+ pendingMoveRef.current = null;
241
+ const { startRatio, startYRatio, targetRatio, dir, totalRatio, leadSec, tailSec } = pm;
242
+ const duration = Number.isFinite(el.duration) && el.duration > 0 ? el.duration : 10.09;
243
+ const travelWindow = Math.max(0.1, duration - leadSec - tailSec);
244
+ const token = ++moveTokenRef.current;
245
+ const step = () => {
246
+ if (moveTokenRef.current !== token) return;
247
+ const t = el.currentTime || 0;
248
+ const rootEl = rootRef.current;
249
+ if (rootEl) {
250
+ const W = window.innerWidth;
251
+ const H = window.innerHeight;
252
+ let ratioX;
253
+ if (t <= leadSec) ratioX = startRatio;
254
+ else if (t >= duration - tailSec) ratioX = targetRatio;
255
+ else ratioX = startRatio + dir * totalRatio * ((t - leadSec) / travelWindow);
256
+ const px = ratioX * W;
257
+ const py = startYRatio * H;
258
+ rootEl.style.left = px - halfW + 'px';
259
+ rootEl.style.top = py - halfH + 'px';
260
+ rootEl.style.right = 'auto';
261
+ rootEl.style.bottom = 'auto';
262
+ }
263
+ if (t < duration - tailSec) moveRef.current = requestAnimationFrame(step);
264
+ else {
265
+ moveRef.current = null;
266
+ setCustomPos({ rx: targetRatio, ry: startYRatio });
267
+ }
268
+ };
269
+ moveRef.current = requestAnimationFrame(step);
270
+ };
271
+
272
+ const tryMove = () => {
273
+ if (moveRef.current !== null || pendingMoveRef.current) return true;
274
+ const actions = ANIM.moves.actions;
275
+ if (!actions.length) return false;
276
+ const chosen = actions[Math.floor(Math.random() * actions.length)];
277
+ const mp = Object.assign({}, ANIM.moves.default, chosen.params || {});
278
+ const dir = (facingRef.current === 'right') !== ANIM.turn.includes(animRef.current) ? 1 : -1;
279
+ const W = window.innerWidth;
280
+ const cx = currentCenterX();
281
+ const distance = randomBetween(mp.minDist, mp.maxDist);
282
+ const target = cx + dir * distance;
283
+ const leftBound = mp.margin + halfW;
284
+ const rightBound = W - mp.margin - halfW;
285
+ if (target < leftBound || target > rightBound) return false;
286
+ pendingMoveRef.current = {
287
+ startRatio: cx / W,
288
+ startYRatio: currentCenterY() / window.innerHeight,
289
+ targetRatio: target / W,
290
+ dir,
291
+ totalRatio: Math.abs(target - cx) / W,
292
+ leadSec: mp.leadSec,
293
+ tailSec: mp.tailSec,
294
+ };
295
+ setOnce(true);
296
+ setAnim(chosen.name);
297
+ return true;
298
+ };
299
+ const stopMove = () => {
300
+ pendingMoveRef.current = null;
301
+ moveTokenRef.current++;
302
+ if (moveRef.current !== null) {
303
+ cancelAnimationFrame(moveRef.current);
304
+ moveRef.current = null;
305
+ }
306
+ };
307
+
308
+ const facingRef = useRef<'left' | 'right'>(facing);
309
+ facingRef.current = facing;
310
+
311
+ // ---- 点击 vs 拖拽 ----
312
+ const handlePointerDown = (e: any) => {
313
+ e.currentTarget.classList.add('dragging');
314
+ stopMove();
315
+ e.currentTarget.setPointerCapture(e.pointerId);
316
+ const rootEl = rootRef.current;
317
+ let offX = 0;
318
+ let offY = 0;
319
+ if (rootEl) {
320
+ const rr = rootEl.getBoundingClientRect();
321
+ offX = e.clientX - (rr.left + rr.width / 2);
322
+ offY = e.clientY - (rr.top + rr.height / 2);
323
+ }
324
+ dragRef.current = { active: true, dragging: false, sx: e.clientX, sy: e.clientY, offX, offY };
325
+ };
326
+ const handlePointerMove = (e: any) => {
327
+ const d = dragRef.current;
328
+ if (!d.active) return;
329
+ const dx = e.clientX - d.sx;
330
+ const dy = e.clientY - d.sy;
331
+ if (!d.dragging) {
332
+ if (Math.hypot(dx, dy) < DRAG_THRESHOLD) return;
333
+ d.dragging = true;
334
+ setDragging(true);
335
+ setOnce(true);
336
+ if (ANIM.drag.length) setAnim(pick(ANIM.drag));
337
+ }
338
+ const rootEl = rootRef.current;
339
+ if (rootEl) {
340
+ rootEl.style.left = e.clientX - d.offX - halfW + 'px';
341
+ rootEl.style.top = e.clientY - d.offY - halfH + 'px';
342
+ rootEl.style.right = 'auto';
343
+ rootEl.style.bottom = 'auto';
344
+ }
345
+ const stageEl = stageRef.current;
346
+ if (stageEl) stageEl.style.transform = 'none';
347
+ };
348
+ const handlePointerUp = (e: any) => {
349
+ const d = dragRef.current;
350
+ const wasDragging = d.dragging;
351
+ d.active = false;
352
+ d.dragging = false;
353
+ e.currentTarget.classList.remove('dragging');
354
+ if (wasDragging) {
355
+ justDraggedRef.current = true;
356
+ setTimeout(() => {
357
+ justDraggedRef.current = false;
358
+ }, 100);
359
+ setDragging(false);
360
+ setCustomPos({ rx: (e.clientX - d.offX) / window.innerWidth, ry: (e.clientY - d.offY) / window.innerHeight });
361
+ const stageEl = stageRef.current;
362
+ if (stageEl) stageEl.style.transform = 'translateY(' + bottomPad + 'px)';
363
+ if (ANIM.idle.length) setAnim(pick(ANIM.idle, animRef.current));
364
+ setOnce(false);
365
+ }
366
+ };
367
+ const handleClick = (_e: any) => {
368
+ const d = dragRef.current;
369
+ if (d.active || d.dragging || justDraggedRef.current) return;
370
+ if (once && !ANIM.idle.includes(animRef.current)) return;
371
+ stopMove();
372
+ setOnce(true);
373
+ if (ANIM.clicks.length) setAnim(pick(ANIM.clicks));
374
+ };
375
+
376
+ // ---- 渲染 ----
377
+ const bottomPad = (size * (9 / 16) * (CANVAS_H - FEET_Y)) / CANVAS_H;
378
+ const stageStyle = dragging ? { transform: 'none' } : { transform: 'translateY(' + bottomPad + 'px)' };
379
+ const rootStyle = customPos
380
+ ? (() => {
381
+ const rx = customPos.rx;
382
+ const ry = customPos.ry;
383
+ const left = Math.min(Math.max(rx * window.innerWidth - halfW, 0), window.innerWidth - size);
384
+ const top = Math.min(Math.max(ry * window.innerHeight - halfH, 0), window.innerHeight - (size * 9) / 16);
385
+ return { left: left + 'px', top: top + 'px', right: 'auto', bottom: 'auto' };
386
+ })()
387
+ : {};
388
+ const commonVideoProps = { muted: true, playsInline: true, autoPlay: true, title: 'dsh-pet' };
389
+ const hitProps = {
390
+ className: 'dsh-pet-hit',
391
+ style: {
392
+ left: ((HIT_BOX.x0 / 640) * 100) + '%',
393
+ top: ((HIT_BOX.y0 / 360) * 100) + '%',
394
+ width: (((HIT_BOX.x1 - HIT_BOX.x0) / 640) * 100) + '%',
395
+ height: (((HIT_BOX.y1 - HIT_BOX.y0) / 360) * 100) + '%',
396
+ },
397
+ onMouseEnter: (e: any) => {
398
+ if (!dragRef.current.active) e.currentTarget.style.cursor = 'grab';
399
+ },
400
+ onMouseLeave: (e: any) => {
401
+ if (!dragRef.current.active) e.currentTarget.style.cursor = 'default';
402
+ },
403
+ onClick: handleClick,
404
+ onPointerDown: handlePointerDown,
405
+ onPointerMove: handlePointerMove,
406
+ onPointerUp: handlePointerUp,
407
+ onPointerCancel: handlePointerUp,
408
+ title: 'dsh-pet',
409
+ };
410
+ return h(
411
+ 'div',
412
+ {
413
+ ref: rootRef,
414
+ className: 'dsh-pet-root',
415
+ 'data-corner': corner,
416
+ 'data-facing': facing,
417
+ style: Object.assign(
418
+ { '--dsh-pet-size': size + 'px', '--dsh-pet-mx': margin.x + 'px', '--dsh-pet-my': margin.y + 'px' },
419
+ rootStyle,
420
+ ),
421
+ children: h('div', {
422
+ ref: stageRef,
423
+ className: 'dsh-pet-stage',
424
+ style: stageStyle,
425
+ children: [
426
+ h('video', Object.assign({}, commonVideoProps, { ref: videoARef, className: 'dsh-pet-video is-front' })),
427
+ h('video', Object.assign({}, commonVideoProps, { ref: videoBRef, className: 'dsh-pet-video' })),
428
+ h('div', hitProps),
429
+ ],
430
+ }),
431
+ },
432
+ );
433
+ }
434
+
435
+ // ============================================================================
436
+ // 插件主体(Cordis 插件三件套)
437
+ // ============================================================================
438
+ const name = 'pet';
439
+ const inject = ['slots'];
440
+ function apply(ctx: any, _config: any) {
441
+ ctx.slots.inject('shell.overlay', function* () {
442
+ yield ctx.slots.register(
443
+ { name: 'shell.overlay', id: 'pet', order: 1000 },
444
+ () => h(Pet, {}),
445
+ );
446
+ });
447
+ }
448
+
449
+ module.exports = { apply, inject, name };
450
+ return module.exports;
451
+ };
452
+ }
@@ -0,0 +1,77 @@
1
+ // 配置层:拉取 / 解析 config.jsonc,构建运行时动作配置(唯一事实来源)。
2
+ import type { AnimConfig } from './types';
3
+
4
+ /** 安全兜底待机名:用于初始渲染 / 配置缺失时宠物不至于空白 */
5
+ export const FALLBACK_IDLE = '待机呼吸休闲';
6
+ /** 移动默认参数(仅当 config.jsonc 未提供 moves.default 时兜底) */
7
+ export const DEFAULT_MOVE_PARAMS: Record<string, number> = {
8
+ minDist: 60,
9
+ maxDist: 240,
10
+ margin: 20,
11
+ leadSec: 2,
12
+ tailSec: 2,
13
+ };
14
+
15
+ /** 剥除 JSONC 注释(行注释 // 与块注释),得到纯 JSON 字符串。仅用于插件自带配置(无含 // 的 URL 值) */
16
+ export const stripJsonc = (src: string): string =>
17
+ src
18
+ .replace(/\/\*[\s\S]*?\*\//g, '')
19
+ .replace(/(^|[^\\:])\/\/.*$/gm, '$1')
20
+ .trim();
21
+
22
+ /** 最小兜底配置:仅当 config.jsonc 缺失时使用,保证宠物不至于空白 */
23
+ export const buildMinimalAnim = (): AnimConfig => ({
24
+ idle: [FALLBACK_IDLE],
25
+ turn: [],
26
+ drag: [],
27
+ clicks: [],
28
+ moves: {
29
+ default: { ...DEFAULT_MOVE_PARAMS },
30
+ actions: [],
31
+ },
32
+ categories: [],
33
+ weights: { idle: 100, turn: 0, move: 0 },
34
+ });
35
+
36
+ /** 从 config.jsonc 解析出的对象构建运行时动作配置(缺字段/写错尽量安全兜底) */
37
+ export const buildAnim = (obj: unknown): AnimConfig => {
38
+ const a = (obj as any)?.animations;
39
+ const w = (obj as any)?.animationWeights ?? {};
40
+ const strArr = (x: unknown): string[] => (Array.isArray(x) ? x.map(String).filter(Boolean) : []);
41
+ const idle = strArr(a?.idle);
42
+ if (!idle.length) idle.push(FALLBACK_IDLE);
43
+ const turn = strArr(a?.turn);
44
+ const drag = strArr(a?.drag);
45
+ const clicks = strArr(a?.clicks);
46
+ let mdef: Record<string, number> = { ...DEFAULT_MOVE_PARAMS };
47
+ let mActs: { name: string; params?: Record<string, number> }[] = [];
48
+ if (a?.moves && typeof a.moves === 'object') {
49
+ if (a.moves.default && typeof a.moves.default === 'object') mdef = { ...mdef, ...a.moves.default };
50
+ if (Array.isArray(a.moves.actions)) {
51
+ const list = a.moves.actions
52
+ .filter((x: any) => x && x.name)
53
+ .map((x: any) => ({ name: String(x.name), params: x.params && typeof x.params === 'object' ? { ...x.params } : undefined }));
54
+ if (list.length) mActs = list;
55
+ }
56
+ }
57
+ let cats: { id: string; weight: number; noMirror?: boolean; actions: string[] }[] = [];
58
+ if (Array.isArray(a?.categories)) {
59
+ const list = a.categories
60
+ .filter((c: any) => c && c.id && Array.isArray(c.actions) && c.actions.length)
61
+ .map((c: any) => ({ id: String(c.id), weight: Number(c.weight), noMirror: !!c.noMirror, actions: c.actions.map(String) }));
62
+ if (list.length) cats = list;
63
+ }
64
+ return {
65
+ idle,
66
+ turn,
67
+ drag,
68
+ clicks,
69
+ moves: { default: mdef, actions: mActs },
70
+ categories: cats,
71
+ weights: {
72
+ idle: Number(w.idle) >= 0 ? Number(w.idle) : 10,
73
+ turn: Number(w.turn) >= 0 ? Number(w.turn) : 5,
74
+ move: Number(w.move) >= 0 ? Number(w.move) : 5,
75
+ },
76
+ };
77
+ };
@@ -0,0 +1,9 @@
1
+ // 画布 / 几何常量:与 640×360 播放变体强耦合,属于运行时几何,不作为配置。
2
+ /** thumb 画布高度 */
3
+ export const CANVAS_H = 360;
4
+ /** thumb 画布上「脚底」的 y 坐标(人物站在 y=330 线上) */
5
+ export const FEET_Y = 330;
6
+ /** 点击/拖拽命中矩形(thumb 640×360 像素坐标) */
7
+ export const HIT_BOX = { x0: 200, y0: 50, x1: 440, y1: 335 };
8
+ /** 拖拽判定阈值(px) */
9
+ export const DRAG_THRESHOLD = 5;
@@ -0,0 +1,15 @@
1
+ // client 半侧 bundle 外壳:由 tsdown 构建为 lib/client.js。
2
+ // 必须是一个「普通副作用脚本」——加载时调用 window.__ModuleLoader__.load,
3
+ // 不能包含顶层 ESM export / import(react 由 factory 的 require 取得)。
4
+ import { makeFactory } from './app';
5
+
6
+ declare const window: {
7
+ __ModuleLoader__: {
8
+ load(info: { id: string; factory: (require: (m: string) => any) => any }): void;
9
+ };
10
+ };
11
+
12
+ window.__ModuleLoader__.load({
13
+ id: 'dsh-pet',
14
+ factory: makeFactory(),
15
+ });
@@ -0,0 +1,30 @@
1
+ // 纯选择逻辑:不依赖 React / DOM,可独立单测。
2
+ import type { Category } from './types';
3
+
4
+ /** 从字符串池里等概率随机抽一个;exclude 排除某个名字(避免连续重复) */
5
+ export const pick = <T,>(pool: T[], exclude?: T): T => {
6
+ const entries = exclude ? pool.filter((n) => n !== exclude) : pool;
7
+ return entries[Math.floor(Math.random() * entries.length)];
8
+ };
9
+
10
+ /** 生成 [min, max) 区间内的随机整数 */
11
+ export const randomBetween = (min: number, max: number): number =>
12
+ Math.floor(min + Math.random() * (max - min));
13
+
14
+ /**
15
+ * 按权重在分类池中选一个分类;noMirror 分类在镜像(facing=right)时被排除,
16
+ * 剩余权重自动归一化。分类池为空时返回 null。
17
+ */
18
+ export const pickWeightedCategory = (categories: Category[], facing: string): Category | null => {
19
+ const cats = categories.filter((c) => c.actions.length > 0);
20
+ if (!cats.length) return null;
21
+ const filtered = cats.filter((c) => !(c.noMirror && facing === 'right'));
22
+ const eligible = filtered.length ? filtered : cats;
23
+ const totalW = eligible.reduce((s, c) => s + c.weight, 0) || 1;
24
+ let t = Math.random() * totalW;
25
+ for (const c of eligible) {
26
+ t -= c.weight;
27
+ if (t <= 0) return c;
28
+ }
29
+ return eligible[eligible.length - 1];
30
+ };
@@ -0,0 +1,37 @@
1
+ // 动作配置的类型模型(唯一事实来源为 config.jsonc 的 animations/animationWeights)。
2
+ /** 移动动作:一个动作名 + 可选覆盖参数(未写字段取 moves.default) */
3
+ export interface MoveSpec {
4
+ name: string;
5
+ params?: Record<string, number>;
6
+ }
7
+ /** 移动池配置 */
8
+ export interface MovesConfig {
9
+ default: Record<string, number>;
10
+ actions: MoveSpec[];
11
+ }
12
+ /** 随机动作分类 */
13
+ export interface Category {
14
+ id: string;
15
+ weight: number;
16
+ /** 带文字、镜像会颠倒:facing=right 时跳过 */
17
+ noMirror?: boolean;
18
+ actions: string[];
19
+ }
20
+ /** 动画权重(百分比) */
21
+ export interface Weights {
22
+ idle: number;
23
+ turn: number;
24
+ move: number;
25
+ }
26
+ /** 运行时动作配置(由 config.jsonc 构建) */
27
+ export interface AnimConfig {
28
+ idle: string[];
29
+ turn: string[];
30
+ drag: string[];
31
+ clicks: string[];
32
+ moves: MovesConfig;
33
+ categories: Category[];
34
+ weights: Weights;
35
+ }
36
+ /** 支持的角落 */
37
+ export type Corner = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
package/src/env.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ // DSH peer 依赖类型补充:这些包由 DSH 宿主在运行时提供,本地开发无 node_modules,
2
+ // 这里为它们声明最小类型,便于 tsc 通过(运行时以 DSH 提供的为准)。
3
+ declare module '@deepseek-ai/dsh-home-paths' {
4
+ /** 解析 DSH 主目录($DSH_HOME,默认 ~/.dsh) */
5
+ export function resolveDshHome(): string;
6
+ }