dsh-pet-live2d 2.0.2 → 2.1.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # 更新日志
2
2
 
3
+ ## 2.1.0
4
+
5
+ ### 会话相位改成真正的"接管"
6
+
7
+ 用户报的:"我在爱心眼冒爱心掏出手机自拍的时候进入会话状态,右手会停在手机状态且一直冒爱心。"
8
+
9
+ 原来相位**只接管它点名的槽位**,其余槽位继续用你自己的选择 —— 于是那套"手机 + 爱心"
10
+ 在整场会话里一直挂着。现在相位一开始,宠物换相位那一套样子:
11
+
12
+ - 相位点名的槽位用它抽到的;
13
+ - 相位**没点名**的槽位**一律让位**(收回);挂在槽位上的定格动作(掏出手机)也跟着
14
+ 交还身体,手放下来 —— 手机停着不放是有两个原因叠在一起:槽位没让位,而且
15
+ "替别的槽位保姿势"那份名单相位走不到,一直每帧把手机姿势写回去;
16
+ - **装扮槽永远归你**(眼镜 / 发饰 / 魔爪 / 巴菲 / 桌布 / 手机换色),相位不碰;
17
+ - 会话进行中手点某个槽位会**临时盖过**相位的那一格(以前点了没反应,因为相位压着它);
18
+ - 会话结束后你原来的样子回来 —— 让位不是删除。
19
+
20
+ ### 右键面板的「装扮」跟着相位走
21
+
22
+ 用户报的:"会话的状态没有在右键菜单的装扮里同步 button。"
23
+
24
+ 面板原来读的是**你自己的选择**,相位接管后画面上是相位的样子、面板还显示你上次手选的。
25
+ 现在两边读同一个"此刻显示什么"(`effectiveSlotChoice`):相位点名用相位的,会话中没点名
26
+ 的整格空着,装扮槽永远显示你自己的。
27
+
3
28
  ## 2.0.2
4
29
 
5
30
  **只改文档**(宠物自带的那份 README,`pets/ds-whale-girl/README.md`)。它落后得比主 README
package/README.md CHANGED
@@ -85,6 +85,17 @@ dsh plugin --profile web add "link:./dsh-pet-live2d/dsh-live2d-pet"
85
85
  改动**只在真的编辑时才落盘**,所以"看一眼"不会留下覆盖 —— 以后宠物更新了默认值,
86
86
  没改过的相位照样吃得到。
87
87
 
88
+ **相位是"接管",不是"叠加"**:会话一开始,宠物换相位那一套样子 ——
89
+
90
+ - 相位**点名**的槽位用它抽到的(`thinking` 就是头顶鲸 + 画笔);
91
+ - 相位**没点名**的槽位**一律让位**(收回):你手选的掏出手机、爱心眼、自拍不会在整场
92
+ 会话里一直挂着。挂在槽位上的动作(掏出手机是定格动作)也会跟着交还身体,手放下来;
93
+ - **装扮槽永远归你**:眼镜 / 发饰 / 魔爪 / 巴菲 / 桌布 / 手机换色 会话不碰;
94
+ - 会话进行中想改也能改:手点一下会**临时盖过**相位的那一格(这场会话里有效);
95
+ - 会话**结束**后,你原来的样子回来(让位不是删除)。
96
+
97
+ 右键面板的「装扮」页签跟着相位走 —— 会话中它显示的是**相位抽到的**,不是你上次手选的。
98
+
88
99
  ### 关系:同时(pairs)与前提(requires)
89
100
 
90
101
  条目下面缩进那一层是**关系**,两种刻意用不同前缀:
package/lib/client.js CHANGED
@@ -3248,7 +3248,18 @@ window.__ModuleLoader__.load({ id: "dsh-pet-live2d", factory: (require) => {
3248
3248
  // 相位池和摸鱼池是同一套抽签,所以诊断也照抄摸鱼的形状:`phaseNow` 立刻重抽
3249
3249
  // 一次(不用等真的相位切换),`phaseTally` 给出每个相位各抽中了什么。
3250
3250
  // 没有这两个的话,"池子里是随机的"就只能靠反复推 SSE 再数,慢且会抖。
3251
- api.phaseNow = (phase) => { if (typeof phase === "string") flushPhaseRef.current?.(phase); };
3251
+ /**
3252
+ * 诊断:直接应用一个相位(测试用)。
3253
+ *
3254
+ * **连状态一起设**,跟真实 SSE 路径一致:只调 applyPhase 的话 `data-phase` 还是
3255
+ * 旧值、React 也不会重渲染 —— 面板跟着相位走这件事就测不出来(而且测出来的行为
3256
+ * 跟真实路径不一样)。
3257
+ */
3258
+ api.phaseNow = (phase) => {
3259
+ if (typeof phase !== "string") return;
3260
+ setPhaseState(phase);
3261
+ flushPhaseRef.current?.(phase);
3262
+ };
3252
3263
  api.phaseTally = () => phaseTallyRef.current;
3253
3264
  api.resetPhaseTally = () => { phaseTallyRef.current = {}; };
3254
3265
  // 诊断:**有效关系**(pet.json + 用户覆盖合并后的结果)与存档里的覆盖。
@@ -3809,6 +3820,114 @@ window.__ModuleLoader__.load({ id: "dsh-pet-live2d", factory: (require) => {
3809
3820
  motion.current.playOnce(group, index, { kind: "panel" });
3810
3821
  }, []);
3811
3822
 
3823
+ /**
3824
+ * 一个槽位**此刻**显示的是什么 —— 面板高亮、配对、动作守卫三处都读它。
3825
+ *
3826
+ * 三段:
3827
+ * 1. **装扮槽**永远归用户:眼镜/发饰/魔爪/巴菲/桌布/手机换色是"穿在身上的",
3828
+ * 会话相位不碰它们(用户明确要求),哪怕相位的池子里点了名;
3829
+ * 2. 相位**点名**了这个槽位 → 用相位的选择(可能是 null = 相位说这里空着);
3830
+ * 3. 会话进行中、而相位没点名 → **空**:相位是"接管",没点名的槽位也要让位。
3831
+ * 以前这里退回用户的选择,于是"爱心眼 + 冒爱心 + 掏出手机"在整场会话里一直
3832
+ * 挂着(用户报的"会话时没有把插槽重置");
3833
+ * 4. 其余 → 用户自己的选择。
3834
+ */
3835
+ const effectiveSlotChoice = (slotId) => {
3836
+ if (OUTFIT_SLOTS.indexOf(slotId) !== -1) return slotSelectionsRef.current[slotId];
3837
+ if (Object.prototype.hasOwnProperty.call(phaseChoicesRef.current, slotId)) {
3838
+ return phaseChoicesRef.current[slotId];
3839
+ }
3840
+ if (phaseRef.current !== "idle") return undefined;
3841
+ return slotSelectionsRef.current[slotId];
3842
+ };
3843
+
3844
+ /** 某个槽位此刻挂着哪个动作组(读**有效选择**)。 */
3845
+ const motionGroupOfSlot = (slotId) => {
3846
+ const label = effectiveSlotChoice(slotId);
3847
+ if (label === undefined || label === null) return null;
3848
+ const picked = (petRef.current?.expressionSlots ?? [])
3849
+ .find((slot) => slot.id === slotId)?.options.find((o) => o.label === label);
3850
+ return typeof picked?.motion === "string" ? picked.motion : null;
3851
+ };
3852
+
3853
+ /**
3854
+ * 身体此刻该播哪个动作 —— 按**有效选择**扫一遍槽位。
3855
+ *
3856
+ * "最后点的那个槽位"优先(`motionOwnerRef`),它不再是动作选项时回退到"扫描全部槽位
3857
+ * 取第一个带 motion 的"。只用扫描会在"右手拿着手机时点吹泡泡糖"时把后者静默忽略
3858
+ * (右手在清单里排在嘴部之前),只用记忆会在槽位被换掉后留下陈旧值。
3859
+ */
3860
+ const desiredSlotMotion = () => {
3861
+ const owner = motionOwnerRef.current;
3862
+ if (owner !== null) {
3863
+ const owned = motionGroupOfSlot(owner);
3864
+ if (owned !== null) return owned;
3865
+ }
3866
+ for (const other of petRef.current?.expressionSlots ?? []) {
3867
+ const found = motionGroupOfSlot(other.id);
3868
+ if (found !== null) return found;
3869
+ }
3870
+ return null;
3871
+ };
3872
+
3873
+ /**
3874
+ * 重算"替哪些*别的*槽位保姿势"。
3875
+ *
3876
+ * 身体只有一个动作,但姿势可以同时存在(掏出手机 + 吹泡泡糖,两组参数不相交),
3877
+ * 所以除了当前演的那个,其余还挂着动作的槽位要把最后一帧写回去。
3878
+ *
3879
+ * **相位接管后也要重算**:那时候用户的槽位动作已经让位,姿势就不该再保着 —— 不然
3880
+ * 会一直举着手机(用户报的"进入会话状态右手会停在手机状态",根因就是这份名单只在
3881
+ * `chooseSlotOption` 里更新,相位走不到)。
3882
+ */
3883
+ const syncKeptPoses = (playing) => {
3884
+ const keepGroups = [];
3885
+ for (const other of petRef.current?.expressionSlots ?? []) {
3886
+ const found = motionGroupOfSlot(other.id);
3887
+ if (found !== null && found !== playing && keepGroups.indexOf(found) === -1) keepGroups.push(found);
3888
+ }
3889
+ motion.current.setKeptPoses(keepGroups);
3890
+ };
3891
+
3892
+ /**
3893
+ * 把身体切到"有效选择要求的那一个动作"。
3894
+ *
3895
+ * **槽位点选和相位接管共用这一个**:相位接管时用户的槽位让位,挂在槽位上的
3896
+ * `hold + persist` 动作(掏出手机/吹泡泡糖/自拍)必须跟着交还身体 —— 不然手会一直
3897
+ * 举着手机(用户报的"进入会话状态右手会停在手机状态")。相位接管以前不走这条,
3898
+ * 于是那只手谁也放不下来。
3899
+ */
3900
+ const syncSlotMotion = () => {
3901
+ const desired = desiredSlotMotion();
3902
+ const previous = slotMotionRef.current;
3903
+ slotMotionRef.current = desired;
3904
+ // 只在"该播的动作真的换了"时才播 —— 原来还有个 `|| option !== null`,
3905
+ // 意思是点任何表情都顺手把当前动作重播一遍。它会**重新快照**,而这时
3906
+ // 动作早就在最后一帧停着了:掏出手机之后点爱心眼,快照里的 phone 记的就是
3907
+ // 1(手机已在手里),于是"还原"忠实地把手机举着不放。
3908
+ // 用户报的"掏出手机切不到其他状态"就是这个。
3909
+ if (desired !== null && desired !== previous) {
3910
+ motion.current.playOnce(desired, 0, { kind: "slot", hold: true, persist: true });
3911
+ } else if (desired === null && previous !== null) {
3912
+ // The slot gave up its motion: hand the body back. Other slots' pins
3913
+ // are untouched, so their look survives.
3914
+ motion.current.playIdle();
3915
+ }
3916
+ };
3917
+
3918
+ /**
3919
+ * 会话进行中,手点(或配对带出)某个槽位时,**同时**改掉相位的这一格。
3920
+ *
3921
+ * 相位接管着这个槽位,只改用户的选择是看不见的 —— 用户会以为"点了没反应"。
3922
+ * 覆盖只在这一场会话里有效:下一条相位消息来了就重新抽(`applyPhase` 从空开始)。
3923
+ * 装扮槽不参与(它永远归用户,相位本来也压不过它)。
3924
+ */
3925
+ const markPhaseOverride = (slotId, label) => {
3926
+ if (phaseRef.current === "idle") return;
3927
+ if (OUTFIT_SLOTS.indexOf(slotId) !== -1) return;
3928
+ phaseChoicesRef.current[slotId] = label === undefined ? null : label;
3929
+ };
3930
+
3812
3931
  /**
3813
3932
  * 「同时」(pairs) 的**不变量**:把当前选择里所有配对的目标表达式补进来。
3814
3933
  *
@@ -3819,14 +3938,12 @@ window.__ModuleLoader__.load({ id: "dsh-pet-live2d", factory: (require) => {
3819
3938
  * 放在这里而不是 commitPins:**这是所有路径都经过的那个漏斗**(面板点选、摸鱼抽中、
3820
3939
  * 相位切换、归位、换宠物),只有一处的规则才不会走岔。
3821
3940
  *
3822
- * 源头取**有效选择**:相位正在接管某个槽位时以相位为准,否则相位演别的表情时
3823
- * 还会硬把配对目标钉在脸上(相位结束后自动回来)。
3941
+ * 源头取**有效选择**(`effectiveSlotChoice`):相位接管中以相位为准。
3824
3942
  */
3825
3943
  const pairPinsNow = () => {
3826
3944
  const pins = {};
3827
3945
  for (const slotId of slotByIdRef.current.keys()) {
3828
- const owns = Object.prototype.hasOwnProperty.call(phaseChoicesRef.current, slotId);
3829
- const label = owns ? phaseChoicesRef.current[slotId] : slotSelectionsRef.current[slotId];
3946
+ const label = effectiveSlotChoice(slotId);
3830
3947
  if (label === null || label === undefined) continue;
3831
3948
  for (const [targetSlot, targetLabel] of Object.entries(relationsOf(slotId, label).pairs)) {
3832
3949
  const target = slotByIdRef.current.get(targetSlot)?.options.find((o) => o.label === targetLabel);
@@ -4038,6 +4155,7 @@ window.__ModuleLoader__.load({ id: "dsh-pet-live2d", factory: (require) => {
4038
4155
  if (wanted) chosen[slotId] = label;
4039
4156
  else delete chosen[slotId];
4040
4157
  slotSelectionsRef.current = chosen;
4158
+ markPhaseOverride(slotId, wanted ? label : null);
4041
4159
  saveOutfit();
4042
4160
  if (!wanted && typeof target.options.find((o) => o.label === label)?.motion === "string") {
4043
4161
  slotMotionRef.current = null;
@@ -4111,6 +4229,9 @@ window.__ModuleLoader__.load({ id: "dsh-pet-live2d", factory: (require) => {
4111
4229
  if (option === null) delete chosen[slot.id];
4112
4230
  else chosen[slot.id] = option.label;
4113
4231
  slotSelectionsRef.current = chosen;
4232
+ // 会话进行中点的:相位正接管着这个槽位,只改用户选择是**看不见的** —— 同时把
4233
+ // 相位的这一格也改掉(只在这一场会话里有效,下一条相位消息来了就重抽)。
4234
+ markPhaseOverride(slot.id, option === null ? null : option.label);
4114
4235
  applyExpressions(next);
4115
4236
  // A motion attached to a slot plays and PARKS on its last frame, so the
4116
4237
  // chosen look stays put instead of dropping back to the idle loop.
@@ -4125,51 +4246,17 @@ window.__ModuleLoader__.load({ id: "dsh-pet-live2d", factory: (require) => {
4125
4246
  else userSweepRef.current = option.sweep;
4126
4247
  applySweep();
4127
4248
  // 身体只有一个动作。**最后点的那个槽位**优先(见 motionOwnerRef),它不再是动作
4128
- // 选项时回退到"扫描全部槽位取第一个带 motion 的" —— 后者是原来的唯一规则,也正是
4129
- // "右手拿着手机时点吹泡泡糖被静默忽略"的原因(右手在清单里排在嘴部之前)。
4130
- const motionOf = (slotId) => {
4131
- const label = slotSelectionsRef.current[slotId];
4132
- if (label === undefined) return null;
4133
- const picked = (petRef.current?.expressionSlots ?? [])
4134
- .find((slot) => slot.id === slotId)?.options.find((o) => o.label === label);
4135
- return typeof picked?.motion === "string" ? picked.motion : null;
4136
- };
4249
+ // 选项时回退到扫描 —— 规则收在 `desiredSlotMotion()` 里,相位接管走的是同一个。
4250
+ //
4137
4251
  // 先把归属改掉,再算 desired:否则"刚点的这个"要等下一次点击才生效。
4138
4252
  if (option !== null && typeof option.motion === "string") motionOwnerRef.current = slot.id;
4139
4253
  else if (motionOwnerRef.current === slot.id) motionOwnerRef.current = null;
4140
- let desired = motionOwnerRef.current === null ? null : motionOf(motionOwnerRef.current);
4141
- if (desired === null) {
4142
- for (const other of petRef.current?.expressionSlots ?? []) {
4143
- const found = motionOf(other.id);
4144
- if (found !== null) {
4145
- desired = found;
4146
- break;
4147
- }
4148
- }
4149
- }
4254
+ const desired = desiredSlotMotion();
4150
4255
  // 身体只有一个动作,但**姿势可以同时存在**:别的槽位还选着动作时,替它们把最后
4151
4256
  // 一帧的姿势写回去(掏出手机 + 吹泡泡糖,两组参数不相交)。清掉那个槽位就等于把
4152
4257
  // 它从名单里去掉,它写过的手会交还出去。
4153
- const keepGroups = [];
4154
- for (const other of petRef.current?.expressionSlots ?? []) {
4155
- const found = motionOf(other.id);
4156
- if (found !== null && found !== desired && keepGroups.indexOf(found) === -1) keepGroups.push(found);
4157
- }
4158
- motion.current.setKeptPoses(keepGroups);
4159
- const previous = slotMotionRef.current;
4160
- slotMotionRef.current = desired;
4161
- // 只在"该播的动作真的换了"时才播 —— 原来还有个 `|| option !== null`,
4162
- // 意思是点任何表情都顺手把当前动作重播一遍。它会**重新快照**,而这时
4163
- // 动作早就在最后一帧停着了:掏出手机之后点爱心眼,快照里的 phone 记的就是
4164
- // 1(手机已在手里),于是"还原"忠实地把手机举着不放。
4165
- // 用户报的"掏出手机切不到其他状态"就是这个。
4166
- if (desired !== null && desired !== previous) {
4167
- motion.current.playOnce(desired, 0, { kind: "slot", hold: true, persist: true });
4168
- } else if (desired === null && previous !== null) {
4169
- // The slot gave up its motion: hand the body back. Other slots' pins
4170
- // are untouched, so their look survives.
4171
- motion.current.playIdle();
4172
- }
4258
+ syncKeptPoses(desired);
4259
+ syncSlotMotion();
4173
4260
  // A dress-up choice PERSISTS. The auto-clear exists so a reaction or a
4174
4261
  // session phase cannot leave the pet stuck, but an outfit is an explicit
4175
4262
  // choice the user reverses from this panel (or with 归位), and expiring it
@@ -4256,6 +4343,10 @@ window.__ModuleLoader__.load({ id: "dsh-pet-live2d", factory: (require) => {
4256
4343
  * 三轮不动就收手。
4257
4344
  */
4258
4345
  const applyPhase = (phase) => {
4346
+ // 相位名记在这里(而不是只记在 SSE 回调里):`effectiveSlotChoice` 靠它判断
4347
+ // "会话进行中",而诊断读口 `phaseNow()` 是直接调这个函数的 —— 分开写的话,
4348
+ // 诊断走的相位不会让槽位让位,测出来的行为跟真实路径不一样。
4349
+ phaseRef.current = phase;
4259
4350
  const slotById = new Map((pet?.expressionSlots ?? []).map((slot) => [slot.id, slot]));
4260
4351
  const pools = phasePoolsFor(phase);
4261
4352
  // 从**空**开始,而不是继承上一个相位的答案:相位是接管,不是叠加。
@@ -4367,6 +4458,9 @@ window.__ModuleLoader__.load({ id: "dsh-pet-live2d", factory: (require) => {
4367
4458
  }
4368
4459
  phaseGroupRef.current[phase] = group;
4369
4460
  phasePoolsRev.current = JSON.stringify(pools);
4461
+ // 相位接管后重算"替谁保姿势":用户那套槽位动作已经让位,姿势就不该再保着,
4462
+ // 否则手会一直举着手机。名单过去只在 chooseSlotOption 里更新,相位走不到。
4463
+ syncKeptPoses(group);
4370
4464
  const sustained = PHASE_SUSTAIN.indexOf(phase) !== -1;
4371
4465
  if (phase === "idle" || group === undefined) {
4372
4466
  // No motion for this phase: stop sustaining and return to rest.
@@ -4395,13 +4489,8 @@ window.__ModuleLoader__.load({ id: "dsh-pet-live2d", factory: (require) => {
4395
4489
  // selections, so it stays true while the look keeps the phone out and goes
4396
4490
  // false the moment the slot changes.
4397
4491
  motion.current.setGuardResolver((group) => {
4398
- // 相位"抽空了"这个槽位时不能退回用户的选择:owns 为真且值是 null 就是空。
4399
- const chosenOf = (slotId) => {
4400
- const owns = Object.prototype.hasOwnProperty.call(phaseChoicesRef.current, slotId);
4401
- return owns ? phaseChoicesRef.current[slotId] : slotSelectionsRef.current[slotId];
4402
- };
4403
4492
  const holds = (slotId, labels) => {
4404
- const chosen = chosenOf(slotId);
4493
+ const chosen = effectiveSlotChoice(slotId);
4405
4494
  return chosen !== undefined && chosen !== null && labels.includes(chosen);
4406
4495
  };
4407
4496
  const guard = guardsRef.current[group];
@@ -4937,7 +5026,11 @@ window.__ModuleLoader__.load({ id: "dsh-pet-live2d", factory: (require) => {
4937
5026
  // "every expression is pinned": a motion-only option has an EMPTY
4938
5027
  // expression list, and [].every(...) is vacuously true, so
4939
5028
  // 掏出手机 and 吹泡泡糖 rendered as permanently pressed.
4940
- const chosenLabel = slotSelectionsRef.current[slot.id];
5029
+ //
5030
+ // 读的是**有效选择**(`effectiveSlotChoice`):会话相位接管时,面板要跟着
5031
+ // 显示相位抽到什么,而不是你上一次手选的 —— 以前两边脱节,用户报过
5032
+ // "会话的状态没有在右键菜单的装扮里同步 button"。
5033
+ const chosenLabel = effectiveSlotChoice(slot.id);
4941
5034
  const active = slot.options.find((option) => option.label === chosenLabel);
4942
5035
  return h("div", { "data-group": "", key: slot.id, "data-slot": slot.id },
4943
5036
  h("span", null, slot.label),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-pet-live2d",
3
- "version": "2.0.2",
3
+ "version": "2.1.0",
4
4
  "description": "Live2D 桌宠插件:为 DSH Web GUI 挂一只可拖动、会跟随鼠标、能换动作与表情的 Live2D 宠物。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",