dsh-theme-gallery 0.4.1 → 0.5.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/README.md +106 -9
- package/lib/client.js +3530 -115
- package/lib/mood-lines.json +412 -0
- package/package.json +8 -6
package/lib/client.js
CHANGED
|
@@ -146,6 +146,8 @@ window.__ModuleLoader__.load({
|
|
|
146
146
|
'meng-hai-you-yu': 80,
|
|
147
147
|
'hu-po-mao-mi': 76,
|
|
148
148
|
'hu-zi-a-huang': 75,
|
|
149
|
+
'pet-family': 74,
|
|
150
|
+
'mood-greeting': 73,
|
|
149
151
|
}
|
|
150
152
|
|
|
151
153
|
/**
|
|
@@ -215,6 +217,45 @@ window.__ModuleLoader__.load({
|
|
|
215
217
|
/** 所选配色方案记在这里。与皮肤 id 一样,**绝不**写进 `ui-theme.preference`。 */
|
|
216
218
|
const SCHEME_KEY = 'theme-gallery:palette'
|
|
217
219
|
|
|
220
|
+
/* ═══════════════════ 宠物挂件卡(宠物家族 × 7)═══════════════════
|
|
221
|
+
*
|
|
222
|
+
* 这张卡**不是主题**:它不进 `lib/themes/`、不被 `contribute()` 注册进主题服务、
|
|
223
|
+
* 官方「设置 → 通用 → 外观」不会多出这一项,点它也**不切主题** —— 只开关/挑选挂件。
|
|
224
|
+
* 因此它天然能与任何皮肤(包括山青婷彩 / 梦海游鱼)同时开启,也不消耗主题写入预算。
|
|
225
|
+
* 卡片数据只在面板里合成(见 `petWidgetCardTheme`),挂件本体画在
|
|
226
|
+
* `#dsh-theme-pet` 固定层,锚定对话输入框 —— 不配 `ambient`,不碰侧栏素材区。
|
|
227
|
+
* 二期:卡内是 7 只宠物的头像选择行(`petPickerElement`),点头像换宠物并放它出场。
|
|
228
|
+
*/
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* 挂件卡的 id、标题与 tooltip 文案。
|
|
232
|
+
*
|
|
233
|
+
* 卡片正面**不渲染** description(与「纯色/拼色」卡同一条分支,见 `ThemeCard`):
|
|
234
|
+
* 卡面是标题、状态徽标、当前宠物与它的道具图案,再加一行头像选择,这段文案只作
|
|
235
|
+
* 悬停提示(title)。schema 对主题 JSON 的 description 非空约束管不到它 —— 它
|
|
236
|
+
* 本来就不是主题 JSON。
|
|
237
|
+
*/
|
|
238
|
+
const PET_WIDGET = {
|
|
239
|
+
id: 'pet-family',
|
|
240
|
+
label: '宠物挂件 · 七只小伙伴',
|
|
241
|
+
description: '七只小宠物轮流陪聊:点头像换一只并放它出场,点卡身空白处仅开关。'
|
|
242
|
+
+ '挂件不改变当前主题皮肤;移到宠物身上可改名、戳一戳;拖动宠物或它的道具,'
|
|
243
|
+
+ '它会跑向道具。它还会在你想事情时说悄悄话、在模型生成时为你加油。',
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** 挂件的启停 / 名字 / 位置记在这里。与皮肤 id 同一条铁律:绝不写 `ui-theme.preference`。 */
|
|
247
|
+
const PET_KEY = 'theme-gallery:pet'
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* 宠物的显示缩放。注册表 `size` 是逻辑坐标(与 viewBox 1:1,全部几何以它推),
|
|
251
|
+
* 实机验收反馈整体偏小 —— 屏幕显示统一放大 15%,宠物与道具同缩放。
|
|
252
|
+
* 放大只发生在**消费端**(petDisplaySize / petPropDisplaySize 两个助手):
|
|
253
|
+
* 注册表数据与 viewBox 一个数字都不动(check-pet-registry 的 P3 钉的就是 1:1)。
|
|
254
|
+
* 声明在这里(而非宠物运行区)是因为面板卡 petCardSceneHTML 也要读它 ——
|
|
255
|
+
* TDZ 审计要求 const 声明在第一个读取者之前。
|
|
256
|
+
*/
|
|
257
|
+
const PET_SCALE = 1.15
|
|
258
|
+
|
|
218
259
|
/**
|
|
219
260
|
* 按 id 取一套配色方案。
|
|
220
261
|
* @param id - 方案 id。
|
|
@@ -599,7 +640,7 @@ window.__ModuleLoader__.load({
|
|
|
599
640
|
* package.json, and the release workflow fails when re-running the embed step
|
|
600
641
|
* changes a tracked file.
|
|
601
642
|
*/
|
|
602
|
-
const BUNDLED_VERSION = '0.
|
|
643
|
+
const BUNDLED_VERSION = '0.5.0'
|
|
603
644
|
|
|
604
645
|
/**
|
|
605
646
|
* Themes this package contributes, inlined from lib/themes/*.json.
|
|
@@ -2835,6 +2876,419 @@ window.__ModuleLoader__.load({
|
|
|
2835
2876
|
"source": "蓝 8-2 藏青(深蓝)+ 金银 8-0 赤金 + 精白 + 红 0-0 粉红 + 绿 2-14 翡翠色"
|
|
2836
2877
|
}
|
|
2837
2878
|
]
|
|
2879
|
+
|
|
2880
|
+
const BUNDLED_MOOD_LINES = {
|
|
2881
|
+
"$comment": "心情问候语库(标题栏跑马灯)。出处:40 条种子复制自作者自己的「更新管理平台」站点(装修管理-心情问候语,2026-09-27 抓取,seq 1-40);seq 101+ 为插件新增。分类沿用站点五类:morning 早安 / day 白天 / night 夜深 / general 通用 / holiday 节假日。节日区间为 MM-DD(去年份,每年复用)。红线:禁止负面、消极或敏感言论——embed-themes.mjs 建期黑名单校验,命中即构建失败。单条 text 不超过 100 字符。",
|
|
2882
|
+
"version": 1,
|
|
2883
|
+
"lines": [
|
|
2884
|
+
{
|
|
2885
|
+
"id": "m001",
|
|
2886
|
+
"seq": 1,
|
|
2887
|
+
"category": "morning",
|
|
2888
|
+
"text": "寻必寻见,想必到达"
|
|
2889
|
+
},
|
|
2890
|
+
{
|
|
2891
|
+
"id": "m002",
|
|
2892
|
+
"seq": 2,
|
|
2893
|
+
"category": "morning",
|
|
2894
|
+
"text": "晨光不负赶路人,新的一天从心安开始"
|
|
2895
|
+
},
|
|
2896
|
+
{
|
|
2897
|
+
"id": "m003",
|
|
2898
|
+
"seq": 3,
|
|
2899
|
+
"category": "morning",
|
|
2900
|
+
"text": "今天也要做自己的太阳,无需凭借谁的光"
|
|
2901
|
+
},
|
|
2902
|
+
{
|
|
2903
|
+
"id": "m004",
|
|
2904
|
+
"seq": 4,
|
|
2905
|
+
"category": "morning",
|
|
2906
|
+
"text": "万物皆有裂痕,那是光照进来的地方"
|
|
2907
|
+
},
|
|
2908
|
+
{
|
|
2909
|
+
"id": "m005",
|
|
2910
|
+
"seq": 5,
|
|
2911
|
+
"category": "morning",
|
|
2912
|
+
"text": "早安,愿你所遇皆温柔,所行皆坦途"
|
|
2913
|
+
},
|
|
2914
|
+
{
|
|
2915
|
+
"id": "m006",
|
|
2916
|
+
"seq": 6,
|
|
2917
|
+
"category": "morning",
|
|
2918
|
+
"text": "把昨天的疲惫留在梦里,把今天的期待装进行囊"
|
|
2919
|
+
},
|
|
2920
|
+
{
|
|
2921
|
+
"id": "m007",
|
|
2922
|
+
"seq": 7,
|
|
2923
|
+
"category": "morning",
|
|
2924
|
+
"text": "山高路远,看世界,也找自己"
|
|
2925
|
+
},
|
|
2926
|
+
{
|
|
2927
|
+
"id": "m008",
|
|
2928
|
+
"seq": 8,
|
|
2929
|
+
"category": "morning",
|
|
2930
|
+
"text": "每一个清晨,都是世界温柔的重新开始"
|
|
2931
|
+
},
|
|
2932
|
+
{
|
|
2933
|
+
"id": "m009",
|
|
2934
|
+
"seq": 9,
|
|
2935
|
+
"category": "day",
|
|
2936
|
+
"text": "你对我来说真的很重要"
|
|
2937
|
+
},
|
|
2938
|
+
{
|
|
2939
|
+
"id": "m010",
|
|
2940
|
+
"seq": 10,
|
|
2941
|
+
"category": "day",
|
|
2942
|
+
"text": "心若无所求,有风无风皆自由"
|
|
2943
|
+
},
|
|
2944
|
+
{
|
|
2945
|
+
"id": "m011",
|
|
2946
|
+
"seq": 11,
|
|
2947
|
+
"category": "day",
|
|
2948
|
+
"text": "慢慢来,比较快"
|
|
2949
|
+
},
|
|
2950
|
+
{
|
|
2951
|
+
"id": "m012",
|
|
2952
|
+
"seq": 12,
|
|
2953
|
+
"category": "day",
|
|
2954
|
+
"text": "生活的答案,都藏在认真过好的今天里"
|
|
2955
|
+
},
|
|
2956
|
+
{
|
|
2957
|
+
"id": "m013",
|
|
2958
|
+
"seq": 13,
|
|
2959
|
+
"category": "day",
|
|
2960
|
+
"text": "保持热爱,奔赴山海"
|
|
2961
|
+
},
|
|
2962
|
+
{
|
|
2963
|
+
"id": "m014",
|
|
2964
|
+
"seq": 14,
|
|
2965
|
+
"category": "day",
|
|
2966
|
+
"text": "心中有丘壑,眉目作山河"
|
|
2967
|
+
},
|
|
2968
|
+
{
|
|
2969
|
+
"id": "m015",
|
|
2970
|
+
"seq": 15,
|
|
2971
|
+
"category": "day",
|
|
2972
|
+
"text": "世界盛大,欢迎回家"
|
|
2973
|
+
},
|
|
2974
|
+
{
|
|
2975
|
+
"id": "m016",
|
|
2976
|
+
"seq": 16,
|
|
2977
|
+
"category": "day",
|
|
2978
|
+
"text": "别慌,月亮也正在大海的某处迷茫"
|
|
2979
|
+
},
|
|
2980
|
+
{
|
|
2981
|
+
"id": "m017",
|
|
2982
|
+
"seq": 17,
|
|
2983
|
+
"category": "day",
|
|
2984
|
+
"text": "与其追风去,不如等风来;等风来,不如乘风起"
|
|
2985
|
+
},
|
|
2986
|
+
{
|
|
2987
|
+
"id": "m018",
|
|
2988
|
+
"seq": 18,
|
|
2989
|
+
"category": "day",
|
|
2990
|
+
"text": "认真生活的人,生活从不亏待"
|
|
2991
|
+
},
|
|
2992
|
+
{
|
|
2993
|
+
"id": "m019",
|
|
2994
|
+
"seq": 19,
|
|
2995
|
+
"category": "night",
|
|
2996
|
+
"text": "我来到你待过的星空,算不算相逢"
|
|
2997
|
+
},
|
|
2998
|
+
{
|
|
2999
|
+
"id": "m020",
|
|
3000
|
+
"seq": 20,
|
|
3001
|
+
"category": "night",
|
|
3002
|
+
"text": "夜色温柔,愿你好梦"
|
|
3003
|
+
},
|
|
3004
|
+
{
|
|
3005
|
+
"id": "m021",
|
|
3006
|
+
"seq": 21,
|
|
3007
|
+
"category": "night",
|
|
3008
|
+
"text": "晚风吹人醒,万事藏于心"
|
|
3009
|
+
},
|
|
3010
|
+
{
|
|
3011
|
+
"id": "m022",
|
|
3012
|
+
"seq": 22,
|
|
3013
|
+
"category": "night",
|
|
3014
|
+
"text": "星光不问赶路人,时光不负有心人"
|
|
3015
|
+
},
|
|
3016
|
+
{
|
|
3017
|
+
"id": "m023",
|
|
3018
|
+
"seq": 23,
|
|
3019
|
+
"category": "night",
|
|
3020
|
+
"text": "今晚的月色替我说晚安"
|
|
3021
|
+
},
|
|
3022
|
+
{
|
|
3023
|
+
"id": "m024",
|
|
3024
|
+
"seq": 24,
|
|
3025
|
+
"category": "night",
|
|
3026
|
+
"text": "熬过漫长黑夜,才配拥有满天星辰"
|
|
3027
|
+
},
|
|
3028
|
+
{
|
|
3029
|
+
"id": "m025",
|
|
3030
|
+
"seq": 25,
|
|
3031
|
+
"category": "night",
|
|
3032
|
+
"text": "愿所有的疲惫,都被这个夜晚温柔收留"
|
|
3033
|
+
},
|
|
3034
|
+
{
|
|
3035
|
+
"id": "m026",
|
|
3036
|
+
"seq": 26,
|
|
3037
|
+
"category": "night",
|
|
3038
|
+
"text": "深夜的坚持,是明天惊喜的伏笔"
|
|
3039
|
+
},
|
|
3040
|
+
{
|
|
3041
|
+
"id": "m027",
|
|
3042
|
+
"seq": 27,
|
|
3043
|
+
"category": "holiday",
|
|
3044
|
+
"text": "盛世华诞,与国同庆;山河远阔,人间烟火",
|
|
3045
|
+
"holiday": {
|
|
3046
|
+
"from": "10-01",
|
|
3047
|
+
"to": "10-08"
|
|
3048
|
+
}
|
|
3049
|
+
},
|
|
3050
|
+
{
|
|
3051
|
+
"id": "m028",
|
|
3052
|
+
"seq": 28,
|
|
3053
|
+
"category": "holiday",
|
|
3054
|
+
"text": "新年快乐,愿新岁胜旧年,万事尽可期",
|
|
3055
|
+
"holiday": {
|
|
3056
|
+
"from": "01-01",
|
|
3057
|
+
"to": "01-03"
|
|
3058
|
+
}
|
|
3059
|
+
},
|
|
3060
|
+
{
|
|
3061
|
+
"id": "m029",
|
|
3062
|
+
"seq": 29,
|
|
3063
|
+
"category": "holiday",
|
|
3064
|
+
"text": "新春吉祥,灯火可亲,岁岁常安",
|
|
3065
|
+
"holiday": {
|
|
3066
|
+
"from": "02-05",
|
|
3067
|
+
"to": "02-12"
|
|
3068
|
+
}
|
|
3069
|
+
},
|
|
3070
|
+
{
|
|
3071
|
+
"id": "m030",
|
|
3072
|
+
"seq": 30,
|
|
3073
|
+
"category": "holiday",
|
|
3074
|
+
"text": "清明时节,追思故人,也惜眼前春光",
|
|
3075
|
+
"holiday": {
|
|
3076
|
+
"from": "04-04",
|
|
3077
|
+
"to": "04-06"
|
|
3078
|
+
}
|
|
3079
|
+
},
|
|
3080
|
+
{
|
|
3081
|
+
"id": "m031",
|
|
3082
|
+
"seq": 31,
|
|
3083
|
+
"category": "holiday",
|
|
3084
|
+
"text": "劳动节快乐,向每一位认真生活的人致敬",
|
|
3085
|
+
"holiday": {
|
|
3086
|
+
"from": "05-01",
|
|
3087
|
+
"to": "05-05"
|
|
3088
|
+
}
|
|
3089
|
+
},
|
|
3090
|
+
{
|
|
3091
|
+
"id": "m032",
|
|
3092
|
+
"seq": 32,
|
|
3093
|
+
"category": "holiday",
|
|
3094
|
+
"text": "粽叶飘香,端午安康",
|
|
3095
|
+
"holiday": {
|
|
3096
|
+
"from": "06-09",
|
|
3097
|
+
"to": "06-11"
|
|
3098
|
+
}
|
|
3099
|
+
},
|
|
3100
|
+
{
|
|
3101
|
+
"id": "m033",
|
|
3102
|
+
"seq": 33,
|
|
3103
|
+
"category": "holiday",
|
|
3104
|
+
"text": "但愿人长久,千里共婵娟",
|
|
3105
|
+
"holiday": {
|
|
3106
|
+
"from": "09-15",
|
|
3107
|
+
"to": "09-16"
|
|
3108
|
+
}
|
|
3109
|
+
},
|
|
3110
|
+
{
|
|
3111
|
+
"id": "m034",
|
|
3112
|
+
"seq": 34,
|
|
3113
|
+
"category": "holiday",
|
|
3114
|
+
"text": "举国同庆,家和人安,愿岁并谢,与友长兮",
|
|
3115
|
+
"holiday": {
|
|
3116
|
+
"from": "10-01",
|
|
3117
|
+
"to": "10-08"
|
|
3118
|
+
}
|
|
3119
|
+
},
|
|
3120
|
+
{
|
|
3121
|
+
"id": "m035",
|
|
3122
|
+
"seq": 35,
|
|
3123
|
+
"category": "general",
|
|
3124
|
+
"text": "愿你眼中总有光芒,活成你想要的模样"
|
|
3125
|
+
},
|
|
3126
|
+
{
|
|
3127
|
+
"id": "m036",
|
|
3128
|
+
"seq": 36,
|
|
3129
|
+
"category": "general",
|
|
3130
|
+
"text": "万事顺遂,毫无蹉跎"
|
|
3131
|
+
},
|
|
3132
|
+
{
|
|
3133
|
+
"id": "m037",
|
|
3134
|
+
"seq": 37,
|
|
3135
|
+
"category": "general",
|
|
3136
|
+
"text": "所有的美好,都在赶来的路上"
|
|
3137
|
+
},
|
|
3138
|
+
{
|
|
3139
|
+
"id": "m038",
|
|
3140
|
+
"seq": 38,
|
|
3141
|
+
"category": "general",
|
|
3142
|
+
"text": "心里有光,慢一点又何妨"
|
|
3143
|
+
},
|
|
3144
|
+
{
|
|
3145
|
+
"id": "m039",
|
|
3146
|
+
"seq": 39,
|
|
3147
|
+
"category": "general",
|
|
3148
|
+
"text": "平安喜乐,得偿所愿"
|
|
3149
|
+
},
|
|
3150
|
+
{
|
|
3151
|
+
"id": "m040",
|
|
3152
|
+
"seq": 40,
|
|
3153
|
+
"category": "general",
|
|
3154
|
+
"text": "岁月不扰,余生静好"
|
|
3155
|
+
},
|
|
3156
|
+
{
|
|
3157
|
+
"id": "m101",
|
|
3158
|
+
"seq": 101,
|
|
3159
|
+
"category": "morning",
|
|
3160
|
+
"text": "清晨的风,是今天寄来的第一封信"
|
|
3161
|
+
},
|
|
3162
|
+
{
|
|
3163
|
+
"id": "m102",
|
|
3164
|
+
"seq": 102,
|
|
3165
|
+
"category": "morning",
|
|
3166
|
+
"text": "新的一天:把小事做好,把心情调亮"
|
|
3167
|
+
},
|
|
3168
|
+
{
|
|
3169
|
+
"id": "m103",
|
|
3170
|
+
"seq": 103,
|
|
3171
|
+
"category": "morning",
|
|
3172
|
+
"text": "太阳照常升起,你也照常了不起"
|
|
3173
|
+
},
|
|
3174
|
+
{
|
|
3175
|
+
"id": "m104",
|
|
3176
|
+
"seq": 104,
|
|
3177
|
+
"category": "morning",
|
|
3178
|
+
"text": "今天的你,比昨天多一点点勇敢"
|
|
3179
|
+
},
|
|
3180
|
+
{
|
|
3181
|
+
"id": "m105",
|
|
3182
|
+
"seq": 105,
|
|
3183
|
+
"category": "morning",
|
|
3184
|
+
"text": "晨光已就位,慢慢来,别着急"
|
|
3185
|
+
},
|
|
3186
|
+
{
|
|
3187
|
+
"id": "m106",
|
|
3188
|
+
"seq": 106,
|
|
3189
|
+
"category": "day",
|
|
3190
|
+
"text": "慢慢走,路上有花"
|
|
3191
|
+
},
|
|
3192
|
+
{
|
|
3193
|
+
"id": "m107",
|
|
3194
|
+
"seq": 107,
|
|
3195
|
+
"category": "day",
|
|
3196
|
+
"text": "把日子过成自己喜欢的样子"
|
|
3197
|
+
},
|
|
3198
|
+
{
|
|
3199
|
+
"id": "m108",
|
|
3200
|
+
"seq": 108,
|
|
3201
|
+
"category": "day",
|
|
3202
|
+
"text": "今天的风很温柔,很适合认真生活"
|
|
3203
|
+
},
|
|
3204
|
+
{
|
|
3205
|
+
"id": "m109",
|
|
3206
|
+
"seq": 109,
|
|
3207
|
+
"category": "day",
|
|
3208
|
+
"text": "一步一步,都是向上的路"
|
|
3209
|
+
},
|
|
3210
|
+
{
|
|
3211
|
+
"id": "m110",
|
|
3212
|
+
"seq": 110,
|
|
3213
|
+
"category": "day",
|
|
3214
|
+
"text": "保持微笑,好运正在排队"
|
|
3215
|
+
},
|
|
3216
|
+
{
|
|
3217
|
+
"id": "m111",
|
|
3218
|
+
"seq": 111,
|
|
3219
|
+
"category": "night",
|
|
3220
|
+
"text": "夜深了,把烦恼调成静音"
|
|
3221
|
+
},
|
|
3222
|
+
{
|
|
3223
|
+
"id": "m112",
|
|
3224
|
+
"seq": 112,
|
|
3225
|
+
"category": "night",
|
|
3226
|
+
"text": "星星已就位,陪你收工"
|
|
3227
|
+
},
|
|
3228
|
+
{
|
|
3229
|
+
"id": "m113",
|
|
3230
|
+
"seq": 113,
|
|
3231
|
+
"category": "night",
|
|
3232
|
+
"text": "今晚适合早点休息,明天见"
|
|
3233
|
+
},
|
|
3234
|
+
{
|
|
3235
|
+
"id": "m114",
|
|
3236
|
+
"seq": 114,
|
|
3237
|
+
"category": "night",
|
|
3238
|
+
"text": "愿你被月光温柔以待"
|
|
3239
|
+
},
|
|
3240
|
+
{
|
|
3241
|
+
"id": "m115",
|
|
3242
|
+
"seq": 115,
|
|
3243
|
+
"category": "night",
|
|
3244
|
+
"text": "辛苦了,今天也好好完成了"
|
|
3245
|
+
},
|
|
3246
|
+
{
|
|
3247
|
+
"id": "m116",
|
|
3248
|
+
"seq": 116,
|
|
3249
|
+
"category": "general",
|
|
3250
|
+
"text": "慢慢来,一切都会如期而至"
|
|
3251
|
+
},
|
|
3252
|
+
{
|
|
3253
|
+
"id": "m117",
|
|
3254
|
+
"seq": 117,
|
|
3255
|
+
"category": "general",
|
|
3256
|
+
"text": "生活明朗,万物可爱"
|
|
3257
|
+
},
|
|
3258
|
+
{
|
|
3259
|
+
"id": "m118",
|
|
3260
|
+
"seq": 118,
|
|
3261
|
+
"category": "general",
|
|
3262
|
+
"text": "你走的每一步,都算数"
|
|
3263
|
+
},
|
|
3264
|
+
{
|
|
3265
|
+
"id": "m119",
|
|
3266
|
+
"seq": 119,
|
|
3267
|
+
"category": "general",
|
|
3268
|
+
"text": "所求皆如愿,所行化坦途"
|
|
3269
|
+
},
|
|
3270
|
+
{
|
|
3271
|
+
"id": "m120",
|
|
3272
|
+
"seq": 120,
|
|
3273
|
+
"category": "holiday",
|
|
3274
|
+
"text": "致每一位闪闪发光的她,节日快乐",
|
|
3275
|
+
"holiday": {
|
|
3276
|
+
"from": "03-08",
|
|
3277
|
+
"to": "03-08"
|
|
3278
|
+
}
|
|
3279
|
+
},
|
|
3280
|
+
{
|
|
3281
|
+
"id": "m121",
|
|
3282
|
+
"seq": 121,
|
|
3283
|
+
"category": "holiday",
|
|
3284
|
+
"text": "愿你我童心未泯,简单快乐",
|
|
3285
|
+
"holiday": {
|
|
3286
|
+
"from": "06-01",
|
|
3287
|
+
"to": "06-01"
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3290
|
+
]
|
|
3291
|
+
}
|
|
2838
3292
|
PALETTE_SCHEMES = Array.isArray(BUNDLED_PALETTES) ? BUNDLED_PALETTES : []
|
|
2839
3293
|
|
|
2840
3294
|
/**
|
|
@@ -3195,9 +3649,11 @@ window.__ModuleLoader__.load({
|
|
|
3195
3649
|
const zh = {
|
|
3196
3650
|
title: '主题皮肤',
|
|
3197
3651
|
hint: '点一下即切换,选中会记入设置',
|
|
3198
|
-
count: '{count}
|
|
3652
|
+
count: '{count} 款皮肤(另有内置浅色/深色、宠物挂件与心情问候四张卡)',
|
|
3199
3653
|
current: '当前',
|
|
3200
3654
|
applied: '已应用',
|
|
3655
|
+
petOn: '已开启',
|
|
3656
|
+
petOff: '已关闭',
|
|
3201
3657
|
empty: '正在读取官方主题注册表…',
|
|
3202
3658
|
// The diagnostics below the cards are a FEATURE — a live reading of the active skin —
|
|
3203
3659
|
// and a wall of monospace text reads as a fault unless the panel says otherwise.
|
|
@@ -3207,9 +3663,11 @@ window.__ModuleLoader__.load({
|
|
|
3207
3663
|
const en = {
|
|
3208
3664
|
title: 'Theme skins',
|
|
3209
3665
|
hint: 'Click to switch; the choice is remembered',
|
|
3210
|
-
count: '{count} skins (+ built-in light/dark
|
|
3666
|
+
count: '{count} skins (+ built-in light/dark, the pet-widget card and the mood-greeting card)',
|
|
3211
3667
|
current: 'Current',
|
|
3212
3668
|
applied: 'Applied',
|
|
3669
|
+
petOn: 'On',
|
|
3670
|
+
petOff: 'Off',
|
|
3213
3671
|
empty: 'Reading the theme registry…',
|
|
3214
3672
|
diagNote: 'The lines below are intentional diagnostics of the live skin state, '
|
|
3215
3673
|
+ 'not an error report; only lines starting with ⚠ mean the scenery is not working',
|
|
@@ -3308,6 +3766,62 @@ window.__ModuleLoader__.load({
|
|
|
3308
3766
|
'font-size:11px;line-height:1.3;background:var(--dsw-alias-bg-base);',
|
|
3309
3767
|
'color:var(--dsw-alias-label-primary);border:.5px solid var(--dsw-alias-border-l3);',
|
|
3310
3768
|
'box-shadow:0 2px 8px rgba(0,0,0,.18)}',
|
|
3769
|
+
/* ── the PET-FAMILY card (宠物挂件 · 七只小伙伴) ────────────────────────
|
|
3770
|
+
卡面放标题、状态徽标、当前宠物与道具的图案,再加一行 7 颗头像(`petPickerElement`)
|
|
3771
|
+
—— description 只作 tooltip(与配色卡同一条"不渲染 desc"分支)。卡身是 div
|
|
3772
|
+
(内含头像按钮,button 套 button 非法):点卡身 = 开关挂件,点头像 = 换宠物。 */
|
|
3773
|
+
'.tg-pet-card{justify-content:space-between}',
|
|
3774
|
+
'.tg-pet-body{display:flex;flex-direction:column;gap:6px}',
|
|
3775
|
+
'.tg-pet-art{display:flex;align-items:flex-end;gap:14px;padding:6px 2px 2px;min-height:48px}',
|
|
3776
|
+
'.tg-pet-art svg{display:block}',
|
|
3777
|
+
'.tg-petrow{display:flex;gap:6px;flex-wrap:wrap;padding:2px}',
|
|
3778
|
+
'.tg-petswatch{display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;'
|
|
3779
|
+
+ 'padding:2px;border:none;border-radius:9px;cursor:pointer;background:var(--dsw-alias-bg-layer-2)}',
|
|
3780
|
+
'.tg-petswatch:hover{background:var(--dsw-alias-bg-layer-3)}',
|
|
3781
|
+
'.tg-petswatch:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}',
|
|
3782
|
+
'.tg-petswatch[aria-pressed="true"]{background:var(--dsw-alias-state-business-primary);box-shadow:0 0 0 2px var(--dsw-alias-state-business-primary) inset}',
|
|
3783
|
+
'.tg-petswatch svg{display:block}',
|
|
3784
|
+
/* 挂件"关着"的徽标:中性色,与亮着的"已应用"/"已开启"品牌徽标区分开。 */
|
|
3785
|
+
'.tg-badge-off{background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-tertiary)}',
|
|
3786
|
+
/* ── 心情问候卡:胶囊开关 + 三组设置 chips ───────────────────────────
|
|
3787
|
+
卡身是 div(内含按钮),点卡身与点开关等效(toggleMood);
|
|
3788
|
+
chip 点亮态走品牌 token,与头像选择行同一视觉语言。 */
|
|
3789
|
+
'.tg-mood-body{display:flex;flex-direction:column;gap:8px;padding:4px 2px 2px}',
|
|
3790
|
+
'.tg-mood-row{display:flex;align-items:center;gap:6px;flex-wrap:wrap}',
|
|
3791
|
+
'.tg-mood-row-label{flex:none;font-size:11px;color:var(--dsw-alias-label-tertiary)}',
|
|
3792
|
+
/* ── 心情问候卡(用户二轮返工重设计):分段选择器(segmented)替代散排 pill——
|
|
3793
|
+
一组内相连、共享圆角、选中段"浮起",视觉整齐得多;区域四档放不下一行,用 2×2 网格。 */
|
|
3794
|
+
'.tg-mood-body{display:flex;flex-direction:column;gap:10px;padding:4px 2px 2px}',
|
|
3795
|
+
'.tg-mood-row{display:flex;align-items:center;gap:8px}',
|
|
3796
|
+
'.tg-mood-row-label{flex:none;width:26px;font-size:11px;color:var(--dsw-alias-label-tertiary)}',
|
|
3797
|
+
/* 胶囊开关(一轮返工的版本保留):胶囊与文字分离,圆球滑块滑到端点。 */
|
|
3798
|
+
'.tg-mood-switch{display:inline-flex;align-items:center;gap:8px;height:24px;border:none;'
|
|
3799
|
+
+ 'background:none;padding:0;cursor:pointer;font:inherit;font-size:12px;'
|
|
3800
|
+
+ 'color:var(--dsw-alias-label-secondary)}',
|
|
3801
|
+
'.tg-mood-pill{position:relative;flex:none;width:40px;height:22px;border-radius:999px;'
|
|
3802
|
+
+ 'background:var(--dsw-alias-bg-layer-3);box-shadow:inset 0 0 0 1px var(--dsw-alias-border-l3);'
|
|
3803
|
+
+ 'transition:background .18s var(--ds-ease-in-out)}',
|
|
3804
|
+
'.tg-mood-pill::after{content:"";position:absolute;top:2px;left:2px;width:18px;height:18px;'
|
|
3805
|
+
+ 'border-radius:50%;background:var(--dsw-alias-label-primary);'
|
|
3806
|
+
+ 'box-shadow:0 1px 3px rgba(0,0,0,.2);transition:transform .18s var(--ds-ease-in-out)}',
|
|
3807
|
+
'.tg-mood-switch[aria-checked="true"] .tg-mood-pill{background:var(--dsw-alias-state-business-primary);'
|
|
3808
|
+
+ 'box-shadow:none}',
|
|
3809
|
+
'.tg-mood-switch[aria-checked="true"] .tg-mood-pill::after{transform:translateX(18px);background:#fff}',
|
|
3810
|
+
'.tg-mood-switch:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);'
|
|
3811
|
+
+ 'outline-offset:2px;border-radius:12px}',
|
|
3812
|
+
/* 分段选择器:槽用 layer-3,选中段是浮起的"卡片底"小块 + 主文字色。 */
|
|
3813
|
+
'.tg-mood-seg{flex:1;display:inline-flex;gap:2px;background:var(--dsw-alias-bg-layer-3);'
|
|
3814
|
+
+ 'border-radius:9px;padding:2px;box-sizing:border-box}',
|
|
3815
|
+
'.tg-mood-zone{flex:1;display:grid;grid-template-columns:1fr 1fr;gap:2px;'
|
|
3816
|
+
+ 'background:var(--dsw-alias-bg-layer-3);border-radius:9px;padding:2px;box-sizing:border-box}',
|
|
3817
|
+
'.tg-mood-chip{height:20px;padding:0 6px;border:none;border-radius:7px;background:none;'
|
|
3818
|
+
+ 'color:var(--dsw-alias-label-tertiary);font:inherit;font-size:11px;cursor:pointer;'
|
|
3819
|
+
+ 'white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
|
|
3820
|
+
'.tg-mood-seg .tg-mood-chip{flex:1;min-width:0}',
|
|
3821
|
+
'.tg-mood-chip:hover{color:var(--dsw-alias-label-secondary)}',
|
|
3822
|
+
'.tg-mood-chip[aria-pressed="true"]{background:var(--dsw-alias-bg-base);'
|
|
3823
|
+
+ 'color:var(--dsw-alias-label-primary);box-shadow:0 1px 2px rgba(0,0,0,.12)}',
|
|
3824
|
+
'.tg-mood-chip:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}',
|
|
3311
3825
|
].join('')
|
|
3312
3826
|
|
|
3313
3827
|
/**
|
|
@@ -3323,6 +3837,8 @@ window.__ModuleLoader__.load({
|
|
|
3323
3837
|
init: () => ({
|
|
3324
3838
|
ids: [], labels: {}, descriptions: {}, swatches: {}, cardRows: {},
|
|
3325
3839
|
scheme: '', selected: 'system', status: '', revision: -1,
|
|
3840
|
+
petEnabled: false, petKind: 'ban-ban',
|
|
3841
|
+
moodOn: true, moodSpeed: 'medium', moodDirection: 'ltr', moodZone: 'center',
|
|
3326
3842
|
}),
|
|
3327
3843
|
actions: {
|
|
3328
3844
|
sync: (draft, themes, selected, revision) => {
|
|
@@ -3386,6 +3902,40 @@ window.__ModuleLoader__.load({
|
|
|
3386
3902
|
* @param id - the scheme id.
|
|
3387
3903
|
*/
|
|
3388
3904
|
markScheme: (draft, id) => { draft.scheme = String(id) },
|
|
3905
|
+
/**
|
|
3906
|
+
* 记录宠物挂件的开与关,给挂件卡的徽标与 aria-pressed 用。
|
|
3907
|
+
*
|
|
3908
|
+
* 与 `markScheme` 同一条理由:挂件状态不是主题,混进主题列表会让计数口径失真,
|
|
3909
|
+
* 所以单独放一个字段,由 `publish()` 从 localStorage 读出后写进来。
|
|
3910
|
+
* @param draft - store draft。
|
|
3911
|
+
* @param on - 挂件是否已开启。
|
|
3912
|
+
*/
|
|
3913
|
+
markPet: (draft, on) => { draft.petEnabled = on === true },
|
|
3914
|
+
/**
|
|
3915
|
+
* 记住挂件当前是哪只宠物(头像选择行的点亮态与卡面图案都读它)。
|
|
3916
|
+
*
|
|
3917
|
+
* 与 `markPet` 同一条理由:宠物选择不是主题,单独一个字段,由 `publish()`
|
|
3918
|
+
* 从 localStorage 读出后写进来。**测试桩的 action 表必须跟着长**(规则 8:
|
|
3919
|
+
* 桩少一个 action,publish 就抛错、整个挂载被 guard 拦下)。
|
|
3920
|
+
* @param draft - store 草稿。
|
|
3921
|
+
* @param id - 宠物 id(PET_KINDS 之一)。
|
|
3922
|
+
*/
|
|
3923
|
+
markPetKind: (draft, id) => { draft.petKind = String(id) },
|
|
3924
|
+
/**
|
|
3925
|
+
* 记录心情问候的开关与三组设置(速度/方向/区域),给第 14 张卡的
|
|
3926
|
+
* 胶囊开关、chip 点亮态与 aria-pressed 用。
|
|
3927
|
+
*
|
|
3928
|
+
* 与 `markPet` 同一条理由:问候状态不是主题,单独字段,由 `publish()`
|
|
3929
|
+
* 从 localStorage 读出后写进来。**测试桩的 action 表必须跟着长**(规则 8)。
|
|
3930
|
+
* @param draft - store 草稿。
|
|
3931
|
+
* @param state - `readMoodState()` 的消毒结果。
|
|
3932
|
+
*/
|
|
3933
|
+
markMood: (draft, state) => {
|
|
3934
|
+
draft.moodOn = state.on === true
|
|
3935
|
+
draft.moodSpeed = String(state.speed)
|
|
3936
|
+
draft.moodDirection = String(state.direction)
|
|
3937
|
+
draft.moodZone = String(state.zone)
|
|
3938
|
+
},
|
|
3389
3939
|
},
|
|
3390
3940
|
})
|
|
3391
3941
|
}
|
|
@@ -3465,25 +4015,196 @@ window.__ModuleLoader__.load({
|
|
|
3465
4015
|
})
|
|
3466
4016
|
}
|
|
3467
4017
|
|
|
4018
|
+
/**
|
|
4019
|
+
* 挂件卡面上的当前宠物与它的道具(静态装饰,markup 与运行时舞台同源 —— 都出自
|
|
4020
|
+
* PET_KINDS 注册表,结构上不可能漂移)。
|
|
4021
|
+
*
|
|
4022
|
+
* 卡面在面板里由 jsx 渲染,HTML 经 React 的正规注入通道 dangerouslySetInnerHTML
|
|
4023
|
+
* 进来:内容是插件自己的静态注册表字符串,没有任何用户输入,注入面为零。
|
|
4024
|
+
* @param kindId - 当前宠物 id(store 里的 petKind)。
|
|
4025
|
+
* @returns 卡面图案。
|
|
4026
|
+
*/
|
|
4027
|
+
function petCardSceneHTML(kindId) {
|
|
4028
|
+
const kind = petKindById(kindId)
|
|
4029
|
+
// 卡面图案与舞台同源同比例:舞台显示尺寸(已含 PET_SCALE)再缩到 0.72。
|
|
4030
|
+
// 道具缩 0.9 与舞台一致 —— 卡面是舞台的缩影,不是注册表的直译。
|
|
4031
|
+
const scale = 0.72
|
|
4032
|
+
const pet = `<svg viewBox="${kind.viewBox}" width="${Math.round(petDisplaySize(kind).w * scale)}"`
|
|
4033
|
+
+ ` height="${Math.round(petDisplaySize(kind).h * scale)}" aria-hidden="true">${kind.art.side}</svg>`
|
|
4034
|
+
const prop = `<svg viewBox="${kind.propViewBox}" width="${Math.round(petPropDisplaySize(kind).w * 0.9)}"`
|
|
4035
|
+
+ ` height="${Math.round(petPropDisplaySize(kind).h * 0.9)}" aria-hidden="true">${kind.propArt}</svg>`
|
|
4036
|
+
return pet + prop
|
|
4037
|
+
}
|
|
4038
|
+
|
|
4039
|
+
/** 卡面图案的元素形态(排版交给 .tg-pet-art 的 flex)。 */
|
|
4040
|
+
function petCardArt(kindId) {
|
|
4041
|
+
return jsx('span', {
|
|
4042
|
+
className: 'tg-pet-art',
|
|
4043
|
+
dangerouslySetInnerHTML: { __html: petCardSceneHTML(kindId) },
|
|
4044
|
+
})
|
|
4045
|
+
}
|
|
4046
|
+
|
|
4047
|
+
/** 头像素描(选择行里那颗小圆头像)。 */
|
|
4048
|
+
function petFaceHTML(kind, edge) {
|
|
4049
|
+
return `<svg viewBox="${kind.faceViewBox}" width="${edge}" height="${edge}" aria-hidden="true">${kind.faceArt}</svg>`
|
|
4050
|
+
}
|
|
4051
|
+
|
|
4052
|
+
/**
|
|
4053
|
+
* 挂件卡上的头像选择行:每只宠物一颗头(7 个 button,绝不能套进 button 卡身 ——
|
|
4054
|
+
* 所以挂件卡的卡身是 div,与配色卡同一条规则)。
|
|
4055
|
+
* @param selectedKind - 当前宠物 id(点亮那一颗)。
|
|
4056
|
+
* @param onPick - 点头像的回调(参数是宠物 id)。
|
|
4057
|
+
* @returns 选择行元素。
|
|
4058
|
+
*/
|
|
4059
|
+
function petPickerElement(selectedKind, onPick) {
|
|
4060
|
+
return jsx('span', {
|
|
4061
|
+
className: 'tg-petrow',
|
|
4062
|
+
children: PET_KINDS.map((kind, index) => jsx('button', {
|
|
4063
|
+
type: 'button',
|
|
4064
|
+
className: 'tg-petswatch',
|
|
4065
|
+
'aria-pressed': kind.id === selectedKind,
|
|
4066
|
+
title: `${kind.home} · ${kind.species}`,
|
|
4067
|
+
// 与配色格同一条规矩:必须拦冒泡,否则"点了头像"先执行、随后被卡身的
|
|
4068
|
+
// 开关处理器覆盖回去(配色卡踩过的坑,见 schemeSwatch 的注释)。
|
|
4069
|
+
onClick: (event) => {
|
|
4070
|
+
if (event !== undefined && typeof event.stopPropagation === 'function') event.stopPropagation()
|
|
4071
|
+
onPick(kind.id)
|
|
4072
|
+
},
|
|
4073
|
+
dangerouslySetInnerHTML: { __html: petFaceHTML(kind, 26) },
|
|
4074
|
+
}, index)),
|
|
4075
|
+
})
|
|
4076
|
+
}
|
|
4077
|
+
|
|
4078
|
+
/** 方向的两档文案(卡面 chips 与自检行共用)。 */
|
|
4079
|
+
const MOOD_DIR_LABELS = { ltr: '自左至右', rtl: '自右至左' }
|
|
4080
|
+
|
|
4081
|
+
/**
|
|
4082
|
+
* 心情问候卡的卡面控件:胶囊开关(role="switch")+ 三组设置 chips。
|
|
4083
|
+
*
|
|
4084
|
+
* ── 为什么点什么都必须拦冒泡 ─────────────────────────────────────────────
|
|
4085
|
+
*
|
|
4086
|
+
* 卡身自己的 onClick = 开关问候;不拦冒泡的话,"点慢速"会先写速度、随后被
|
|
4087
|
+
* 卡身的开关处理器覆盖成一次 toggle —— 配色卡色格踩过的同一个坑
|
|
4088
|
+
* (schemeSwatch 注释里的原话)。事件参数要容错:jsx 桩调用处理器时不给事件。
|
|
4089
|
+
* @param props - `{ mood, onToggleMood, onSetMood, t }`:消毒后的状态、开关回调、
|
|
4090
|
+
* 设置回调、字典(取 已开启/已关闭 文案)。
|
|
4091
|
+
* @returns 卡面控件元素。
|
|
4092
|
+
*/
|
|
4093
|
+
function moodControlsElement({ mood, onToggleMood, onSetMood, dict }) {
|
|
4094
|
+
const swallowEvent = (event) => {
|
|
4095
|
+
if (event !== undefined && typeof event.stopPropagation === 'function') event.stopPropagation()
|
|
4096
|
+
}
|
|
4097
|
+
const chip = (label, pressed, title, onPick) => jsx('button', {
|
|
4098
|
+
type: 'button',
|
|
4099
|
+
className: 'tg-mood-chip',
|
|
4100
|
+
'aria-pressed': pressed,
|
|
4101
|
+
title,
|
|
4102
|
+
onClick: (event) => { swallowEvent(event); onPick() },
|
|
4103
|
+
children: label,
|
|
4104
|
+
})
|
|
4105
|
+
return jsxs('div', {
|
|
4106
|
+
className: 'tg-mood-body',
|
|
4107
|
+
children: [
|
|
4108
|
+
jsxs('div', {
|
|
4109
|
+
className: 'tg-mood-row',
|
|
4110
|
+
children: [
|
|
4111
|
+
// 胶囊(圆球滑块)+ 状态文字分离 —— 用户返工:去掉「标题栏问候」、
|
|
4112
|
+
// 胶囊两端全半圆、开/关时圆球滑到一侧顶端。
|
|
4113
|
+
jsxs('button', {
|
|
4114
|
+
type: 'button',
|
|
4115
|
+
className: 'tg-mood-switch',
|
|
4116
|
+
role: 'switch',
|
|
4117
|
+
'aria-checked': mood.on,
|
|
4118
|
+
title: '开或关标题栏的滚动问候(与点卡身等效)',
|
|
4119
|
+
onClick: (event) => { swallowEvent(event); onToggleMood() },
|
|
4120
|
+
children: [
|
|
4121
|
+
jsx('span', { className: 'tg-mood-pill', 'aria-hidden': 'true' }),
|
|
4122
|
+
jsx('span', { className: 'tg-mood-state', children: mood.on ? dict('petOn') : dict('petOff') }),
|
|
4123
|
+
],
|
|
4124
|
+
}),
|
|
4125
|
+
],
|
|
4126
|
+
}),
|
|
4127
|
+
jsx('div', {
|
|
4128
|
+
className: 'tg-mood-row',
|
|
4129
|
+
children: [
|
|
4130
|
+
jsx('span', { className: 'tg-mood-row-label', children: '速度' }),
|
|
4131
|
+
jsx('span', {
|
|
4132
|
+
className: 'tg-mood-seg',
|
|
4133
|
+
children: Object.keys(MOOD_SPEEDS).map((id) => chip(
|
|
4134
|
+
MOOD_SPEED_LABELS[id],
|
|
4135
|
+
mood.speed === id,
|
|
4136
|
+
`滚动速度:${MOOD_SPEED_LABELS[id]}`,
|
|
4137
|
+
() => { onSetMood({ speed: id }) },
|
|
4138
|
+
)),
|
|
4139
|
+
}),
|
|
4140
|
+
],
|
|
4141
|
+
}),
|
|
4142
|
+
jsx('div', {
|
|
4143
|
+
className: 'tg-mood-row',
|
|
4144
|
+
children: [
|
|
4145
|
+
jsx('span', { className: 'tg-mood-row-label', children: '方向' }),
|
|
4146
|
+
jsx('span', {
|
|
4147
|
+
className: 'tg-mood-seg',
|
|
4148
|
+
children: Object.keys(MOOD_DIR_LABELS).map((id) => chip(
|
|
4149
|
+
MOOD_DIR_LABELS[id],
|
|
4150
|
+
mood.direction === id,
|
|
4151
|
+
`滚动方向:${MOOD_DIR_LABELS[id]}`,
|
|
4152
|
+
() => { onSetMood({ direction: id }) },
|
|
4153
|
+
)),
|
|
4154
|
+
}),
|
|
4155
|
+
],
|
|
4156
|
+
}),
|
|
4157
|
+
jsx('div', {
|
|
4158
|
+
className: 'tg-mood-row',
|
|
4159
|
+
children: [
|
|
4160
|
+
jsx('span', { className: 'tg-mood-row-label', children: '区域' }),
|
|
4161
|
+
// 四档一行放不下(卡片 ~217px):2×2 网格,选中态与分段选择器同一语言。
|
|
4162
|
+
jsx('span', {
|
|
4163
|
+
className: 'tg-mood-zone',
|
|
4164
|
+
children: Object.keys(MOOD_ZONE_GEOMETRY).map((id) => chip(
|
|
4165
|
+
MOOD_ZONE_LABELS[id],
|
|
4166
|
+
mood.zone === id,
|
|
4167
|
+
`位置区域:${MOOD_ZONE_LABELS[id]}`,
|
|
4168
|
+
() => { onSetMood({ zone: id }) },
|
|
4169
|
+
)),
|
|
4170
|
+
}),
|
|
4171
|
+
],
|
|
4172
|
+
}),
|
|
4173
|
+
],
|
|
4174
|
+
})
|
|
4175
|
+
}
|
|
4176
|
+
|
|
3468
4177
|
/**
|
|
3469
4178
|
* Render one card.
|
|
3470
4179
|
*
|
|
3471
|
-
*
|
|
3472
|
-
* theme that declares `card.rows`),
|
|
3473
|
-
*
|
|
3474
|
-
* (`title`), which is what the schema says the field is for.
|
|
4180
|
+
* Four layouts share the card: the default token strip, the palette picker (a
|
|
4181
|
+
* theme that declares `card.rows`), the pet-widget toggle card, and — for the
|
|
4182
|
+
* picker and the widget — no description body. The description still becomes
|
|
4183
|
+
* the tooltip (`title`), which is what the schema says the field is for.
|
|
4184
|
+
*
|
|
4185
|
+
* The pet-widget card is NOT a theme: clicking it never touches the theme
|
|
4186
|
+
* service, it toggles the widget (`onTogglePet`), and its badge shows the
|
|
4187
|
+
* widget's on/off state instead of "applied". Like the picker flag, the
|
|
4188
|
+
* widget flag is read off the DATA (the id table), never off a rendered
|
|
4189
|
+
* element — the jsx stub returns null while React returns elements, so an
|
|
4190
|
+
* element-identity test silently takes the wrong branch in every test.
|
|
3475
4191
|
*
|
|
3476
4192
|
* A picker card is a `div`, not a `button`: it CONTAINS 15 buttons, and nesting
|
|
3477
4193
|
* buttons inside a button is invalid markup that browsers silently re-parent.
|
|
4194
|
+
* The pet-family card is a `div` for the same reason (7 portrait buttons).
|
|
3478
4195
|
* @param props - id, label, description, swatches, rows, selected, applied,
|
|
3479
|
-
* selectedScheme, onSelect, onPickScheme
|
|
4196
|
+
* selectedScheme, onSelect, onPickScheme, onTogglePet, petEnabled,
|
|
4197
|
+
* petKind, onPickPet, t.
|
|
3480
4198
|
* @returns the card element.
|
|
3481
4199
|
*/
|
|
3482
4200
|
function ThemeCard(props) {
|
|
3483
4201
|
const {
|
|
3484
4202
|
id, label, description, swatches, rows, selected, applied,
|
|
3485
|
-
selectedScheme, onSelect, onPickScheme,
|
|
4203
|
+
selectedScheme, onSelect, onPickScheme, onTogglePet, petEnabled, petKind, onPickPet,
|
|
4204
|
+
mood, onToggleMood, onSetMood, t,
|
|
3486
4205
|
} = props
|
|
4206
|
+
const isWidget = id === PET_WIDGET.id
|
|
4207
|
+
const isMood = id === MOOD_WIDGET.id
|
|
3487
4208
|
// ── WHY THE FLAG, AND NOT `blocks !== null` ──────────────────────────────
|
|
3488
4209
|
//
|
|
3489
4210
|
// Whether this card is a picker has to be read off the DATA, not off the rendered
|
|
@@ -3500,24 +4221,56 @@ window.__ModuleLoader__.load({
|
|
|
3500
4221
|
// the whole panel — the silent-failure shape this project keeps paying for.
|
|
3501
4222
|
const shape = cardRowShape({ card: { rows } }, PALETTE_SCHEMES)
|
|
3502
4223
|
const hasPicker = shape !== undefined
|
|
4224
|
+
// 挂件/问候卡的徽标永远在场(它就是"开关状态"的读数);其余卡保持原样:选中才亮。
|
|
4225
|
+
const badgeText = (isWidget || isMood)
|
|
4226
|
+
? ((isWidget ? petEnabled === true : mood?.on === true) ? t('petOn') : t('petOff'))
|
|
4227
|
+
: applied
|
|
3503
4228
|
const head = jsxs('div', {
|
|
3504
4229
|
className: 'tg-card-top',
|
|
3505
4230
|
children: [
|
|
3506
4231
|
jsx('span', { className: 'tg-name', children: label || id }),
|
|
3507
|
-
selected
|
|
4232
|
+
(selected || isWidget || isMood)
|
|
4233
|
+
? jsx('span', {
|
|
4234
|
+
className: (isWidget && petEnabled !== true) || (isMood && mood?.on !== true)
|
|
4235
|
+
? 'tg-badge tg-badge-off'
|
|
4236
|
+
: 'tg-badge',
|
|
4237
|
+
children: badgeText,
|
|
4238
|
+
})
|
|
4239
|
+
: null,
|
|
3508
4240
|
],
|
|
3509
4241
|
})
|
|
3510
4242
|
const body = hasPicker
|
|
3511
4243
|
? cardPickerElement(shape, selectedScheme, onPickScheme, PALETTE_SCHEMES)
|
|
3512
|
-
:
|
|
3513
|
-
?
|
|
3514
|
-
className: 'tg-
|
|
3515
|
-
children:
|
|
4244
|
+
: isWidget
|
|
4245
|
+
? jsxs('div', {
|
|
4246
|
+
className: 'tg-pet-body',
|
|
4247
|
+
children: [
|
|
4248
|
+
petCardArt(petKind || DEFAULT_PET_KIND.id),
|
|
4249
|
+
petPickerElement(petKind || DEFAULT_PET_KIND.id, onPickPet),
|
|
4250
|
+
],
|
|
3516
4251
|
})
|
|
3517
|
-
:
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
4252
|
+
: isMood
|
|
4253
|
+
? moodControlsElement({
|
|
4254
|
+
mood: {
|
|
4255
|
+
on: mood?.on === true,
|
|
4256
|
+
speed: mood?.speed ?? 'medium',
|
|
4257
|
+
direction: mood?.direction ?? 'ltr',
|
|
4258
|
+
zone: mood?.zone ?? 'center',
|
|
4259
|
+
},
|
|
4260
|
+
onToggleMood,
|
|
4261
|
+
onSetMood,
|
|
4262
|
+
dict: t,
|
|
4263
|
+
})
|
|
4264
|
+
: swatches.length > 0
|
|
4265
|
+
? jsx('span', {
|
|
4266
|
+
className: 'tg-strip',
|
|
4267
|
+
children: swatches.map((colour, index) => jsx('span', { style: { background: colour } }, index)),
|
|
4268
|
+
})
|
|
4269
|
+
: null
|
|
4270
|
+
// The picker fills the card and the widget/mood cards draw their own controls,
|
|
4271
|
+
// so none of them prints body copy; `description` stays the tooltip, which is
|
|
4272
|
+
// what the schema says the field is for.
|
|
4273
|
+
const desc = !hasPicker && !isWidget && !isMood && description
|
|
3521
4274
|
? jsx('span', { className: 'tg-desc', children: description })
|
|
3522
4275
|
: null
|
|
3523
4276
|
if (hasPicker) {
|
|
@@ -3534,6 +4287,30 @@ window.__ModuleLoader__.load({
|
|
|
3534
4287
|
children: [head, body, desc],
|
|
3535
4288
|
})
|
|
3536
4289
|
}
|
|
4290
|
+
if (isWidget) {
|
|
4291
|
+
// 挂件卡是 div 而不是 button:卡里现在装着 7 颗头像按钮,button 套 button 是
|
|
4292
|
+
// 非法标记(浏览器会静默重排),与配色卡同一条规则。点卡身 = 开关挂件,
|
|
4293
|
+
// **绝不**调用 setTheme —— 这是它与任何皮肤同时开启的前提;点头像 = 换宠物
|
|
4294
|
+
// (selectPetWidget,见挂载体)。开关状态由徽标文字(已开启/已关闭)表达,
|
|
4295
|
+
// "选中的是哪只"由头像的 aria-pressed 表达。
|
|
4296
|
+
return jsxs('div', {
|
|
4297
|
+
className: 'tg-card tg-pet-card',
|
|
4298
|
+
onClick: () => { onTogglePet() },
|
|
4299
|
+
title: description || label,
|
|
4300
|
+
children: [head, body, desc],
|
|
4301
|
+
})
|
|
4302
|
+
}
|
|
4303
|
+
if (isMood) {
|
|
4304
|
+
// 心情问候卡与挂件卡同一条规矩:div 卡身(内含胶囊开关与 chips 按钮)、
|
|
4305
|
+
// 点卡身 = 开关问候(toggleMood),**绝不**调用 setTheme;开关状态由徽标与
|
|
4306
|
+
// 胶囊开关的 aria-checked 表达,三组设置的点亮态由各 chip 的 aria-pressed 表达。
|
|
4307
|
+
return jsxs('div', {
|
|
4308
|
+
className: 'tg-card tg-mood-card',
|
|
4309
|
+
onClick: () => { onToggleMood() },
|
|
4310
|
+
title: description || label,
|
|
4311
|
+
children: [head, body, desc],
|
|
4312
|
+
})
|
|
4313
|
+
}
|
|
3537
4314
|
return jsxs('button', {
|
|
3538
4315
|
type: 'button',
|
|
3539
4316
|
className: 'tg-card',
|
|
@@ -6049,7 +6826,7 @@ window.__ModuleLoader__.load({
|
|
|
6049
6826
|
// (「切到内置主题后装饰层保留」) — do not "fix" it by clearing the layer here.
|
|
6050
6827
|
removeAllAmbientSeats()
|
|
6051
6828
|
if (record) noteAmbientAttempt({ kind, column: true, band: '无装饰' })
|
|
6052
|
-
ambientReport = { found: true, kind: '(none)', note: '该主题无装饰' }
|
|
6829
|
+
ambientReport = { found: true, kind: '(none)', note: '该主题无装饰', css: ambientStylesheetReady() }
|
|
6053
6830
|
return
|
|
6054
6831
|
}
|
|
6055
6832
|
|
|
@@ -6569,108 +7346,2545 @@ window.__ModuleLoader__.load({
|
|
|
6569
7346
|
})
|
|
6570
7347
|
}
|
|
6571
7348
|
|
|
6572
|
-
|
|
6573
|
-
* Report whether the accent token layer actually reached the theme snapshot.
|
|
6574
|
-
*
|
|
6575
|
-
* ── WHY THIS READING IS NEEDED ───────────────────────────────────────────
|
|
6576
|
-
*
|
|
6577
|
-
* `syncAccent` stacks three tokens (`button-ghost-active-fill` / `-border` / `-hover`) with
|
|
6578
|
-
* `ctx.theme.overrideTokens`. Whether that layer works had **never been verified**, and one
|
|
6579
|
-
* earlier attempt to verify it used the wrong evidence: the active workspace FOLDER icon
|
|
6580
|
-
* turns the accent colour, which was taken as proof — but that icon is painted by the skin's
|
|
6581
|
-
* own `--dsw-alias-state-business-primary` token and has nothing to do with the layer.
|
|
6582
|
-
*
|
|
6583
|
-
* The service makes this checkable without any guesswork: `composeActive` folds every
|
|
6584
|
-
* override layer into `snapshot.active.tokens` before publishing, so the composed value is
|
|
6585
|
-
* readable straight off `getTheme()`.
|
|
7349
|
+
/* ═══════════════════ 宠物挂件运行时(宠物家族 × 7)═══════════════════
|
|
6586
7350
|
*
|
|
6587
|
-
*
|
|
7351
|
+
* 与侧栏素材(ambient)完全独立的第二层装饰:画在自己的固定层里,锚定**对话输入框**
|
|
7352
|
+
* (阅读态已验证的 composer 定位法),地面线 = 输入框下缘。规则照抄ambient 的教训:
|
|
6588
7353
|
*
|
|
6589
|
-
*
|
|
6590
|
-
*
|
|
7354
|
+
* · 幂等判据(廉价指纹)在**一切 DOM 写入之前**(规则 3);
|
|
7355
|
+
* · 移动循环复用工厂级单帧助手 `step()`(本文件 requestAnimationFrame 只此一处),
|
|
7356
|
+
* 且只在跑动中自续,停下即断 —— 没有任何常驻定时器(闲聊是单个可取消句柄,见后);
|
|
7357
|
+
* · 脚印 / 气泡都有**数量上限**与超时回收,绝不无界增长;
|
|
7358
|
+
* · 不写主题服务:开关、宠物、名字、位置全部存 localStorage,绝不碰 preference;
|
|
7359
|
+
* · 容器 pointer-events:none,只有宠物与它的道具可交互 —— 绝不挡输入框的点击。
|
|
6591
7360
|
*
|
|
6592
|
-
*
|
|
6593
|
-
* palette layer supplies them too — so the token COUNT is 67 either way. An earlier
|
|
6594
|
-
* comment here claimed a count that "does not move" disproves the layer; for these skins
|
|
6595
|
-
* it cannot move, so the claim was wrong and the count is reported for context only;
|
|
6596
|
-
* • 梦海游鱼 declares `#2B74B5` for that token but its accent is `#FFD166`, so reading back
|
|
6597
|
-
* `#FFD166` proves the accent layer composed OVER the palette — the palette alone cannot
|
|
6598
|
-
* produce it;
|
|
6599
|
-
* • 山青婷彩's accent is `#E88BB0` and it declares `#E88BB0` for the same token, so its
|
|
6600
|
-
* reading is identical whether or not the layer is there. That is a property of the SKIN,
|
|
6601
|
-
* not of the layer, and the verdict below says so instead of implying the reading proved
|
|
6602
|
-
* something.
|
|
7361
|
+
* ── 二期(宠物家族)───────────────────────────────────────────────────────
|
|
6603
7362
|
*
|
|
6604
|
-
*
|
|
6605
|
-
*
|
|
6606
|
-
*
|
|
7363
|
+
* "这只宠物长什么样、跑多快、说什么话"全部在下面的 PET_KINDS 注册表里,框架零硬编码:
|
|
7364
|
+
* 美术常量只存 <svg> 内部内容(不含标签本身),由 petSvg/petPropSvg 统一包裹 ——
|
|
7365
|
+
* 舞台、面板卡面、预览页三处共用同一份字符串,结构上不可能漂移。
|
|
7366
|
+
* 活性机制(闲聊 / 打字回头 / 生成中加油)见本区后半(petNoteInteraction 起)。
|
|
6607
7367
|
*/
|
|
6608
|
-
function describeAccentLayer() {
|
|
6609
|
-
try {
|
|
6610
|
-
const theme = ctx?.theme
|
|
6611
|
-
if (theme === undefined) return '激活层[无 theme 服务]'
|
|
6612
|
-
const snapshot = theme.getTheme()
|
|
6613
|
-
const active = snapshot?.active
|
|
6614
|
-
const tokens = active?.tokens ?? {}
|
|
6615
|
-
const TOKEN = '--dsw-alias-button-ghost-active-border'
|
|
6616
|
-
const composed = tokens[TOKEN]
|
|
6617
|
-
const definition = typeof active?.id === 'string' ? bundledTheme(active.id) : undefined
|
|
6618
|
-
const declared = declaredToken(definition, TOKEN)
|
|
6619
|
-
const accent = definition?.accent
|
|
6620
|
-
const layers = snapshot?.overrides === undefined
|
|
6621
|
-
? '(服务未暴露)'
|
|
6622
|
-
: String(snapshot.overrides.size ?? snapshot.overrides.length ?? '?')
|
|
6623
|
-
// Computed, not assumed — see the note above. `不可分辨` is a real answer: it says this
|
|
6624
|
-
// skin's own value coincides with its accent, so this reading proves nothing either way.
|
|
6625
|
-
const verdict = accent === undefined
|
|
6626
|
-
? '非本包皮肤(无法判断)'
|
|
6627
|
-
: accent === declared
|
|
6628
|
-
? '不可分辨(强调色=皮肤自备值)'
|
|
6629
|
-
: composed === accent
|
|
6630
|
-
? '生效(强调色压过自备值)'
|
|
6631
|
-
: composed === declared
|
|
6632
|
-
? '未生效(仍是自备值)'
|
|
6633
|
-
: `未知(既非强调色也非自备值)`
|
|
6634
|
-
return `激活层[id=${active?.id ?? '?'}`
|
|
6635
|
-
+ ` 层数=${layers}`
|
|
6636
|
-
+ ` token数=${Object.keys(tokens).length}(自备同名令牌,不构成判据)`
|
|
6637
|
-
+ ` ${TOKEN.replace('--dsw-alias-', '')}=${composed === undefined ? '(缺失)' : String(composed)}`
|
|
6638
|
-
+ ` 自备=${declared === undefined ? '(无)' : String(declared)}`
|
|
6639
|
-
+ ` 强调色=${accent ?? '(无)'}`
|
|
6640
|
-
+ ` 判据=${verdict}]`
|
|
6641
|
-
} catch (error) {
|
|
6642
|
-
return `激活层[读取失败: ${String(error && error.message ? error.message : error)}]`
|
|
6643
|
-
}
|
|
6644
|
-
}
|
|
6645
7368
|
|
|
7369
|
+
/** 挂件舞台的 id;也是清扫跨热重载残留节点的依据。 */
|
|
7370
|
+
const PET_STAGE_ID = 'dsh-theme-pet'
|
|
7371
|
+
|
|
7372
|
+
/** 跨物种一致的安全预算:脚印枚数 / 脚印寿命 / 同场气泡数。 */
|
|
7373
|
+
const PET_TRAIL_MAX = 14
|
|
7374
|
+
const PET_TRAIL_FADE_MS = 3500
|
|
7375
|
+
const PET_BUBBLE_MAX = 3
|
|
7376
|
+
/** hop 步态:一跳的水平长度与最高抬升(px)。 */
|
|
7377
|
+
const PET_HOP_LEN_PX = 34
|
|
7378
|
+
const PET_HOP_LIFT_PX = 9
|
|
6646
7379
|
/**
|
|
6647
|
-
*
|
|
7380
|
+
* 视口回退时的地面线抬高量(px)。锚点(输入框)在屏上时地面线贴它的下缘;
|
|
7381
|
+
* 主题面板页等 composer 隐藏的场合走视口回退,宠物直接贴窗口底缘会被裁掉一截
|
|
7382
|
+
* (实机验收反馈)—— 抬高一段让整只宠物完整落地。
|
|
7383
|
+
*/
|
|
7384
|
+
const PET_FALLBACK_GROUND_GAP = 48
|
|
7385
|
+
|
|
7386
|
+
/* ── 美术常量(<svg> 内部内容;斑斑是一期资产拆分,形状与色值零改动)────── */
|
|
7387
|
+
|
|
7388
|
+
const PET_BAN_SIDE = ''
|
|
7389
|
+
+ '<path class="dsh-pet-tail" d="M15 36 Q4 31 6.5 21" stroke="#A9744F" stroke-width="5" fill="none" stroke-linecap="round" style="transform-origin:14px 36px"/>'
|
|
7390
|
+
+ '<rect class="dsh-pet-leg dsh-pet-leg-b" x="19" y="41" width="7" height="13" rx="3" fill="#C79A6B" style="transform-origin:22px 42px"/>'
|
|
7391
|
+
+ '<rect class="dsh-pet-leg dsh-pet-leg-a" x="41" y="41" width="7" height="13" rx="3" fill="#C79A6B" style="transform-origin:44px 42px"/>'
|
|
7392
|
+
+ '<ellipse cx="32" cy="35" rx="17.5" ry="13.5" fill="#F5E7CE"/>'
|
|
7393
|
+
+ '<ellipse cx="25" cy="31.5" rx="7" ry="5" fill="#C79A6B" opacity=".9"/>'
|
|
7394
|
+
+ '<rect class="dsh-pet-leg dsh-pet-leg-a" x="25" y="42" width="7" height="13" rx="3" fill="#F5E7CE" style="transform-origin:28px 43px"/>'
|
|
7395
|
+
+ '<rect class="dsh-pet-leg dsh-pet-leg-b" x="37" y="42" width="7" height="13" rx="3" fill="#FBF3E4" style="transform-origin:40px 43px"/>'
|
|
7396
|
+
+ '<circle cx="46" cy="18" r="12.5" fill="#F5E7CE"/>'
|
|
7397
|
+
+ '<circle cx="51.5" cy="9.5" r="5.2" fill="#C79A6B"/>'
|
|
7398
|
+
+ '<ellipse cx="40" cy="10.5" rx="6" ry="8.6" fill="#A9744F" transform="rotate(-20 40 10.5)"/>'
|
|
7399
|
+
+ '<ellipse cx="55" cy="22.5" rx="6.2" ry="5" fill="#FBF3E4"/>'
|
|
7400
|
+
+ '<circle cx="58.6" cy="20.6" r="2.2" fill="#4A372F"/>'
|
|
7401
|
+
+ '<circle cx="49" cy="16" r="1.9" fill="#3B2B23" class="dsh-pet-eye"/>'
|
|
7402
|
+
+ '<path d="M54.5 27 q2.2 2 4.4 .4" stroke="#4A372F" stroke-width="1.2" fill="none" stroke-linecap="round"/>'
|
|
7403
|
+
|
|
7404
|
+
const PET_BAN_FRONT = ''
|
|
7405
|
+
+ '<path d="M50 40 q7 -3 6 -9" stroke="#A9744F" stroke-width="4.6" fill="none" stroke-linecap="round"/>'
|
|
7406
|
+
+ '<ellipse cx="24" cy="12" rx="5.6" ry="9" fill="#A9744F" transform="rotate(-17 24 12)"/>'
|
|
7407
|
+
+ '<ellipse cx="40" cy="12" rx="5.6" ry="9" fill="#A9744F" transform="rotate(17 40 12)"/>'
|
|
7408
|
+
+ '<circle cx="32" cy="19" r="13" fill="#F5E7CE"/>'
|
|
7409
|
+
+ '<circle cx="38.5" cy="10.5" r="5" fill="#C79A6B"/>'
|
|
7410
|
+
+ '<circle cx="27" cy="17.5" r="1.9" fill="#3B2B23" class="dsh-pet-eye"/>'
|
|
7411
|
+
+ '<circle cx="37" cy="17.5" r="1.9" fill="#3B2B23" class="dsh-pet-eye"/>'
|
|
7412
|
+
+ '<ellipse cx="32" cy="24.5" rx="7" ry="5.4" fill="#FBF3E4"/>'
|
|
7413
|
+
+ '<circle cx="32" cy="22.4" r="2.2" fill="#4A372F"/>'
|
|
7414
|
+
+ '<ellipse cx="32" cy="28.4" rx="2.9" ry="2.4" fill="#7C4A3A"/>'
|
|
7415
|
+
+ '<ellipse cx="32" cy="41.5" rx="14" ry="10.5" fill="#F5E7CE"/>'
|
|
7416
|
+
+ '<ellipse cx="32" cy="44" rx="8" ry="6.2" fill="#FBF3E4"/>'
|
|
7417
|
+
+ '<ellipse cx="22.5" cy="38" rx="4.6" ry="3.6" fill="#C79A6B" opacity=".9"/>'
|
|
7418
|
+
+ '<ellipse cx="25" cy="51" rx="4.2" ry="3.1" fill="#FBF3E4"/>'
|
|
7419
|
+
+ '<ellipse cx="39" cy="51" rx="4.2" ry="3.1" fill="#FBF3E4"/>'
|
|
7420
|
+
|
|
7421
|
+
const PET_BAN_PROP = ''
|
|
7422
|
+
+ '<circle cx="13" cy="12" r="9.5" fill="#E06A4E"/>'
|
|
7423
|
+
+ '<path d="M5 8.5 Q13 4 21 8.5" stroke="#B84A33" stroke-width="1.6" fill="none" stroke-linecap="round"/>'
|
|
7424
|
+
+ '<path d="M4.5 14 Q13 10 21.5 14" stroke="#B84A33" stroke-width="1.6" fill="none" stroke-linecap="round"/>'
|
|
7425
|
+
+ '<path d="M8 18.6 Q13 15 18 18.6" stroke="#B84A33" stroke-width="1.4" fill="none" stroke-linecap="round"/>'
|
|
7426
|
+
+ '<path d="M21.6 11 q5 1.4 3.8 6.4" stroke="#B84A33" stroke-width="1.5" fill="none" stroke-linecap="round"/>'
|
|
7427
|
+
+ '<ellipse cx="9.6" cy="7.4" rx="2.6" ry="1.5" fill="#FFFFFF" opacity=".3" transform="rotate(-24 9.6 7.4)"/>'
|
|
7428
|
+
|
|
7429
|
+
const PET_BAN_FACE = ''
|
|
7430
|
+
+ '<ellipse cx="8.5" cy="7.5" rx="3.4" ry="5" fill="#A9744F" transform="rotate(-18 8.5 7.5)"/>'
|
|
7431
|
+
+ '<ellipse cx="19.5" cy="7.5" rx="3.4" ry="5" fill="#A9744F" transform="rotate(18 19.5 7.5)"/>'
|
|
7432
|
+
+ '<circle cx="14" cy="15.5" r="9.6" fill="#F5E7CE"/>'
|
|
7433
|
+
+ '<circle cx="17.5" cy="9.6" r="3.6" fill="#C79A6B"/>'
|
|
7434
|
+
+ '<circle cx="10.4" cy="14.5" r="1.5" fill="#3B2B23" class="dsh-pet-eye"/>'
|
|
7435
|
+
+ '<circle cx="17.6" cy="14.5" r="1.5" fill="#3B2B23" class="dsh-pet-eye"/>'
|
|
7436
|
+
+ '<ellipse cx="14" cy="19" rx="4.6" ry="3.4" fill="#FBF3E4"/>'
|
|
7437
|
+
+ '<circle cx="14" cy="17.8" r="1.6" fill="#4A372F"/>'
|
|
7438
|
+
+ '<path d="M12.6 21.4 q1.4 1.2 2.8 0" stroke="#4A372F" stroke-width="1" fill="none" stroke-linecap="round"/>'
|
|
7439
|
+
|
|
7440
|
+
const PET_BAN_TRAIL = ''
|
|
7441
|
+
+ '<g fill="#8F969C"><ellipse cx="6" cy="7.6" rx="3.1" ry="2.5"/>'
|
|
7442
|
+
+ '<circle cx="2.7" cy="3.7" r="1.35"/><circle cx="5" cy="2.3" r="1.35"/>'
|
|
7443
|
+
+ '<circle cx="7.3" cy="2.3" r="1.35"/><circle cx="9.5" cy="3.8" r="1.35"/></g>'
|
|
7444
|
+
|
|
7445
|
+
const PET_JU_SIDE = ''
|
|
7446
|
+
+ '<path class="dsh-pet-tail" d="M14 36 Q2 28 7 13" stroke="#F2A65A" stroke-width="5" fill="none" stroke-linecap="round" style="transform-origin:14px 36px"/>'
|
|
7447
|
+
+ '<rect class="dsh-pet-leg dsh-pet-leg-b" x="18" y="40" width="7" height="12" rx="3" fill="#E0A060" style="transform-origin:21px 41px"/>'
|
|
7448
|
+
+ '<rect class="dsh-pet-leg dsh-pet-leg-a" x="40" y="40" width="7" height="12" rx="3" fill="#F2A65A" style="transform-origin:43px 41px"/>'
|
|
7449
|
+
+ '<ellipse cx="29" cy="36" rx="17" ry="12.5" fill="#F2A65A"/>'
|
|
7450
|
+
+ '<path d="M17 34 q7 -9 15 -9" stroke="#D98032" stroke-width="2.2" fill="none" stroke-linecap="round"/>'
|
|
7451
|
+
+ '<path d="M26 28 q7 -7 14 -6" stroke="#D98032" stroke-width="2.2" fill="none" stroke-linecap="round"/>'
|
|
7452
|
+
+ '<path d="M35 27 q6 -5 11 -2" stroke="#D98032" stroke-width="2.2" fill="none" stroke-linecap="round"/>'
|
|
7453
|
+
+ '<ellipse cx="40" cy="39" rx="6.5" ry="5" fill="#FBE8CC"/>'
|
|
7454
|
+
+ '<rect class="dsh-pet-leg dsh-pet-leg-a" x="24" y="41" width="7" height="11" rx="3" fill="#F2A65A" style="transform-origin:27px 42px"/>'
|
|
7455
|
+
+ '<rect class="dsh-pet-leg dsh-pet-leg-b" x="35" y="41" width="7" height="11" rx="3" fill="#F2A65A" style="transform-origin:38px 42px"/>'
|
|
7456
|
+
+ '<ellipse cx="27.5" cy="51.4" rx="3" ry="1.8" fill="#FBE8CC"/>'
|
|
7457
|
+
+ '<ellipse cx="38.5" cy="51.4" rx="3" ry="1.8" fill="#FBE8CC"/>'
|
|
7458
|
+
+ '<circle cx="45" cy="19" r="11" fill="#F2A65A"/>'
|
|
7459
|
+
+ '<path d="M36 13 L33.5 4 L43.5 8 Z" fill="#F2A65A"/>'
|
|
7460
|
+
+ '<path d="M37.4 11.6 L36.2 6.6 L41.4 9 Z" fill="#E79B72"/>'
|
|
7461
|
+
// 后耳:两只耳尖对称朝外上,基边两点都落在头圆上(实机验收"一个耳朵位置
|
|
7462
|
+
// 不对"——旧后耳的基点浮在头缘外 4.6 单位,看起来悬空长错了地方)。
|
|
7463
|
+
+ '<path d="M48.5 9 L55.5 3.5 L54.5 12 Z" fill="#F2A65A"/>'
|
|
7464
|
+
+ '<path d="M50.3 9.6 L54.4 5.6 L53.9 10.7 Z" fill="#E79B72"/>'
|
|
7465
|
+
+ '<path d="M42 8.5 l1.6 3.6" stroke="#D98032" stroke-width="1.6" fill="none" stroke-linecap="round"/>'
|
|
7466
|
+
+ '<path d="M46 8 l.6 3.8" stroke="#D98032" stroke-width="1.6" fill="none" stroke-linecap="round"/>'
|
|
7467
|
+
+ '<path d="M49.6 8.8 l-.6 3.4" stroke="#D98032" stroke-width="1.6" fill="none" stroke-linecap="round"/>'
|
|
7468
|
+
+ '<circle cx="49.5" cy="16.5" r="1.9" fill="#3F3A36" class="dsh-pet-eye"/>'
|
|
7469
|
+
// 嘴:小鼻三角贴脸前缘 + 从鼻下勾回的嘴弧 + 脸侧两根短须,与正面像同款
|
|
7470
|
+
// (实机验收"嘴巴像是鸟嘴"——旧版是一个凸出脸缘的实心三角,读成喙)。
|
|
7471
|
+
+ '<path d="M54.8 19.9 l2.5 .9 l-1.9 1.9 z" fill="#C96F5E"/>'
|
|
7472
|
+
+ '<path d="M56.4 23.2 q-1.5 1.5 -3.1 .8" stroke="#B98A5F" stroke-width="1" fill="none" stroke-linecap="round"/>'
|
|
7473
|
+
+ '<path d="M51.6 20.6 l5.3 -1.1 M52.1 22.7 l5.3 .5" stroke="#B98A5F" stroke-width=".9" fill="none" stroke-linecap="round"/>'
|
|
7474
|
+
+ '<path d="M18 27 Q26 20.5 36 19" stroke="#F8C48C" stroke-width="2" fill="none" stroke-linecap="round" opacity=".8"/>'
|
|
7475
|
+
|
|
7476
|
+
const PET_JU_FRONT = ''
|
|
7477
|
+
+ '<path d="M45 46 q9 -2 8 -9" stroke="#F2A65A" stroke-width="4.6" fill="none" stroke-linecap="round"/>'
|
|
7478
|
+
+ '<path d="M21 10 L18.5 1.5 L27.5 5.5 Z" fill="#F2A65A"/>'
|
|
7479
|
+
+ '<path d="M22 8.6 L20.6 4.2 L25.8 6.4 Z" fill="#E79B72"/>'
|
|
7480
|
+
+ '<path d="M41 10 L43.5 1.5 L34.5 5.5 Z" fill="#F2A65A"/>'
|
|
7481
|
+
+ '<path d="M40 8.6 L41.4 4.2 L36.2 6.4 Z" fill="#E79B72"/>'
|
|
7482
|
+
+ '<ellipse cx="31" cy="42" rx="13.5" ry="9.5" fill="#F2A65A"/>'
|
|
7483
|
+
+ '<ellipse cx="31" cy="43" rx="8" ry="6.5" fill="#FBE8CC"/>'
|
|
7484
|
+
+ '<path d="M20 40 q-2 4 1 7" stroke="#D98032" stroke-width="1.8" fill="none" stroke-linecap="round"/>'
|
|
7485
|
+
+ '<path d="M42 40 q2 4 -1 7" stroke="#D98032" stroke-width="1.8" fill="none" stroke-linecap="round"/>'
|
|
7486
|
+
+ '<ellipse cx="25" cy="50.6" rx="3.4" ry="2.4" fill="#F2A65A"/>'
|
|
7487
|
+
+ '<ellipse cx="37" cy="50.6" rx="3.4" ry="2.4" fill="#F2A65A"/>'
|
|
7488
|
+
+ '<circle cx="31" cy="17" r="12" fill="#F2A65A"/>'
|
|
7489
|
+
+ '<path d="M27 6.5 l1.2 3.2" stroke="#D98032" stroke-width="1.5" fill="none" stroke-linecap="round"/>'
|
|
7490
|
+
+ '<path d="M31 5.8 l.4 3.4" stroke="#D98032" stroke-width="1.5" fill="none" stroke-linecap="round"/>'
|
|
7491
|
+
+ '<path d="M35 6.5 l-.8 3.2" stroke="#D98032" stroke-width="1.5" fill="none" stroke-linecap="round"/>'
|
|
7492
|
+
+ '<circle cx="26" cy="16" r="2" fill="#3F3A36" class="dsh-pet-eye"/>'
|
|
7493
|
+
+ '<circle cx="36" cy="16" r="2" fill="#3F3A36" class="dsh-pet-eye"/>'
|
|
7494
|
+
+ '<path d="M29.8 20 l2.4 0 l-1.2 1.8 z" fill="#C96F5E"/>'
|
|
7495
|
+
+ '<path d="M31 21.8 q-1.6 2 -3.4 1" stroke="#B98A5F" stroke-width="1" fill="none" stroke-linecap="round"/>'
|
|
7496
|
+
+ '<path d="M31 21.8 q1.6 2 3.4 1" stroke="#B98A5F" stroke-width="1" fill="none" stroke-linecap="round"/>'
|
|
7497
|
+
+ '<path d="M20 18 l-6 -1.6 M20 20.5 l-6 .4 M42 18 l6 -1.6 M42 20.5 l6 .4" stroke="#B98A5F" stroke-width=".9" fill="none" stroke-linecap="round"/>'
|
|
7498
|
+
|
|
7499
|
+
const PET_JU_PROP = ''
|
|
7500
|
+
+ '<path d="M17 9 l5 -3.5 l-1.4 3.5 l1.4 3.5 z" fill="#D99A3F"/>'
|
|
7501
|
+
+ '<ellipse cx="10" cy="9" rx="8" ry="4" fill="#E8B15C"/>'
|
|
7502
|
+
+ '<path d="M7 6.4 q1 2.6 0 5.2" stroke="#D99A3F" stroke-width="1.1" fill="none" stroke-linecap="round"/>'
|
|
7503
|
+
+ '<path d="M11 6 q1 2.8 0 5.6" stroke="#D99A3F" stroke-width="1.1" fill="none" stroke-linecap="round"/>'
|
|
7504
|
+
+ '<circle cx="4.6" cy="8.2" r="1" fill="#5B4632"/>'
|
|
7505
|
+
|
|
7506
|
+
const PET_JU_FACE = ''
|
|
7507
|
+
+ '<path d="M8 9 L5.5 2.5 L13 5.5 Z" fill="#F2A65A"/>'
|
|
7508
|
+
+ '<path d="M8.6 7.9 L7.3 4.5 L11.6 6.2 Z" fill="#E79B72"/>'
|
|
7509
|
+
+ '<path d="M20 9 L22.5 2.5 L15 5.5 Z" fill="#F2A65A"/>'
|
|
7510
|
+
+ '<path d="M19.4 7.9 L20.7 4.5 L16.4 6.2 Z" fill="#E79B72"/>'
|
|
7511
|
+
+ '<circle cx="14" cy="15" r="9.8" fill="#F2A65A"/>'
|
|
7512
|
+
+ '<path d="M11.5 6.8 l.9 2.8 M14 6.4 l.3 3 M16.5 6.8 l-.7 2.8" stroke="#D98032" stroke-width="1.4" fill="none" stroke-linecap="round"/>'
|
|
7513
|
+
+ '<circle cx="10.4" cy="14.5" r="1.5" fill="#3F3A36" class="dsh-pet-eye"/>'
|
|
7514
|
+
+ '<circle cx="17.6" cy="14.5" r="1.5" fill="#3F3A36" class="dsh-pet-eye"/>'
|
|
7515
|
+
+ '<path d="M13 18 l2 0 l-1 1.5 z" fill="#C96F5E"/>'
|
|
7516
|
+
+ '<path d="M14 19.6 q-1.2 1.4 -2.4 .7 M14 19.6 q1.2 1.4 2.4 .7" stroke="#B98A5F" stroke-width=".9" fill="none" stroke-linecap="round"/>'
|
|
7517
|
+
+ '<path d="M6.5 16.5 l-4 -1 M6.8 18.4 l-4 .5 M21.5 16.5 l4 -1 M21.2 18.4 l4 .5" stroke="#B98A5F" stroke-width=".8" fill="none" stroke-linecap="round"/>'
|
|
7518
|
+
|
|
7519
|
+
const PET_JU_TRAIL = ''
|
|
7520
|
+
+ '<g fill="#A98F7A"><ellipse cx="5.5" cy="6.8" rx="2.7" ry="2.1"/>'
|
|
7521
|
+
+ '<circle cx="2.4" cy="3.3" r="1.15"/><circle cx="4.6" cy="2" r="1.15"/>'
|
|
7522
|
+
+ '<circle cx="6.6" cy="2" r="1.15"/><circle cx="8.6" cy="3.4" r="1.15"/></g>'
|
|
7523
|
+
|
|
7524
|
+
const PET_TU_SIDE = ''
|
|
7525
|
+
+ '<circle cx="7.5" cy="36" r="4.5" fill="#FFFFFF"/>'
|
|
7526
|
+
+ '<ellipse cx="20" cy="42" rx="7.5" ry="6" fill="#EFE6D8"/>'
|
|
7527
|
+
+ '<rect class="dsh-pet-leg dsh-pet-leg-a" x="26" y="42" width="5.5" height="10" rx="2.7" fill="#FBF6EF" style="transform-origin:28.7px 43px"/>'
|
|
7528
|
+
+ '<rect class="dsh-pet-leg dsh-pet-leg-b" x="34" y="42" width="5.5" height="10" rx="2.7" fill="#F4EADF" style="transform-origin:36.7px 43px"/>'
|
|
7529
|
+
+ '<ellipse cx="26" cy="38" rx="16" ry="12.5" fill="#FBF6EF"/>'
|
|
7530
|
+
+ '<ellipse cx="28" cy="42" rx="9" ry="6" fill="#F4EADF" opacity=".7"/>'
|
|
7531
|
+
+ '<path class="dsh-pet-ear" d="M37 5 Q30 6 31 16 Q31.5 24 35 24 Q38.5 24 38.5 16 Q38.5 8 37 5 Z" fill="#FBF6EF" style="transform-origin:37px 6px"/>'
|
|
7532
|
+
+ '<path d="M34.6 9.5 Q32.6 11 33 17 Q33.3 21.5 35 21.5 Q36.7 21.5 36.7 16.5 Q36.6 11.5 34.6 9.5 Z" fill="#F2C9C4"/>'
|
|
7533
|
+
+ '<circle cx="40" cy="23" r="10.5" fill="#FBF6EF"/>'
|
|
7534
|
+
+ '<path d="M33 17 Q37 12.5 43 12" stroke="#FFFFFF" stroke-width="2" fill="none" stroke-linecap="round" opacity=".9"/>'
|
|
7535
|
+
+ '<circle cx="44.5" cy="21.5" r="1.8" fill="#4A4038" class="dsh-pet-eye"/>'
|
|
7536
|
+
+ '<path d="M49.5 23.8 l2.4 .6 l-1.7 1.7 z" fill="#EFA8A0"/>'
|
|
7537
|
+
+ '<path d="M50.6 26.2 q-1.2 1.4 -2.8 .8" stroke="#C9A9A0" stroke-width="1" fill="none" stroke-linecap="round"/>'
|
|
7538
|
+
+ '<circle cx="46.5" cy="25" r="1.6" fill="#F6CFC9" opacity=".85"/>'
|
|
7539
|
+
|
|
7540
|
+
const PET_TU_FRONT = ''
|
|
7541
|
+
+ '<path class="dsh-pet-ear" d="M9 6 Q3 8 4.5 17 Q5.6 23.5 9 23 Q12 22.6 11.6 15 Q11.2 8.6 9 6 Z" fill="#FBF6EF" style="transform-origin:9px 7px"/>'
|
|
7542
|
+
+ '<path d="M7.4 10.5 Q5.8 12 6.4 17 Q6.9 21 8.8 20.8 Q10.4 20.6 10.2 15.4 Q10 11.5 7.4 10.5 Z" fill="#F2C9C4"/>'
|
|
7543
|
+
+ '<path class="dsh-pet-ear" d="M49 6 Q55 8 53.5 17 Q52.4 23.5 49 23 Q46 22.6 46.4 15 Q46.8 8.6 49 6 Z" fill="#FBF6EF" style="transform-origin:49px 7px"/>'
|
|
7544
|
+
+ '<path d="M50.6 10.5 Q52.2 12 51.6 17 Q51.1 21 49.2 20.8 Q47.6 20.6 47.8 15.4 Q48 11.5 50.6 10.5 Z" fill="#F2C9C4"/>'
|
|
7545
|
+
+ '<ellipse cx="29" cy="41" rx="12.5" ry="10" fill="#FBF6EF"/>'
|
|
7546
|
+
+ '<ellipse cx="29" cy="42" rx="7.5" ry="6.5" fill="#FFFFFF"/>'
|
|
7547
|
+
+ '<ellipse cx="24.5" cy="36" rx="2.6" ry="2" fill="#F4EADF"/>'
|
|
7548
|
+
+ '<ellipse cx="33.5" cy="36" rx="2.6" ry="2" fill="#F4EADF"/>'
|
|
7549
|
+
+ '<ellipse cx="24" cy="50.6" rx="2.8" ry="2.2" fill="#FBF6EF"/>'
|
|
7550
|
+
+ '<ellipse cx="34" cy="50.6" rx="2.8" ry="2.2" fill="#FBF6EF"/>'
|
|
7551
|
+
+ '<circle cx="29" cy="17" r="11.5" fill="#FBF6EF"/>'
|
|
7552
|
+
+ '<circle cx="24" cy="16" r="1.9" fill="#4A4038" class="dsh-pet-eye"/>'
|
|
7553
|
+
+ '<circle cx="34" cy="16" r="1.9" fill="#4A4038" class="dsh-pet-eye"/>'
|
|
7554
|
+
+ '<path d="M28 20 l2 0 l-1 1.6 z" fill="#EFA8A0"/>'
|
|
7555
|
+
+ '<path d="M29 21.6 q-1.4 1.6 -2.8 .8 M29 21.6 q1.4 1.6 2.8 .8" stroke="#C9A9A0" stroke-width="1" fill="none" stroke-linecap="round"/>'
|
|
7556
|
+
+ '<rect x="27.2" y="22.6" width="1.7" height="2.2" rx=".6" fill="#FFFFFF"/>'
|
|
7557
|
+
+ '<rect x="29.9" y="22.6" width="1.7" height="2.2" rx=".6" fill="#FFFFFF"/>'
|
|
7558
|
+
+ '<circle cx="20.5" cy="19.5" r="1.8" fill="#F6CFC9"/>'
|
|
7559
|
+
+ '<circle cx="37.5" cy="19.5" r="1.8" fill="#F6CFC9"/>'
|
|
7560
|
+
|
|
7561
|
+
const PET_TU_PROP = ''
|
|
7562
|
+
+ '<path d="M12 8 q-2.5 -5 -6 -6 q2 5 6 6 z" fill="#67B35F"/>'
|
|
7563
|
+
+ '<path d="M13 8 q2.5 -5 6 -6 q-2 5 -6 6 z" fill="#4E9B4A"/>'
|
|
7564
|
+
+ '<path d="M12 7 Q16 10 15 15 Q14 20 12 21 Q10 20 9 15 Q8 10 12 7 Z" fill="#EF8A3C"/>'
|
|
7565
|
+
+ '<path d="M10 11.5 q2 1 4 0 M9.7 15 q2.3 1 4.6 0" stroke="#D9702A" stroke-width="1.1" fill="none" stroke-linecap="round"/>'
|
|
7566
|
+
|
|
7567
|
+
const PET_TU_FACE = ''
|
|
7568
|
+
+ '<path d="M6.5 8 Q3.5 12 5 17 Q6 20 8 19 Q9.8 18 9.4 13 Q9 9 6.5 8 Z" fill="#FBF6EF"/>'
|
|
7569
|
+
+ '<path d="M6.9 10.5 Q5.9 13 6.7 16.4 Q7.4 18.2 8.3 17.8 Q9.2 17.4 9 13.6 Q8.8 11 6.9 10.5 Z" fill="#F2C9C4"/>'
|
|
7570
|
+
+ '<path d="M21.5 8 Q24.5 12 23 17 Q22 20 20 19 Q18.2 18 18.6 13 Q19 9 21.5 8 Z" fill="#FBF6EF"/>'
|
|
7571
|
+
+ '<path d="M21.1 10.5 Q22.1 13 21.3 16.4 Q20.6 18.2 19.7 17.8 Q18.8 17.4 19 13.6 Q19.2 11 21.1 10.5 Z" fill="#F2C9C4"/>'
|
|
7572
|
+
+ '<circle cx="14" cy="14" r="9.6" fill="#FBF6EF"/>'
|
|
7573
|
+
+ '<circle cx="10.3" cy="13.5" r="1.5" fill="#4A4038" class="dsh-pet-eye"/>'
|
|
7574
|
+
+ '<circle cx="17.7" cy="13.5" r="1.5" fill="#4A4038" class="dsh-pet-eye"/>'
|
|
7575
|
+
+ '<path d="M13 16.6 l2 0 l-1 1.5 z" fill="#EFA8A0"/>'
|
|
7576
|
+
+ '<rect x="12.8" y="18.4" width="1.2" height="1.7" rx=".5" fill="#FFFFFF"/>'
|
|
7577
|
+
+ '<rect x="14.4" y="18.4" width="1.2" height="1.7" rx=".5" fill="#FFFFFF"/>'
|
|
7578
|
+
+ '<circle cx="7.6" cy="16.4" r="1.5" fill="#F6CFC9"/>'
|
|
7579
|
+
+ '<circle cx="20.4" cy="16.4" r="1.5" fill="#F6CFC9"/>'
|
|
7580
|
+
|
|
7581
|
+
const PET_TU_TRAIL = ''
|
|
7582
|
+
+ '<g fill="#C9BCA8"><circle cx="3" cy="4.5" r="1.3"/><circle cx="7" cy="3.5" r="1.3"/></g>'
|
|
7583
|
+
|
|
7584
|
+
const PET_KE_SIDE = ''
|
|
7585
|
+
+ '<path d="M9.6 44 l-4.5 3 l5.2 .8 z" fill="#26303C"/>'
|
|
7586
|
+
+ '<ellipse cx="20" cy="54.5" rx="4.6" ry="2.4" fill="#F5A623"/>'
|
|
7587
|
+
+ '<ellipse cx="30" cy="54.5" rx="4.6" ry="2.4" fill="#E8961B"/>'
|
|
7588
|
+
+ '<ellipse cx="26" cy="33" rx="17" ry="21" fill="#2E3A48"/>'
|
|
7589
|
+
+ '<ellipse cx="30" cy="38" rx="11.5" ry="14" fill="#FDFDFB"/>'
|
|
7590
|
+
+ '<path class="dsh-pet-wing" d="M12 24 Q6 32 10 42 Q14 40 15.5 31 Q15 25 12 24 Z" fill="#26303C" style="transform-origin:12px 25px"/>'
|
|
7591
|
+
+ '<path d="M18 14 Q24 9.5 31 11" stroke="#3E4E60" stroke-width="2.4" fill="none" stroke-linecap="round" opacity=".9"/>'
|
|
7592
|
+
+ '<circle cx="33.5" cy="22" r="2" fill="#242C36" class="dsh-pet-eye"/>'
|
|
7593
|
+
+ '<circle cx="34.2" cy="21.3" r=".6" fill="#FFFFFF"/>'
|
|
7594
|
+
+ '<path d="M42 24 l7 2.2 l-7 2.6 z" fill="#F5A623"/>'
|
|
7595
|
+
|
|
7596
|
+
const PET_KE_FRONT = ''
|
|
7597
|
+
+ '<ellipse cx="19" cy="55" rx="4.4" ry="2.3" fill="#F5A623"/>'
|
|
7598
|
+
+ '<ellipse cx="33" cy="55" rx="4.4" ry="2.3" fill="#F5A623"/>'
|
|
7599
|
+
+ '<ellipse cx="26" cy="34" rx="17" ry="21" fill="#2E3A48"/>'
|
|
7600
|
+
+ '<ellipse cx="26" cy="39" rx="12" ry="14.5" fill="#FDFDFB"/>'
|
|
7601
|
+
+ '<path class="dsh-pet-wing" d="M9 26 Q3 34 7.5 44 Q12 42 13 32 Q12.4 26 9 26 Z" fill="#26303C" style="transform-origin:9px 27px"/>'
|
|
7602
|
+
+ '<path class="dsh-pet-wing" d="M43 26 Q49 34 44.5 44 Q40 42 39 32 Q39.6 26 43 26 Z" fill="#26303C" style="transform-origin:43px 27px"/>'
|
|
7603
|
+
+ '<path d="M16 13 Q22 8.5 29 10" stroke="#3E4E60" stroke-width="2.2" fill="none" stroke-linecap="round" opacity=".9"/>'
|
|
7604
|
+
+ '<circle cx="20" cy="23" r="2" fill="#242C36" class="dsh-pet-eye"/>'
|
|
7605
|
+
+ '<circle cx="32" cy="23" r="2" fill="#242C36" class="dsh-pet-eye"/>'
|
|
7606
|
+
+ '<circle cx="20.7" cy="22.3" r=".6" fill="#FFFFFF"/>'
|
|
7607
|
+
+ '<circle cx="32.7" cy="22.3" r=".6" fill="#FFFFFF"/>'
|
|
7608
|
+
+ '<path d="M22.5 27.5 l7 0 l-3.5 4.4 z" fill="#F5A623"/>'
|
|
7609
|
+
|
|
7610
|
+
const PET_KE_PROP = ''
|
|
7611
|
+
+ '<path d="M18 7 l4.4 -3 l-1 3 l1 3 z" fill="#C7D6E0"/>'
|
|
7612
|
+
+ '<path d="M2 7 Q8 2.5 15 5.5 Q18.5 7 18.5 7 Q18.5 7 15 8.5 Q8 11.5 2 7 Z" fill="#9FB6C6"/>'
|
|
7613
|
+
+ '<path d="M4 5.8 Q10 2.8 16 5.8" stroke="#7C96A8" stroke-width="1.4" fill="none" stroke-linecap="round"/>'
|
|
7614
|
+
+ '<circle cx="4.8" cy="6.4" r=".9" fill="#33424E"/>'
|
|
7615
|
+
|
|
7616
|
+
const PET_KE_FACE = ''
|
|
7617
|
+
+ '<circle cx="14" cy="15" r="10" fill="#2E3A48"/>'
|
|
7618
|
+
+ '<ellipse cx="9.5" cy="14" rx="4" ry="4.6" fill="#FDFDFB"/>'
|
|
7619
|
+
+ '<ellipse cx="18.5" cy="14" rx="4" ry="4.6" fill="#FDFDFB"/>'
|
|
7620
|
+
+ '<path d="M9 7.5 Q12 5.6 15 6.4" stroke="#3E4E60" stroke-width="1.6" fill="none" stroke-linecap="round" opacity=".9"/>'
|
|
7621
|
+
+ '<circle cx="9.5" cy="14" r="1.5" fill="#242C36" class="dsh-pet-eye"/>'
|
|
7622
|
+
+ '<circle cx="18.5" cy="14" r="1.5" fill="#242C36" class="dsh-pet-eye"/>'
|
|
7623
|
+
+ '<circle cx="10" cy="13.5" r=".5" fill="#FFFFFF"/>'
|
|
7624
|
+
+ '<circle cx="19" cy="13.5" r=".5" fill="#FFFFFF"/>'
|
|
7625
|
+
+ '<path d="M12 18.5 l4 0 l-2 2.6 z" fill="#F5A623"/>'
|
|
7626
|
+
|
|
7627
|
+
const PET_KE_TRAIL = ''
|
|
7628
|
+
+ '<g fill="#8FA3B0"><ellipse cx="6" cy="7.6" rx="3.1" ry="2.5"/>'
|
|
7629
|
+
+ '<circle cx="2.7" cy="3.7" r="1.35"/><circle cx="5" cy="2.3" r="1.35"/>'
|
|
7630
|
+
+ '<circle cx="7.3" cy="2.3" r="1.35"/><circle cx="9.5" cy="3.8" r="1.35"/></g>'
|
|
7631
|
+
|
|
7632
|
+
const PET_SHU_SIDE = ''
|
|
7633
|
+
+ '<circle cx="6.2" cy="26" r="1.6" fill="#E3C9A4"/>'
|
|
7634
|
+
+ '<rect class="dsh-pet-leg dsh-pet-leg-b" x="12" y="31" width="4" height="5.5" rx="2" fill="#D9BE97" style="transform-origin:14px 32px"/>'
|
|
7635
|
+
+ '<rect class="dsh-pet-leg dsh-pet-leg-a" x="22" y="31" width="4" height="5.5" rx="2" fill="#E3C9A4" style="transform-origin:24px 32px"/>'
|
|
7636
|
+
+ '<rect class="dsh-pet-leg dsh-pet-leg-b" x="29" y="31" width="4" height="5.5" rx="2" fill="#E3C9A4" style="transform-origin:31px 32px"/>'
|
|
7637
|
+
+ '<ellipse cx="21" cy="24" rx="16" ry="11" fill="#F0DCC0"/>'
|
|
7638
|
+
+ '<ellipse cx="21" cy="18.5" rx="13" ry="5.5" fill="#E3C9A4" opacity=".8"/>'
|
|
7639
|
+
+ '<ellipse class="dsh-pet-belly" cx="20" cy="28" rx="9" ry="4.5" fill="#F9EDD8" style="transform-origin:20px 28px"/>'
|
|
7640
|
+
+ '<path d="M12 17.5 Q17 13.5 23 13.5" stroke="#F9EDD8" stroke-width="2" fill="none" stroke-linecap="round" opacity=".9"/>'
|
|
7641
|
+
+ '<circle cx="31.5" cy="12.5" r="2.8" fill="#E3C9A4"/>'
|
|
7642
|
+
+ '<circle cx="31.7" cy="12.6" r="1.5" fill="#EFB9A8"/>'
|
|
7643
|
+
+ '<circle cx="29.5" cy="15.5" r="1.7" fill="#3F3630" class="dsh-pet-eye"/>'
|
|
7644
|
+
+ '<ellipse cx="33.5" cy="22" rx="5" ry="4.2" fill="#F6E6CE"/>'
|
|
7645
|
+
+ '<circle cx="38.8" cy="18.5" r="1.1" fill="#E2A69B"/>'
|
|
7646
|
+
+ '<path d="M37.4 20.6 q1.2 1 2.4 .2" stroke="#C89F82" stroke-width=".9" fill="none" stroke-linecap="round"/>'
|
|
7647
|
+
+ '<path d="M36.5 19.5 l5.5 -1 M36.8 20.6 l5.5 .6" stroke="#C9AD8C" stroke-width=".8" fill="none" stroke-linecap="round"/>'
|
|
7648
|
+
|
|
7649
|
+
const PET_SHU_FRONT = ''
|
|
7650
|
+
+ '<ellipse cx="23" cy="26" rx="14" ry="10.5" fill="#F0DCC0"/>'
|
|
7651
|
+
+ '<ellipse cx="23" cy="28" rx="8.5" ry="6.5" fill="#F9EDD8"/>'
|
|
7652
|
+
+ '<ellipse cx="18.5" cy="36" rx="2.6" ry="1.8" fill="#E3C9A4"/>'
|
|
7653
|
+
+ '<ellipse cx="27.5" cy="36" rx="2.6" ry="1.8" fill="#E3C9A4"/>'
|
|
7654
|
+
+ '<circle cx="13.5" cy="19" r="6.5" fill="#F6E6CE"/>'
|
|
7655
|
+
+ '<circle cx="32.5" cy="19" r="6.5" fill="#F6E6CE"/>'
|
|
7656
|
+
+ '<circle cx="23" cy="17" r="9.5" fill="#F0DCC0"/>'
|
|
7657
|
+
+ '<circle cx="16.5" cy="9" r="2.6" fill="#E3C9A4"/>'
|
|
7658
|
+
+ '<circle cx="16.6" cy="9.1" r="1.3" fill="#EFB9A8"/>'
|
|
7659
|
+
+ '<circle cx="29.5" cy="9" r="2.6" fill="#E3C9A4"/>'
|
|
7660
|
+
+ '<circle cx="29.4" cy="9.1" r="1.3" fill="#EFB9A8"/>'
|
|
7661
|
+
+ '<circle cx="19" cy="15.5" r="1.7" fill="#3F3630" class="dsh-pet-eye"/>'
|
|
7662
|
+
+ '<circle cx="27" cy="15.5" r="1.7" fill="#3F3630" class="dsh-pet-eye"/>'
|
|
7663
|
+
+ '<circle cx="23" cy="19.2" r="1.1" fill="#E2A69B"/>'
|
|
7664
|
+
+ '<path d="M23 20.3 q-1.2 1.2 -2.3 .6 M23 20.3 q1.2 1.2 2.3 .6" stroke="#C89F82" stroke-width=".9" fill="none" stroke-linecap="round"/>'
|
|
7665
|
+
+ '<rect x="21.7" y="21.4" width="1.9" height="2.4" rx=".7" fill="#FFFFFF"/>'
|
|
7666
|
+
+ '<rect x="24.4" y="21.4" width="1.9" height="2.4" rx=".7" fill="#FFFFFF"/>'
|
|
7667
|
+
+ '<ellipse cx="19.5" cy="29" rx="2.4" ry="2" fill="#E3C9A4"/>'
|
|
7668
|
+
+ '<ellipse cx="26.5" cy="29" rx="2.4" ry="2" fill="#E3C9A4"/>'
|
|
7669
|
+
+ '<ellipse cx="23" cy="28" rx="2" ry="1.3" fill="#4A4038"/>'
|
|
7670
|
+
+ '<circle cx="24.6" cy="27.6" r=".7" fill="#F7F2E8"/>'
|
|
7671
|
+
|
|
7672
|
+
const PET_SHU_PROP = ''
|
|
7673
|
+
+ '<path d="M3 7 Q7 2.5 13 4.5 Q17 6 17 7 Q17 8 13 9.5 Q7 11.5 3 7 Z" fill="#4A4038"/>'
|
|
7674
|
+
+ '<path d="M7 4.2 l0 5.6 M10.5 3.8 l0 6.2 M14 4.8 l0 4.4" stroke="#2E2A26" stroke-width="1.1" fill="none" stroke-linecap="round"/>'
|
|
7675
|
+
+ '<ellipse cx="16.8" cy="7" rx="1.5" ry="1.1" fill="#F7F2E8"/>'
|
|
7676
|
+
|
|
7677
|
+
const PET_SHU_FACE = ''
|
|
7678
|
+
+ '<circle cx="8.5" cy="8.5" r="2.5" fill="#E3C9A4"/>'
|
|
7679
|
+
+ '<circle cx="8.6" cy="8.6" r="1.2" fill="#EFB9A8"/>'
|
|
7680
|
+
+ '<circle cx="19.5" cy="8.5" r="2.5" fill="#E3C9A4"/>'
|
|
7681
|
+
+ '<circle cx="19.4" cy="8.6" r="1.2" fill="#EFB9A8"/>'
|
|
7682
|
+
+ '<circle cx="14" cy="15.5" r="9.8" fill="#F0DCC0"/>'
|
|
7683
|
+
+ '<circle cx="7.2" cy="18" r="3.8" fill="#F6E6CE"/>'
|
|
7684
|
+
+ '<circle cx="20.8" cy="18" r="3.8" fill="#F6E6CE"/>'
|
|
7685
|
+
+ '<circle cx="10.5" cy="14.5" r="1.5" fill="#3F3630" class="dsh-pet-eye"/>'
|
|
7686
|
+
+ '<circle cx="17.5" cy="14.5" r="1.5" fill="#3F3630" class="dsh-pet-eye"/>'
|
|
7687
|
+
+ '<circle cx="14" cy="17.6" r="1" fill="#E2A69B"/>'
|
|
7688
|
+
+ '<rect x="12.6" y="18.8" width="1.3" height="1.9" rx=".5" fill="#FFFFFF"/>'
|
|
7689
|
+
+ '<rect x="14.2" y="18.8" width="1.3" height="1.9" rx=".5" fill="#FFFFFF"/>'
|
|
7690
|
+
|
|
7691
|
+
const PET_SHU_TRAIL = ''
|
|
7692
|
+
+ '<g fill="#B9A88C"><circle cx="2.5" cy="3.2" r="1.1"/><circle cx="5.6" cy="2.6" r="1.1"/></g>'
|
|
7693
|
+
|
|
7694
|
+
const PET_HU_SIDE = ''
|
|
7695
|
+
+ '<g class="dsh-pet-tail" style="transform-origin:16px 34px">'
|
|
7696
|
+
+ '<path d="M16 34 Q4 26 6.5 14 Q8 8 13 10 Q20 13 21.5 24 Q22 31 16 34 Z" fill="#D9663D"/>'
|
|
7697
|
+
+ '<path d="M7 14 Q8.2 9.6 12 10.4 Q10.8 14.4 8.6 17.4 Q7.2 16 7 14 Z" fill="#FBF3E8"/></g>'
|
|
7698
|
+
+ '<rect class="dsh-pet-leg dsh-pet-leg-b" x="18" y="38" width="6" height="12" rx="3" fill="#8A4A2F" style="transform-origin:21px 39px"/>'
|
|
7699
|
+
+ '<rect class="dsh-pet-leg dsh-pet-leg-a" x="40" y="38" width="6" height="12" rx="3" fill="#8A4A2F" style="transform-origin:43px 39px"/>'
|
|
7700
|
+
+ '<path d="M14 30 Q20 20 34 21 Q46 22 50 30 Q47 37 36 37.5 Q22 38 14 30 Z" fill="#D9663D"/>'
|
|
7701
|
+
+ '<ellipse cx="22" cy="32" rx="6.5" ry="5.5" fill="#C25832" opacity=".9"/>'
|
|
7702
|
+
+ '<ellipse cx="46" cy="31" rx="6" ry="6.5" fill="#FBF3E8"/>'
|
|
7703
|
+
+ '<rect class="dsh-pet-leg dsh-pet-leg-a" x="24" y="38" width="5.5" height="12" rx="2.7" fill="#D9663D" style="transform-origin:26.7px 39px"/>'
|
|
7704
|
+
+ '<rect class="dsh-pet-leg dsh-pet-leg-b" x="35" y="38" width="5.5" height="12" rx="2.7" fill="#D9663D" style="transform-origin:37.7px 39px"/>'
|
|
7705
|
+
+ '<ellipse cx="27" cy="49.6" rx="2.8" ry="1.7" fill="#8A4A2F"/>'
|
|
7706
|
+
+ '<ellipse cx="37.6" cy="49.6" rx="2.8" ry="1.7" fill="#8A4A2F"/>'
|
|
7707
|
+
+ '<path d="M20 26 Q30 20.5 40 22" stroke="#E8845C" stroke-width="2.4" fill="none" stroke-linecap="round" opacity=".9"/>'
|
|
7708
|
+
+ '<path d="M44.5 11.5 L43 2 L51 7.5 Z" fill="#D9663D"/>'
|
|
7709
|
+
+ '<path d="M43 2 L47 5.6 L45.9 8.4 Z" fill="#4A372F"/>'
|
|
7710
|
+
+ '<circle cx="51" cy="17" r="9.5" fill="#D9663D"/>'
|
|
7711
|
+
+ '<path d="M49.5 10 L51 2 L56.5 8.5 Z" fill="#D9663D"/>'
|
|
7712
|
+
+ '<path d="M51 2 L54.8 6.8 L52.9 9 Z" fill="#4A372F"/>'
|
|
7713
|
+
+ '<circle cx="51.5" cy="15.5" r="1.8" fill="#3A2E28" class="dsh-pet-eye"/>'
|
|
7714
|
+
+ '<path d="M56 15 Q63 16.5 63.5 19.5 Q60 22 55.6 21 Z" fill="#FBF3E8"/>'
|
|
7715
|
+
+ '<circle cx="63" cy="18.6" r="1.5" fill="#3A2E28"/>'
|
|
7716
|
+
+ '<path d="M57 20.8 q2.4 1.4 4.6 -.2" stroke="#8A4A2F" stroke-width=".9" fill="none" stroke-linecap="round"/>'
|
|
7717
|
+
|
|
7718
|
+
const PET_HU_FRONT = ''
|
|
7719
|
+
+ '<path d="M52 42 Q62 38 61 28 Q57 30.5 54.5 36 Q53 39.5 52 42 Z" fill="#D9663D"/>'
|
|
7720
|
+
+ '<path d="M61 28 Q58.6 29.6 57 33.4 Q59.6 32.6 61 28 Z" fill="#FBF3E8"/>'
|
|
7721
|
+
+ '<ellipse cx="31" cy="40" rx="13" ry="10" fill="#D9663D"/>'
|
|
7722
|
+
+ '<ellipse cx="31" cy="40" rx="7" ry="7.5" fill="#FBF3E8"/>'
|
|
7723
|
+
+ '<rect x="25.5" y="44" width="5" height="7" rx="2.5" fill="#8A4A2F"/>'
|
|
7724
|
+
+ '<rect x="33" y="44" width="5" height="7" rx="2.5" fill="#8A4A2F"/>'
|
|
7725
|
+
+ '<path d="M22.5 9 L19.5 1 L27.5 5 Z" fill="#D9663D"/>'
|
|
7726
|
+
+ '<path d="M19.5 1 L23.6 4.7 L22.2 8 Z" fill="#4A372F"/>'
|
|
7727
|
+
+ '<path d="M39.5 9 L42.5 1 L34.5 5 Z" fill="#D9663D"/>'
|
|
7728
|
+
+ '<path d="M42.5 1 L38.4 4.7 L39.8 8 Z" fill="#4A372F"/>'
|
|
7729
|
+
+ '<path d="M20.5 14 l-4.5 -1.5 l3 5 z" fill="#D9663D"/>'
|
|
7730
|
+
+ '<path d="M41.5 14 l4.5 -1.5 l-3 5 z" fill="#D9663D"/>'
|
|
7731
|
+
+ '<circle cx="31" cy="16" r="11" fill="#D9663D"/>'
|
|
7732
|
+
+ '<ellipse cx="31" cy="20.5" rx="6" ry="4.6" fill="#FBF3E8"/>'
|
|
7733
|
+
+ '<circle cx="26" cy="15" r="1.9" fill="#3A2E28" class="dsh-pet-eye"/>'
|
|
7734
|
+
+ '<circle cx="36" cy="15" r="1.9" fill="#3A2E28" class="dsh-pet-eye"/>'
|
|
7735
|
+
+ '<ellipse cx="31" cy="18.6" rx="1.5" ry="1.2" fill="#3A2E28"/>'
|
|
7736
|
+
+ '<path d="M31 19.8 q-1.5 1.6 -3 .8 M31 19.8 q1.5 1.6 3 .8" stroke="#8A4A2F" stroke-width=".9" fill="none" stroke-linecap="round"/>'
|
|
7737
|
+
|
|
7738
|
+
const PET_HU_PROP = ''
|
|
7739
|
+
+ '<rect x="9.2" y="0.5" width="1.6" height="3" rx=".8" fill="#5E3D22"/>'
|
|
7740
|
+
+ '<path d="M10 3 Q15.5 7 15 13 Q14.5 19.5 10 21.5 Q5.5 19.5 5 13 Q4.5 7 10 3 Z" fill="#9C6B3F"/>'
|
|
7741
|
+
+ '<path d="M7 8 q3 1.6 6 0 M6.4 12 q3.6 1.8 7.2 0 M6.8 16 q3.2 1.7 6.4 0 M8.4 5 q1.6 1.2 3.2 0" stroke="#7C5230" stroke-width="1.2" fill="none" stroke-linecap="round"/>'
|
|
7742
|
+
|
|
7743
|
+
const PET_HU_FACE = ''
|
|
7744
|
+
+ '<path d="M8 8 L5.5 1 L12 4.5 Z" fill="#D9663D"/>'
|
|
7745
|
+
+ '<path d="M5.5 1 L9 3.9 L7.6 6.4 Z" fill="#4A372F"/>'
|
|
7746
|
+
+ '<path d="M20 8 L22.5 1 L16 4.5 Z" fill="#D9663D"/>'
|
|
7747
|
+
+ '<path d="M22.5 1 L19 3.9 L20.4 6.4 Z" fill="#4A372F"/>'
|
|
7748
|
+
+ '<circle cx="14" cy="15" r="9.8" fill="#D9663D"/>'
|
|
7749
|
+
+ '<ellipse cx="14" cy="19.5" rx="5.4" ry="4.2" fill="#FBF3E8"/>'
|
|
7750
|
+
+ '<circle cx="10" cy="14" r="1.5" fill="#3A2E28" class="dsh-pet-eye"/>'
|
|
7751
|
+
+ '<circle cx="18" cy="14" r="1.5" fill="#3A2E28" class="dsh-pet-eye"/>'
|
|
7752
|
+
+ '<ellipse cx="14" cy="17.8" rx="1.3" ry="1" fill="#3A2E28"/>'
|
|
7753
|
+
+ '<path d="M14 18.8 q-1.3 1.4 -2.6 .7 M14 18.8 q1.3 1.4 2.6 .7" stroke="#8A4A2F" stroke-width=".9" fill="none" stroke-linecap="round"/>'
|
|
7754
|
+
|
|
7755
|
+
const PET_HU_TRAIL = ''
|
|
7756
|
+
+ '<g fill="#8F7A66"><ellipse cx="5.5" cy="6.6" rx="2.3" ry="1.9"/>'
|
|
7757
|
+
+ '<circle cx="2.7" cy="3.5" r="1.05"/><circle cx="5" cy="2.3" r="1.05"/>'
|
|
7758
|
+
+ '<circle cx="7.3" cy="2.3" r="1.05"/><circle cx="9.3" cy="3.7" r="1.05"/></g>'
|
|
7759
|
+
|
|
7760
|
+
const PET_XIONG_SIDE = ''
|
|
7761
|
+
// 熊猫的黑色分布(实机验收"完全不像是熊猫"后的重画):黑耳 / 大黑眼斑 /
|
|
7762
|
+
// 黑肩带连前肢 / 黑后臀连后腿,脸与身体中段是白 —— 旧版身体上下各一条
|
|
7763
|
+
// 孤立黑边(像被黑布勒住的白身体),观感不是熊猫。眼睛用黑斑里的白点
|
|
7764
|
+
// (旧版棕色眼在黑斑里看不清,眼斑读不出来)。
|
|
7765
|
+
+ '<ellipse cx="15.5" cy="40" rx="9.5" ry="11" fill="#2E2A28"/>'
|
|
7766
|
+
+ '<ellipse cx="30" cy="37" rx="16.5" ry="12.5" fill="#FAFAF7"/>'
|
|
7767
|
+
+ '<ellipse cx="42" cy="34" rx="6.5" ry="9.5" fill="#2E2A28"/>'
|
|
7768
|
+
+ '<rect class="dsh-pet-leg dsh-pet-leg-b" x="16" y="38" width="7.5" height="13" rx="3.7" fill="#2E2A28" style="transform-origin:19.7px 39px"/>'
|
|
7769
|
+
+ '<rect class="dsh-pet-leg dsh-pet-leg-a" x="38.5" y="38" width="7.5" height="13" rx="3.7" fill="#2E2A28" style="transform-origin:42.2px 39px"/>'
|
|
7770
|
+
+ '<ellipse class="dsh-pet-belly" cx="29" cy="42" rx="9" ry="7.5" fill="#FFFFFF" style="transform-origin:29px 42px"/>'
|
|
7771
|
+
+ '<circle cx="38.5" cy="7" r="4.2" fill="#2E2A28"/>'
|
|
7772
|
+
+ '<circle cx="53" cy="7" r="4.2" fill="#2E2A28"/>'
|
|
7773
|
+
+ '<circle cx="45.5" cy="17" r="11.5" fill="#FAFAF7"/>'
|
|
7774
|
+
+ '<ellipse cx="41" cy="15" rx="4.4" ry="5.6" fill="#2E2A28" transform="rotate(-18 41 15)"/>'
|
|
7775
|
+
+ '<ellipse cx="51.5" cy="14.5" rx="4.4" ry="5.6" fill="#2E2A28" transform="rotate(14 51.5 14.5)"/>'
|
|
7776
|
+
+ '<circle cx="41.5" cy="15.8" r="1.5" fill="#FFFFFF" class="dsh-pet-eye"/>'
|
|
7777
|
+
+ '<circle cx="51" cy="15.2" r="1.5" fill="#FFFFFF" class="dsh-pet-eye"/>'
|
|
7778
|
+
+ '<ellipse cx="46.8" cy="21.8" rx="2" ry="1.5" fill="#2E2A28"/>'
|
|
7779
|
+
+ '<path d="M46.8 23.3 q-1.4 1.6 -3 1 M46.8 23.3 q1.2 1.6 2.6 .8" stroke="#6B6560" stroke-width="1" fill="none" stroke-linecap="round"/>'
|
|
7780
|
+
|
|
7781
|
+
const PET_XIONG_FRONT = ''
|
|
7782
|
+
// 正面:头下加黑色肩领(熊猫脖子处本就是黑肩带,头身连接也由它承载——
|
|
7783
|
+
// 白头直接坐在白身上会"头身分离",琥珀猫咪那次的教训),手臂加粗成
|
|
7784
|
+
// 圆肩短臂,眼斑放大让"黑眼圈"这个第一辨识特征读得出来。
|
|
7785
|
+
+ '<circle cx="16" cy="8.5" r="4.6" fill="#2E2A28"/>'
|
|
7786
|
+
+ '<circle cx="42" cy="8.5" r="4.6" fill="#2E2A28"/>'
|
|
7787
|
+
+ '<ellipse cx="29" cy="39.5" rx="15.5" ry="11.5" fill="#FAFAF7"/>'
|
|
7788
|
+
+ '<ellipse cx="29" cy="30" rx="11" ry="5" fill="#2E2A28"/>'
|
|
7789
|
+
+ '<path d="M16.5 32 q-4.5 9 1 15.5 q7 -1.5 8.5 -8 Q24 33.5 16.5 32 Z" fill="#2E2A28"/>'
|
|
7790
|
+
+ '<path d="M41.5 32 q4.5 9 -1 15.5 q-7 -1.5 -8.5 -8 Q34 33.5 41.5 32 Z" fill="#2E2A28"/>'
|
|
7791
|
+
+ '<ellipse cx="21.5" cy="51" rx="5.4" ry="3" fill="#2E2A28"/>'
|
|
7792
|
+
+ '<ellipse cx="36.5" cy="51" rx="5.4" ry="3" fill="#2E2A28"/>'
|
|
7793
|
+
+ '<ellipse class="dsh-pet-belly" cx="29" cy="41" rx="8.5" ry="6.5" fill="#FFFFFF" style="transform-origin:29px 41px"/>'
|
|
7794
|
+
+ '<circle cx="29" cy="16.5" r="12.5" fill="#FAFAF7"/>'
|
|
7795
|
+
+ '<ellipse cx="22.5" cy="14.8" rx="4.8" ry="6" fill="#2E2A28" transform="rotate(-16 22.5 14.8)"/>'
|
|
7796
|
+
+ '<ellipse cx="35.5" cy="14.8" rx="4.8" ry="6" fill="#2E2A28" transform="rotate(16 35.5 14.8)"/>'
|
|
7797
|
+
+ '<circle cx="23" cy="15.6" r="1.55" fill="#FFFFFF" class="dsh-pet-eye"/>'
|
|
7798
|
+
+ '<circle cx="35" cy="15.6" r="1.55" fill="#FFFFFF" class="dsh-pet-eye"/>'
|
|
7799
|
+
+ '<ellipse cx="29" cy="20.8" rx="2.1" ry="1.6" fill="#2E2A28"/>'
|
|
7800
|
+
+ '<path d="M29 22.4 q-1.4 1.6 -2.8 .9 M29 22.4 q1.4 1.6 2.8 .9" stroke="#6B6560" stroke-width="1" fill="none" stroke-linecap="round"/>'
|
|
7801
|
+
|
|
7802
|
+
const PET_XIONG_PROP = ''
|
|
7803
|
+
+ '<path d="M10 3 Q9 .8 7.4 .6 Q9.6 -.2 10.9 1.4 Q10.7 2.4 10 3 Z" fill="#67B35F"/>'
|
|
7804
|
+
+ '<path d="M10 3 Q15 8 14 14 Q13 20.5 10 22.5 Q7 20.5 6 14 Q5 8 10 3 Z" fill="#D9C08A"/>'
|
|
7805
|
+
+ '<path d="M7.6 8.4 q2.4 1.6 4.8 0 M7 13 q3 1.8 6 0 M7.8 17.6 q2.2 1.5 4.4 0 M8.8 5.2 q1.2 1 2.4 0" stroke="#B99C63" stroke-width="1.3" fill="none" stroke-linecap="round"/>'
|
|
7806
|
+
|
|
7807
|
+
const PET_XIONG_FACE = ''
|
|
7808
|
+
+ '<circle cx="7.5" cy="7" r="3.8" fill="#2E2A28"/>'
|
|
7809
|
+
+ '<circle cx="20.5" cy="7" r="3.8" fill="#2E2A28"/>'
|
|
7810
|
+
+ '<circle cx="14" cy="15.5" r="10" fill="#FAFAF7"/>'
|
|
7811
|
+
+ '<ellipse cx="9.6" cy="14.3" rx="4.2" ry="5.2" fill="#2E2A28" transform="rotate(-15 9.6 14.3)"/>'
|
|
7812
|
+
+ '<ellipse cx="18.4" cy="14.3" rx="4.2" ry="5.2" fill="#2E2A28" transform="rotate(15 18.4 14.3)"/>'
|
|
7813
|
+
+ '<circle cx="9.9" cy="14.8" r="1.4" fill="#FFFFFF" class="dsh-pet-eye"/>'
|
|
7814
|
+
+ '<circle cx="18.1" cy="14.8" r="1.4" fill="#FFFFFF" class="dsh-pet-eye"/>'
|
|
7815
|
+
+ '<ellipse cx="14" cy="18.8" rx="1.8" ry="1.4" fill="#2E2A28"/>'
|
|
7816
|
+
+ '<path d="M14 20.2 q-1.3 1.4 -2.6 .8 M14 20.2 q1.3 1.4 2.6 .8" stroke="#6B6560" stroke-width=".9" fill="none" stroke-linecap="round"/>'
|
|
7817
|
+
|
|
7818
|
+
const PET_XIONG_TRAIL = ''
|
|
7819
|
+
+ '<g fill="#57534E"><ellipse cx="7" cy="8" rx="3.6" ry="2.8"/>'
|
|
7820
|
+
+ '<circle cx="2.6" cy="4" r="1.15"/><circle cx="4.8" cy="2.6" r="1.15"/>'
|
|
7821
|
+
+ '<circle cx="7" cy="2.2" r="1.15"/><circle cx="9.2" cy="2.7" r="1.15"/>'
|
|
7822
|
+
+ '<circle cx="11.2" cy="4.2" r="1.15"/></g>'
|
|
7823
|
+
|
|
7824
|
+
/**
|
|
7825
|
+
* 宠物注册表 —— 二期的核心数据。顺序 = 面板头像行顺序 = 预览页顺序;
|
|
7826
|
+
* `ban-ban` 永远第一(缺省宠物)。全部字段是**数据**,不许出现函数。
|
|
6648
7827
|
*
|
|
6649
|
-
* `
|
|
6650
|
-
*
|
|
6651
|
-
*
|
|
6652
|
-
* @param props - composed slot props.
|
|
6653
|
-
* @returns the page element tree.
|
|
7828
|
+
* · `motion`:run(平滑直线)/ hop(抛物线蹦跳,起伏由运行时驱动)/ waddle(左右摇步)。
|
|
7829
|
+
* · `place`:未被拖过时的缺省摆位(视口右/下缘偏移;语义同一期)。
|
|
7830
|
+
* · `react.lines`:戳一下的两声;`cheer/done`:生成中加油 / 完成庆祝;`chat`:闲聊。
|
|
6654
7831
|
*/
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
6658
|
-
|
|
6659
|
-
|
|
6660
|
-
|
|
6661
|
-
|
|
6662
|
-
|
|
6663
|
-
|
|
6664
|
-
|
|
6665
|
-
|
|
6666
|
-
|
|
6667
|
-
|
|
6668
|
-
|
|
6669
|
-
|
|
6670
|
-
|
|
6671
|
-
|
|
6672
|
-
|
|
6673
|
-
|
|
7832
|
+
const PET_KINDS = [
|
|
7833
|
+
{
|
|
7834
|
+
id: 'ban-ban', species: '小奶狗', home: '斑斑',
|
|
7835
|
+
viewBox: '0 0 64 56', size: { w: 64, h: 56 },
|
|
7836
|
+
art: { side: PET_BAN_SIDE, front: PET_BAN_FRONT },
|
|
7837
|
+
propViewBox: '0 0 26 22', propSize: { w: 26, h: 22 }, propArt: PET_BAN_PROP,
|
|
7838
|
+
faceViewBox: '0 0 28 28', faceArt: PET_BAN_FACE,
|
|
7839
|
+
trail: { art: PET_BAN_TRAIL, w: 12, h: 11, everyPx: 26, side: 5 },
|
|
7840
|
+
motion: 'run', speed: 130,
|
|
7841
|
+
react: { lines: ['汪!', '汪汪!'], ms: 1450 },
|
|
7842
|
+
cheer: '汪汪!加油!', done: '汪汪!完成啦!',
|
|
7843
|
+
chat: [
|
|
7844
|
+
'汪…斑斑在看着你写代码~',
|
|
7845
|
+
'要不要休息一下?斑斑可以陪你玩毛线团!',
|
|
7846
|
+
'汪!今天的代码也写得棒棒的!',
|
|
7847
|
+
'(歪头)这个 bug 闻起来有线索的味道…',
|
|
7848
|
+
],
|
|
7849
|
+
place: { pet: { dx: -100, dy: -56 }, prop: { dx: -132, dy: -22 } },
|
|
7850
|
+
},
|
|
7851
|
+
{
|
|
7852
|
+
id: 'da-ju', species: '橘猫', home: '大橘',
|
|
7853
|
+
viewBox: '0 0 62 54', size: { w: 62, h: 54 },
|
|
7854
|
+
art: { side: PET_JU_SIDE, front: PET_JU_FRONT },
|
|
7855
|
+
propViewBox: '0 0 24 18', propSize: { w: 24, h: 18 }, propArt: PET_JU_PROP,
|
|
7856
|
+
faceViewBox: '0 0 28 28', faceArt: PET_JU_FACE,
|
|
7857
|
+
trail: { art: PET_JU_TRAIL, w: 11, h: 10, everyPx: 22, side: 4 },
|
|
7858
|
+
motion: 'run', speed: 140,
|
|
7859
|
+
react: { lines: ['喵~?', '喵喵!'], ms: 1350 },
|
|
7860
|
+
cheer: '喵!在跑了在跑了!', done: '喵~搞定,可以吸猫了',
|
|
7861
|
+
chat: [
|
|
7862
|
+
'喵~敲键盘的手不可以摸鱼,但是可以摸猫',
|
|
7863
|
+
'代码写累了?本橘允许你吸三分钟猫',
|
|
7864
|
+
'喵呜…小鱼干和 commit,都再来一点嘛',
|
|
7865
|
+
'本橘监工中,摸鱼是会被记小本本的哦',
|
|
7866
|
+
],
|
|
7867
|
+
place: { pet: { dx: -96, dy: -54 }, prop: { dx: -130, dy: -18 } },
|
|
7868
|
+
},
|
|
7869
|
+
{
|
|
7870
|
+
id: 'xue-qiu', species: '垂耳兔', home: '雪球',
|
|
7871
|
+
viewBox: '0 0 58 56', size: { w: 58, h: 56 },
|
|
7872
|
+
art: { side: PET_TU_SIDE, front: PET_TU_FRONT },
|
|
7873
|
+
propViewBox: '0 0 24 22', propSize: { w: 24, h: 22 }, propArt: PET_TU_PROP,
|
|
7874
|
+
faceViewBox: '0 0 28 28', faceArt: PET_TU_FACE,
|
|
7875
|
+
trail: { art: PET_TU_TRAIL, w: 10, h: 8, everyPx: 34, side: 0 },
|
|
7876
|
+
motion: 'hop', speed: 150,
|
|
7877
|
+
react: { lines: ['哼哼!', '蹦!'], ms: 1250 },
|
|
7878
|
+
cheer: '加油加油!蹦!', done: '写完啦!蹦蹦庆祝!',
|
|
7879
|
+
chat: [
|
|
7880
|
+
'胡萝卜含量:0。兔兔有点失望…',
|
|
7881
|
+
'蹦蹦跳跳,bug 全都跑掉!',
|
|
7882
|
+
'兔兔在呢,慢慢想,不着急~',
|
|
7883
|
+
'耳朵告诉你:这一版写得不错哦!',
|
|
7884
|
+
],
|
|
7885
|
+
place: { pet: { dx: -92, dy: -56 }, prop: { dx: -124, dy: -22 } },
|
|
7886
|
+
},
|
|
7887
|
+
{
|
|
7888
|
+
id: 'bo-bo', species: '小企鹅', home: '波波',
|
|
7889
|
+
viewBox: '0 0 52 58', size: { w: 52, h: 58 },
|
|
7890
|
+
art: { side: PET_KE_SIDE, front: PET_KE_FRONT },
|
|
7891
|
+
propViewBox: '0 0 24 14', propSize: { w: 24, h: 14 }, propArt: PET_KE_PROP,
|
|
7892
|
+
faceViewBox: '0 0 28 28', faceArt: PET_KE_FACE,
|
|
7893
|
+
trail: { art: PET_KE_TRAIL, w: 12, h: 10, everyPx: 30, side: 6 },
|
|
7894
|
+
motion: 'waddle', speed: 110,
|
|
7895
|
+
react: { lines: ['嘎!', '嘎嘎!'], ms: 1300 },
|
|
7896
|
+
cheer: '嘎!冲鸭!', done: '嘎!收工,鱼干时间!',
|
|
7897
|
+
chat: [
|
|
7898
|
+
'嘎!南极的网速都没这么慢过…',
|
|
7899
|
+
'扑通扑通,为你打 call!',
|
|
7900
|
+
'企鹅不怕冷,你的代码也要不惧重构!',
|
|
7901
|
+
'鱼给你留着,记得吃饭呀~',
|
|
7902
|
+
],
|
|
7903
|
+
place: { pet: { dx: -86, dy: -58 }, prop: { dx: -118, dy: -14 } },
|
|
7904
|
+
},
|
|
7905
|
+
{
|
|
7906
|
+
id: 'nuo-mi', species: '仓鼠', home: '糯米',
|
|
7907
|
+
viewBox: '0 0 46 38', size: { w: 46, h: 38 },
|
|
7908
|
+
art: { side: PET_SHU_SIDE, front: PET_SHU_FRONT },
|
|
7909
|
+
propViewBox: '0 0 20 14', propSize: { w: 20, h: 14 }, propArt: PET_SHU_PROP,
|
|
7910
|
+
faceViewBox: '0 0 28 28', faceArt: PET_SHU_FACE,
|
|
7911
|
+
trail: { art: PET_SHU_TRAIL, w: 8, h: 6, everyPx: 14, side: 3 },
|
|
7912
|
+
motion: 'run', speed: 170,
|
|
7913
|
+
react: { lines: ['吱!', '吱吱!'], ms: 1100 },
|
|
7914
|
+
cheer: '吱吱吱!跑起来!', done: '吱!存进颊囊啦!',
|
|
7915
|
+
chat: [
|
|
7916
|
+
'吱吱!瓜子分你一半,bug 也分你一半!',
|
|
7917
|
+
'仓鼠球蓄力中…能量 +1',
|
|
7918
|
+
'颊囊已清空,可以继续装需求了!',
|
|
7919
|
+
'吱…今天的代码香香的,像葵花籽',
|
|
7920
|
+
],
|
|
7921
|
+
place: { pet: { dx: -78, dy: -38 }, prop: { dx: -108, dy: -14 } },
|
|
7922
|
+
},
|
|
7923
|
+
{
|
|
7924
|
+
id: 'a-chi', species: '小狐狸', home: '阿赤',
|
|
7925
|
+
viewBox: '0 0 66 52', size: { w: 66, h: 52 },
|
|
7926
|
+
art: { side: PET_HU_SIDE, front: PET_HU_FRONT },
|
|
7927
|
+
propViewBox: '0 0 20 24', propSize: { w: 20, h: 24 }, propArt: PET_HU_PROP,
|
|
7928
|
+
faceViewBox: '0 0 28 28', faceArt: PET_HU_FACE,
|
|
7929
|
+
trail: { art: PET_HU_TRAIL, w: 11, h: 10, everyPx: 24, side: 4 },
|
|
7930
|
+
motion: 'run', speed: 155,
|
|
7931
|
+
react: { lines: ['咕?', '咕咕!'], ms: 1250 },
|
|
7932
|
+
cheer: '咕!冲!', done: '咕~漂亮!收工!',
|
|
7933
|
+
chat: [
|
|
7934
|
+
'咕?这条路径,狐狸觉得可疑哦',
|
|
7935
|
+
'悄悄告诉你:尾巴比代码先跑完~',
|
|
7936
|
+
'森林法则第一条:先写测试!',
|
|
7937
|
+
'咕~松果给你,创意也给你!',
|
|
7938
|
+
],
|
|
7939
|
+
place: { pet: { dx: -100, dy: -52 }, prop: { dx: -138, dy: -24 } },
|
|
7940
|
+
},
|
|
7941
|
+
{
|
|
7942
|
+
id: 'tuan-tuan', species: '熊猫', home: '团团',
|
|
7943
|
+
viewBox: '0 0 60 56', size: { w: 60, h: 56 },
|
|
7944
|
+
art: { side: PET_XIONG_SIDE, front: PET_XIONG_FRONT },
|
|
7945
|
+
propViewBox: '0 0 20 24', propSize: { w: 20, h: 24 }, propArt: PET_XIONG_PROP,
|
|
7946
|
+
faceViewBox: '0 0 28 28', faceArt: PET_XIONG_FACE,
|
|
7947
|
+
trail: { art: PET_XIONG_TRAIL, w: 14, h: 12, everyPx: 34, side: 6 },
|
|
7948
|
+
motion: 'waddle', speed: 95,
|
|
7949
|
+
react: { lines: ['嘿嘿!', '抱抱!'], ms: 1500 },
|
|
7950
|
+
cheer: '嘿嘿,冲呀!', done: '嘿嘿,完成!干饭!',
|
|
7951
|
+
chat: [
|
|
7952
|
+
'嘿嘿…竹子要一口一口吃,代码要一行一行写',
|
|
7953
|
+
'团团营业中,干饭与陪写两不误~',
|
|
7954
|
+
'黑眼圈是天生的,你的可不能熬出来!',
|
|
7955
|
+
'抱住竹笋,也抱住今天的 KPI!',
|
|
7956
|
+
],
|
|
7957
|
+
place: { pet: { dx: -94, dy: -56 }, prop: { dx: -128, dy: -24 } },
|
|
7958
|
+
},
|
|
7959
|
+
]
|
|
7960
|
+
|
|
7961
|
+
/** id → 注册表项;不认识的 id 一律回落第一只(缺省宠物)。 */
|
|
7962
|
+
const PET_KIND_BY_ID = new Map(PET_KINDS.map((kind) => [kind.id, kind]))
|
|
7963
|
+
|
|
7964
|
+
/** 缺省宠物 = 注册表第一只(ban-ban)。切换逻辑与兜底逻辑都以它为准。 */
|
|
7965
|
+
const DEFAULT_PET_KIND = PET_KINDS[0]
|
|
7966
|
+
|
|
7967
|
+
/** 宠物的显示尺寸(px)—— 注册表 size × PET_SCALE。消费尺寸一律走这里。 */
|
|
7968
|
+
function petDisplaySize(kind) {
|
|
7969
|
+
return { w: kind.size.w * PET_SCALE, h: kind.size.h * PET_SCALE }
|
|
7970
|
+
}
|
|
7971
|
+
|
|
7972
|
+
/** 道具的显示尺寸(px)。与宠物同一缩放:道具不同步放大就会显得相对变小。 */
|
|
7973
|
+
function petPropDisplaySize(kind) {
|
|
7974
|
+
return { w: kind.propSize.w * PET_SCALE, h: kind.propSize.h * PET_SCALE }
|
|
7975
|
+
}
|
|
7976
|
+
|
|
7977
|
+
/** 按注册表包裹一件姿势/道具/头像(<svg> 内部内容 → 完整 svg 字符串)。 */
|
|
7978
|
+
function petKindById(id) {
|
|
7979
|
+
return PET_KIND_BY_ID.get(id) ?? DEFAULT_PET_KIND
|
|
7980
|
+
}
|
|
7981
|
+
|
|
7982
|
+
/**
|
|
7983
|
+
* 包一层姿势 svg。cls 是 dsh-pet-side / dsh-pet-front(正反面由 CSS 切换显示)。
|
|
7984
|
+
* 只做拼接、不读任何运行态 —— 面板卡面与预览页都能单独调用它。
|
|
7985
|
+
*/
|
|
7986
|
+
function petSvg(kind, pose, cls) {
|
|
7987
|
+
return `<svg class="${cls}" viewBox="${kind.viewBox}" aria-hidden="true">${kind.art[pose]}</svg>`
|
|
7988
|
+
}
|
|
7989
|
+
|
|
7990
|
+
/** 道具 svg(无类名:道具没有姿势切换)。 */
|
|
7991
|
+
function petPropSvg(kind) {
|
|
7992
|
+
return `<svg viewBox="${kind.propViewBox}" aria-hidden="true">${kind.propArt}</svg>`
|
|
7993
|
+
}
|
|
7994
|
+
|
|
7995
|
+
/**
|
|
7996
|
+
* 挂件状态的缺省值:默认关;名字按注册表逐只给(names 是 kind → 名字的表,
|
|
7997
|
+
* 每只宠物各自记住自己的名字);没拖过时按**当前宠物**的 place 摆位。
|
|
7998
|
+
*/
|
|
7999
|
+
const PET_DEFAULTS = {
|
|
8000
|
+
on: false,
|
|
8001
|
+
kind: DEFAULT_PET_KIND.id,
|
|
8002
|
+
names: Object.fromEntries(PET_KINDS.map((kind) => [kind.id, kind.home])),
|
|
8003
|
+
placed: false,
|
|
8004
|
+
pet: { ...DEFAULT_PET_KIND.place.pet },
|
|
8005
|
+
prop: { ...DEFAULT_PET_KIND.place.prop },
|
|
8006
|
+
}
|
|
8007
|
+
|
|
8008
|
+
/** 读到不认识的 kind 时置位(自检行要用),下一次成功读取时清掉。 */
|
|
8009
|
+
let stateKindFallback = false
|
|
8010
|
+
|
|
8011
|
+
/**
|
|
8012
|
+
* 挂件运行态。`enabled` 是 localStorage 的内存镜像:resync 每帧都会进来,
|
|
8013
|
+
* 不能每次都读一遍 localStorage。`mode` = idle(待机)/ run(跑向道具)/
|
|
8014
|
+
* drag(被拖住)/ react(面对用户的反应)。
|
|
8015
|
+
*
|
|
8016
|
+
* 坐标语义(相对舞台左上角的 px):x/y 是宠物的**左上角**;propX 是道具的
|
|
8017
|
+
* **中心 x**、propY 是它的上缘。dir 是水平朝向:1 = 朝右(美术原方向),-1 = 朝左
|
|
8018
|
+
* (镜像)—— 缺省朝左,因为缺省摆位里道具就在宠物左侧,它一出生就该看着玩具。
|
|
8019
|
+
* hop 步态:hopFrac 是 0..1 的相位,lift 是当前视觉抬升(只影响显示,不动逻辑坐标)。
|
|
8020
|
+
*/
|
|
8021
|
+
const petRun = {
|
|
8022
|
+
enabled: false, mode: 'idle', dir: -1, kindId: '',
|
|
8023
|
+
x: 0, y: 0, propX: 0, propY: 0, groundY: 0,
|
|
8024
|
+
hopFrac: 0, lift: 0,
|
|
8025
|
+
dragDX: 0, dragDY: 0, dragging: null,
|
|
8026
|
+
trailDist: 0, trailSide: 1, lastAt: 0, anchorKey: '', reactWasRunning: false,
|
|
8027
|
+
working: false, firstSyncAt: 0, everWorked: false,
|
|
8028
|
+
}
|
|
8029
|
+
/** 舞台与关键子节点的引用;null = 未挂载。 */
|
|
8030
|
+
let petStage = null
|
|
8031
|
+
const petRefs = { actor: null, prop: null, flip: null, figure: null, tag: null, name: null, input: null }
|
|
8032
|
+
/** 活着的脚印 / 气泡数量(有上限)。 */
|
|
8033
|
+
let petTrailCount = 0
|
|
8034
|
+
let petBubbleCount = 0
|
|
8035
|
+
/** react/cheer 的短时定时器;卸载时统一清掉,避免对已拆除的节点说话。 */
|
|
8036
|
+
let petTimers = []
|
|
8037
|
+
/** 本次模块实例的属主戳:热重载后旧实例留下的舞台会被清扫(ambient 同款做法)。 */
|
|
8038
|
+
const PET_OWNER = `p${Date.now()}`
|
|
8039
|
+
|
|
8040
|
+
/**
|
|
8041
|
+
* 读取并消毒挂件状态。
|
|
8042
|
+
*
|
|
8043
|
+
* localStorage 里的东西不可信:换版本、手改、损坏的 JSON 都可能遇到。宁可回到缺省,
|
|
8044
|
+
* 也不把一个 `undefined` 画进 transform。旧形状(一期:name/dog/ball 标量键)在这里
|
|
8045
|
+
* **原地迁移**:dog→pet、ball→prop、name→names['ban-ban'],用户实机已在一期存过
|
|
8046
|
+
* 数据,升级不该让他重新摆位、重新起名。
|
|
8047
|
+
* @returns `{ on, kind, names, placed, pet: {dx,dy}, prop: {dx,dy} }`。
|
|
8048
|
+
*/
|
|
8049
|
+
function readPetState() {
|
|
8050
|
+
const defaults = () => ({
|
|
8051
|
+
on: PET_DEFAULTS.on,
|
|
8052
|
+
kind: PET_DEFAULTS.kind,
|
|
8053
|
+
names: { ...PET_DEFAULTS.names },
|
|
8054
|
+
placed: PET_DEFAULTS.placed,
|
|
8055
|
+
pet: { ...PET_DEFAULTS.pet },
|
|
8056
|
+
prop: { ...PET_DEFAULTS.prop },
|
|
8057
|
+
})
|
|
8058
|
+
try {
|
|
8059
|
+
const raw = window.localStorage?.getItem(PET_KEY)
|
|
8060
|
+
if (typeof raw !== 'string' || raw === '') return defaults()
|
|
8061
|
+
const parsed = JSON.parse(raw)
|
|
8062
|
+
if (parsed === null || typeof parsed !== 'object') return defaults()
|
|
8063
|
+
const out = defaults()
|
|
8064
|
+
const num = (value, fallback) =>
|
|
8065
|
+
(typeof value === 'number' && Number.isFinite(value) ? value : fallback)
|
|
8066
|
+
const offset = (value, fallback) => {
|
|
8067
|
+
const obj = value !== null && typeof value === 'object' ? value : {}
|
|
8068
|
+
return { dx: num(obj.dx, fallback.dx), dy: num(obj.dy, fallback.dy) }
|
|
8069
|
+
}
|
|
8070
|
+
// kind:不认识的(含被手改成乱码)一律回落缺省宠物,并留痕给自检行。
|
|
8071
|
+
stateKindFallback = false
|
|
8072
|
+
if (typeof parsed.kind === 'string' && PET_KIND_BY_ID.has(parsed.kind)) {
|
|
8073
|
+
out.kind = parsed.kind
|
|
8074
|
+
} else if (typeof parsed.kind === 'string' && parsed.kind !== '') {
|
|
8075
|
+
stateKindFallback = true
|
|
8076
|
+
}
|
|
8077
|
+
const kind = petKindById(out.kind)
|
|
8078
|
+
// 名字表:按宠物各自记忆;当前宠物保证有名字(缺 → 注册表的 home)。
|
|
8079
|
+
if (parsed.names !== null && typeof parsed.names === 'object') {
|
|
8080
|
+
for (const key of Object.keys(out.names)) {
|
|
8081
|
+
const value = parsed.names[key]
|
|
8082
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
8083
|
+
out.names[key] = value.trim().slice(0, 10)
|
|
8084
|
+
}
|
|
8085
|
+
}
|
|
8086
|
+
}
|
|
8087
|
+
// 一期迁移:顶层 name 只属于斑斑;dog/ball 是 pet/prop 的旧键名。
|
|
8088
|
+
if (typeof parsed.name === 'string' && parsed.name.trim() !== '') {
|
|
8089
|
+
out.names['ban-ban'] = parsed.name.trim().slice(0, 10)
|
|
8090
|
+
}
|
|
8091
|
+
if (typeof out.names[kind.id] !== 'string' || out.names[kind.id] === '') {
|
|
8092
|
+
out.names[kind.id] = kind.home
|
|
8093
|
+
}
|
|
8094
|
+
out.on = parsed.on === true
|
|
8095
|
+
out.placed = parsed.placed === true
|
|
8096
|
+
const petOffset = parsed.pet !== undefined ? parsed.pet : parsed.dog
|
|
8097
|
+
const propOffset = parsed.prop !== undefined ? parsed.prop : parsed.ball
|
|
8098
|
+
out.pet = offset(petOffset, kind.place.pet)
|
|
8099
|
+
out.prop = offset(propOffset, kind.place.prop)
|
|
8100
|
+
return out
|
|
8101
|
+
} catch {
|
|
8102
|
+
return defaults()
|
|
8103
|
+
}
|
|
8104
|
+
}
|
|
8105
|
+
|
|
8106
|
+
/**
|
|
8107
|
+
* 合并写回挂件状态。写失败只记录 —— localStorage 被禁用时挂件仍可在本次会话里玩。
|
|
8108
|
+
* @param patch - 要合并进状态的部分字段。
|
|
8109
|
+
*/
|
|
8110
|
+
function savePetState(patch) {
|
|
8111
|
+
try {
|
|
8112
|
+
window.localStorage?.setItem(PET_KEY, JSON.stringify({ ...readPetState(), ...patch }))
|
|
8113
|
+
} catch (error) {
|
|
8114
|
+
console.error('[theme-gallery] could not save the pet state:', error)
|
|
8115
|
+
}
|
|
8116
|
+
}
|
|
8117
|
+
|
|
8118
|
+
/** 挂件现在是否开着(给卡片徽标与同步判据用)。 */
|
|
8119
|
+
function petWidgetOn() {
|
|
8120
|
+
return readPetState().on
|
|
8121
|
+
}
|
|
8122
|
+
|
|
8123
|
+
/**
|
|
8124
|
+
* 挂件卡的"伪主题"形态。
|
|
8125
|
+
*
|
|
8126
|
+
* 面板卡片完全从主题注册表推导,而挂件不是主题 —— 所以给它一张**只在面板里存在**
|
|
8127
|
+
* 的卡片数据:走同一条排序与渲染管线(CARD_ORDER 序号、tooltip、禁用逻辑与其他卡
|
|
8128
|
+
* 一致),却不会被注册进主题服务(BUNDLED_THEMES 里没有它),官方外观列表也就
|
|
8129
|
+
* 多不出这一项。
|
|
8130
|
+
* @returns 只带卡片字段的对象(没有 tokens,默认色带为空)。
|
|
8131
|
+
*/
|
|
8132
|
+
function petWidgetCardTheme() {
|
|
8133
|
+
return {
|
|
8134
|
+
id: PET_WIDGET.id,
|
|
8135
|
+
label: PET_WIDGET.label,
|
|
8136
|
+
description: PET_WIDGET.description,
|
|
8137
|
+
colorScheme: 'light',
|
|
8138
|
+
tokens: {},
|
|
8139
|
+
}
|
|
8140
|
+
}
|
|
8141
|
+
|
|
8142
|
+
/**
|
|
8143
|
+
* 心情问候卡的卡片数据(petWidgetCardTheme 同构):只在面板里存在的非主题卡,
|
|
8144
|
+
* 走同一条排序与渲染管线(CARD_ORDER 73、tooltip),不注册进主题服务,
|
|
8145
|
+
* 官方外观列表不会多出这一项。
|
|
8146
|
+
* @returns 只带卡片字段的对象(没有 tokens,默认色带为空)。
|
|
8147
|
+
*/
|
|
8148
|
+
function moodCardTheme() {
|
|
8149
|
+
return {
|
|
8150
|
+
id: MOOD_WIDGET.id,
|
|
8151
|
+
label: MOOD_WIDGET.label,
|
|
8152
|
+
description: MOOD_WIDGET.description,
|
|
8153
|
+
colorScheme: 'light',
|
|
8154
|
+
tokens: {},
|
|
8155
|
+
}
|
|
8156
|
+
}
|
|
8157
|
+
|
|
8158
|
+
/**
|
|
8159
|
+
* 输入框(composer 座位)的矩形。
|
|
8160
|
+
*
|
|
8161
|
+
* 这里**故意**不复用 mountGallery 里的 centreColumn / composerOf:那是挂载体内部的
|
|
8162
|
+
* 函数,工厂级的本函数够不到(硬性规则 4 的形状)。查找逻辑与它们一致,注释写明
|
|
8163
|
+
* 重复的原因 —— 两份实现谁坏了都会在"挂件不出现"上显形,而不是静默错位。
|
|
8164
|
+
*
|
|
8165
|
+
* ── 为什么不能只写 `.centerCol`(真机事故:挂件永远建不出来)────────────────
|
|
8166
|
+
*
|
|
8167
|
+
* 框架类名是**构建期哈希**:侧栏列在真机上叫 `ZTP-Xa_sidebarCol`,中心列同理是
|
|
8168
|
+
* `…_centerCol`。SKILL §13 的老教训在这里换了个形态复发——选择器命中 0 个元素、
|
|
8169
|
+
* 无异常无日志。所以中心列的查找必须像 `sidebarColumn()` 一样**哈希容忍**
|
|
8170
|
+
* (`[class*="_centerCol"]`),并再加两级兜底:全局找第一个 contenteditable /
|
|
8171
|
+
* textarea(聊天输入框几乎必然是其一);连它都没有时由调用方回退到视口定位。
|
|
8172
|
+
* @returns 矩形,或 null(连全局输入框都找不到)。
|
|
8173
|
+
*/
|
|
8174
|
+
function petAnchorBox() {
|
|
8175
|
+
if (typeof document === 'undefined') return null
|
|
8176
|
+
const centre = document.querySelector('[data-windows-titlebar] .centerCol')
|
|
8177
|
+
|| document.querySelector('[class*="_centerCol"]')
|
|
8178
|
+
|| document.querySelector('[class*="centerCol" i]')
|
|
8179
|
+
const composer = (centre === null ? null
|
|
8180
|
+
: centre.querySelector('[contenteditable="true"]') || centre.querySelector('textarea'))
|
|
8181
|
+
|| document.querySelector('[contenteditable="true"]')
|
|
8182
|
+
|| document.querySelector('textarea')
|
|
8183
|
+
if (composer === null && centre === null) return null
|
|
8184
|
+
for (const node of [composer === null ? undefined : composer.parentElement, composer, centre]) {
|
|
8185
|
+
if (node === undefined || node === null) continue
|
|
8186
|
+
const rect = node.getBoundingClientRect()
|
|
8187
|
+
if (rect.width > 0 || rect.height > 0) return rect
|
|
8188
|
+
}
|
|
8189
|
+
return null
|
|
8190
|
+
}
|
|
8191
|
+
|
|
8192
|
+
/** 把 x 夹进窗口(狗按其半宽、球按其半径留边),拖出去也会被接住。 */
|
|
8193
|
+
function petClampX(x, half, width) {
|
|
8194
|
+
return Math.max(half, Math.min(x, Math.max(half, width - half)))
|
|
8195
|
+
}
|
|
8196
|
+
|
|
8197
|
+
/**
|
|
8198
|
+
* 视口尺寸,带兜底:个别环境读不到 innerWidth / innerHeight 时钳位会变成 NaN,
|
|
8199
|
+
* 狗会瞬间飞出屏幕 —— 与其信任它们存在,不如在这里回退成常见值。
|
|
8200
|
+
* @returns `{ width, height }`(px)。
|
|
8201
|
+
*/
|
|
8202
|
+
function petViewport() {
|
|
8203
|
+
const pick = (value, fallback) =>
|
|
8204
|
+
(typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : fallback)
|
|
8205
|
+
try {
|
|
8206
|
+
return {
|
|
8207
|
+
width: pick(window.innerWidth, 1280),
|
|
8208
|
+
height: pick(window.innerHeight, 800),
|
|
8209
|
+
}
|
|
8210
|
+
} catch {
|
|
8211
|
+
return { width: 1280, height: 800 }
|
|
8212
|
+
}
|
|
8213
|
+
}
|
|
8214
|
+
|
|
8215
|
+
/** 指针坐标;桩环境给不出数字时返回 null(调用方直接忽略这一帧)。 */
|
|
8216
|
+
function petPointOf(event) {
|
|
8217
|
+
const x = event?.clientX
|
|
8218
|
+
const y = event?.clientY
|
|
8219
|
+
if (typeof x !== 'number' || typeof y !== 'number') return null
|
|
8220
|
+
return { x, y }
|
|
8221
|
+
}
|
|
8222
|
+
|
|
8223
|
+
/** 挂件层自己的样式表(与 PAGE_CSS 分开:它属于运行时舞台,不属于面板页面)。 */
|
|
8224
|
+
const PET_CSS = [
|
|
8225
|
+
`#${PET_STAGE_ID}{position:fixed;left:0;top:0;right:0;bottom:0;pointer-events:none;z-index:1200}`,
|
|
8226
|
+
'.dsh-pet-actor,.dsh-pet-prop{position:absolute;left:0;top:0;pointer-events:auto;cursor:grab;will-change:transform}',
|
|
8227
|
+
'.dsh-pet-actor:active,.dsh-pet-prop:active{cursor:grabbing}',
|
|
8228
|
+
// flip 层与 figure 层必须分开:**镜像(内联 transform)绝不能和跑步动画(keyframes)
|
|
8229
|
+
// 放在同一个元素上** —— CSS 动画会覆盖内联 transform,放在一层就会"朝左跑时镜像
|
|
8230
|
+
// 失效、倒着跑"(真机事故)。尺寸由 buildPetStage 按注册表写内联(每只宠物不同)。
|
|
8231
|
+
'.dsh-pet-flip,.dsh-pet-figure{display:block;width:100%;height:100%}',
|
|
8232
|
+
'.dsh-pet-side,.dsh-pet-front{display:block;width:100%;height:100%}',
|
|
8233
|
+
'.dsh-pet-front{display:none}',
|
|
8234
|
+
'.dsh-pet-react .dsh-pet-side{display:none}',
|
|
8235
|
+
'.dsh-pet-react .dsh-pet-front{display:block}',
|
|
8236
|
+
// 眨眼:所有宠物共用(markup 里带 dsh-pet-eye 类的眼睛都会眨)。
|
|
8237
|
+
'.dsh-pet-eye{transform-box:fill-box;transform-origin:center;animation:dsh-pet-blink 4.6s infinite}',
|
|
8238
|
+
'@keyframes dsh-pet-blink{0%,91%,94.5%,100%{transform:scaleY(1)}92.8%{transform:scaleY(.12)}}',
|
|
8239
|
+
// 待机微动画锚点(幅度刻意克制:"活着",不是"表演");origin 由各宠物 markup 内联。
|
|
8240
|
+
'.dsh-pet-tail{animation:dsh-pet-wag 1.5s ease-in-out infinite}',
|
|
8241
|
+
'@keyframes dsh-pet-wag{from{transform:rotate(-7deg)}to{transform:rotate(9deg)}}',
|
|
8242
|
+
'.dsh-pet-ear{animation:dsh-pet-ear-flop 3.4s ease-in-out infinite}',
|
|
8243
|
+
'@keyframes dsh-pet-ear-flop{0%,86%,100%{transform:rotate(0)}91%{transform:rotate(7deg)}96%{transform:rotate(-5deg)}}',
|
|
8244
|
+
'.dsh-pet-wing{animation:dsh-pet-wing-idle 3s ease-in-out infinite}',
|
|
8245
|
+
'@keyframes dsh-pet-wing-idle{0%,88%,100%{transform:rotate(0)}93%{transform:rotate(-8deg)}}',
|
|
8246
|
+
'.dsh-pet-belly{animation:dsh-pet-breathe 3.8s ease-in-out infinite}',
|
|
8247
|
+
'@keyframes dsh-pet-breathe{0%,100%{transform:scale(1)}50%{transform:scale(1.025)}}',
|
|
8248
|
+
// 跑动(run 步态):腿部摆动 + 身体起伏。
|
|
8249
|
+
'@keyframes dsh-pet-gallop-a{0%,100%{transform:rotate(17deg)}50%{transform:rotate(-17deg)}}',
|
|
8250
|
+
'@keyframes dsh-pet-gallop-b{0%,100%{transform:rotate(-15deg)}50%{transform:rotate(15deg)}}',
|
|
8251
|
+
'.dsh-pet-run .dsh-pet-leg-a{animation:dsh-pet-gallop-a .3s linear infinite}',
|
|
8252
|
+
'.dsh-pet-run .dsh-pet-leg-b{animation:dsh-pet-gallop-b .3s linear infinite}',
|
|
8253
|
+
'.dsh-pet-run .dsh-pet-figure{animation:dsh-pet-bob .3s ease-in-out infinite}',
|
|
8254
|
+
'@keyframes dsh-pet-bob{0%,100%{transform:translateY(0)}50%{transform:translateY(-1.6px)}}',
|
|
8255
|
+
// 摇摆步态(企鹅/熊猫):身体左右摇;不挂腿部动画(脚贴地)。
|
|
8256
|
+
'.dsh-pet-waddle .dsh-pet-figure{animation:dsh-pet-rock .46s ease-in-out infinite}',
|
|
8257
|
+
'@keyframes dsh-pet-rock{0%,100%{transform:rotate(-4deg)}50%{transform:rotate(4deg)}}',
|
|
8258
|
+
// 蹦跳步态(兔):无循环类,起伏由运行时驱动(petRun.lift),落地即脚印。
|
|
8259
|
+
// 戳一下:正面像 + 两次小跳;加油/庆祝/精神一下共用这条 hop(关键帧在后)。
|
|
8260
|
+
'@keyframes dsh-pet-hop{0%,100%{transform:translateY(0)}30%{transform:translateY(-5px)}60%{transform:translateY(-2px)}}',
|
|
8261
|
+
'.dsh-pet-react .dsh-pet-figure{animation:dsh-pet-hop .55s ease-in-out 2}',
|
|
8262
|
+
'.dsh-pet-cheer .dsh-pet-figure{animation:dsh-pet-hop .5s ease-in-out 2}',
|
|
8263
|
+
'.dsh-pet-perk .dsh-pet-figure{animation:dsh-pet-perk .9s ease-out 1}',
|
|
8264
|
+
'@keyframes dsh-pet-perk{0%{transform:scale(1)}30%{transform:scale(1.045)}100%{transform:scale(1)}}',
|
|
8265
|
+
// 名牌:opacity/visibility 显隐 + 隐藏侧 1.2s 延迟。实机验收:鼠标从宠物
|
|
8266
|
+
// 移向「改名/戳一下」的途中要穿过名牌与宠物之间的 5px 空隙,hover 一断名牌
|
|
8267
|
+
// 就消失。两处纯 CSS 的修法(不引 JS 定时器,符合有界性纪律):
|
|
8268
|
+
// · ::before 是跨过空隙的**透明桥**——名牌是 actor 的子元素,鼠标在桥上
|
|
8269
|
+
// 时 actor:hover 仍成立,可以一路走上名牌;
|
|
8270
|
+
// · 隐藏侧 visibility 延迟 1.2s:离开 actor 后名牌仍可交互一小会儿,
|
|
8271
|
+
// 鼠标来得及走上去。显示侧 delay 归零(transition-delay 作用于全部属性)。
|
|
8272
|
+
'.dsh-pet-tag{display:inline-flex;position:absolute;left:50%;bottom:calc(100% + 5px);',
|
|
8273
|
+
'transform:translateX(-50%);pointer-events:auto;white-space:nowrap;z-index:3;',
|
|
8274
|
+
'align-items:center;gap:4px;padding:3px 6px;border-radius:8px;background:rgba(40,33,28,.92);',
|
|
8275
|
+
'color:#fff;font:11.5px/1.5 system-ui,"Segoe UI","Microsoft YaHei",sans-serif;',
|
|
8276
|
+
'box-shadow:0 3px 10px rgba(0,0,0,.22);opacity:0;visibility:hidden;',
|
|
8277
|
+
'transition:opacity .18s ease,visibility 0s linear 1.2s}',
|
|
8278
|
+
'.dsh-pet-tag::before{content:"";position:absolute;left:0;right:0;bottom:-12px;height:12px}',
|
|
8279
|
+
'.dsh-pet-actor:hover .dsh-pet-tag,.dsh-pet-tag.dsh-pet-editing{opacity:1;visibility:visible;transition-delay:0s}',
|
|
8280
|
+
'.dsh-pet-name{max-width:96px;overflow:hidden;text-overflow:ellipsis}',
|
|
8281
|
+
'.dsh-pet-actions{display:inline-flex;gap:4px}',
|
|
8282
|
+
'.dsh-pet-tag button{border:none;border-radius:5px;padding:1px 7px;cursor:pointer;',
|
|
8283
|
+
'font:11px/1.6 inherit;color:#fff;background:rgba(255,255,255,.18)}',
|
|
8284
|
+
'.dsh-pet-tag button:hover{background:rgba(255,255,255,.34)}',
|
|
8285
|
+
'.dsh-pet-input{display:none;width:88px;border:none;border-radius:5px;padding:2px 6px;',
|
|
8286
|
+
'font:11.5px/1.5 inherit;color:#2b2320}',
|
|
8287
|
+
'.dsh-pet-editing .dsh-pet-name,.dsh-pet-editing .dsh-pet-actions{display:none}',
|
|
8288
|
+
'.dsh-pet-editing .dsh-pet-input{display:block}',
|
|
8289
|
+
// 拖拽中的反馈:影子加深,让人看清"它离地了"。
|
|
8290
|
+
'.dsh-pet-dragging .dsh-pet-figure{filter:drop-shadow(0 7px 6px rgba(0,0,0,.28))}',
|
|
8291
|
+
// 脚印:随物种换形状与颜色(注册表 trail.art),落一次淡一次;
|
|
8292
|
+
// 超时由 JS 兜底回收(reduced-motion 关掉动画时也成立)。
|
|
8293
|
+
'.dsh-pet-trailmark{position:absolute;left:0;top:0;opacity:.5;',
|
|
8294
|
+
'pointer-events:none;animation:dsh-pet-trail-fade 3.4s ease-out forwards}',
|
|
8295
|
+
'@keyframes dsh-pet-trail-fade{0%{opacity:.5}70%{opacity:.34}100%{opacity:0}}',
|
|
8296
|
+
// 叫声/闲聊/加油气泡。
|
|
8297
|
+
'.dsh-pet-bubble{position:absolute;left:0;top:0;pointer-events:none;white-space:nowrap;',
|
|
8298
|
+
'padding:2px 8px;border-radius:9px;background:#fff;color:#3B2B23;border:1px solid #E4CBA2;',
|
|
8299
|
+
'font:12px/1.6 system-ui,"Segoe UI","Microsoft YaHei",sans-serif;',
|
|
8300
|
+
'box-shadow:0 2px 7px rgba(0,0,0,.16);animation:dsh-pet-bubble 1.15s ease-out forwards}',
|
|
8301
|
+
'@keyframes dsh-pet-bubble{0%{opacity:0;transform-origin:bottom left;scale:.6}',
|
|
8302
|
+
'14%{opacity:1;scale:1}78%{opacity:1;scale:1}100%{opacity:0;scale:1;translate:0 -9px}}',
|
|
8303
|
+
'@media (prefers-reduced-motion:reduce){',
|
|
8304
|
+
`#${PET_STAGE_ID} *{animation:none!important}`,
|
|
8305
|
+
// 名牌的 visibility 延迟是"鼠标能走上去"的功能窗口(透明桥),不是装饰动画:
|
|
8306
|
+
// reduced-motion 只去掉 opacity 淡入淡出,把交互窗口原样保留。
|
|
8307
|
+
`#${PET_STAGE_ID} .dsh-pet-tag{transition:visibility 0s linear 1.2s}}`,
|
|
8308
|
+
].join('\n')
|
|
8309
|
+
|
|
8310
|
+
/**
|
|
8311
|
+
* 挂件层样式表的就位与自愈(与 ambient 的 ensureAmbientStylesheet 同一策略):
|
|
8312
|
+
* 每次同步都检查,不在就装、内容陈旧就换 —— "一次安装应该能活下来"这个假设
|
|
8313
|
+
* 在这台机器上已经失败过一次。
|
|
8314
|
+
* @returns 样式表元素,或 null(没有 document)。
|
|
8315
|
+
*/
|
|
8316
|
+
function ensurePetStylesheet() {
|
|
8317
|
+
if (typeof document === 'undefined') return null
|
|
8318
|
+
const existing = document.querySelector('style[data-plugin-css="theme-gallery/pet"]')
|
|
8319
|
+
if (existing !== null) {
|
|
8320
|
+
if (existing.textContent === PET_CSS) return existing
|
|
8321
|
+
existing.remove()
|
|
8322
|
+
}
|
|
8323
|
+
const tag = document.createElement('style')
|
|
8324
|
+
tag.dataset.plugin = 'theme-gallery'
|
|
8325
|
+
tag.dataset.pluginCss = 'theme-gallery/pet'
|
|
8326
|
+
tag.textContent = PET_CSS
|
|
8327
|
+
if (document.head !== null) document.head.append(tag)
|
|
8328
|
+
else document.documentElement.append(tag)
|
|
8329
|
+
return tag
|
|
8330
|
+
}
|
|
8331
|
+
|
|
8332
|
+
/* ── 活性机制的常量与状态(闲聊 / 打字回头 / 生成中加油)───────────────────
|
|
8333
|
+
*
|
|
8334
|
+
* 三重有界:闲聊是**单个可取消句柄**(petChatterTimer,任意时刻至多一个);
|
|
8335
|
+
* 加油/庆祝只挂短时类名(句柄进 petTimers);打字回头是 document 上唯一的
|
|
8336
|
+
* input 监听(搭舞台才装、拆舞台即卸)。任何一级检测失效都静默降级。
|
|
8337
|
+
*/
|
|
8338
|
+
|
|
8339
|
+
/** 闲聊:距最近互动至少 2 分钟才可能出现;之后每 2.5 分钟一个窗口。 */
|
|
8340
|
+
const CHATTER_FIRST_MS = 120000
|
|
8341
|
+
const CHATTER_GAP_MS = 150000
|
|
8342
|
+
/** 窗口内的随机延迟上限:避免"整点报时"的机械感,也挡住测试桩的立即定时器。 */
|
|
8343
|
+
const CHATTER_JITTER_MS = 90000
|
|
8344
|
+
/** 最近一次互动时刻(戳/拖/切宠/改名/加油都算);0 = 尚未发生过。 */
|
|
8345
|
+
let petLastInteractAt = 0
|
|
8346
|
+
/** 闲聊的单句柄;0 = 无排程。 */
|
|
8347
|
+
let petChatterTimer = 0
|
|
8348
|
+
|
|
8349
|
+
/** 打字回头:两次反应至少隔 15s;监听只此一个。 */
|
|
8350
|
+
const PET_TYPE_REACT_MS = 15000
|
|
8351
|
+
let petLastTypingAt = 0
|
|
8352
|
+
|
|
8353
|
+
/**
|
|
8354
|
+
* "模型正在生成"的 DOM 信号:composer 主按钮在生成中会把 aria-label 置为
|
|
8355
|
+
* 「停止生成」/「Stop generating」(app.asar 内嵌 InputBar 实测,无哈希、
|
|
8356
|
+
* 框架有意设置的语义属性 —— SKILL §13 认可的稳定钩子类型)。
|
|
8357
|
+
* 查不到 = 未在生成(或界面改版),两种都按"闲"处理,静默降级。
|
|
8358
|
+
*/
|
|
8359
|
+
const PET_WORKING_SELECTORS = [
|
|
8360
|
+
'button[aria-label="停止生成"]',
|
|
8361
|
+
'button[aria-label="Stop generating"]',
|
|
8362
|
+
]
|
|
8363
|
+
/** 最近一次确认"在生成"的时刻;用于"忙过又闲下来"才庆祝。 */
|
|
8364
|
+
let petLastWorkedAt = 0
|
|
8365
|
+
|
|
8366
|
+
/**
|
|
8367
|
+
* 记一次互动并重置闲聊计时。三机制共用的时间轴:宠物"和你玩过"之后,
|
|
8368
|
+
* 悄悄话才愿意再等一会儿。
|
|
8369
|
+
*/
|
|
8370
|
+
function petNoteInteraction() {
|
|
8371
|
+
petLastInteractAt = Date.now()
|
|
8372
|
+
schedulePetChatter()
|
|
8373
|
+
}
|
|
8374
|
+
|
|
8375
|
+
/** 排一次闲聊(先清旧句柄;舞台不在就不排)。 */
|
|
8376
|
+
function schedulePetChatter() {
|
|
8377
|
+
if (petStage === null) return
|
|
8378
|
+
if (typeof window.clearTimeout === 'function') window.clearTimeout(petChatterTimer)
|
|
8379
|
+
const base = petLastInteractAt === 0 ? CHATTER_FIRST_MS : CHATTER_GAP_MS
|
|
8380
|
+
const wait = base + Math.floor(Math.random() * CHATTER_JITTER_MS)
|
|
8381
|
+
petChatterTimer = window.setTimeout(petChatterTick, wait)
|
|
8382
|
+
}
|
|
8383
|
+
|
|
8384
|
+
/**
|
|
8385
|
+
* 闲聊到点。三道闸都过才冒泡:舞台在、宠物待机、页面可见、距最近互动 ≥ 首闸。
|
|
8386
|
+
* 时间闸同时是**测试闸**:桩的 drive() 会把定时器立即执行,而桩里只过去了几毫秒,
|
|
8387
|
+
* 闸门挡住 → 测试不会看到伪闲聊(断言见 check-boot-path)。
|
|
8388
|
+
*/
|
|
8389
|
+
function petChatterTick() {
|
|
8390
|
+
petChatterTimer = 0
|
|
8391
|
+
if (petStage === null) return
|
|
8392
|
+
if (petRun.mode !== 'idle') { schedulePetChatter(); return }
|
|
8393
|
+
if (typeof document !== 'undefined' && document.visibilityState === 'hidden') {
|
|
8394
|
+
schedulePetChatter()
|
|
8395
|
+
return
|
|
8396
|
+
}
|
|
8397
|
+
const idleFor = Date.now() - petLastInteractAt
|
|
8398
|
+
if (petLastInteractAt === 0 || idleFor < CHATTER_FIRST_MS) { schedulePetChatter(); return }
|
|
8399
|
+
const kind = petKindById(petRun.kindId)
|
|
8400
|
+
const line = kind.chat[Math.floor(Math.random() * kind.chat.length)]
|
|
8401
|
+
if (typeof line === 'string' && line !== '') petBubble(line)
|
|
8402
|
+
petLastInteractAt = Date.now()
|
|
8403
|
+
schedulePetChatter()
|
|
8404
|
+
}
|
|
8405
|
+
|
|
8406
|
+
/** 是否检出"生成中"信号(找不到 = false,绝不抛错)。 */
|
|
8407
|
+
function petWorkingSignal() {
|
|
8408
|
+
if (typeof document === 'undefined' || typeof document.querySelector !== 'function') return false
|
|
8409
|
+
try {
|
|
8410
|
+
for (const selector of PET_WORKING_SELECTORS) {
|
|
8411
|
+
if (document.querySelector(selector) !== null) return true
|
|
8412
|
+
}
|
|
8413
|
+
} catch {
|
|
8414
|
+
return false
|
|
8415
|
+
}
|
|
8416
|
+
return false
|
|
8417
|
+
}
|
|
8418
|
+
|
|
8419
|
+
/** 生成开始:冒加油台词 + 原地小跳两下(CSS 两次迭代,无 JS 循环)。 */
|
|
8420
|
+
function petCheer() {
|
|
8421
|
+
if (petStage === null || petRun.mode === 'drag') return
|
|
8422
|
+
petNoteInteraction()
|
|
8423
|
+
petBubble(petKindById(petRun.kindId).cheer)
|
|
8424
|
+
petFlash('dsh-pet-cheer', 1700)
|
|
8425
|
+
}
|
|
8426
|
+
|
|
8427
|
+
/** 生成结束(忙过又闲下来):冒完成台词 + 小跳一下。 */
|
|
8428
|
+
function petCelebrate() {
|
|
8429
|
+
if (petStage === null || petRun.mode === 'drag') return
|
|
8430
|
+
petNoteInteraction()
|
|
8431
|
+
petBubble(petKindById(petRun.kindId).done)
|
|
8432
|
+
petFlash('dsh-pet-cheer', 900)
|
|
8433
|
+
}
|
|
8434
|
+
|
|
8435
|
+
/** 在宠物身上挂一个短时类名(挂/摘各一次,句柄入 petTimers,拆台统一清)。 */
|
|
8436
|
+
function petFlash(cls, ms) {
|
|
8437
|
+
if (petRefs.actor === null) return
|
|
8438
|
+
petRefs.actor.classList.add(cls)
|
|
8439
|
+
petTimers.push(window.setTimeout(() => {
|
|
8440
|
+
if (petRefs.actor !== null) petRefs.actor.classList.remove(cls)
|
|
8441
|
+
}, ms))
|
|
8442
|
+
}
|
|
8443
|
+
|
|
8444
|
+
/**
|
|
8445
|
+
* 打字回头:用户在聊天输入框里敲字(节流 15s 内不重复)→ 宠物转身面向输入框,
|
|
8446
|
+
* 精神一下。反应是锦上添花,任何失败都吞掉保持安静。
|
|
8447
|
+
*/
|
|
8448
|
+
function petOnTyping(event) {
|
|
8449
|
+
try {
|
|
8450
|
+
if (petStage === null || petRun.mode === 'drag') return
|
|
8451
|
+
const target = event?.target
|
|
8452
|
+
if (target === undefined || target === null) return
|
|
8453
|
+
const isComposer = target.isContentEditable === true || target.tagName === 'TEXTAREA'
|
|
8454
|
+
if (!isComposer) return
|
|
8455
|
+
const now = Date.now()
|
|
8456
|
+
if (now - petLastTypingAt < PET_TYPE_REACT_MS) return
|
|
8457
|
+
petLastTypingAt = now
|
|
8458
|
+
const box = petAnchorBox()
|
|
8459
|
+
if (box !== null && petRefs.actor !== null) {
|
|
8460
|
+
const kind = petKindById(petRun.kindId)
|
|
8461
|
+
const centre = petRun.x + petDisplaySize(kind).w / 2
|
|
8462
|
+
const nextDir = (box.left + box.right) / 2 > centre ? 1 : -1
|
|
8463
|
+
if (nextDir !== petRun.dir) {
|
|
8464
|
+
petRun.dir = nextDir
|
|
8465
|
+
applyPetPositions()
|
|
8466
|
+
}
|
|
8467
|
+
}
|
|
8468
|
+
petFlash('dsh-pet-perk', 900)
|
|
8469
|
+
} catch {
|
|
8470
|
+
// 反应失败不值得打扰任何人。
|
|
8471
|
+
}
|
|
8472
|
+
}
|
|
8473
|
+
|
|
8474
|
+
/**
|
|
8475
|
+
* 拆掉舞台与其样式表,清空运行态。
|
|
8476
|
+
*
|
|
8477
|
+
* 定时器、闲聊句柄、document 监听一并清掉:任何一句话都不许对已拆除的节点说。
|
|
8478
|
+
*/
|
|
8479
|
+
function removePetStage() {
|
|
8480
|
+
for (const handle of petTimers) window.clearTimeout(handle)
|
|
8481
|
+
petTimers = []
|
|
8482
|
+
if (typeof window.clearTimeout === 'function') window.clearTimeout(petChatterTimer)
|
|
8483
|
+
petChatterTimer = 0
|
|
8484
|
+
if (typeof document !== 'undefined' && typeof document.removeEventListener === 'function') {
|
|
8485
|
+
document.removeEventListener('input', petOnTyping, true)
|
|
8486
|
+
}
|
|
8487
|
+
if (petStage !== null) petStage.remove()
|
|
8488
|
+
petStage = null
|
|
8489
|
+
for (const key of Object.keys(petRefs)) petRefs[key] = null
|
|
8490
|
+
petTrailCount = 0
|
|
8491
|
+
petBubbleCount = 0
|
|
8492
|
+
petRun.mode = 'idle'
|
|
8493
|
+
petRun.dragging = null
|
|
8494
|
+
petRun.anchorKey = ''
|
|
8495
|
+
petRun.lastAt = 0
|
|
8496
|
+
petRun.kindId = ''
|
|
8497
|
+
petRun.lift = 0
|
|
8498
|
+
petRun.hopFrac = 0
|
|
8499
|
+
petLastInteractAt = 0
|
|
8500
|
+
petLastTypingAt = 0
|
|
8501
|
+
// 注意:petRun.working / petLastWorkedAt / everWorked **不**随舞台重置 ——
|
|
8502
|
+
// "模型在不在生成"是对应用的观察,不是舞台的状态;重置它会让换宠/重搭期间的
|
|
8503
|
+
// 边沿(尤其忙→闲的完成庆祝)静默丢失。陈旧触发由庆祝的 60s 时效窗挡住。
|
|
8504
|
+
}
|
|
8505
|
+
|
|
8506
|
+
/** 把宠物与道具画到它们该在的位置(transform-only,不触发布局;hop 抬升只进显示)。 */
|
|
8507
|
+
function applyPetPositions() {
|
|
8508
|
+
if (petRefs.actor === null || petRefs.prop === null || petRefs.flip === null) return
|
|
8509
|
+
const kind = petKindById(petRun.kindId)
|
|
8510
|
+
petRefs.actor.style.transform = `translate(${Math.round(petRun.x)}px,${Math.round(petRun.y - petRun.lift)}px)`
|
|
8511
|
+
petRefs.prop.style.transform
|
|
8512
|
+
= `translate(${Math.round(petRun.propX - petPropDisplaySize(kind).w / 2)}px,${Math.round(petRun.propY)}px)`
|
|
8513
|
+
// 镜像写在独立的 flip 层 —— 绝不与跑步动画同层(动画会覆盖内联 transform,
|
|
8514
|
+
// 那正是"朝左跑却倒着跑"的事故根因)。
|
|
8515
|
+
petRefs.flip.style.transform = petRun.dir === 1 ? 'none' : 'scaleX(-1)'
|
|
8516
|
+
}
|
|
8517
|
+
|
|
8518
|
+
/**
|
|
8519
|
+
* 跑动类名按步态挂:run → 腿部摆动 + 起伏;waddle → 身体左右摇;
|
|
8520
|
+
* hop 不挂循环类(起伏由运行时驱动,落地节奏即脚步)。
|
|
8521
|
+
*/
|
|
8522
|
+
function petMotionClass() {
|
|
8523
|
+
return petKindById(petRun.kindId).motion === 'waddle' ? 'dsh-pet-waddle' : 'dsh-pet-run'
|
|
8524
|
+
}
|
|
8525
|
+
|
|
8526
|
+
/** 开始跑向道具(幂等:跑动中 / 拖拽中 / 已在道具旁都不会白跑一轮)。 */
|
|
8527
|
+
function petStartRun() {
|
|
8528
|
+
if (petStage === null || petRun.mode === 'run' || petRun.mode === 'drag') return
|
|
8529
|
+
if (petDistanceToProp() <= 5) {
|
|
8530
|
+
petRun.mode = 'idle'
|
|
8531
|
+
return
|
|
8532
|
+
}
|
|
8533
|
+
petRun.mode = 'run'
|
|
8534
|
+
petRun.trailDist = 0
|
|
8535
|
+
petRun.hopFrac = 0
|
|
8536
|
+
petRun.lift = 0
|
|
8537
|
+
petRun.lastAt = 0
|
|
8538
|
+
petRefs.actor.classList.add(petMotionClass())
|
|
8539
|
+
// 复用工厂级单帧助手 step():本文件的 requestAnimationFrame 预算因此不加一。
|
|
8540
|
+
step(petTick)
|
|
8541
|
+
}
|
|
8542
|
+
|
|
8543
|
+
/** 宠物(左上角)到道具(中心)的直线距离(显示坐标系)。 */
|
|
8544
|
+
function petDistanceToProp() {
|
|
8545
|
+
const kind = petKindById(petRun.kindId)
|
|
8546
|
+
return Math.hypot(petRun.propX - petRun.x,
|
|
8547
|
+
(petRun.propY + petPropDisplaySize(kind).h / 2) - (petRun.y + petDisplaySize(kind).h / 2))
|
|
8548
|
+
}
|
|
8549
|
+
|
|
8550
|
+
/** 停下(回到待机)。步态类名一并摘掉,腿/摇摆动画随之停止。 */
|
|
8551
|
+
function petStopRun() {
|
|
8552
|
+
if (petRefs.actor !== null) {
|
|
8553
|
+
petRefs.actor.classList.remove('dsh-pet-run')
|
|
8554
|
+
petRefs.actor.classList.remove('dsh-pet-waddle')
|
|
8555
|
+
}
|
|
8556
|
+
petRun.mode = 'idle'
|
|
8557
|
+
petRun.lastAt = 0
|
|
8558
|
+
petRun.lift = 0
|
|
8559
|
+
}
|
|
8560
|
+
|
|
8561
|
+
/**
|
|
8562
|
+
* 跑动的每一帧。只在 mode === 'run' 时自续,停下即断 —— 这条循环有界:
|
|
8563
|
+
* 终点是道具,距离单调收敛,超时(标签页隐藏时 rAF 自动暂停)也不会累积。
|
|
8564
|
+
* 移动是**二维**的:朝道具的直线方向走,头始终朝向它(水平分量定镜像)。
|
|
8565
|
+
* 三种步态在这里分岔:run/waddle 直线推进(后者靠 CSS 摇摆);hop 抛物线蹦跳
|
|
8566
|
+
* (hopFrac 是 0..1 的相位,lift 只影响显示坐标,落地一枚脚印)。
|
|
8567
|
+
*/
|
|
8568
|
+
function petTick() {
|
|
8569
|
+
if (petStage === null || petRun.mode !== 'run') return
|
|
8570
|
+
const kind = petKindById(petRun.kindId)
|
|
8571
|
+
const now = Date.now()
|
|
8572
|
+
const dt = petRun.lastAt === 0 ? 0.016 : Math.min((now - petRun.lastAt) / 1000, 0.064)
|
|
8573
|
+
// 防线:任何非有限数进来都立即停车并留痕 —— NaN 若混进位置,宠物会凭空消失,
|
|
8574
|
+
// 而"消失"没有任何报错。宁可停下来让面板/控制台说一句话。
|
|
8575
|
+
if (!Number.isFinite(dt) || !Number.isFinite(petRun.x) || !Number.isFinite(petRun.y)
|
|
8576
|
+
|| !Number.isFinite(petRun.propX) || !Number.isFinite(petRun.propY)) {
|
|
8577
|
+
console.error('[theme-gallery] pet run got a non-finite number; stopping the run loop.'
|
|
8578
|
+
+ ` dt=${dt} pet=(${petRun.x},${petRun.y}) prop=(${petRun.propX},${petRun.propY})`)
|
|
8579
|
+
petStopRun()
|
|
8580
|
+
return
|
|
8581
|
+
}
|
|
8582
|
+
petRun.lastAt = now
|
|
8583
|
+
const dx = petRun.propX - petRun.x
|
|
8584
|
+
const dy = (petRun.propY + petPropDisplaySize(kind).h / 2) - (petRun.y + petDisplaySize(kind).h / 2)
|
|
8585
|
+
const dist = Math.hypot(dx, dy)
|
|
8586
|
+
if (dist <= 5) {
|
|
8587
|
+
petStopRun()
|
|
8588
|
+
return
|
|
8589
|
+
}
|
|
8590
|
+
// 先调头再跑:水平朝向由水平分量决定(几乎竖直的短途不改变朝向)。
|
|
8591
|
+
if (dx > 1) petRun.dir = 1
|
|
8592
|
+
else if (dx < -1) petRun.dir = -1
|
|
8593
|
+
const pace = Math.min(kind.speed * dt, dist)
|
|
8594
|
+
petRun.x += (dx / dist) * pace
|
|
8595
|
+
petRun.y += (dy / dist) * pace
|
|
8596
|
+
if (kind.motion === 'hop') {
|
|
8597
|
+
// 一跳 = PET_HOP_LEN_PX 的水平路程:相位推进,抛物线抬升,落地留印。
|
|
8598
|
+
petRun.hopFrac += pace / PET_HOP_LEN_PX
|
|
8599
|
+
if (petRun.hopFrac >= 1) {
|
|
8600
|
+
petRun.hopFrac -= 1
|
|
8601
|
+
petRun.trailSide = -petRun.trailSide
|
|
8602
|
+
dropTrail()
|
|
8603
|
+
}
|
|
8604
|
+
petRun.lift = Math.round(Math.sin(Math.PI * Math.min(petRun.hopFrac, 1)) * PET_HOP_LIFT_PX)
|
|
8605
|
+
} else {
|
|
8606
|
+
petRun.lift = 0
|
|
8607
|
+
petRun.trailDist += pace
|
|
8608
|
+
if (petRun.trailDist >= kind.trail.everyPx) {
|
|
8609
|
+
petRun.trailDist = 0
|
|
8610
|
+
petRun.trailSide = -petRun.trailSide
|
|
8611
|
+
dropTrail()
|
|
8612
|
+
}
|
|
8613
|
+
}
|
|
8614
|
+
applyPetPositions()
|
|
8615
|
+
step(petTick)
|
|
8616
|
+
}
|
|
8617
|
+
|
|
8618
|
+
/**
|
|
8619
|
+
* 落一枚脚印(随物种换形状与颜色),落在宠物当前脚下;
|
|
8620
|
+
* 超时由 JS 兜底回收,活节点数有上限。
|
|
8621
|
+
*/
|
|
8622
|
+
function dropTrail() {
|
|
8623
|
+
if (petStage === null || petTrailCount >= PET_TRAIL_MAX) return
|
|
8624
|
+
const kind = petKindById(petRun.kindId)
|
|
8625
|
+
const display = petDisplaySize(kind)
|
|
8626
|
+
const mark = document.createElement('span')
|
|
8627
|
+
mark.className = 'dsh-pet-trailmark'
|
|
8628
|
+
mark.innerHTML = `<svg viewBox="0 0 ${kind.trail.w} ${kind.trail.h}" width="${kind.trail.w}"`
|
|
8629
|
+
+ ` height="${kind.trail.h}" aria-hidden="true">${kind.trail.art}</svg>`
|
|
8630
|
+
mark.style.transform = `translate(${Math.round(petRun.x + display.w / 2 + kind.trail.side * petRun.trailSide) - kind.trail.w / 2}px,`
|
|
8631
|
+
+ `${Math.round(petRun.y + display.h) - kind.trail.h + 1}px)`
|
|
8632
|
+
petStage.append(mark)
|
|
8633
|
+
petTrailCount += 1
|
|
8634
|
+
window.setTimeout(() => {
|
|
8635
|
+
mark.remove()
|
|
8636
|
+
petTrailCount = Math.max(0, petTrailCount - 1)
|
|
8637
|
+
}, PET_TRAIL_FADE_MS + 200)
|
|
8638
|
+
}
|
|
8639
|
+
|
|
8640
|
+
/** 冒一句气泡(叫声/闲聊/加油共用;文案是常量;数量有上限)。 */
|
|
8641
|
+
function petBubble(text) {
|
|
8642
|
+
if (petStage === null || petBubbleCount >= PET_BUBBLE_MAX) return
|
|
8643
|
+
const kind = petKindById(petRun.kindId)
|
|
8644
|
+
const bubble = document.createElement('span')
|
|
8645
|
+
bubble.className = 'dsh-pet-bubble'
|
|
8646
|
+
bubble.textContent = String(text)
|
|
8647
|
+
bubble.style.transform = `translate(${Math.round(petRun.x) + Math.round(petDisplaySize(kind).w) + 6}px,`
|
|
8648
|
+
+ `${Math.round(petRun.y) - 30}px)`
|
|
8649
|
+
petStage.append(bubble)
|
|
8650
|
+
petBubbleCount += 1
|
|
8651
|
+
window.setTimeout(() => {
|
|
8652
|
+
bubble.remove()
|
|
8653
|
+
petBubbleCount = Math.max(0, petBubbleCount - 1)
|
|
8654
|
+
}, 1250)
|
|
8655
|
+
}
|
|
8656
|
+
|
|
8657
|
+
/**
|
|
8658
|
+
* 戳一下:宠物转过身面对用户,说两句它的口头禅,然后该干嘛干嘛(刚才在跑就接着跑)。
|
|
8659
|
+
* react 期间再戳是空操作 —— 不然定时器会互相叠。
|
|
8660
|
+
*/
|
|
8661
|
+
function petReact() {
|
|
8662
|
+
if (petStage === null || petRun.mode === 'drag' || petRun.mode === 'react') return
|
|
8663
|
+
const kind = petKindById(petRun.kindId)
|
|
8664
|
+
petNoteInteraction()
|
|
8665
|
+
petRun.reactWasRunning = petRun.mode === 'run'
|
|
8666
|
+
petStopRun()
|
|
8667
|
+
petRun.mode = 'react'
|
|
8668
|
+
petRefs.actor.classList.add('dsh-pet-react')
|
|
8669
|
+
petBubble(kind.react.lines[0])
|
|
8670
|
+
petTimers.push(window.setTimeout(() => petBubble(kind.react.lines[1]), Math.round(kind.react.ms * 0.4)))
|
|
8671
|
+
petTimers.push(window.setTimeout(() => {
|
|
8672
|
+
// react 不可重入(mode 门在函数开头),所以此刻挂起的定时器只有这一组:
|
|
8673
|
+
// 直接清空即可,数组不会随戳的次数增长。
|
|
8674
|
+
petTimers = []
|
|
8675
|
+
if (petRefs.actor === null) return
|
|
8676
|
+
petRefs.actor.classList.remove('dsh-pet-react')
|
|
8677
|
+
petRun.mode = 'idle'
|
|
8678
|
+
if (petRun.reactWasRunning || petDistanceToProp() > 5) petStartRun()
|
|
8679
|
+
}, kind.react.ms))
|
|
8680
|
+
}
|
|
8681
|
+
|
|
8682
|
+
/** 点「改名」:名牌切换成输入框并钉住显示(不依赖悬停)。 */
|
|
8683
|
+
function petStartRename() {
|
|
8684
|
+
if (petRefs.tag === null || petRefs.input === null) return
|
|
8685
|
+
const kind = petKindById(petRun.kindId)
|
|
8686
|
+
petRefs.tag.classList.add('dsh-pet-editing')
|
|
8687
|
+
petRefs.input.value = readPetState().names[kind.id] ?? kind.home
|
|
8688
|
+
try {
|
|
8689
|
+
petRefs.input.focus()
|
|
8690
|
+
petRefs.input.select()
|
|
8691
|
+
} catch {
|
|
8692
|
+
// 无焦点可给(极端环境)就跳过;输入框仍在,点击即得焦点。
|
|
8693
|
+
}
|
|
8694
|
+
}
|
|
8695
|
+
|
|
8696
|
+
/** 提交改名:空值回退旧名,最长 10 字;textContent 写回,注入面为零;名字按宠记忆。 */
|
|
8697
|
+
function petCommitRename() {
|
|
8698
|
+
if (petRefs.tag === null || petRefs.input === null) return
|
|
8699
|
+
if (!petRefs.tag.classList.contains('dsh-pet-editing')) return
|
|
8700
|
+
petRefs.tag.classList.remove('dsh-pet-editing')
|
|
8701
|
+
const kind = petKindById(petRun.kindId)
|
|
8702
|
+
const state = readPetState()
|
|
8703
|
+
const current = state.names[kind.id] ?? kind.home
|
|
8704
|
+
const raw = String(petRefs.input.value ?? '').trim().slice(0, 10)
|
|
8705
|
+
const name = raw === '' ? current : raw
|
|
8706
|
+
savePetState({ names: { ...state.names, [kind.id]: name } })
|
|
8707
|
+
if (petRefs.name !== null) petRefs.name.textContent = name
|
|
8708
|
+
petNoteInteraction()
|
|
8709
|
+
}
|
|
8710
|
+
|
|
8711
|
+
/** 取消改名(Esc)。 */
|
|
8712
|
+
function petCancelRename() {
|
|
8713
|
+
if (petRefs.tag !== null) petRefs.tag.classList.remove('dsh-pet-editing')
|
|
8714
|
+
}
|
|
8715
|
+
|
|
8716
|
+
/**
|
|
8717
|
+
* 开始拖宠物 / 拖道具。指针捕获尽量用上(指针出了窗口也能收到 pointerup),
|
|
8718
|
+
* 事件监听挂在被捕获元素上、结束时成对摘除 —— 不给全局留下常驻监听。
|
|
8719
|
+
* @param event - pointerdown 事件(桩环境可能缺字段,逐项容错)。
|
|
8720
|
+
* @param which - 'actor' 或 'prop'。
|
|
8721
|
+
*/
|
|
8722
|
+
function petDragStart(event, which) {
|
|
8723
|
+
try {
|
|
8724
|
+
if (petStage === null || petRun.mode === 'react') return
|
|
8725
|
+
const point = petPointOf(event)
|
|
8726
|
+
if (point === null) return
|
|
8727
|
+
const target = event.currentTarget
|
|
8728
|
+
if (target === undefined || target === null) return
|
|
8729
|
+
// 先停跑、再进拖拽态 —— 顺序不能反:petStopRun 会把 mode 写回 idle,
|
|
8730
|
+
// 而宠物移动的每一帧都靠 mode === 'drag' 才跟手(反着写,拖动就静默失效)。
|
|
8731
|
+
petStopRun()
|
|
8732
|
+
petRun.mode = 'drag'
|
|
8733
|
+
petRun.dragging = which
|
|
8734
|
+
petRun.dragDX = point.x - (which === 'actor' ? petRun.x : petRun.propX)
|
|
8735
|
+
petRun.dragDY = point.y - (which === 'actor' ? petRun.y : petRun.propY)
|
|
8736
|
+
petStage.classList.add('dsh-pet-dragging')
|
|
8737
|
+
if (typeof target.setPointerCapture === 'function' && typeof event.pointerId === 'number') {
|
|
8738
|
+
try { target.setPointerCapture(event.pointerId) } catch { /* 指针可能已经释放 */ }
|
|
8739
|
+
}
|
|
8740
|
+
const move = (moveEvent) => petDragMove(moveEvent)
|
|
8741
|
+
const end = (endEvent) => {
|
|
8742
|
+
target.removeEventListener('pointermove', move)
|
|
8743
|
+
target.removeEventListener('pointerup', end)
|
|
8744
|
+
target.removeEventListener('pointercancel', end)
|
|
8745
|
+
petDragEnd(endEvent)
|
|
8746
|
+
}
|
|
8747
|
+
target.addEventListener('pointermove', move)
|
|
8748
|
+
target.addEventListener('pointerup', end)
|
|
8749
|
+
target.addEventListener('pointercancel', end)
|
|
8750
|
+
} catch (error) {
|
|
8751
|
+
console.error('[theme-gallery] pet drag could not start:', error)
|
|
8752
|
+
}
|
|
8753
|
+
}
|
|
8754
|
+
|
|
8755
|
+
/** 拖动中:全屏二维跟手(x / y 都自由,钳位只保证不出屏)。 */
|
|
8756
|
+
function petDragMove(event) {
|
|
8757
|
+
if (petRun.mode !== 'drag') return
|
|
8758
|
+
const point = petPointOf(event)
|
|
8759
|
+
if (point === null) return
|
|
8760
|
+
const viewport = petViewport()
|
|
8761
|
+
const kind = petKindById(petRun.kindId)
|
|
8762
|
+
const display = petDisplaySize(kind)
|
|
8763
|
+
const propDisplay = petPropDisplaySize(kind)
|
|
8764
|
+
if (petRun.dragging === 'actor') {
|
|
8765
|
+
// 宠物的 transform 用左上角坐标,钳位按左上角算(右/下各留出它的宽高)。
|
|
8766
|
+
petRun.x = petClampX(point.x - petRun.dragDX, 0, viewport.width - display.w)
|
|
8767
|
+
petRun.y = petClampX(point.y - petRun.dragDY, 0, viewport.height - display.h)
|
|
8768
|
+
} else {
|
|
8769
|
+
petRun.propX = petClampX(point.x - petRun.dragDX, propDisplay.w / 2, viewport.width)
|
|
8770
|
+
petRun.propY = petClampX(point.y - petRun.dragDY, 0, viewport.height - propDisplay.h)
|
|
8771
|
+
}
|
|
8772
|
+
applyPetPositions()
|
|
8773
|
+
}
|
|
8774
|
+
|
|
8775
|
+
/**
|
|
8776
|
+
* 松手:记住新位置(视口相对偏移,重启/缩放后自愈),然后宠物跑向道具
|
|
8777
|
+
* (拖道具同理 —— 道具到哪它追到哪,直线跑过去)。
|
|
8778
|
+
*/
|
|
8779
|
+
function petDragEnd() {
|
|
8780
|
+
if (petStage === null) return
|
|
8781
|
+
petStage.classList.remove('dsh-pet-dragging')
|
|
8782
|
+
const which = petRun.dragging
|
|
8783
|
+
petRun.dragging = null
|
|
8784
|
+
petRun.mode = 'idle'
|
|
8785
|
+
const viewport = petViewport()
|
|
8786
|
+
if (which === 'actor') {
|
|
8787
|
+
savePetState({
|
|
8788
|
+
placed: true,
|
|
8789
|
+
pet: { dx: Math.round(petRun.x - viewport.width), dy: Math.round(petRun.y - viewport.height) },
|
|
8790
|
+
})
|
|
8791
|
+
} else if (which === 'prop') {
|
|
8792
|
+
savePetState({
|
|
8793
|
+
placed: true,
|
|
8794
|
+
prop: { dx: Math.round(petRun.propX - viewport.width), dy: Math.round(petRun.propY - viewport.height) },
|
|
8795
|
+
})
|
|
8796
|
+
}
|
|
8797
|
+
petNoteInteraction()
|
|
8798
|
+
applyPetPositions()
|
|
8799
|
+
petStartRun()
|
|
8800
|
+
}
|
|
8801
|
+
|
|
8802
|
+
/**
|
|
8803
|
+
* 搭建舞台。结构节点全部用 createElement 逐个搭建(事件挂得上、桩环境找得到),
|
|
8804
|
+
* 美术件按注册表包裹注入(petSvg/petPropSvg);名字用 textContent 写入 ——
|
|
8805
|
+
* 用户输入永远不进 HTML 字符串。尺寸(每只宠物不同)写内联,CSS 只管布局。
|
|
8806
|
+
*/
|
|
8807
|
+
function buildPetStage() {
|
|
8808
|
+
const state = readPetState()
|
|
8809
|
+
const kind = petKindById(state.kind)
|
|
8810
|
+
const display = petDisplaySize(kind)
|
|
8811
|
+
const propDisplay = petPropDisplaySize(kind)
|
|
8812
|
+
const stage = document.createElement('div')
|
|
8813
|
+
stage.id = PET_STAGE_ID
|
|
8814
|
+
stage.dataset.plugin = 'theme-gallery'
|
|
8815
|
+
stage.dataset.petOwner = PET_OWNER
|
|
8816
|
+
stage.dataset.petKind = kind.id
|
|
8817
|
+
|
|
8818
|
+
const prop = document.createElement('div')
|
|
8819
|
+
prop.className = 'dsh-pet-prop'
|
|
8820
|
+
prop.style.width = `${propDisplay.w}px`
|
|
8821
|
+
prop.style.height = `${propDisplay.h}px`
|
|
8822
|
+
prop.innerHTML = petPropSvg(kind)
|
|
8823
|
+
|
|
8824
|
+
const actor = document.createElement('div')
|
|
8825
|
+
actor.className = 'dsh-pet-actor'
|
|
8826
|
+
actor.style.width = `${display.w}px`
|
|
8827
|
+
actor.style.height = `${display.h}px`
|
|
8828
|
+
|
|
8829
|
+
// flip 层(水平镜像,内联 transform)与 figure 层(跑步/跳跃动画)必须分开 ——
|
|
8830
|
+
// 同层时 CSS 动画覆盖内联 transform,宠物会朝左跑却头朝右(真机事故)。
|
|
8831
|
+
const flip = document.createElement('span')
|
|
8832
|
+
flip.className = 'dsh-pet-flip'
|
|
8833
|
+
const figure = document.createElement('span')
|
|
8834
|
+
figure.className = 'dsh-pet-figure'
|
|
8835
|
+
figure.innerHTML = petSvg(kind, 'side', 'dsh-pet-side') + petSvg(kind, 'front', 'dsh-pet-front')
|
|
8836
|
+
flip.append(figure)
|
|
8837
|
+
|
|
8838
|
+
const tag = document.createElement('span')
|
|
8839
|
+
tag.className = 'dsh-pet-tag'
|
|
8840
|
+
const name = document.createElement('span')
|
|
8841
|
+
name.className = 'dsh-pet-name'
|
|
8842
|
+
const actions = document.createElement('span')
|
|
8843
|
+
actions.className = 'dsh-pet-actions'
|
|
8844
|
+
const renameButton = document.createElement('button')
|
|
8845
|
+
renameButton.type = 'button'
|
|
8846
|
+
renameButton.className = 'dsh-pet-rename'
|
|
8847
|
+
renameButton.textContent = '改名'
|
|
8848
|
+
const pokeButton = document.createElement('button')
|
|
8849
|
+
pokeButton.type = 'button'
|
|
8850
|
+
pokeButton.className = 'dsh-pet-poke'
|
|
8851
|
+
pokeButton.textContent = '戳一下'
|
|
8852
|
+
const input = document.createElement('input')
|
|
8853
|
+
input.className = 'dsh-pet-input'
|
|
8854
|
+
input.maxLength = 10
|
|
8855
|
+
input.setAttribute('aria-label', `${kind.home}的名字`)
|
|
8856
|
+
actions.append(renameButton, pokeButton)
|
|
8857
|
+
tag.append(name, actions, input)
|
|
8858
|
+
actor.append(tag, flip)
|
|
8859
|
+
stage.append(prop, actor)
|
|
8860
|
+
|
|
8861
|
+
petRefs.actor = actor
|
|
8862
|
+
petRefs.prop = prop
|
|
8863
|
+
petRefs.flip = flip
|
|
8864
|
+
petRefs.figure = figure
|
|
8865
|
+
petRefs.tag = tag
|
|
8866
|
+
petRefs.name = name
|
|
8867
|
+
petRefs.input = input
|
|
8868
|
+
petRefs.actor.addEventListener('pointerdown', (event) => petDragStart(event, 'actor'))
|
|
8869
|
+
petRefs.prop.addEventListener('pointerdown', (event) => petDragStart(event, 'prop'))
|
|
8870
|
+
// 名牌里的按钮不许触发拖拽:按住"戳一下"把宠物拖着走,谁都会觉得坏了。
|
|
8871
|
+
for (const control of [renameButton, pokeButton, input]) {
|
|
8872
|
+
control.addEventListener('pointerdown', (event) => {
|
|
8873
|
+
if (event !== undefined && typeof event.stopPropagation === 'function') event.stopPropagation()
|
|
8874
|
+
})
|
|
8875
|
+
}
|
|
8876
|
+
renameButton.addEventListener('click', () => petStartRename())
|
|
8877
|
+
pokeButton.addEventListener('click', () => petReact())
|
|
8878
|
+
input.addEventListener('keydown', (event) => {
|
|
8879
|
+
if (event?.key === 'Enter') petCommitRename()
|
|
8880
|
+
else if (event?.key === 'Escape') petCancelRename()
|
|
8881
|
+
})
|
|
8882
|
+
input.addEventListener('blur', () => petCommitRename())
|
|
8883
|
+
name.textContent = state.names[kind.id] ?? kind.home
|
|
8884
|
+
petRun.kindId = kind.id
|
|
8885
|
+
|
|
8886
|
+
petStage = stage
|
|
8887
|
+
document.body.append(stage)
|
|
8888
|
+
// 活性机制随舞台一起起停:打字监听装上、闲聊排一次(时间闸见 petChatterTick)。
|
|
8889
|
+
if (typeof document !== 'undefined' && typeof document.addEventListener === 'function') {
|
|
8890
|
+
document.addEventListener('input', petOnTyping, true)
|
|
8891
|
+
}
|
|
8892
|
+
schedulePetChatter()
|
|
8893
|
+
}
|
|
8894
|
+
|
|
8895
|
+
/** 最近一次挂件同步的读数(面板诊断用,与 ambientReport 同一角色)。 */
|
|
8896
|
+
let petReport
|
|
8897
|
+
|
|
8898
|
+
/**
|
|
8899
|
+
* 把最近一次同步的读数写进 petReport。
|
|
8900
|
+
* @param entry - 读数片段(enabled / stage / display / anchor / 几何 / css / note)。
|
|
8901
|
+
*/
|
|
8902
|
+
function notePetReport(entry) {
|
|
8903
|
+
petReport = { ...entry }
|
|
8904
|
+
}
|
|
8905
|
+
|
|
8906
|
+
/** 面板上的挂件读数,一句话(与 describeAmbientReport 同一风格,刻意笨)。 */
|
|
8907
|
+
function describePetReport() {
|
|
8908
|
+
const r = petReport
|
|
8909
|
+
if (r === undefined) return '挂件[尚未同步]'
|
|
8910
|
+
const parts = [r.enabled === true ? '开' : '关', `舞台=${r.stage ?? '?'}`, `显示=${r.display === true ? '在' : '收'}`,
|
|
8911
|
+
`锚点=${r.anchor ?? '?'}`, `品种=${r.kind ?? '-'}`, `活动=${r.activity ?? '-'}`]
|
|
8912
|
+
if (r.right !== undefined) parts.push(`锚右=${r.right}`, `锚底=${r.bottom}`)
|
|
8913
|
+
// 地面= 独立输出:视口回退(锚点断)时恰恰最需要这个读数来核对抬量 ——
|
|
8914
|
+
// 旧实现把它挂在 锚右 那一支里,而 right 只在锚点找到时才写,回退时地面
|
|
8915
|
+
// 读数跟着一起消失(读数通道静默断裂,实机核对无从做起)。
|
|
8916
|
+
if (r.groundY !== undefined) parts.push(`地面=${r.groundY}`)
|
|
8917
|
+
if (r.x !== undefined) parts.push(`宠x=${r.x}`, `宠y=${r.y ?? '?'}`, `道具x=${r.propX}`, `道具y=${r.propY ?? '?'}`)
|
|
8918
|
+
parts.push(`样式=${r.css === true ? '在' : '缺'}`, `模式=${r.mode ?? '-'}`)
|
|
8919
|
+
if (r.note !== undefined) parts.push(r.note)
|
|
8920
|
+
return `挂件[${parts.join(' ')}]`
|
|
8921
|
+
}
|
|
8922
|
+
|
|
8923
|
+
/**
|
|
8924
|
+
* 面板上的挂件自检行。
|
|
8925
|
+
*
|
|
8926
|
+
* 挂件关闭时不产出行(不打扰);开启时的**警告**(舞台没建出 / 被收起 / 样式缺失)
|
|
8927
|
+
* 必须无条件到达面板 —— "开了但什么都不出现"正是本项目最典型的静默失败,
|
|
8928
|
+
* 这一行就是它的可观测出口。
|
|
8929
|
+
* @param petEnabled - store 里的挂件开关。
|
|
8930
|
+
* @returns 一行自检文字,或 null(挂件关闭)。
|
|
8931
|
+
*/
|
|
8932
|
+
function petLineOf(petEnabled) {
|
|
8933
|
+
try {
|
|
8934
|
+
if (petEnabled !== true) return null
|
|
8935
|
+
const r = petReport
|
|
8936
|
+
if (r === undefined || r.enabled !== true) return '⚠ 挂件已开启但尚未同步(syncPetStage 未运行)'
|
|
8937
|
+
if (r.stage === 'absent') return `⚠ 挂件未建出舞台:${r.note ?? '锚点(输入框)未找到'}`
|
|
8938
|
+
if (r.display !== true) return `⚠ 挂件舞台已建但被收起:锚点=${r.anchor}`
|
|
8939
|
+
if (r.css !== true) return '⚠ 挂件样式表未生效(PET_CSS 不在文档中)'
|
|
8940
|
+
return `挂件自检通过 · ${describePetReport()}`
|
|
8941
|
+
} catch (error) {
|
|
8942
|
+
return `⚠ 挂件自检失败: ${String(error && error.message ? error.message : error)}`
|
|
8943
|
+
}
|
|
8944
|
+
}
|
|
8945
|
+
|
|
8946
|
+
/** 与 sceneryLineIsWarning 同一条判据:⚠ 开头或带"失败"的行不算例行汇报。 */
|
|
8947
|
+
function petLineIsWarning(line) {
|
|
8948
|
+
return typeof line === 'string' && (line.startsWith('⚠') || line.includes('失败'))
|
|
8949
|
+
}
|
|
8950
|
+
|
|
8951
|
+
/**
|
|
8952
|
+
* 挂件层的同步入口:resync(每帧至多一次,与 ambient 共用节流)与开关切换都会进来。
|
|
8953
|
+
*
|
|
8954
|
+
* 幂等判据在最前面(规则 3):几何指纹没变就一个字节都不写。输入框不在屏幕上
|
|
8955
|
+
* (比如正看着别的面板)时挂件收起,等它回来 —— 收起不算状态变化。
|
|
8956
|
+
* 每一条退出路径都写 petReport:挂件的失败形态就是"什么都不出现",没有读数就永远查不下去。
|
|
8957
|
+
*
|
|
8958
|
+
* 二期新增两件事,都在一切写入之前完成判定:
|
|
8959
|
+
* · **换宠物**:localStorage 里的 kind 与舞台当前的 kindId 不一致 → 拆旧舞台、
|
|
8960
|
+
* 按新宠物重建(缺省摆位 + 出场跑向道具由既有逻辑自然发生)。
|
|
8961
|
+
* · **生成中信号**(petWorkingSignal)在这里顺路检测 —— resync 本来就随 DOM
|
|
8962
|
+
* 变化节流触发,不需要新观察器;上升沿加油、下降沿(忙过之后)庆祝。
|
|
8963
|
+
*/
|
|
8964
|
+
function syncPetStage() {
|
|
8965
|
+
if (typeof document === 'undefined') return
|
|
8966
|
+
// 急停开关与 ambient 同源:config.ambient:false 把这层装饰一并停掉。
|
|
8967
|
+
if (!petRun.enabled || !ambientEnabled()) {
|
|
8968
|
+
if (petStage !== null) removePetStage()
|
|
8969
|
+
notePetReport({ enabled: petRun.enabled, stage: 'absent', display: false, 'anchor': '-', note: ambientEnabled() ? '开关为关' : '急停开关(ambient:false)' })
|
|
8970
|
+
return
|
|
8971
|
+
}
|
|
8972
|
+
const state = readPetState()
|
|
8973
|
+
const kind = petKindById(state.kind)
|
|
8974
|
+
const viewport = petViewport()
|
|
8975
|
+
if (petRun.firstSyncAt === 0) petRun.firstSyncAt = Date.now()
|
|
8976
|
+
// 地面线:锚点(输入框)找到时贴它的下缘;找不到回退到屏幕底缘**抬高一段**
|
|
8977
|
+
// (实机验收:主题面板页 composer 隐藏 → 视口回退 → 宠物贴着窗口最底边被裁掉
|
|
8978
|
+
// 一截。抬高 48px 让它完整可见;输入框在屏上时本行一个像素都不变)。
|
|
8979
|
+
// 自检行有 锚点=视口回退 与 地面= 两个读数,实机可直接核对回退边距。
|
|
8980
|
+
const anchor = petAnchorBox()
|
|
8981
|
+
const anchorKind = anchor === null ? '视口回退' : 'found'
|
|
8982
|
+
const anchorRead = anchor === null ? {} : { right: Math.round(anchor.right), bottom: Math.round(anchor.bottom) }
|
|
8983
|
+
const groundTop = anchor === null ? viewport.height - PET_FALLBACK_GROUND_GAP : anchor.bottom
|
|
8984
|
+
petRun.groundY = Math.min(groundTop - 2, viewport.height - 6)
|
|
8985
|
+
ensurePetStylesheet()
|
|
8986
|
+
// 换宠物:不是当前舞台这只 → 拆掉重建(removePetStage 会清干净定时器与监听)。
|
|
8987
|
+
if (petStage !== null && petRun.kindId !== kind.id) removePetStage()
|
|
8988
|
+
// 热重载会把上一个实例的舞台留在 body 上:不是本实例属主的直接清扫(ambient 同款)。
|
|
8989
|
+
for (const stray of document.querySelectorAll(`#${PET_STAGE_ID}`)) {
|
|
8990
|
+
if (stray.dataset.petOwner !== PET_OWNER) stray.remove()
|
|
8991
|
+
}
|
|
8992
|
+
if (petStage === null) buildPetStage()
|
|
8993
|
+
if (petStage === null) {
|
|
8994
|
+
notePetReport({ enabled: true, stage: 'absent', display: false, anchor: anchorKind, note: '舞台搭建失败(见控制台)' })
|
|
8995
|
+
return
|
|
8996
|
+
}
|
|
8997
|
+
petStage.style.display = ''
|
|
8998
|
+
// 生成中信号:边缘检测(闲→忙加油;忙→闲且忙过才庆祝)。放在舞台建成**之后**:
|
|
8999
|
+
// 边沿若恰好落在"舞台刚被拆掉待重建"的那次同步里(比如换宠物),在前面检测就会
|
|
9000
|
+
// 静默丢失这次加油。任何检测失败都按"闲"处理,静默降级。
|
|
9001
|
+
const working = petWorkingSignal()
|
|
9002
|
+
if (working !== petRun.working) {
|
|
9003
|
+
petRun.working = working
|
|
9004
|
+
if (working) petCheer()
|
|
9005
|
+
// 60s 时效窗:忙过的记录太旧(比如挂件关了一阵再开)就不庆祝,
|
|
9006
|
+
// 那不是"刚完成的对话"。
|
|
9007
|
+
else if (petLastWorkedAt !== 0 && Date.now() - petLastWorkedAt < 60000) petCelebrate()
|
|
9008
|
+
}
|
|
9009
|
+
if (working) {
|
|
9010
|
+
petRun.everWorked = true
|
|
9011
|
+
petLastWorkedAt = Date.now()
|
|
9012
|
+
} else {
|
|
9013
|
+
petLastWorkedAt = 0
|
|
9014
|
+
}
|
|
9015
|
+
const key = [
|
|
9016
|
+
Math.round(viewport.width), Math.round(viewport.height),
|
|
9017
|
+
Math.round(petRun.groundY), state.placed === true, kind.id,
|
|
9018
|
+
].join('|')
|
|
9019
|
+
const shownNow = petStage.style.display !== 'none'
|
|
9020
|
+
const cssReady = ambientStylesheetReady()
|
|
9021
|
+
&& document.querySelector('style[data-plugin-css="theme-gallery/pet"]') !== null
|
|
9022
|
+
const activity = petRun.everWorked === false && Date.now() - petRun.firstSyncAt > 300000
|
|
9023
|
+
? '未检出'
|
|
9024
|
+
: (petRun.working ? '生成中' : '闲')
|
|
9025
|
+
const kindNote = stateKindFallback ? '注=kind回落' : undefined
|
|
9026
|
+
const withNote = (base) => [base, kindNote].filter(Boolean).join(' ')
|
|
9027
|
+
if (key === petRun.anchorKey) {
|
|
9028
|
+
notePetReport({
|
|
9029
|
+
enabled: true, stage: 'built', display: shownNow, anchor: anchorKind,
|
|
9030
|
+
kind: kind.id, activity, ...anchorRead,
|
|
9031
|
+
groundY: Math.round(petRun.groundY),
|
|
9032
|
+
x: Math.round(petRun.x), y: Math.round(petRun.y),
|
|
9033
|
+
propX: Math.round(petRun.propX), propY: Math.round(petRun.propY),
|
|
9034
|
+
css: cssReady, mode: petRun.mode, note: withNote('几何未变化'),
|
|
9035
|
+
})
|
|
9036
|
+
return
|
|
9037
|
+
}
|
|
9038
|
+
petRun.anchorKey = key
|
|
9039
|
+
// 拖拽中位置归指针管:几何变化(如拖动的同时改了窗口)等松手后再算,
|
|
9040
|
+
// 否则这里改写位置会和指针打架,松手时还会存下错的偏移。
|
|
9041
|
+
if (petRun.mode === 'drag') {
|
|
9042
|
+
notePetReport({
|
|
9043
|
+
enabled: true, stage: 'built', display: true, anchor: anchorKind,
|
|
9044
|
+
kind: kind.id, activity, ...anchorRead,
|
|
9045
|
+
groundY: Math.round(petRun.groundY), css: cssReady, mode: petRun.mode, note: withNote('拖拽中:位置归指针管'),
|
|
9046
|
+
})
|
|
9047
|
+
return
|
|
9048
|
+
}
|
|
9049
|
+
if (state.placed) {
|
|
9050
|
+
// 用户拖过:以视口右/下缘的偏移还原(窗口缩放后自愈),只保证不出屏。
|
|
9051
|
+
const display = petDisplaySize(kind)
|
|
9052
|
+
const propDisplay = petPropDisplaySize(kind)
|
|
9053
|
+
petRun.propX = petClampX(viewport.width + state.prop.dx, propDisplay.w / 2, viewport.width)
|
|
9054
|
+
petRun.propY = petClampX(viewport.height + state.prop.dy, 0, viewport.height - propDisplay.h)
|
|
9055
|
+
if (petRun.mode === 'idle') {
|
|
9056
|
+
petRun.x = petClampX(viewport.width + state.pet.dx, 0, viewport.width - display.w)
|
|
9057
|
+
petRun.y = petClampX(viewport.height + state.pet.dy, 0, viewport.height - display.h)
|
|
9058
|
+
}
|
|
9059
|
+
} else {
|
|
9060
|
+
// 缺省摆位(每只宠物自带):道具在宠物**左侧**、宠物**面朝左**看着玩具,
|
|
9061
|
+
// 两者都站在地面线上;水平位置锚定屏幕右缘(输入框右侧的空白区域)。
|
|
9062
|
+
const display = petDisplaySize(kind)
|
|
9063
|
+
const propDisplay = petPropDisplaySize(kind)
|
|
9064
|
+
petRun.propX = petClampX(viewport.width + kind.place.prop.dx, propDisplay.w / 2, viewport.width)
|
|
9065
|
+
petRun.propY = petRun.groundY - propDisplay.h
|
|
9066
|
+
if (petRun.mode === 'idle') {
|
|
9067
|
+
petRun.x = petClampX(viewport.width + kind.place.pet.dx, 0, viewport.width - display.w)
|
|
9068
|
+
petRun.y = petRun.groundY - display.h
|
|
9069
|
+
}
|
|
9070
|
+
}
|
|
9071
|
+
applyPetPositions()
|
|
9072
|
+
// 开局(或几何变化后)离道具远就跑过去 —— 也是它每次启用的小入场。
|
|
9073
|
+
if (petRun.mode === 'idle' && petDistanceToProp() > 6) petStartRun()
|
|
9074
|
+
notePetReport({
|
|
9075
|
+
enabled: true, stage: 'built', display: true, anchor: anchorKind,
|
|
9076
|
+
kind: kind.id, activity, ...anchorRead,
|
|
9077
|
+
groundY: Math.round(petRun.groundY),
|
|
9078
|
+
x: Math.round(petRun.x), y: Math.round(petRun.y),
|
|
9079
|
+
propX: Math.round(petRun.propX), propY: Math.round(petRun.propY),
|
|
9080
|
+
css: cssReady, mode: petRun.mode, note: kindNote,
|
|
9081
|
+
})
|
|
9082
|
+
}
|
|
9083
|
+
|
|
9084
|
+
/**
|
|
9085
|
+
* Report whether the accent token layer actually reached the theme snapshot.
|
|
9086
|
+
*
|
|
9087
|
+
* ── WHY THIS READING IS NEEDED ───────────────────────────────────────────
|
|
9088
|
+
*
|
|
9089
|
+
* `syncAccent` stacks three tokens (`button-ghost-active-fill` / `-border` / `-hover`) with
|
|
9090
|
+
* `ctx.theme.overrideTokens`. Whether that layer works had **never been verified**, and one
|
|
9091
|
+
* earlier attempt to verify it used the wrong evidence: the active workspace FOLDER icon
|
|
9092
|
+
* turns the accent colour, which was taken as proof — but that icon is painted by the skin's
|
|
9093
|
+
* own `--dsw-alias-state-business-primary` token and has nothing to do with the layer.
|
|
9094
|
+
*
|
|
9095
|
+
* The service makes this checkable without any guesswork: `composeActive` folds every
|
|
9096
|
+
* override layer into `snapshot.active.tokens` before publishing, so the composed value is
|
|
9097
|
+
* readable straight off `getTheme()`.
|
|
9098
|
+
*
|
|
9099
|
+
* ── WHAT CAN AND CANNOT PROVE IT ─────────────────────────────────────────
|
|
9100
|
+
*
|
|
9101
|
+
* The composed VALUE is the only channel that carries information, and only when it differs
|
|
9102
|
+
* from what the skin declares for that same token:
|
|
9103
|
+
*
|
|
9104
|
+
* • both bundled skins already ship `button-ghost-active-*` in their own palettes, AND the
|
|
9105
|
+
* palette layer supplies them too — so the token COUNT is 67 either way. An earlier
|
|
9106
|
+
* comment here claimed a count that "does not move" disproves the layer; for these skins
|
|
9107
|
+
* it cannot move, so the claim was wrong and the count is reported for context only;
|
|
9108
|
+
* • 梦海游鱼 declares `#2B74B5` for that token but its accent is `#FFD166`, so reading back
|
|
9109
|
+
* `#FFD166` proves the accent layer composed OVER the palette — the palette alone cannot
|
|
9110
|
+
* produce it;
|
|
9111
|
+
* • 山青婷彩's accent is `#E88BB0` and it declares `#E88BB0` for the same token, so its
|
|
9112
|
+
* reading is identical whether or not the layer is there. That is a property of the SKIN,
|
|
9113
|
+
* not of the layer, and the verdict below says so instead of implying the reading proved
|
|
9114
|
+
* something.
|
|
9115
|
+
*
|
|
9116
|
+
* The verdict is therefore computed, never asserted: it compares the composed value with the
|
|
9117
|
+
* skin's declared value and with the accent the layer was built from.
|
|
9118
|
+
* @returns the reading, as one segment of the panel line.
|
|
9119
|
+
*/
|
|
9120
|
+
function describeAccentLayer() {
|
|
9121
|
+
try {
|
|
9122
|
+
const theme = ctx?.theme
|
|
9123
|
+
if (theme === undefined) return '激活层[无 theme 服务]'
|
|
9124
|
+
const snapshot = theme.getTheme()
|
|
9125
|
+
const active = snapshot?.active
|
|
9126
|
+
const tokens = active?.tokens ?? {}
|
|
9127
|
+
const TOKEN = '--dsw-alias-button-ghost-active-border'
|
|
9128
|
+
const composed = tokens[TOKEN]
|
|
9129
|
+
const definition = typeof active?.id === 'string' ? bundledTheme(active.id) : undefined
|
|
9130
|
+
const declared = declaredToken(definition, TOKEN)
|
|
9131
|
+
const accent = definition?.accent
|
|
9132
|
+
const layers = snapshot?.overrides === undefined
|
|
9133
|
+
? '(服务未暴露)'
|
|
9134
|
+
: String(snapshot.overrides.size ?? snapshot.overrides.length ?? '?')
|
|
9135
|
+
// Computed, not assumed — see the note above. `不可分辨` is a real answer: it says this
|
|
9136
|
+
// skin's own value coincides with its accent, so this reading proves nothing either way.
|
|
9137
|
+
const verdict = accent === undefined
|
|
9138
|
+
? '非本包皮肤(无法判断)'
|
|
9139
|
+
: accent === declared
|
|
9140
|
+
? '不可分辨(强调色=皮肤自备值)'
|
|
9141
|
+
: composed === accent
|
|
9142
|
+
? '生效(强调色压过自备值)'
|
|
9143
|
+
: composed === declared
|
|
9144
|
+
? '未生效(仍是自备值)'
|
|
9145
|
+
: `未知(既非强调色也非自备值)`
|
|
9146
|
+
return `激活层[id=${active?.id ?? '?'}`
|
|
9147
|
+
+ ` 层数=${layers}`
|
|
9148
|
+
+ ` token数=${Object.keys(tokens).length}(自备同名令牌,不构成判据)`
|
|
9149
|
+
+ ` ${TOKEN.replace('--dsw-alias-', '')}=${composed === undefined ? '(缺失)' : String(composed)}`
|
|
9150
|
+
+ ` 自备=${declared === undefined ? '(无)' : String(declared)}`
|
|
9151
|
+
+ ` 强调色=${accent ?? '(无)'}`
|
|
9152
|
+
+ ` 判据=${verdict}]`
|
|
9153
|
+
} catch (error) {
|
|
9154
|
+
return `激活层[读取失败: ${String(error && error.message ? error.message : error)}]`
|
|
9155
|
+
}
|
|
9156
|
+
}
|
|
9157
|
+
|
|
9158
|
+
/* ═══════════════════ 心情问候语(标题栏跑马灯)═══════════════════
|
|
9159
|
+
*
|
|
9160
|
+
* 面板第 14 张卡「心情问候」控制的第三种非主题卡面(与宠物挂件同一模式):
|
|
9161
|
+
* 一条滚动的问候文字住在标题栏条内(应用/编辑菜单右侧、原生窗口按钮左侧的
|
|
9162
|
+
* 空拖拽带上),每 35 分钟换一条,语言料库里的正向短句。需求与决策记录见
|
|
9163
|
+
* docs/DESIGN-心情问候语-位置与语料方案.md,实施细节见 IMP-心情问候语-A/B/C。
|
|
9164
|
+
*
|
|
9165
|
+
* · 不写主题服务:开关、设置、选句指针全部存 localStorage,绝不碰 preference;
|
|
9166
|
+
* · 文字**只经 textContent** 进 DOM —— 语料虽是自家数据,注入面也按零算;
|
|
9167
|
+
* · 有界工作:一个链式 setTimeout(35 分钟换条)+ 一个 resize listener,
|
|
9168
|
+
* 没有轮询、没有 interval(自旋事故的家规);
|
|
9169
|
+
* · 座位是 shell.overlay 槽位里的一张静态 div(见 mountGallery 的槽位注册段),
|
|
9170
|
+
* 本模块命令式地往里建条;座位还没渲染时留读数等下一次 sync,不轮询。
|
|
9171
|
+
*/
|
|
9172
|
+
|
|
9173
|
+
/** 卡片数据:id、标题与 tooltip(卡面**不渲染** description,与配色/挂件卡同一条分支)。 */
|
|
9174
|
+
const MOOD_WIDGET = {
|
|
9175
|
+
id: 'mood-greeting',
|
|
9176
|
+
label: '心情问候',
|
|
9177
|
+
description: '标题栏上滚动的问候语:默认开启,35 分钟换一条。卡上可开关,'
|
|
9178
|
+
+ '并可调滚动速度(慢/中/快)、方向(左→右/右→左)与位置区域'
|
|
9179
|
+
+ '(居中/左靠齐/左右缩进/右靠齐)。它不改变当前主题皮肤,可与任何皮肤同时开启。',
|
|
9180
|
+
}
|
|
9181
|
+
|
|
9182
|
+
/** 问候的开关与三组设置记在这里。挂件同一条铁律:绝不写 `ui-theme.preference`。 */
|
|
9183
|
+
const MOOD_KEY = 'theme-gallery:mood'
|
|
9184
|
+
|
|
9185
|
+
/** 选句指针(当天消费到第几条)单独存:形状与用户设置无关,坏了自己愈合。 */
|
|
9186
|
+
const MOOD_POINTER_KEY = 'theme-gallery:mood-pointer'
|
|
9187
|
+
|
|
9188
|
+
/** 换条周期(用户定的 35 分钟)。 */
|
|
9189
|
+
const MOOD_PERIOD_MS = 35 * 60 * 1000
|
|
9190
|
+
|
|
9191
|
+
/** 滚动速度三档(px/s)。中速默认;数值是初值,实机看着不合适就调表——
|
|
9192
|
+
* 调了要同步改 check-mood 的常量断言。60 偏快(用户二轮返工),中速降到 50。 */
|
|
9193
|
+
const MOOD_SPEEDS = { slow: 30, medium: 50, fast: 100 }
|
|
9194
|
+
|
|
9195
|
+
/**
|
|
9196
|
+
* 位置区域的 5 等份映射(用户原话的几何化,DESIGN 第九节):
|
|
9197
|
+
* 区域 = 标题栏条减去左侧菜单后的水平段分成 5 等份,每一档占用其中几份。
|
|
9198
|
+
* 留空的等份是给"将来装的其他插件"的(用户原话)。
|
|
9199
|
+
*/
|
|
9200
|
+
const MOOD_ZONE_GEOMETRY = {
|
|
9201
|
+
left: { first: 1, parts: 4 }, // 左靠齐:占第 1–4 等份,右侧留 1
|
|
9202
|
+
center: { first: 1, parts: 5 }, // 居中(默认):占满
|
|
9203
|
+
indent: { first: 2, parts: 3 }, // 左右缩进:中间 3 等份,左右各留 1
|
|
9204
|
+
right: { first: 2, parts: 4 }, // 右靠齐:占第 2–5 等份,左侧留 1
|
|
9205
|
+
}
|
|
9206
|
+
|
|
9207
|
+
/**
|
|
9208
|
+
* 时段池边界(已确认):早安 05:00–11:00 / 白天 11:00–22:00 / 夜深 22:00–次日 05:00。
|
|
9209
|
+
* 夜深的 `to: 29` 是环形写法:小时数 <5 时先加 24(见 moodPoolName),24–28 落进夜深。
|
|
9210
|
+
*/
|
|
9211
|
+
const MOOD_TIME_POOLS = [
|
|
9212
|
+
{ pool: 'morning', from: 5, to: 11 },
|
|
9213
|
+
{ pool: 'day', from: 11, to: 22 },
|
|
9214
|
+
{ pool: 'night', from: 22, to: 29 },
|
|
9215
|
+
]
|
|
9216
|
+
|
|
9217
|
+
/**
|
|
9218
|
+
* 原生窗口控制按钮(最小化/最大化/关闭)的预留宽度。它们是 OS 层 overlay,
|
|
9219
|
+
* DOM 测不到,只能预留——135px 是按钮本身,**用户返工(2026-09-27 截图)**:
|
|
9220
|
+
* 文字要离右上角按钮"远一点",所以再让出 45px 呼吸位,共 180。
|
|
9221
|
+
*/
|
|
9222
|
+
const MOOD_WINBTN_RESERVE = 180
|
|
9223
|
+
|
|
9224
|
+
/**
|
|
9225
|
+
* 左端让位(用户二轮返工):侧栏分界线到问候文字的呼吸位;菜单右缘的间隙
|
|
9226
|
+
* (侧栏收起时才由菜单决定左端,此时多让一些)。兜底常量 = 展开时侧栏 280 + 呼吸位。
|
|
9227
|
+
*/
|
|
9228
|
+
const MOOD_SIDEBAR_GAP = 12
|
|
9229
|
+
const MOOD_MENU_GAP = 48
|
|
9230
|
+
const MOOD_FALLBACK_MENU_RIGHT = 292
|
|
9231
|
+
|
|
9232
|
+
/** shell.overlay 里那张静态座位的选择器(槽位注册段渲染),与问候条元素 id。 */
|
|
9233
|
+
const MOOD_SEAT_SELECTOR = '[data-dsh-mood-seat]'
|
|
9234
|
+
const MOOD_BAR_ID = 'dsh-mood-bar'
|
|
9235
|
+
|
|
9236
|
+
/** 语料分类枚举——与 embed-themes.mjs 的 MOOD_CATEGORIES 同一张表(建期已校验,运行期再认一遍)。 */
|
|
9237
|
+
const MOOD_CATEGORIES = ['morning', 'day', 'night', 'general', 'holiday']
|
|
9238
|
+
|
|
9239
|
+
/**
|
|
9240
|
+
* 运行期再消毒一遍内联语料(不信任模块级常量的形状)。
|
|
9241
|
+
* 手改过 client.js、或旧版本内联了坏数据时,坏条目**逐条丢弃**,
|
|
9242
|
+
* 好条目照用——而不是整体抛错把问候功能打死。
|
|
9243
|
+
* @param pack - 内联的 BUNDLED_MOOD_LINES。
|
|
9244
|
+
* @returns 消毒后的行数组(可能为空;空库由 moodLineOf 报警告)。
|
|
9245
|
+
*/
|
|
9246
|
+
function sanitizeMoodLines(pack) {
|
|
9247
|
+
const source = pack !== null && typeof pack === 'object' && Array.isArray(pack.lines) ? pack.lines : []
|
|
9248
|
+
const out = []
|
|
9249
|
+
for (const line of source) {
|
|
9250
|
+
if (line === null || typeof line !== 'object') continue
|
|
9251
|
+
if (typeof line.text !== 'string' || line.text.length === 0 || line.text.length > 200) continue
|
|
9252
|
+
if (!MOOD_CATEGORIES.includes(line.category)) continue
|
|
9253
|
+
if (typeof line.id !== 'string' || line.id === '') continue
|
|
9254
|
+
const clean = {
|
|
9255
|
+
id: line.id,
|
|
9256
|
+
seq: typeof line.seq === 'number' && Number.isFinite(line.seq) ? line.seq : 0,
|
|
9257
|
+
category: line.category,
|
|
9258
|
+
text: line.text,
|
|
9259
|
+
}
|
|
9260
|
+
if (line.category === 'holiday'
|
|
9261
|
+
&& line.holiday !== null && typeof line.holiday === 'object'
|
|
9262
|
+
&& typeof line.holiday.from === 'string' && typeof line.holiday.to === 'string') {
|
|
9263
|
+
clean.holiday = { from: line.holiday.from, to: line.holiday.to }
|
|
9264
|
+
}
|
|
9265
|
+
out.push(clean)
|
|
9266
|
+
}
|
|
9267
|
+
return out
|
|
9268
|
+
}
|
|
9269
|
+
|
|
9270
|
+
/** 消毒副本:mood 运行区只读这一份,永远不直接摸 BUNDLED_MOOD_LINES。 */
|
|
9271
|
+
const MOOD_LINES = sanitizeMoodLines(BUNDLED_MOOD_LINES)
|
|
9272
|
+
|
|
9273
|
+
/**
|
|
9274
|
+
* 读用户设置。localStorage 里的东西不可信(readPetState 同款理由):
|
|
9275
|
+
* 逐字段消毒,坏字段回缺省;**缺省是开**(用户定的"默认开")。
|
|
9276
|
+
* @returns `{ on, speed, direction, zone }`。
|
|
9277
|
+
*/
|
|
9278
|
+
function readMoodState() {
|
|
9279
|
+
const fallback = { on: true, speed: 'medium', direction: 'ltr', zone: 'center' }
|
|
9280
|
+
try {
|
|
9281
|
+
const raw = window.localStorage?.getItem(MOOD_KEY)
|
|
9282
|
+
if (typeof raw !== 'string' || raw === '') return fallback
|
|
9283
|
+
const parsed = JSON.parse(raw)
|
|
9284
|
+
if (parsed === null || typeof parsed !== 'object') return fallback
|
|
9285
|
+
return {
|
|
9286
|
+
on: parsed.on === true,
|
|
9287
|
+
speed: typeof parsed.speed === 'string' && parsed.speed in MOOD_SPEEDS ? parsed.speed : fallback.speed,
|
|
9288
|
+
direction: parsed.direction === 'rtl' ? 'rtl' : fallback.direction,
|
|
9289
|
+
zone: typeof parsed.zone === 'string' && parsed.zone in MOOD_ZONE_GEOMETRY ? parsed.zone : fallback.zone,
|
|
9290
|
+
}
|
|
9291
|
+
} catch {
|
|
9292
|
+
return fallback
|
|
9293
|
+
}
|
|
9294
|
+
}
|
|
9295
|
+
|
|
9296
|
+
/**
|
|
9297
|
+
* 合并写回设置。写失败只记录 —— localStorage 被禁用时问候仍在本次会话里活着。
|
|
9298
|
+
* @param patch - 要合并的字段(先经调用方认可)。
|
|
9299
|
+
*/
|
|
9300
|
+
function saveMoodState(patch) {
|
|
9301
|
+
try {
|
|
9302
|
+
window.localStorage?.setItem(MOOD_KEY, JSON.stringify({ ...readMoodState(), ...patch }))
|
|
9303
|
+
} catch (error) {
|
|
9304
|
+
console.error('[theme-gallery] could not save the mood state:', error)
|
|
9305
|
+
}
|
|
9306
|
+
}
|
|
9307
|
+
|
|
9308
|
+
/** 本地日期键 `YYYY-MM-DD`(选句的"今天"以本地时区为准)。 */
|
|
9309
|
+
function moodDateKey(date) {
|
|
9310
|
+
const pad = (n) => String(n).padStart(2, '0')
|
|
9311
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
|
|
9312
|
+
}
|
|
9313
|
+
|
|
9314
|
+
/** `MM-DD` 键(节日区间比较用)。 */
|
|
9315
|
+
function moodMonthDay(date) {
|
|
9316
|
+
const pad = (n) => String(n).padStart(2, '0')
|
|
9317
|
+
return `${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
|
|
9318
|
+
}
|
|
9319
|
+
|
|
9320
|
+
/**
|
|
9321
|
+
* 当前小时属于哪个时段池。夜深的环形边界:小时 <5 时加 24 归入"昨晚延续"。
|
|
9322
|
+
* @param hour - `date.getHours()`(0–23)。
|
|
9323
|
+
* @returns 池名。
|
|
9324
|
+
*/
|
|
9325
|
+
function moodPoolName(hour) {
|
|
9326
|
+
const h = hour < 5 ? hour + 24 : hour
|
|
9327
|
+
for (const range of MOOD_TIME_POOLS) {
|
|
9328
|
+
if (h >= range.from && h < range.to) return range.pool
|
|
9329
|
+
}
|
|
9330
|
+
return 'general'
|
|
9331
|
+
}
|
|
9332
|
+
|
|
9333
|
+
/**
|
|
9334
|
+
* 一条节日语料今天是否命中。`from > to` 视为跨年区间(如 12-31 → 01-02)。
|
|
9335
|
+
* @param line - 语料行(带 holiday 区间)。
|
|
9336
|
+
* @param mmdd - 今天的 `MM-DD`。
|
|
9337
|
+
* @returns 命中与否。
|
|
9338
|
+
*/
|
|
9339
|
+
function moodHolidayMatches(line, mmdd) {
|
|
9340
|
+
if (line.holiday === undefined) return false
|
|
9341
|
+
const { from, to } = line.holiday
|
|
9342
|
+
if (from <= to) return mmdd >= from && mmdd <= to
|
|
9343
|
+
return mmdd >= from || mmdd <= to
|
|
9344
|
+
}
|
|
9345
|
+
|
|
9346
|
+
/**
|
|
9347
|
+
* 此刻该用的语料池:节日命中 → 节日池(全部命中条目的并集);否则时段池;
|
|
9348
|
+
* 池空(理论不发生——建期断言五类非空)回落 general。
|
|
9349
|
+
* @param date - "现在"。
|
|
9350
|
+
* @param lines - 消毒后的语料。
|
|
9351
|
+
* @returns `{ pool, items }`;items 可为空数组。
|
|
9352
|
+
*/
|
|
9353
|
+
function moodPicksFor(date, lines) {
|
|
9354
|
+
const mmdd = moodMonthDay(date)
|
|
9355
|
+
const holiday = lines.filter((line) => line.category === 'holiday' && moodHolidayMatches(line, mmdd))
|
|
9356
|
+
if (holiday.length > 0) return { pool: 'holiday', items: holiday }
|
|
9357
|
+
const poolName = moodPoolName(date.getHours())
|
|
9358
|
+
const picked = lines.filter((line) => line.category === poolName)
|
|
9359
|
+
if (picked.length > 0) return { pool: poolName, items: picked }
|
|
9360
|
+
return { pool: 'general', items: lines.filter((line) => line.category === 'general') }
|
|
9361
|
+
}
|
|
9362
|
+
|
|
9363
|
+
/** FNV-1a 32 位——日期种子的原料(无依赖、确定性)。 */
|
|
9364
|
+
function moodHash(text) {
|
|
9365
|
+
let hash = 0x811c9dc5
|
|
9366
|
+
for (let at = 0; at < text.length; at += 1) {
|
|
9367
|
+
hash ^= text.charCodeAt(at)
|
|
9368
|
+
hash = Math.imul(hash, 0x01000193)
|
|
9369
|
+
}
|
|
9370
|
+
return hash >>> 0
|
|
9371
|
+
}
|
|
9372
|
+
|
|
9373
|
+
/** mulberry32 PRNG——小、快、确定性,够洗牌用。 */
|
|
9374
|
+
function mulberry32(seed) {
|
|
9375
|
+
let a = seed >>> 0
|
|
9376
|
+
// 先把函数赋给常量再返回:`return (…) =>` 里的 `return (` 会被 scope 审计
|
|
9377
|
+
// 读成"调用了名为 return 的函数"(wcagContrast 同一条教训的第三种形态)。
|
|
9378
|
+
const next = () => {
|
|
9379
|
+
a = (a + 0x6d2b79f5) | 0
|
|
9380
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a)
|
|
9381
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
|
|
9382
|
+
const out = ((t ^ (t >>> 14)) >>> 0) / 4294967296
|
|
9383
|
+
return out
|
|
9384
|
+
}
|
|
9385
|
+
return next
|
|
9386
|
+
}
|
|
9387
|
+
|
|
9388
|
+
/**
|
|
9389
|
+
* 当日确定性洗牌:同一天同池顺序稳定(会话重启不换血),跨天不同——
|
|
9390
|
+
* "每天不重样"的体验由种子里的日期实现,不依赖任何在线内容。
|
|
9391
|
+
* @param items - 池内语料。
|
|
9392
|
+
* @param date - "今天"。
|
|
9393
|
+
* @param pool - 池名(进种子,跨池顺序也不同)。
|
|
9394
|
+
* @returns 新数组(不改入参)。
|
|
9395
|
+
*/
|
|
9396
|
+
function moodDayOrder(items, date, pool) {
|
|
9397
|
+
const next = mulberry32(moodHash(`${moodDateKey(date)}|${pool}`))
|
|
9398
|
+
const order = [...items]
|
|
9399
|
+
for (let at = order.length - 1; at > 0; at -= 1) {
|
|
9400
|
+
const swap = Math.floor(next() * (at + 1))
|
|
9401
|
+
const hold = order[at]
|
|
9402
|
+
order[at] = order[swap]
|
|
9403
|
+
order[swap] = hold
|
|
9404
|
+
}
|
|
9405
|
+
return order
|
|
9406
|
+
}
|
|
9407
|
+
|
|
9408
|
+
/** 读选句指针;坏/缺返回 null(轮换逻辑自愈到 index 0)。 */
|
|
9409
|
+
function moodPointer() {
|
|
9410
|
+
try {
|
|
9411
|
+
const raw = window.localStorage?.getItem(MOOD_POINTER_KEY)
|
|
9412
|
+
if (typeof raw !== 'string' || raw === '') return null
|
|
9413
|
+
const parsed = JSON.parse(raw)
|
|
9414
|
+
if (parsed === null || typeof parsed !== 'object') return null
|
|
9415
|
+
return {
|
|
9416
|
+
date: typeof parsed.date === 'string' ? parsed.date : '',
|
|
9417
|
+
pool: typeof parsed.pool === 'string' ? parsed.pool : '',
|
|
9418
|
+
index: Number.isInteger(parsed.index) && parsed.index >= 0 ? parsed.index : 0,
|
|
9419
|
+
lastId: typeof parsed.lastId === 'string' ? parsed.lastId : '',
|
|
9420
|
+
}
|
|
9421
|
+
} catch {
|
|
9422
|
+
return null
|
|
9423
|
+
}
|
|
9424
|
+
}
|
|
9425
|
+
|
|
9426
|
+
/** 写选句指针;写失败只是明天从头开始,不值得打断谁。 */
|
|
9427
|
+
function saveMoodPointer(pointer) {
|
|
9428
|
+
try {
|
|
9429
|
+
window.localStorage?.setItem(MOOD_POINTER_KEY, JSON.stringify(pointer))
|
|
9430
|
+
} catch { /* 存储被禁时问候仍可显示,只是换条顺序回到队首 */ }
|
|
9431
|
+
}
|
|
9432
|
+
|
|
9433
|
+
/**
|
|
9434
|
+
* 选下一条并推进指针(每次调用都消费一格——启动首条也一样,指针保证
|
|
9435
|
+
* 同一天内尽量不重复;池耗尽回绕,回绕后与上一条相同且池里不止一条时跳一格)。
|
|
9436
|
+
* @param now - "现在"。
|
|
9437
|
+
* @param lines - 消毒后的语料。
|
|
9438
|
+
* @returns 语料行,或 null(语料不可用)。
|
|
9439
|
+
*/
|
|
9440
|
+
function moodNextLine(now, lines) {
|
|
9441
|
+
if (!Array.isArray(lines) || lines.length === 0) return null
|
|
9442
|
+
const { pool, items } = moodPicksFor(now, lines)
|
|
9443
|
+
if (items.length === 0) return null
|
|
9444
|
+
const dateKey = moodDateKey(now)
|
|
9445
|
+
const pointer = moodPointer()
|
|
9446
|
+
const fresh = pointer === null || pointer.date !== dateKey || pointer.pool !== pool
|
|
9447
|
+
const start = fresh ? 0 : pointer.index
|
|
9448
|
+
const order = moodDayOrder(items, now, pool)
|
|
9449
|
+
let pick = start % order.length
|
|
9450
|
+
if (order.length > 1 && order[pick].id === pointer?.lastId) pick = (pick + 1) % order.length
|
|
9451
|
+
saveMoodPointer({ date: dateKey, pool, index: pick + 1, lastId: order[pick].id })
|
|
9452
|
+
return order[pick]
|
|
9453
|
+
}
|
|
9454
|
+
|
|
9455
|
+
/**
|
|
9456
|
+
* 应用/编辑菜单的右缘(问候区左端)。菜单是 preload 注入的固定元素
|
|
9457
|
+
* (`[data-windows-menu]`,见 preload 的 installWindowsMenu),
|
|
9458
|
+
* 侧栏折叠时它会挪位——每次都实测,几何自动跟随,不做写死常量。
|
|
9459
|
+
* @returns 菜单右缘 + 间隙;读不到时给兜底常量。
|
|
9460
|
+
*/
|
|
9461
|
+
/**
|
|
9462
|
+
* 问候区左端。**用户二轮返工(截图)**:左端要让开**整个左侧菜单栏列**——
|
|
9463
|
+
* 侧栏与工作区有明显分界线(卡通素材也渲染在侧栏内),问候从分界线右侧开始,
|
|
9464
|
+
* 不再只是避开"编辑"按钮。所以取两者较大值:
|
|
9465
|
+
*
|
|
9466
|
+
* · 侧栏列的实测右缘(`sidebarColumn()`,展开 ≈ 280,即分界线)+ 小呼吸位;
|
|
9467
|
+
* · 应用/编辑菜单的右缘(侧栏**收起**时列宽为 0、菜单挪到 84px 起,此时由它兜底)。
|
|
9468
|
+
*
|
|
9469
|
+
* 都读不到(异常环境/旧版本)才落常量兜底。每次都实测:侧栏宽度是用户可拖的,
|
|
9470
|
+
* 收起/展开也随时切——写死必错(SKILL §13 同一条哲学)。
|
|
9471
|
+
* @returns 左端 x(px)。
|
|
9472
|
+
*/
|
|
9473
|
+
function moodMenuRight() {
|
|
9474
|
+
let boundary = 0
|
|
9475
|
+
try {
|
|
9476
|
+
const column = sidebarColumn()
|
|
9477
|
+
if (column !== null && typeof column.getBoundingClientRect === 'function') {
|
|
9478
|
+
const rect = column.getBoundingClientRect()
|
|
9479
|
+
if (Number.isFinite(rect.right) && rect.right > 0) boundary = rect.right + MOOD_SIDEBAR_GAP
|
|
9480
|
+
}
|
|
9481
|
+
} catch { /* 桩环境没有 rect —— 由菜单/兜底接管 */ }
|
|
9482
|
+
try {
|
|
9483
|
+
const host = document.querySelector('[data-windows-menu]')
|
|
9484
|
+
if (host !== null && typeof host.getBoundingClientRect === 'function') {
|
|
9485
|
+
const rect = host.getBoundingClientRect()
|
|
9486
|
+
if (Number.isFinite(rect.right) && rect.right > 0) boundary = Math.max(boundary, rect.right + MOOD_MENU_GAP)
|
|
9487
|
+
}
|
|
9488
|
+
} catch { /* 同上 */ }
|
|
9489
|
+
if (boundary > 0) return boundary
|
|
9490
|
+
return MOOD_FALLBACK_MENU_RIGHT
|
|
9491
|
+
}
|
|
9492
|
+
|
|
9493
|
+
/**
|
|
9494
|
+
* 位置区域的矩形:5 等份映射(MOOD_ZONE_GEOMETRY)作用在
|
|
9495
|
+
* 「问候区左端(侧栏分界线右侧)→ 窗口按钮左缘」这段上。
|
|
9496
|
+
* @param zone - `readMoodState().zone`。
|
|
9497
|
+
* @param viewportWidth - 窗口宽。
|
|
9498
|
+
* @param menuRight - `moodMenuRight()` 的读数(即左端)。
|
|
9499
|
+
* @returns `{ left, width }`(px),几何不可算时 null。
|
|
9500
|
+
*/
|
|
9501
|
+
function moodZoneRect(zone, viewportWidth, menuRight) {
|
|
9502
|
+
const geo = MOOD_ZONE_GEOMETRY[zone]
|
|
9503
|
+
if (geo === undefined) return null
|
|
9504
|
+
const right = viewportWidth - MOOD_WINBTN_RESERVE
|
|
9505
|
+
const left = Math.min(menuRight, right - 160)
|
|
9506
|
+
// 左端为负 = 窗口窄到连菜单都放不下(异常环境)——宁可不渲染(自检行会给读数),
|
|
9507
|
+
// 也不要把条画到屏幕外去。
|
|
9508
|
+
if (!(right > left) || left < 0) return null
|
|
9509
|
+
const unit = (right - left) / 5
|
|
9510
|
+
const rectLeft = left + unit * (geo.first - 1)
|
|
9511
|
+
const rectWidth = unit * geo.parts
|
|
9512
|
+
if (!Number.isFinite(rectLeft) || !Number.isFinite(rectWidth) || rectWidth <= 0) return null
|
|
9513
|
+
return { left: rectLeft, width: rectWidth }
|
|
9514
|
+
}
|
|
9515
|
+
|
|
9516
|
+
/** 问候条的样式表(一次性安装;元素树全部经 createElement/textContent)。 */
|
|
9517
|
+
const MOOD_CSS = [
|
|
9518
|
+
`#${MOOD_BAR_ID}{position:fixed;top:0;height:var(--dsh-windows-titlebar-height,40px);`
|
|
9519
|
+
+ 'display:flex;align-items:center;overflow:hidden;pointer-events:none;'
|
|
9520
|
+
+ 'font-size:12px;letter-spacing:.12em;color:var(--dsw-alias-label-tertiary)}',
|
|
9521
|
+
'.dsh-mood-track{display:inline-flex;white-space:nowrap;will-change:transform;'
|
|
9522
|
+
+ 'animation:dsh-mood-rtl linear infinite}',
|
|
9523
|
+
'[data-dsh-mood-dir="ltr"] .dsh-mood-track{animation-name:dsh-mood-ltr}',
|
|
9524
|
+
'@keyframes dsh-mood-rtl{from{transform:translateX(0)}to{transform:translateX(-50%)}}',
|
|
9525
|
+
'@keyframes dsh-mood-ltr{from{transform:translateX(-50%)}to{transform:translateX(0)}}',
|
|
9526
|
+
'.dsh-mood-half{display:inline-flex;align-items:center}',
|
|
9527
|
+
/* 句尾间隙 = **整个区域宽**(内联 width 由 moodBuildBar 按实测写入):
|
|
9528
|
+
间隙 ≥ 区域宽时,数学上保证任一时刻区域内至多一句可见 —— 用户返工
|
|
9529
|
+
(2026-09-27 截图):"同一时刻,只有一句话显示在一处"。 */
|
|
9530
|
+
'.dsh-mood-text{flex:none}',
|
|
9531
|
+
'.dsh-mood-gap{flex:none;display:inline-block}',
|
|
9532
|
+
/* reduced-motion:功能保留、只停动画(挂件先例)——静止显示一份,超长省略。
|
|
9533
|
+
⚠ 全杀规则必须待在媒体查询**里面**:裸写一条无条件 `animation:none!important`
|
|
9534
|
+
会把跑马灯动画本身一起杀掉(实机读数抓到过:animationName 变成 none)。 */
|
|
9535
|
+
'@media (prefers-reduced-motion:reduce){'
|
|
9536
|
+
+ `#${MOOD_BAR_ID} *{animation:none!important}`
|
|
9537
|
+
+ '.dsh-mood-half+.dsh-mood-half{display:none}'
|
|
9538
|
+
+ `#${MOOD_BAR_ID} .dsh-mood-half{overflow:hidden;text-overflow:ellipsis;max-width:100%}}`,
|
|
9539
|
+
].join('\n')
|
|
9540
|
+
|
|
9541
|
+
/** 样式表是否已在文档里(自检读数之一)。 */
|
|
9542
|
+
function moodCssInstalled() {
|
|
9543
|
+
if (typeof document === 'undefined' || typeof document.querySelector !== 'function') return false
|
|
9544
|
+
return document.querySelector('style[data-plugin-css="theme-gallery/mood"]') !== null
|
|
9545
|
+
}
|
|
9546
|
+
|
|
9547
|
+
/** 安装样式表(幂等)。 */
|
|
9548
|
+
function installMoodCss() {
|
|
9549
|
+
if (typeof document === 'undefined' || typeof document.querySelector !== 'function') return
|
|
9550
|
+
if (moodCssInstalled()) return
|
|
9551
|
+
const tag = document.createElement('style')
|
|
9552
|
+
tag.dataset.plugin = 'dsh-theme-gallery'
|
|
9553
|
+
tag.dataset.pluginCss = 'theme-gallery/mood'
|
|
9554
|
+
tag.textContent = MOOD_CSS
|
|
9555
|
+
document.head.appendChild(tag)
|
|
9556
|
+
}
|
|
9557
|
+
|
|
9558
|
+
/** 拆除样式表(teardown 路径用)。 */
|
|
9559
|
+
function removeMoodCss() {
|
|
9560
|
+
if (typeof document === 'undefined' || typeof document.querySelector !== 'function') return
|
|
9561
|
+
for (const stale of document.querySelectorAll('style[data-plugin-css="theme-gallery/mood"]')) stale.remove()
|
|
9562
|
+
}
|
|
9563
|
+
|
|
9564
|
+
/** 问候条运行态。`enabled` 是 localStorage 的内存镜像(petRun 同款理由)。 */
|
|
9565
|
+
const moodRun = { enabled: false, timer: null, retryTimer: null, retryTries: 0, resizeBound: false, fingerprint: '', line: null, pool: '-' }
|
|
9566
|
+
|
|
9567
|
+
/**
|
|
9568
|
+
* 座位缺席的有界重试。shell.overlay 的渲染时机由外壳决定,常常晚于本插件的
|
|
9569
|
+
* 首轮 publish(实机读数:指针已走、座位已在、条还没建 —— 就是这个缺口),
|
|
9570
|
+
* 而之后可能再没有 publish 路过。所以缺席时用**单个可取消句柄**每 2 秒重试,
|
|
9571
|
+
* 至多 `MOOD_SEAT_RETRY_MAX` 次(约 30 秒)—— 建成即停,拆条即清,
|
|
9572
|
+
* 不做持续轮询(有界工作的家规)。
|
|
9573
|
+
*/
|
|
9574
|
+
const MOOD_SEAT_RETRY_MS = 2000
|
|
9575
|
+
const MOOD_SEAT_RETRY_MAX = 15
|
|
9576
|
+
|
|
9577
|
+
function moodArmSeatRetry() {
|
|
9578
|
+
if (moodRun.retryTimer !== null || moodRun.retryTries >= MOOD_SEAT_RETRY_MAX) return
|
|
9579
|
+
if (typeof window === 'undefined' || typeof window.setTimeout !== 'function') return
|
|
9580
|
+
moodRun.retryTimer = window.setTimeout(() => {
|
|
9581
|
+
moodRun.retryTimer = null
|
|
9582
|
+
moodRun.retryTries += 1
|
|
9583
|
+
try {
|
|
9584
|
+
if (!moodRun.enabled) return
|
|
9585
|
+
const seat = document.querySelector(MOOD_SEAT_SELECTOR)
|
|
9586
|
+
if (seat === null) {
|
|
9587
|
+
moodArmSeatRetry()
|
|
9588
|
+
return
|
|
9589
|
+
}
|
|
9590
|
+
moodRun.retryTries = 0
|
|
9591
|
+
moodBuildBar()
|
|
9592
|
+
} catch (error) {
|
|
9593
|
+
console.error('[theme-gallery] could not retry the mood seat:', error)
|
|
9594
|
+
}
|
|
9595
|
+
}, MOOD_SEAT_RETRY_MS)
|
|
9596
|
+
}
|
|
9597
|
+
|
|
9598
|
+
function moodCancelSeatRetry() {
|
|
9599
|
+
moodClearTimer(moodRun.retryTimer)
|
|
9600
|
+
moodRun.retryTimer = null
|
|
9601
|
+
}
|
|
9602
|
+
|
|
9603
|
+
/** 自检读数。挂件 petReport 同款:每条退出路径都写,失败形态是"什么都不出现",没有读数就查不下去。 */
|
|
9604
|
+
let moodReport
|
|
9605
|
+
function noteMoodReport(patch) {
|
|
9606
|
+
moodReport = { ...(moodReport ?? {}), ...patch }
|
|
9607
|
+
}
|
|
9608
|
+
|
|
9609
|
+
/** 例行读数用的中文标签(自检行是给人读的)。 */
|
|
9610
|
+
const MOOD_ZONE_LABELS = { left: '左靠齐', center: '居中', indent: '左右缩进', right: '右靠齐' }
|
|
9611
|
+
const MOOD_SPEED_LABELS = { slow: '慢速', medium: '中速', fast: '快速' }
|
|
9612
|
+
|
|
9613
|
+
/**
|
|
9614
|
+
* 面板自检行(petLineOf 同款形状):关着返回 null;开着出问题给 ⚠ 警告;
|
|
9615
|
+
* 一切正常给一行读数。警告无条件显示、读数走 debug 开关——既有口径。
|
|
9616
|
+
* @param on - 问候是否开启(store 的 moodOn)。
|
|
9617
|
+
* @returns 自检行或 null。
|
|
9618
|
+
*/
|
|
9619
|
+
function moodLineOf(on) {
|
|
9620
|
+
try {
|
|
9621
|
+
if (on !== true) return null
|
|
9622
|
+
const r = moodReport
|
|
9623
|
+
if (r === undefined || r.enabled !== true) return '⚠ 问候已开启但尚未同步(syncMoodBar 未运行)'
|
|
9624
|
+
if (r.seat !== 'ok') return `⚠ 问候条未建出:${r.note ?? '座位(shell.overlay)尚未渲染'}`
|
|
9625
|
+
if (r.css !== true) return '⚠ 问候样式表未生效(MOOD_CSS 不在文档中)'
|
|
9626
|
+
if (r.line === null || r.line === undefined) return `⚠ 语料不可用:${r.note ?? '没有可用语料'}`
|
|
9627
|
+
return `问候自检通过 · 条=${r.line} 池=${r.pool} 区域=${r.zoneLabel} 方向=${r.dirLabel} 速度=${r.speedLabel} 左=${r.left} 右=${r.right}`
|
|
9628
|
+
} catch (error) {
|
|
9629
|
+
return `⚠ 问候自检失败: ${String(error && error.message ? error.message : error)}`
|
|
9630
|
+
}
|
|
9631
|
+
}
|
|
9632
|
+
|
|
9633
|
+
/** 与 petLineIsWarning 同一条判据:⚠ 开头或带"失败"的行不算例行汇报。 */
|
|
9634
|
+
function moodLineIsWarning(line) {
|
|
9635
|
+
return typeof line === 'string' && (line.startsWith('⚠') || line.includes('失败'))
|
|
9636
|
+
}
|
|
9637
|
+
|
|
9638
|
+
/** 拆掉问候条元素(不动计时器——那是 teardown 的事)。 */
|
|
9639
|
+
function removeMoodBar() {
|
|
9640
|
+
if (typeof document === 'undefined' || typeof document.getElementById !== 'function') return
|
|
9641
|
+
const bar = document.getElementById(MOOD_BAR_ID)
|
|
9642
|
+
if (bar !== null) bar.remove()
|
|
9643
|
+
}
|
|
9644
|
+
|
|
9645
|
+
/**
|
|
9646
|
+
* 按当前状态(重)建问候条。幂等判据在最前:指纹没变一个字节都不写
|
|
9647
|
+
* (bounded-work 的守卫规矩;publish 每次都路过这里,不能每次都重建 DOM)。
|
|
9648
|
+
*
|
|
9649
|
+
* 文字**只经 textContent** 进 DOM;两半内容逐字节相同,translateX 0↔-50% 的
|
|
9650
|
+
* 循环天然无缝;时长 = 半宽 ÷ 速度档,速度换算全部落在动画时长上。
|
|
9651
|
+
*/
|
|
9652
|
+
function moodBuildBar() {
|
|
9653
|
+
if (typeof document === 'undefined' || typeof document.getElementById !== 'function') return
|
|
9654
|
+
const state = readMoodState()
|
|
9655
|
+
if (!moodRun.enabled || !ambientEnabled()) {
|
|
9656
|
+
removeMoodBar()
|
|
9657
|
+
noteMoodReport({
|
|
9658
|
+
enabled: moodRun.enabled, seat: 'absent', css: moodCssInstalled(), line: null,
|
|
9659
|
+
note: moodRun.enabled ? '急停开关(ambient:false)' : '开关为关',
|
|
9660
|
+
})
|
|
9661
|
+
return
|
|
9662
|
+
}
|
|
9663
|
+
installMoodCss()
|
|
9664
|
+
const seatNode = document.querySelector(MOOD_SEAT_SELECTOR)
|
|
9665
|
+
if (seatNode === null) {
|
|
9666
|
+
noteMoodReport({
|
|
9667
|
+
enabled: true, seat: 'absent', css: moodCssInstalled(),
|
|
9668
|
+
line: moodRun.line === null ? null : moodRun.line.id,
|
|
9669
|
+
pool: moodRun.pool, note: '座位(shell.overlay)尚未渲染',
|
|
9670
|
+
})
|
|
9671
|
+
moodArmSeatRetry()
|
|
9672
|
+
return
|
|
9673
|
+
}
|
|
9674
|
+
const viewport = petViewport()
|
|
9675
|
+
const menuRight = moodMenuRight()
|
|
9676
|
+
const rect = moodZoneRect(state.zone, viewport.width, menuRight)
|
|
9677
|
+
if (rect === null) {
|
|
9678
|
+
noteMoodReport({
|
|
9679
|
+
enabled: true, seat: 'ok', css: moodCssInstalled(),
|
|
9680
|
+
line: moodRun.line === null ? null : moodRun.line.id, pool: moodRun.pool,
|
|
9681
|
+
note: `几何不可算(viewport=${viewport.width} 菜单右缘=${menuRight})`,
|
|
9682
|
+
})
|
|
9683
|
+
return
|
|
9684
|
+
}
|
|
9685
|
+
const active = moodRun.line
|
|
9686
|
+
if (active === null) {
|
|
9687
|
+
noteMoodReport({ enabled: true, seat: 'ok', css: moodCssInstalled(), line: null, pool: moodRun.pool, note: '语料不可用' })
|
|
9688
|
+
return
|
|
9689
|
+
}
|
|
9690
|
+
const fingerprint = [active.id, state.speed, state.direction, state.zone, rect.left, rect.width, viewport.width].join('|')
|
|
9691
|
+
let bar = document.getElementById(MOOD_BAR_ID)
|
|
9692
|
+
if (bar !== null && moodRun.fingerprint === fingerprint) {
|
|
9693
|
+
moodCancelSeatRetry()
|
|
9694
|
+
moodRun.retryTries = 0
|
|
9695
|
+
noteMoodReport({
|
|
9696
|
+
enabled: true, seat: 'ok', css: true, line: active.id, pool: moodRun.pool,
|
|
9697
|
+
zoneLabel: MOOD_ZONE_LABELS[state.zone], dirLabel: state.direction === 'ltr' ? '左→右' : '右→左',
|
|
9698
|
+
speedLabel: MOOD_SPEED_LABELS[state.speed], left: Math.round(rect.left), right: Math.round(rect.left + rect.width),
|
|
9699
|
+
})
|
|
9700
|
+
return
|
|
9701
|
+
}
|
|
9702
|
+
// —— 指纹变了:整条重建。重建比 diff 便宜,也是把旧速度/旧方向的动画一并带走的唯一途径。
|
|
9703
|
+
if (bar === null) {
|
|
9704
|
+
bar = document.createElement('div')
|
|
9705
|
+
bar.id = MOOD_BAR_ID
|
|
9706
|
+
}
|
|
9707
|
+
bar.textContent = ''
|
|
9708
|
+
bar.dataset.dshMoodDir = state.direction
|
|
9709
|
+
bar.style.left = `${rect.left}px`
|
|
9710
|
+
bar.style.width = `${rect.width}px`
|
|
9711
|
+
// **先把 bar 挂进座位再量宽度**:脱离 DOM 的元素 getBoundingClientRect() 是 0
|
|
9712
|
+
// (实机读数抓到过)。track 也一样——量宽前先把它挂进 bar。
|
|
9713
|
+
seatNode.appendChild(bar)
|
|
9714
|
+
const track = document.createElement('div')
|
|
9715
|
+
track.className = 'dsh-mood-track'
|
|
9716
|
+
bar.appendChild(track)
|
|
9717
|
+
// 每半 = **一句 + 一段等于区域宽的间隙**。间隙 ≥ 区域宽 ⇒ 两句不可能同时
|
|
9718
|
+
// 落在区域内(B 的左缘与 A 的右缘至少隔一个区域宽),滚动到接缝处 A 出、
|
|
9719
|
+
// B 进,视觉上"同一时刻只有一句话显示在一处"(用户返工要求)。
|
|
9720
|
+
const buildHalf = () => {
|
|
9721
|
+
const half = document.createElement('span')
|
|
9722
|
+
half.className = 'dsh-mood-half'
|
|
9723
|
+
const text = document.createElement('span')
|
|
9724
|
+
text.className = 'dsh-mood-text'
|
|
9725
|
+
text.textContent = active.text
|
|
9726
|
+
half.appendChild(text)
|
|
9727
|
+
const gap = document.createElement('span')
|
|
9728
|
+
gap.className = 'dsh-mood-gap'
|
|
9729
|
+
gap.style.width = `${rect.width}px`
|
|
9730
|
+
half.appendChild(gap)
|
|
9731
|
+
track.appendChild(half)
|
|
9732
|
+
return half
|
|
9733
|
+
}
|
|
9734
|
+
const first = buildHalf()
|
|
9735
|
+
track.appendChild(first.cloneNode(true))
|
|
9736
|
+
// 建成即停:座位重试链清零(它只在缺席时武装)。
|
|
9737
|
+
moodCancelSeatRetry()
|
|
9738
|
+
moodRun.retryTries = 0
|
|
9739
|
+
// 区域随窗口宽重算:唯一的事件 listener,首次建条时绑定、teardown 时拆除。
|
|
9740
|
+
if (!moodRun.resizeBound && typeof window !== 'undefined' && typeof window.addEventListener === 'function') {
|
|
9741
|
+
window.addEventListener('resize', moodOnResize)
|
|
9742
|
+
moodRun.resizeBound = true
|
|
9743
|
+
}
|
|
9744
|
+
const halfWidth = first.getBoundingClientRect().width
|
|
9745
|
+
const speed = MOOD_SPEEDS[state.speed] ?? MOOD_SPEEDS.medium
|
|
9746
|
+
const duration = halfWidth > 0 && Number.isFinite(halfWidth) ? halfWidth / speed : 0
|
|
9747
|
+
// 时长非法(量不到宽度)就不写样式——交给 reduced-motion 的静止形态兜底,绝不写 NaN。
|
|
9748
|
+
if (duration > 0 && Number.isFinite(duration)) track.style.animationDuration = `${duration}s`
|
|
9749
|
+
// 字体加载完成后**重建一次**:副本数与滚动时长刚才按回退字体量的(实机读数:
|
|
9750
|
+
// 12 份、47.8s),字体就位后宽度才真实。一次性 promise、指纹守卫仍在 —— 有界。
|
|
9751
|
+
if (typeof document !== 'undefined' && document.fonts?.ready?.then === 'function') {
|
|
9752
|
+
document.fonts.ready.then(() => {
|
|
9753
|
+
if (moodRun.fingerprint !== fingerprint || moodRun.enabled !== true) return
|
|
9754
|
+
moodRun.fingerprint = ''
|
|
9755
|
+
moodBuildBar()
|
|
9756
|
+
}).catch(() => { /* 字体 API 异常就用首次的度量,不追 */ })
|
|
9757
|
+
}
|
|
9758
|
+
moodRun.fingerprint = fingerprint
|
|
9759
|
+
noteMoodReport({
|
|
9760
|
+
enabled: true, seat: 'ok', css: true, line: active.id, pool: moodRun.pool,
|
|
9761
|
+
zoneLabel: MOOD_ZONE_LABELS[state.zone], dirLabel: state.direction === 'ltr' ? '左→右' : '右→左',
|
|
9762
|
+
speedLabel: MOOD_SPEED_LABELS[state.speed], left: Math.round(rect.left), right: Math.round(rect.left + rect.width),
|
|
9763
|
+
})
|
|
9764
|
+
}
|
|
9765
|
+
|
|
9766
|
+
/**
|
|
9767
|
+
* 计时器只走 **`window.setTimeout` / `window.clearTimeout`**(挂件同款):
|
|
9768
|
+
* 测试桩拦得住,也绝不留一个裸的全局定时器把进程吊住(有界工作的家规)。
|
|
9769
|
+
*/
|
|
9770
|
+
function moodClearTimer(handle) {
|
|
9771
|
+
if (handle === null) return
|
|
9772
|
+
if (typeof window !== 'undefined' && typeof window.clearTimeout === 'function') window.clearTimeout(handle)
|
|
9773
|
+
}
|
|
9774
|
+
|
|
9775
|
+
/** 35 分钟后的下一次换条(单句柄链式;任何路径先 clear 自己)。 */
|
|
9776
|
+
function moodScheduleNext() {
|
|
9777
|
+
moodClearTimer(moodRun.timer)
|
|
9778
|
+
moodRun.timer = null
|
|
9779
|
+
if (typeof window === 'undefined' || typeof window.setTimeout !== 'function') return
|
|
9780
|
+
moodRun.timer = window.setTimeout(() => {
|
|
9781
|
+
moodRun.timer = null
|
|
9782
|
+
try {
|
|
9783
|
+
if (!moodRun.enabled) return
|
|
9784
|
+
const line = moodNextLine(new Date(), MOOD_LINES)
|
|
9785
|
+
if (line === null) {
|
|
9786
|
+
noteMoodReport({ enabled: true, seat: moodReport?.seat ?? 'absent', css: moodCssInstalled(), line: null, pool: moodRun.pool, note: '语料不可用' })
|
|
9787
|
+
return
|
|
9788
|
+
}
|
|
9789
|
+
moodRun.line = line
|
|
9790
|
+
moodRun.pool = line.category === 'holiday' ? 'holiday' : moodRun.pool
|
|
9791
|
+
moodBuildBar()
|
|
9792
|
+
} catch (error) {
|
|
9793
|
+
console.error('[theme-gallery] could not rotate the mood line:', error)
|
|
9794
|
+
} finally {
|
|
9795
|
+
moodScheduleNext()
|
|
9796
|
+
}
|
|
9797
|
+
}, MOOD_PERIOD_MS)
|
|
9798
|
+
}
|
|
9799
|
+
|
|
9800
|
+
/**
|
|
9801
|
+
* 问候条的同步入口(幂等):publish 顺路调用、开关与设置路径显式调用。
|
|
9802
|
+
* 与 syncPetStage 同一条哲学——每条退出路径都写 moodReport。
|
|
9803
|
+
* 计时器只在第一次启动(句柄存在就不动它),换条节奏不随 publish 漂移。
|
|
9804
|
+
*/
|
|
9805
|
+
function syncMoodBar() {
|
|
9806
|
+
try {
|
|
9807
|
+
const state = readMoodState()
|
|
9808
|
+
moodRun.enabled = state.on
|
|
9809
|
+
if (!state.on) {
|
|
9810
|
+
moodTeardown()
|
|
9811
|
+
return
|
|
9812
|
+
}
|
|
9813
|
+
if (moodRun.timer === null) moodScheduleNext()
|
|
9814
|
+
if (moodRun.line === null) {
|
|
9815
|
+
const line = moodNextLine(new Date(), MOOD_LINES)
|
|
9816
|
+
moodRun.line = line
|
|
9817
|
+
if (line !== null) moodRun.pool = line.category
|
|
9818
|
+
}
|
|
9819
|
+
moodBuildBar()
|
|
9820
|
+
} catch (error) {
|
|
9821
|
+
console.error('[theme-gallery] could not sync the mood bar:', error)
|
|
9822
|
+
}
|
|
9823
|
+
}
|
|
9824
|
+
|
|
9825
|
+
/** resize 时区域要重算(唯一的事件 listener;挂载/拆卸成对)。 */
|
|
9826
|
+
function moodOnResize() {
|
|
9827
|
+
try {
|
|
9828
|
+
moodBuildBar()
|
|
9829
|
+
} catch (error) {
|
|
9830
|
+
console.error('[theme-gallery] could not resize the mood bar:', error)
|
|
9831
|
+
}
|
|
9832
|
+
}
|
|
9833
|
+
|
|
9834
|
+
/**
|
|
9835
|
+
* 全部拆除:计时器、resize listener、条元素、样式表、运行态。
|
|
9836
|
+
* **刻意不叫 `…Dispose`**——那是自发光守卫审计里主题层专用模式的名字(规则 1)。
|
|
9837
|
+
*/
|
|
9838
|
+
function moodTeardown() {
|
|
9839
|
+
moodClearTimer(moodRun.timer)
|
|
9840
|
+
moodRun.timer = null
|
|
9841
|
+
moodCancelSeatRetry()
|
|
9842
|
+
moodRun.retryTries = 0
|
|
9843
|
+
if (moodRun.resizeBound && typeof window !== 'undefined' && typeof window.removeEventListener === 'function') {
|
|
9844
|
+
window.removeEventListener('resize', moodOnResize)
|
|
9845
|
+
}
|
|
9846
|
+
moodRun.resizeBound = false
|
|
9847
|
+
removeMoodBar()
|
|
9848
|
+
removeMoodCss()
|
|
9849
|
+
moodRun.fingerprint = ''
|
|
9850
|
+
moodRun.line = null
|
|
9851
|
+
noteMoodReport({ enabled: false, seat: 'absent', css: false, line: null, pool: '-', note: '开关为关' })
|
|
9852
|
+
}
|
|
9853
|
+
|
|
9854
|
+
/**
|
|
9855
|
+
* Render the main-column gallery page.
|
|
9856
|
+
*
|
|
9857
|
+
* `usePanelInfo` comes from the layout's GlobalStandardProps, so the page can
|
|
9858
|
+
* render nothing when another panel is selected without the shell having to
|
|
9859
|
+
* mount and unmount it.
|
|
9860
|
+
* @param props - composed slot props.
|
|
9861
|
+
* @returns the page element tree.
|
|
9862
|
+
*/
|
|
9863
|
+
function ThemeGalleryPage({ t, setTheme, pickScheme, togglePet, pickPet, toggleMood, setMood, useStore, usePanelInfo }) {
|
|
9864
|
+
const info = usePanelInfo((s) => s.activePanelId)
|
|
9865
|
+
const ids = useStore((s) => s.ids)
|
|
9866
|
+
const labels = useStore((s) => s.labels)
|
|
9867
|
+
const descriptions = useStore((s) => s.descriptions)
|
|
9868
|
+
const swatches = useStore((s) => s.swatches)
|
|
9869
|
+
const cardRows = useStore((s) => s.cardRows)
|
|
9870
|
+
const scheme = useStore((s) => s.scheme)
|
|
9871
|
+
const petEnabled = useStore((s) => s.petEnabled)
|
|
9872
|
+
const petKind = useStore((s) => s.petKind)
|
|
9873
|
+
const moodOn = useStore((s) => s.moodOn)
|
|
9874
|
+
const moodSpeed = useStore((s) => s.moodSpeed)
|
|
9875
|
+
const moodDirection = useStore((s) => s.moodDirection)
|
|
9876
|
+
const moodZone = useStore((s) => s.moodZone)
|
|
9877
|
+
const selected = useStore((s) => s.selected)
|
|
9878
|
+
const status = useStore((s) => s.status)
|
|
9879
|
+
// The layout keeps this page registered whatever the selection is, so it
|
|
9880
|
+
// renders nothing while another panel owns the main column.
|
|
9881
|
+
if (info !== PANEL_ID) return null
|
|
9882
|
+
// An empty picker must explain itself: the boot screen says only "failed",
|
|
9883
|
+
// and a silent empty page is indistinguishable from a broken one.
|
|
9884
|
+
if (ids.length === 0) {
|
|
9885
|
+
return jsxs('div', {
|
|
9886
|
+
className: 'tg-page',
|
|
9887
|
+
children: [
|
|
6674
9888
|
jsx('div', { className: 'tg-title', children: t('title') }),
|
|
6675
9889
|
jsx('div', { className: 'tg-hint', children: status || t('empty') }),
|
|
6676
9890
|
],
|
|
@@ -6679,13 +9893,22 @@ window.__ModuleLoader__.load({
|
|
|
6679
9893
|
// Detail lines are opt-in; a line that reports a problem is not detail.
|
|
6680
9894
|
const scenery = sceneryLine(selected)
|
|
6681
9895
|
const showScenery = scenery !== null && (debugEnabled() || sceneryLineIsWarning(scenery))
|
|
9896
|
+
// 挂件行同理:开了挂件却建不出舞台,警告必须无条件出现在面板上。
|
|
9897
|
+
const petLine = petLineOf(petEnabled === true)
|
|
9898
|
+
const showPet = petLine !== null && (debugEnabled() || petLineIsWarning(petLine))
|
|
9899
|
+
// 问候行同理:开着却没建出条(或样式缺失),必须无条件可见。
|
|
9900
|
+
const moodLine = moodLineOf(moodOn === true)
|
|
9901
|
+
const showMood = moodLine !== null && (debugEnabled() || moodLineIsWarning(moodLine))
|
|
6682
9902
|
// ── THE HEADER COUNT IS SKINS, NOT CARDS ─────────────────────────────────
|
|
6683
9903
|
//
|
|
6684
9904
|
// The built-in 浅色 / 深色 cards are not skins — they only switch the palette back to the
|
|
6685
9905
|
// official appearance — so counting them here would advertise a number that disagrees
|
|
6686
9906
|
// with the README and with npm's description ("6 skins"). The cards are named in the
|
|
6687
9907
|
// same line instead, so nothing is hidden by leaving them out of the number.
|
|
6688
|
-
|
|
9908
|
+
// 宠物挂件卡同理:它不是主题、不换配色,只开关挂件,所以与内置卡一起排除在
|
|
9909
|
+
// "皮肤数"之外(copy-consistency 的正则钉住的是 skinCount 那一行的形状)。
|
|
9910
|
+
const builtInCards = ids.filter((id) => BUILT_IN_LABELS[id] !== undefined
|
|
9911
|
+
|| id === PET_WIDGET.id || id === MOOD_WIDGET.id).length
|
|
6689
9912
|
const skinCount = ids.length - builtInCards
|
|
6690
9913
|
return jsxs('div', {
|
|
6691
9914
|
className: 'tg-page',
|
|
@@ -6724,6 +9947,17 @@ window.__ModuleLoader__.load({
|
|
|
6724
9947
|
// shell exactly like clicking the card would — so the anchor becomes active
|
|
6725
9948
|
// and the scheme layer has something to sit on.
|
|
6726
9949
|
onPickScheme: (schemeId) => { pickScheme(schemeId) },
|
|
9950
|
+
// 挂件卡:点击 = 开关挂件(见 ThemeCard 的挂件分支)。开关状态也一并传下去,
|
|
9951
|
+
// 徽标与 aria-pressed 都读它。
|
|
9952
|
+
onTogglePet: () => { togglePet() },
|
|
9953
|
+
petEnabled: petEnabled === true,
|
|
9954
|
+
// 头像选择行:点亮哪颗、卡面画哪只,都读这个字段(publish 从 localStorage 报来)。
|
|
9955
|
+
petKind: typeof petKind === 'string' && petKind !== '' ? petKind : 'ban-ban',
|
|
9956
|
+
onPickPet: (kindId) => { pickPet(kindId) },
|
|
9957
|
+
// 心情问候卡:胶囊开关与三组 chips 都读这些字段(publish 从 localStorage 报来)。
|
|
9958
|
+
mood: { on: moodOn === true, speed: moodSpeed, direction: moodDirection, zone: moodZone },
|
|
9959
|
+
onToggleMood: () => { toggleMood() },
|
|
9960
|
+
onSetMood: (patch) => { setMood(patch) },
|
|
6727
9961
|
t,
|
|
6728
9962
|
}, id)),
|
|
6729
9963
|
}),
|
|
@@ -6734,7 +9968,7 @@ window.__ModuleLoader__.load({
|
|
|
6734
9968
|
// reference block — and the caption is what keeps them from being mistaken for a
|
|
6735
9969
|
// malfunction, which is exactly how a user read them. They stay CONDITIONAL:
|
|
6736
9970
|
// detail waits for the debug switch, while a warning or a failure always prints.
|
|
6737
|
-
(debugEnabled() || showScenery)
|
|
9971
|
+
(debugEnabled() || showScenery || showPet || showMood)
|
|
6738
9972
|
? jsxs('div', {
|
|
6739
9973
|
className: 'tg-diag',
|
|
6740
9974
|
children: [
|
|
@@ -6749,6 +9983,18 @@ window.__ModuleLoader__.load({
|
|
|
6749
9983
|
children: scenery,
|
|
6750
9984
|
})
|
|
6751
9985
|
: null,
|
|
9986
|
+
showPet
|
|
9987
|
+
? jsx('div', {
|
|
9988
|
+
className: `tg-debug${petLineIsWarning(petLine) ? ' tg-warn' : ''}`,
|
|
9989
|
+
children: petLine,
|
|
9990
|
+
})
|
|
9991
|
+
: null,
|
|
9992
|
+
showMood
|
|
9993
|
+
? jsx('div', {
|
|
9994
|
+
className: `tg-debug${moodLineIsWarning(moodLine) ? ' tg-warn' : ''}`,
|
|
9995
|
+
children: moodLine,
|
|
9996
|
+
})
|
|
9997
|
+
: null,
|
|
6752
9998
|
],
|
|
6753
9999
|
})
|
|
6754
10000
|
: null,
|
|
@@ -6792,6 +10038,23 @@ window.__ModuleLoader__.load({
|
|
|
6792
10038
|
/** Display name used in client diagnostics. */
|
|
6793
10039
|
exports.name = 'theme-gallery'
|
|
6794
10040
|
|
|
10041
|
+
/**
|
|
10042
|
+
* 心情问候的座位:shell.overlay 槽位里的一张静态 div。
|
|
10043
|
+
*
|
|
10044
|
+
* 零 hooks、零 props——问候条的几何/文本/速度全由 mood 模块命令式管理
|
|
10045
|
+
* (见 moodBuildBar),这张 div 只是给 React 一个可卸载的挂载点。
|
|
10046
|
+
* `data-dsh-mood-seat` 是 MOOD_SEAT_SELECTOR 找它的稳定钩子(自己的数据属性,
|
|
10047
|
+
* 不依赖框架的哈希类名——SKILL §13 的老规矩)。
|
|
10048
|
+
* @returns the seat element.
|
|
10049
|
+
*/
|
|
10050
|
+
function MoodSeat() {
|
|
10051
|
+
return jsx('div', {
|
|
10052
|
+
className: 'dsh-mood-seat',
|
|
10053
|
+
'data-dsh-mood-seat': '',
|
|
10054
|
+
'aria-hidden': 'true',
|
|
10055
|
+
})
|
|
10056
|
+
}
|
|
10057
|
+
|
|
6795
10058
|
/**
|
|
6796
10059
|
* The HARD dependency list — deliberately ONE entry.
|
|
6797
10060
|
*
|
|
@@ -7578,11 +10841,23 @@ window.__ModuleLoader__.load({
|
|
|
7578
10841
|
// The registry keeps the order things were registered in, so re-ranking a card can
|
|
7579
10842
|
// never change which themes exist, what they contain, or the order they were
|
|
7580
10843
|
// contributed — `cardsInDisplayOrder` is a view over the filtered list.
|
|
7581
|
-
|
|
10844
|
+
// 宠物挂件卡不是主题、不在注册表里:作为一张**只存在于面板**的卡片数据追加进来,
|
|
10845
|
+
// 与其他卡走同一条排序管线(见 `petWidgetCardTheme`)。
|
|
10846
|
+
const themes = cardsInDisplayOrder([...snapshot.themes].filter((theme) => !OMITTED_IDS.has(theme.id))
|
|
10847
|
+
.concat([petWidgetCardTheme(), moodCardTheme()]))
|
|
7582
10848
|
storeActions.sync(themes, appliedCardId(snapshot), revision)
|
|
7583
10849
|
// 卡片上点亮哪一格,以及要不要把那一层配色叠上去 —— 两件事都读同一份
|
|
7584
10850
|
// localStorage,所以"看到的选中态"与"屏幕上的颜色"不可能对不上。
|
|
7585
10851
|
storeActions.markScheme(wantedScheme())
|
|
10852
|
+
// 挂件卡的徽标读同一个来源:localStorage 里记着的开关状态。
|
|
10853
|
+
storeActions.markPet(petWidgetOn())
|
|
10854
|
+
// 头像选择行的点亮态与卡面图案也读同一个来源:当前是哪只宠物。
|
|
10855
|
+
storeActions.markPetKind(readPetState().kind)
|
|
10856
|
+
// 心情问候卡的胶囊开关与三组 chips 读同一个来源。
|
|
10857
|
+
storeActions.markMood(readMoodState())
|
|
10858
|
+
// 问候条是幂等同步:状态/几何/文本没变就一个字节都不写(moodBuildBar 指纹守卫),
|
|
10859
|
+
// 所以 publish 顺路调用它没有性能负担,还负责把"座位晚渲染"捡起来。
|
|
10860
|
+
syncMoodBar()
|
|
7586
10861
|
syncSkin(snapshot)
|
|
7587
10862
|
syncRememberedSkin(snapshot)
|
|
7588
10863
|
syncScheme(snapshot)
|
|
@@ -7649,6 +10924,92 @@ window.__ModuleLoader__.load({
|
|
|
7649
10924
|
}
|
|
7650
10925
|
}
|
|
7651
10926
|
|
|
10927
|
+
/**
|
|
10928
|
+
* 用户点了挂件卡(或卡上的开关动作):翻转开关、同步舞台、刷新面板。
|
|
10929
|
+
*
|
|
10930
|
+
* 这条路**完全不碰主题服务** —— 挂件与当前是什么皮肤正交,所以它能与任何皮肤
|
|
10931
|
+
* (包括山青婷彩 / 梦海游鱼)同时开启,也不消耗主题写入预算。
|
|
10932
|
+
* `publish()` 会把新状态经 `markPet` 报给页面,徽标与 aria-pressed 随之更新。
|
|
10933
|
+
*/
|
|
10934
|
+
function togglePetWidget() {
|
|
10935
|
+
try {
|
|
10936
|
+
const next = !petWidgetOn()
|
|
10937
|
+
savePetState({ on: next })
|
|
10938
|
+
petRun.enabled = next
|
|
10939
|
+
syncPetStage()
|
|
10940
|
+
publish(ctx.theme.getTheme())
|
|
10941
|
+
} catch (error) {
|
|
10942
|
+
console.error('[theme-gallery] could not toggle the pet widget:', error)
|
|
10943
|
+
}
|
|
10944
|
+
}
|
|
10945
|
+
|
|
10946
|
+
/**
|
|
10947
|
+
* 用户点了心情问候卡(或卡上的胶囊开关):翻转开关、同步问候条、刷新面板。
|
|
10948
|
+
*
|
|
10949
|
+
* 与 `togglePetWidget` 同一条路:**完全不碰主题服务** —— 问候与当前是什么皮肤
|
|
10950
|
+
* 正交;`publish()` 会把新状态经 `markMood` 报给页面,徽标与 aria-checked 随之更新。
|
|
10951
|
+
*/
|
|
10952
|
+
function toggleMoodWidget() {
|
|
10953
|
+
try {
|
|
10954
|
+
const next = !readMoodState().on
|
|
10955
|
+
saveMoodState({ on: next })
|
|
10956
|
+
syncMoodBar()
|
|
10957
|
+
publish(ctx.theme.getTheme())
|
|
10958
|
+
} catch (error) {
|
|
10959
|
+
console.error('[theme-gallery] could not toggle the mood greeting:', error)
|
|
10960
|
+
}
|
|
10961
|
+
}
|
|
10962
|
+
|
|
10963
|
+
/**
|
|
10964
|
+
* 用户点了问候卡上的一枚设置 chip(速度/方向/区域)。
|
|
10965
|
+
* 只认表里真实存在的值(MOOD_SPEEDS / ltr-rtl / MOOD_ZONE_GEOMETRY),
|
|
10966
|
+
* 不认识的静默丢 —— 与 chooseScheme 拒绝不认识方案 id 同一条规矩:
|
|
10967
|
+
* 写进 localStorage 会让下次启动带着一个点不亮的选中态。
|
|
10968
|
+
* @param patch - `{ speed? , direction?, zone? }` 的部分字段。
|
|
10969
|
+
*/
|
|
10970
|
+
function setMoodSettings(patch) {
|
|
10971
|
+
try {
|
|
10972
|
+
const clean = {}
|
|
10973
|
+
if (patch !== null && typeof patch === 'object') {
|
|
10974
|
+
if (typeof patch.speed === 'string' && patch.speed in MOOD_SPEEDS) clean.speed = patch.speed
|
|
10975
|
+
if (patch.direction === 'ltr' || patch.direction === 'rtl') clean.direction = patch.direction
|
|
10976
|
+
if (typeof patch.zone === 'string' && patch.zone in MOOD_ZONE_GEOMETRY) clean.zone = patch.zone
|
|
10977
|
+
}
|
|
10978
|
+
if (Object.keys(clean).length === 0) return
|
|
10979
|
+
saveMoodState(clean)
|
|
10980
|
+
syncMoodBar()
|
|
10981
|
+
publish(ctx.theme.getTheme())
|
|
10982
|
+
} catch (error) {
|
|
10983
|
+
console.error('[theme-gallery] could not set the mood settings:', error)
|
|
10984
|
+
}
|
|
10985
|
+
}
|
|
10986
|
+
|
|
10987
|
+
/**
|
|
10988
|
+
* 用户点了挂件卡上的一颗头像:换成那只宠物并放它出场。
|
|
10989
|
+
*
|
|
10990
|
+
* 语义(需求 R2):挂件关着时点头像 = 顺路**打开**("点哪只出哪只",即时反馈);
|
|
10991
|
+
* 开着时 = 只换宠物。`placed` 一并复位 —— 不同宠物尺寸不同,继承上一只的
|
|
10992
|
+
* 视口偏移会错位,切宠即按新宠物的缺省摆位重新入座、跑向自己的道具。
|
|
10993
|
+
* 这条路与 `togglePetWidget` 一样**完全不碰主题服务**。
|
|
10994
|
+
* @param kindId - 宠物 id(不认识的静默拒绝,照 chooseScheme 的规矩)。
|
|
10995
|
+
*/
|
|
10996
|
+
function selectPetWidget(kindId) {
|
|
10997
|
+
try {
|
|
10998
|
+
if (!PET_KIND_BY_ID.has(kindId)) return
|
|
10999
|
+
const turningOn = !petWidgetOn()
|
|
11000
|
+
const patch = { kind: kindId, placed: false }
|
|
11001
|
+
if (turningOn) patch.on = true
|
|
11002
|
+
savePetState(patch)
|
|
11003
|
+
petRun.enabled = true
|
|
11004
|
+
removePetStage()
|
|
11005
|
+
petNoteInteraction()
|
|
11006
|
+
syncPetStage()
|
|
11007
|
+
publish(ctx.theme.getTheme())
|
|
11008
|
+
} catch (error) {
|
|
11009
|
+
console.error('[theme-gallery] could not select the pet:', error)
|
|
11010
|
+
}
|
|
11011
|
+
}
|
|
11012
|
+
|
|
7652
11013
|
/**
|
|
7653
11014
|
* 卡片上「已应用」徽标应该落在哪一张卡上。
|
|
7654
11015
|
*
|
|
@@ -7833,6 +11194,9 @@ window.__ModuleLoader__.load({
|
|
|
7833
11194
|
}
|
|
7834
11195
|
const active = lastSnapshot?.active?.id
|
|
7835
11196
|
const kind = typeof active === 'string' ? bundledTheme(active)?.ambient?.kind : undefined
|
|
11197
|
+
// 宠物挂件不依赖侧栏,只锚输入框:放在侧栏存在性判断**之前**,节流与本帧合并
|
|
11198
|
+
// 共用(resync 至多每帧一次)。未开启时它第一行就返回,成本是一次布尔判断。
|
|
11199
|
+
syncPetStage()
|
|
7836
11200
|
if (sidebarColumn() === null) {
|
|
7837
11201
|
// Recorded even though there is nothing to draw. An attempt that finds no sidebar
|
|
7838
11202
|
// is the likeliest explanation for boot-time silence, and the old report could not
|
|
@@ -8019,6 +11383,14 @@ window.__ModuleLoader__.load({
|
|
|
8019
11383
|
// 配色卡上的 15 个色值按钮走这条。它**不直接**切主题(见 `chooseScheme`),
|
|
8020
11384
|
// 所以这里不新增主题写入点。
|
|
8021
11385
|
pickScheme: (schemeId) => { chooseScheme(schemeId) },
|
|
11386
|
+
// 挂件卡走这条:只开关挂件、只写 localStorage,同样不新增主题写入点。
|
|
11387
|
+
togglePet: () => { togglePetWidget() },
|
|
11388
|
+
// 头像选择行走这条:换宠物(必要时顺路打开),同样零主题写入。
|
|
11389
|
+
pickPet: (kindId) => { selectPetWidget(kindId) },
|
|
11390
|
+
// 心情问候卡走这两条:胶囊开关/点卡身开关、三组设置 chips。
|
|
11391
|
+
// 一样零主题写入 —— 问候与皮肤正交。
|
|
11392
|
+
toggleMood: () => { toggleMoodWidget() },
|
|
11393
|
+
setMood: (patch) => { setMoodSettings(patch) },
|
|
8022
11394
|
}
|
|
8023
11395
|
},
|
|
8024
11396
|
}, ThemeGalleryPage)
|
|
@@ -8070,6 +11442,49 @@ window.__ModuleLoader__.load({
|
|
|
8070
11442
|
label: () => zh.title,
|
|
8071
11443
|
}, PanelGlyph))
|
|
8072
11444
|
|
|
11445
|
+
/* ---------------- mood greeting seat ----------------
|
|
11446
|
+
*
|
|
11447
|
+
* 问候条本体由 mood 模块命令式地建进这张座位(`shell.overlay` 是官方扩展点:
|
|
11448
|
+
* 布局壳在 AppFrame 最后渲染 `[data-shell-overlay]`,quota 提示、快捷键弹窗等
|
|
11449
|
+
* 五个官方包都往里注册组件)。座位是一张**静态 div**:零 hooks(本插件只引
|
|
11450
|
+
* jsx/jsxs,不引 react),一切状态由 localStorage 报给 store、由 publish 驱动
|
|
11451
|
+
* syncMoodBar。`aria-hidden`:滚动文字对读屏是噪音,装饰读数走面板自检行。
|
|
11452
|
+
*
|
|
11453
|
+
* 为什么不直接 append 到 body(挂件的做法):overlay 条目随外壳的 React 生命
|
|
11454
|
+
* 周期走,外壳重组合时不会留下孤儿条;"座位还没渲染"由 moodReport 留读数、
|
|
11455
|
+
* 下一次 publish 再捡,不轮询。
|
|
11456
|
+
*
|
|
11457
|
+
* 注入回调返回的清理函数在插件卸载时收尾全部计时器/监听器/样式表 ——
|
|
11458
|
+
* **刻意不叫 `…Dispose`**:那是自发光守卫审计里主题层专用模式的名字(规则 1)。
|
|
11459
|
+
*/
|
|
11460
|
+
ctx.slots.inject('shell.overlay', () => ctx.slots.register({
|
|
11461
|
+
name: 'shell.overlay',
|
|
11462
|
+
id: 'theme-gallery:mood',
|
|
11463
|
+
locale: NS,
|
|
11464
|
+
inject: () => ({}),
|
|
11465
|
+
}, MoodSeat))
|
|
11466
|
+
|
|
11467
|
+
/* ---------------- pet widget ----------------
|
|
11468
|
+
*
|
|
11469
|
+
* 启用状态在挂载时读一次 localStorage(存进 petRun.enabled 作镜像,避免 resync
|
|
11470
|
+
* 每帧都读存储),开着就把舞台建起来;关闭时什么都不装。卸载时整层拆掉 ——
|
|
11471
|
+
* 舞台、样式表与全部定时器都在 removePetStage 里收尾。
|
|
11472
|
+
*/
|
|
11473
|
+
ctx.effect(() => {
|
|
11474
|
+
if (typeof document === 'undefined') return
|
|
11475
|
+
petRun.enabled = petWidgetOn()
|
|
11476
|
+
if (petRun.enabled) syncPetStage()
|
|
11477
|
+
return () => { removePetStage() }
|
|
11478
|
+
}, 'theme-gallery: pet widget')
|
|
11479
|
+
|
|
11480
|
+
/* 心情问候:挂载即首建(座位可能还没渲染——syncMoodBar 会留读数,等 publish
|
|
11481
|
+
* 顺路再捡),卸载时收尾全部计时器/监听器/条/样式表。 */
|
|
11482
|
+
ctx.effect(() => {
|
|
11483
|
+
if (typeof document === 'undefined') return
|
|
11484
|
+
syncMoodBar()
|
|
11485
|
+
return () => { moodTeardown() }
|
|
11486
|
+
}, 'theme-gallery: mood greeting')
|
|
11487
|
+
|
|
8073
11488
|
/* ---------------- reading state ---------------- */
|
|
8074
11489
|
|
|
8075
11490
|
/** The stylesheet element carrying the one reading rule, once installed. */
|