dsh-metaphysics 0.59.0 → 0.60.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.
@@ -0,0 +1,604 @@
1
+ // 盘面顶部的那段「人话总结」:把已经算出来的结构,翻译成一段不用术语也能读的话。
2
+ //
3
+ // 为什么单独成文件、而不是塞进 interpret:
4
+ // 1) interpret 是「解析」,回答的是「凭什么这么判」,一屏几十条,普通人第一眼读不动;
5
+ // 2) 这里是「先说什么」,只讲四件事——底色、感情、卡点、怎么办,每条都必须指回盘面上的某个事实;
6
+ // 3) 它必须是纯函数:同一张盘每次生成的文字完全一样,不能有随机、不能有「大概」「可能很准」这类含糊。
7
+ //
8
+ // 一条硬规矩:这里只做「结构 → 人话」的翻译,不做吉凶断语。
9
+ // 「官杀混」可以说成「你容易遇到两种类型的人」,不能说成「你婚姻不好」。
10
+
11
+ import { ASK_KINDS } from './event-prompt.js';
12
+ import { PALACE_FIELD, GATE_MEANING } from './qimen-prompt.js';
13
+
14
+ const ASK = ASK_KINDS || {};
15
+ const PALACE = PALACE_FIELD || {};
16
+ const GATES = GATE_MEANING || {};
17
+
18
+ // 命盘的「询问方向」与问事提示的「六类问法」不是一套键:
19
+ // 财运要落到「生意/合作」、学业落到「工作/事业」、健康与出行各自归位。
20
+ // 不转这一道,选「财运」问六爻时会取不到现实核对清单——建议那一段就是空的。
21
+ const DIR2KIND = { wealth: 'business', career: 'career', love: 'love', health: 'other', study: 'career', travel: 'action', general: 'other' };
22
+ const askKind = (dir) => DIR2KIND[dir] || (ASK[dir] ? dir : 'other');
23
+
24
+ const ELEMENT_COLOR = {
25
+ 木: { color: '青、绿', where: '东边、有树有水的地方', daily: '早上多出门走走' },
26
+ 火: { color: '红、橙、紫', where: '南边、向阳的房间', daily: '白天多晒太阳,别总待在阴冷的屋里' },
27
+ 土: { color: '黄、棕、米色', where: '中原地带、干爽高燥处', daily: '把作息和吃饭时间固定下来' },
28
+ 金: { color: '白、银、浅灰', where: '西边、通风利落的地方', daily: '把该断的事断干净,别拖' },
29
+ 水: { color: '黑、深蓝', where: '北边、临水处', daily: '留出独处和放空的时间' },
30
+ };
31
+
32
+ const GOD_PLAIN = {
33
+ 正印: '靠吸收和消化来获得安全感,习惯先想明白再动',
34
+ 偏印: '想得比说得多,一件事会在心里推演出很多种可能',
35
+ 正官: '对规矩和责任敏感,别人一句交代会变成自己的压力',
36
+ 七杀: '对压力和竞争敏感,遇强则强,也容易被强势的人吸引',
37
+ 正财: '对现实条件敏感,不太会被空话打动',
38
+ 偏财: '对机会敏感,愿意为看得见的回报承担风险',
39
+ 食神: '表达温和,喜欢把日子过得有滋味',
40
+ 伤官: '有自己的标准,看不上粗糙的东西,容易先挑毛病',
41
+ 比肩: '自我边界清楚,不太愿意被安排',
42
+ 劫财: '同辈之间容易较劲,也容易替朋友出头',
43
+ };
44
+
45
+ const STAGE_PLAIN = {
46
+ 长生: '刚起步,有生机',
47
+ 沐浴: '不安定,容易被外界牵动',
48
+ 冠带: '在成形,开始要面子',
49
+ 临官: '正当力,能扛事',
50
+ 帝旺: '很足,但也容易过满',
51
+ 衰: '开始收力,做事偏保守',
52
+ 病: '使不上劲,需要借外力',
53
+ 死: '静态,不爱主动',
54
+ 墓: '收着、藏着,不爱外露',
55
+ 绝: '在这里使不上劲,容易觉得被压着',
56
+ 胎: '还在孕育,慢热',
57
+ 养: '在积攒,等时机',
58
+ };
59
+
60
+ const STAR_PLAIN = {
61
+ 紫微: '要主导、在意体面',
62
+ 天机: '脑子转得快,爱盘算',
63
+ 太阳: '愿意给出去,也容易被消耗',
64
+ 武曲: '务实,认结果',
65
+ 天同: '图安稳,怕冲突',
66
+ 廉贞: '要强,情绪起伏大',
67
+ 天府: '稳,能存住东西',
68
+ 太阴: '细腻,心思藏在里面',
69
+ 贪狼: '欲望强、多才多艺',
70
+ 巨门: '爱辨析,也容易生口舌',
71
+ 天相: '会配合,擅长讲和',
72
+ 天梁: '老成,愿意护着别人',
73
+ 七杀: '独断,敢冲',
74
+ 破军: '破旧立新,不安于现状',
75
+ };
76
+
77
+ const BRIGHT_PLAIN = { 庙: '得力', 旺: '得力', 得地: '还算得力', 平: '一般', 陷: '不太稳' };
78
+
79
+ const uniq = (arr) => [...new Set(arr.filter(Boolean))];
80
+
81
+ const MONTH_SEASON = {
82
+ 寅: '初春', 卯: '仲春', 辰: '暮春', 巳: '初夏', 午: '盛夏', 未: '长夏',
83
+ 申: '初秋', 酉: '仲秋', 戌: '深秋', 亥: '初冬', 子: '仲冬', 丑: '隆冬',
84
+ };
85
+
86
+ /** 取日支(配偶宫)与年/月/时支之间的破、冲、合、害 */
87
+ function branchLinks(relations, dayBranch) {
88
+ const out = { po: [], chong: [], he: [] };
89
+ for (const t of (relations && relations.branches) || []) {
90
+ if (!dayBranch || !String(t).includes(dayBranch)) continue;
91
+ if (/破/.test(t)) out.po.push(t);
92
+ else if (/冲/.test(t)) out.chong.push(t);
93
+ else if (/合/.test(t)) out.he.push(t);
94
+ }
95
+ return out;
96
+ }
97
+
98
+ function baziSummary(r) {
99
+ const P = r.pillars || {};
100
+ const day = P.day || {};
101
+ const month = P.month || {};
102
+ const dm = r.dayMaster || {};
103
+ const cl = r.climate || {};
104
+ const gender = (r.input && r.input.gender) || 'female';
105
+ const topGods = Object.entries((r.frame && r.frame.godStats && r.frame.godStats.groups) || {})
106
+ .filter(([, v]) => v > 0).sort((a, b) => b[1] - a[1]);
107
+ const godNames = (r.frame && r.frame.yongshen && r.frame.yongshen.favorTenGods) || [];
108
+ const favor = (r.frame && r.frame.yongshen && r.frame.yongshen.favor) || [];
109
+ const avoid = (r.frame && r.frame.yongshen && r.frame.yongshen.avoid) || [];
110
+
111
+ // ── 总纲:日主 + 生月 + 冷暖 + 缺什么 ──
112
+ const season = MONTH_SEASON[month.branch] || `${month.branch}月`;
113
+ const climateWord = cl.label === '寒湿' ? '天寒水旺,木像冬天的藤,不是不强,是冷得伸展不开'
114
+ : cl.label === '燥热' ? '火土偏燥,像久旱的地,需要水和湿气来松一松'
115
+ : '寒暖相对均衡,没有极端的偏枯';
116
+ const needEl = (cl.need && cl.need.element) || favor[0] || '';
117
+ const lead = `${dm.stem || ''}${dm.element || ''}日主,生在${season}${month.branch || ''}月:${climateWord}。`
118
+ + `${needEl ? `最需要的是${needEl}${cl.need && cl.need.why ? `——${cl.need.why}` : ''}。` : ''}`
119
+ + `整体${r.strength ? r.strength.level : '中和'},不是极旺也不是极弱,所以${r.strength && r.strength.level === '身弱' ? '很多事要靠外部条件配合,硬扛不如借力' : '撑得住,关键是别把力气用在错的地方'}。`;
120
+
121
+ // ── 底色:十神权重前两位 ──
122
+ const top2 = topGods.slice(0, 2).map(([k]) => k);
123
+ // 具体到哪一个十神:优先取盘上真出现的那个(天干 + 藏干本气),
124
+ // 否则「食伤偏重」会把伤官说成食神——两者的人话差得远。
125
+ const present = [];
126
+ for (const p of Object.values(P)) {
127
+ if (!p) continue;
128
+ if (p.stemTenGod) present.push(p.stemTenGod);
129
+ if (p.hidden && p.hidden[0] && p.hidden[0].tenGod) present.push(p.hidden[0].tenGod);
130
+ }
131
+ const godSentences = top2.map((g) => {
132
+ const inner = { 印: ['正印', '偏印'], 官杀: ['正官', '七杀'], 食伤: ['食神', '伤官'], 财: ['正财', '偏财'], 比劫: ['比肩', '劫财'] }[g] || [];
133
+ const hit = inner.find((x) => present.includes(x)) || inner[0];
134
+ return hit && GOD_PLAIN[hit] ? `${g}偏重:${GOD_PLAIN[hit]}。` : '';
135
+ }).filter(Boolean);
136
+ const basics = `${godSentences.join('')}${godNames.length ? '' : ''}` +
137
+ (top2.includes('印') ? '想得多是你的默认状态,好处是稳,代价是慢和累。' : '');
138
+
139
+ // ── 感情:女命看官杀、男命看财 ──
140
+ const spouseGods = gender === 'female' ? ['正官', '七杀'] : ['正财', '偏财'];
141
+ const spouseName = gender === 'female' ? '官杀(正官与七杀)' : '财星(正财与偏财)';
142
+ const positions = { year: '年干', month: '月干', day: '日干', hour: '时干' };
143
+ const exposed = Object.entries(P)
144
+ .filter(([, p]) => p && spouseGods.includes(p.stemTenGod))
145
+ .map(([k, p]) => `${positions[k]}${p.stemTenGod}`);
146
+ const dayHiddenGod = (day.hidden && day.hidden[0] && day.hidden[0].tenGod) || '';
147
+ const links = branchLinks(r.relations, day.branch);
148
+ const loveBits = [];
149
+ loveBits.push(`${gender === 'female' ? '女命' : '男命'}看配偶星取${spouseName}${exposed.length ? `,${exposed.join('、')}` : ',盘中未透干'}` +
150
+ `${exposed.length ? '——摆在明处,说明这件事在你生活里是绕不开的主题' : '——不显在明处,往往要靠后天遇到的人把它带出来'}。`);
151
+ if (dayHiddenGod) loveBits.push(`日支(配偶宫)坐${day.branch}${dayHiddenGod}${day.dishi ? `,日主在这里是「${day.dishi}」:${STAGE_PLAIN[day.dishi] || ''}` : ''}。`);
152
+ if (links.po.length) loveBits.push(`日支与另一支相破(${links.po.join('、')}):破不是吵架,是那种说不清、解释不明白的暗耗,容易冷处理。`);
153
+ if (links.chong.length) loveBits.push(`日支逢冲(${links.chong.join('、')}):关系里变动感强,聚散、异地、节奏不一致都可能出现。`);
154
+ if (links.he.length) loveBits.push(`日支有合(${links.he.join('、')}):容易被关系拴住,进去了就不太想出来。`);
155
+ if (r.hourUnknown) loveBits.push('时柱暂缺,所以「子女」这一路的信息读不出,感情结论也就少了三分之一。');
156
+
157
+ // ── 卡点:破冲 + 伤官见官 + 印重内耗 ──
158
+ const gejuNames = ((r.frame && r.frame.geju) || []).map((g) => g.name);
159
+ const stucks = [];
160
+ if (links.po.length) stucks.push('破在日支:最伤人的往往不是大事,是没说开的小事。');
161
+ if (gejuNames.includes('伤官见官')) stucks.push('盘里有「伤官见官」的组合:既想要稳定,又忍不了对方的毛病,这是同一件事的两面。');
162
+ if (top2.includes('印') && !top2.includes('财')) stucks.push('印重而财轻:想得多、落得少;越焦虑越容易继续想。');
163
+ if (!stucks.length) stucks.push('这一盘没有特别尖锐的冲克,卡点更多在「节奏」上——什么时候该动,什么时候该停。');
164
+
165
+ // ── 建议 ──
166
+ const advice = [];
167
+ if (needEl && ELEMENT_COLOR[needEl]) {
168
+ const c = ELEMENT_COLOR[needEl];
169
+ advice.push(`多接触${needEl}:${c.color}这类颜色、${c.where},日常可以${c.daily}。`);
170
+ }
171
+ if (avoid.length) advice.push(`少把自己放在${avoid.slice(0, 2).join('、')}过重的环境里——不是不能碰,是别长期待在那种氛围中。`);
172
+ if (top2.includes('印')) advice.push('给「想」设个时限:一件事想超过十分钟还没结论,就先做一个最小动作,用行动换信息。');
173
+ if (gejuNames.includes('伤官见官')) advice.push('把标准用在事情上,不要用在人身上;对人提要求之前,先说清你需要什么。');
174
+
175
+ return {
176
+ system: 'bazi',
177
+ title: '先看这一张:这张盘大致在说什么',
178
+ chips: uniq([`${dm.stem || ''}${dm.element || ''}日主`, `${month.branch || ''}月`, cl.label || '', r.strength ? r.strength.level : '', r.hourUnknown ? '时柱暂缺' : '']),
179
+ lead,
180
+ sections: [
181
+ { h: '你的底色', t: basics || '结构比较均衡,没有一边倒的偏重。' },
182
+ { h: '感情这件事', t: loveBits.join('') },
183
+ { h: '最容易卡住的地方', t: stucks.join('') },
184
+ ],
185
+ advice,
186
+ caveat: r.hourUnknown
187
+ ? '时柱暂缺:以上只据年、月、日六个字,等于全盘信息的七成左右。子女缘分、晚年走势、以及「内心最深处那一层」都读不全,结论只作参考。'
188
+ : '',
189
+ footer: '以上都是对结构的描述,不是断语;判断好坏要结合你自己的处境。',
190
+ };
191
+ }
192
+
193
+ function ziweiSummary(r) {
194
+ const pal = (r.palaces || []);
195
+ const ming = pal[0] || {};
196
+ const body = r.body || {};
197
+ const mainStars = (ming.stars || []).filter((s) => s.type === '主星');
198
+ const bodyPalace = pal.find((p) => p.isBody) || {};
199
+ const love = pal.find((p) => p.palace === '夫妻') || {};
200
+ const mainName = mainStars.map((s) => s.name).join('、') || '空宫(借对宫)';
201
+ const bright = mainStars.map((s) => BRIGHT_PLAIN[s.brightness] || '').filter(Boolean)[0] || '';
202
+ const plainOf = (stars) => stars.filter((s) => s.type === '主星').map((s) => `${s.name}${STAR_PLAIN[s.name] ? `(${STAR_PLAIN[s.name]})` : ''}`).join(';');
203
+
204
+ const ps = r.patternSummary || {};
205
+ const broken = uniq(ps.broken || []);
206
+ const bad = uniq(ps.bad || []);
207
+ // 被判冲破的格局不再算「成」——否则同一段里既说成、又说破,读的人只会觉得自相矛盾
208
+ const formed = uniq([...(ps.good || []), ...(ps.neutral || [])]).filter((x) => !broken.includes(x));
209
+ const loveStars = (love.stars || []);
210
+ const loveMain = loveStars.filter((s) => s.type === '主星');
211
+ const loveSha = loveStars.filter((s) => s.type === '煞星');
212
+
213
+ return {
214
+ system: 'ziwei',
215
+ title: '先看这一张:这张盘大致在说什么',
216
+ chips: uniq([`命宫在${ming.branch || ''}`, mainName, r.bureau ? r.bureau.name : '', body.lands ? `身宫落${body.lands}` : '']),
217
+ lead: `命宫在${ming.branch || ''},坐${mainName}${bright ? `(${bright})` : ''};身宫落在${body.lands || body.palace || '—'}。`
218
+ + `${mainStars.length ? `底色是${mainStars.map((s) => STAR_PLAIN[s.name] || s.name).join('、')}。` : '命宫无主星,性格更多由对宫与三方决定,可塑性反而大。'}`
219
+ + `${formed.length ? `结构上成的是${formed.slice(0, 3).join('、')}——格局只说明形状,不是吉凶判词。` : '没有完整成套的格局,这一盘更多看单颗星的性质,少看组合。'}`,
220
+ sections: [
221
+ { h: '你的底色', t: `${plainOf(ming.stars || []) || '命宫无主星,靠三方四正补'}。身宫落${body.lands || '—'},说明你这辈子真正花力气的地方在${body.lands || '那一块'}。` },
222
+ { h: '感情这件事', t: `夫妻宫在${love.branch || '—'},坐${loveMain.map((s) => `${s.name}${s.brightness ? `(${BRIGHT_PLAIN[s.brightness] || s.brightness})` : ''}`).join('、') || '空宫(借对宫)'}。`
223
+ + `${loveSha.length ? `同宫还有${loveSha.map((s) => s.name).join('、')}——关系里容易起摩擦,来得快也去得快。` : '没有明显的煞星同宫,关系相对平顺。'}` },
224
+ { h: '最容易卡住的地方', t: `${broken.length ? `被冲破的格局有${broken.join('、')}——结构还在,但成色打折;冲不等于毁,说的是它不如完好时那么顶用。` : '主要格局没有明显被冲破。'}`
225
+ + `${bad.length ? `另有${bad.join('、')}这类忌格,只描述结构,不作吉凶断。` : ''}` },
226
+ ],
227
+ advice: [
228
+ `命宫主星${mainStars.length ? `是${mainStars.map((s) => s.name).join('、')},顺它的长处在做事,比硬改性格省力` : '不显,先靠环境和习惯替自己定节奏'}。`,
229
+ `身宫落${body.lands || '—'}:这块是你真正在意的地方,也是容易耗人的地方,留出余量。`,
230
+ ],
231
+ caveat: '',
232
+ footer: '以上都是对结构的描述,不是断语;判断好坏要结合你自己的处境。',
233
+ };
234
+ }
235
+
236
+ // ── 问事类与走势:同样只翻译结构 ─────────────────────────────
237
+ // 命盘讲「你是什么结构」,问事讲「这件事现在卡在哪、下一步看什么」。
238
+ // 共同底线一致:每条话都要能指回盘面上的某个符号,不写「一定会」「必然」。
239
+
240
+ const MONTH_STATE_PLAIN = { 旺: '当令,气最足', 相: '有生机,正在起', 休: '气在泄,歇着', 囚: '被压着,使不上劲', 死: '最弱,得靠外援' };
241
+ const LIUQIN_PLAIN = {
242
+ 官鬼: '职位、压力、管你的人和事',
243
+ 妻财: '钱、资源、实际回报',
244
+ 父母: '文书、答复、流程与长辈',
245
+ 子孙: '结果本身、松弛、把事情落地',
246
+ 兄弟: '同辈、分利、竞争',
247
+ };
248
+ const DAY_REL_PLAIN = {
249
+ 日生: '日辰生它,当天有人托着',
250
+ 日克: '日辰克它,当天被压一头',
251
+ 比和: '与日辰同气,力量相当',
252
+ 日合: '与日辰相合,黏住了',
253
+ 日冲: '与日辰相冲,当天就动',
254
+ };
255
+
256
+ /** 用神/世爻的「当下状态」:月令一句 + 日辰一句 + 空/动/冲的标志 */
257
+ function stateLine(l) {
258
+ if (!l) return '';
259
+ const bits = [];
260
+ const ms = MONTH_STATE_PLAIN[l.monthState] || l.monthSay || '';
261
+ if (ms) bits.push(`月令${l.monthState || ''}(${ms})`);
262
+ const ds = DAY_REL_PLAIN[l.dayRelation] || l.daySay || '';
263
+ if (ds) bits.push(ds);
264
+ if (l.kong) bits.push('旬空——抓不实,等冲或填');
265
+ if (l.moving) bits.push('发动,是这件事的动因');
266
+ if (l.monthChong) bits.push('月破,先破后立');
267
+ return bits.join(';');
268
+ }
269
+
270
+ /**
271
+ * 六爻在「没指定所问之事」时的版本。
272
+ *
273
+ * 取用神要先知道问什么;方向留在「综合」时,引擎给不出用神,也就给不出成败。
274
+ * 但一张卦并非只能读懂用神——世应、卦体(六冲/六合)、动爻这几样与问题无关,照样能读。
275
+ * 所以这里不硬凑一个用神,只讲这几样,并在结尾把「为什么没有结论」说清楚:
276
+ * 让用户知道该补一步(选方向),而不是以为插件什么都说不出来。
277
+ */
278
+ function liuyaoGeneralSummary(r) {
279
+ const hex = (r.hexStrength && r.hexStrength.lines) || [];
280
+ if (!hex.length) return null;
281
+ const shi = hex.find((l) => l.shi) || null;
282
+ const ying = hex.find((l) => l.ying) || null;
283
+ const body = r.body || {};
284
+ const ben = body.ben || {};
285
+ const bian = body.bian || {};
286
+ const moving = (r.cast && r.cast.movingPositions) || [];
287
+ const movingLines = hex.filter((l) => l.moving);
288
+ const q = String(r.question || '').trim();
289
+ const labels = uniq([...(body.labels || []), ...((r.hexStrength && r.hexStrength.labels) || [])]);
290
+ const month = r.hexStrength && r.hexStrength.month ? r.hexStrength.month.branch : '';
291
+
292
+ const form = [
293
+ ben.chong ? '六冲卦:事情容易散、节奏快' : '',
294
+ ben.he ? '六合卦:事情黏住、快不起来' : '',
295
+ body.fuYin && (body.fuYin.inner || body.fuYin.outer) ? '有伏吟:原地打转' : '',
296
+ body.fanYin && (body.fanYin.inner || body.fanYin.outer) ? '有反吟:来回反复' : '',
297
+ ].filter(Boolean);
298
+
299
+ return {
300
+ system: 'liuyao',
301
+ title: '先看这一卦:这卦大致在说什么',
302
+ chips: uniq([ben.name, bian.name ? `变${bian.name}` : '', moving.length ? `${moving.length} 爻动` : '静卦', month ? `${month}月` : '']),
303
+ lead: `${q ? `你问的是「${q}」。` : ''}本卦「${ben.name || '—'}」${bian.name ? `,${moving.length ? `第 ${moving.join('、')} 爻动` : '六爻安静'},变「${bian.name}」` : ''}。`
304
+ + `方向还留在「综合」,所以这一卦没有取用神——先只讲与问题无关的那几样:你(世爻)、事情(应爻)、卦体。`,
305
+ sections: [
306
+ { h: '代表你的是', t: shi ? `世爻在${shi.name}(${shi.branch}${shi.element}·${shi.liuqin}):${stateLine(shi) || '状态平稳'}。` : '这一卦取不到世爻。' },
307
+ { h: '代表事情的是', t: ying ? `应爻在${ying.name}(${ying.branch}${ying.element}·${ying.liuqin}):${stateLine(ying) || '状态平稳'}。应爻说的是「事情那一头」的状态,不是你努力的程度。` : '这一卦取不到应爻。' },
308
+ { h: '卦体与动爻', t: `${form.length ? `${form.join(';')}。` : '没有六冲、六合、伏吟、反吟这类整体结构。'}`
309
+ + `${movingLines.length ? `动爻在${movingLines.map((l) => `${l.name}(${l.liuqin})`).join('、')}——动的地方,就是这件事会发生变化的那一环。` : '六爻安静:眼下没有明显在动的地方,说明事情的节奏还没起来。'}` },
310
+ ],
311
+ advice: [
312
+ '先在上面选一个「询问方向」(工作、感情、生意、推进、择日):选完才会取用神,旺衰、应期、成败条件才有落点。',
313
+ ...((ASK.other && ASK.other.reality) || []).slice(0, 2),
314
+ ].filter(Boolean),
315
+ caveat: '未指定所问之事:这一版只读世应、卦体与动爻,不判成败,也不给应期——不是算不出来,是取用神要先知道问什么。',
316
+ footer: '以上是把盘面符号翻译成话,不是断语;真正的判断要看现实里那几步。',
317
+ };
318
+ }
319
+
320
+ function liuyaoSummary(r) {
321
+ const ug = r.useGod || {};
322
+ // 没有用神(方向停在「综合」)时走世应版,不让第一屏空着
323
+ if (!ug.liuqin) return liuyaoGeneralSummary(r);
324
+ const found = ug.found || [];
325
+ if (!found.length && !ug.missing) return null;
326
+ const q = String(r.question || '').trim();
327
+ const strengthLines = (r.useGodStrength && r.useGodStrength.lines) || [];
328
+ const byPos = (pos) => strengthLines.find((l) => l.position === pos) || null;
329
+ const hexLines = (r.hexStrength && r.hexStrength.lines) || [];
330
+ const shi = hexLines.find((l) => l.shi) || null;
331
+ const month = r.useGodStrength && r.useGodStrength.month ? r.useGodStrength.month.branch : '';
332
+ const day = (r.dayGanZhi && r.dayGanZhi.name) || '';
333
+
334
+ const positions = found.map((f) => f.name).join('、');
335
+ const lead = `${q ? `你问的是「${q}」。` : '你没填具体问题,这里按「综合」取用神,结论会更粗。'}`
336
+ + `这卦里代表它的是「${ug.liuqin}」(${ug.ask || ''}),`
337
+ + (found.length
338
+ ? `落在${positions}${found.length > 1 ? '——两处都出现了,两个条件要一起看' : ''}。`
339
+ : '盘中看不见它(伏藏),要等冲开或引出才有动静。')
340
+ + `日辰是${day || '—'},月建是${month || '—'}。`;
341
+
342
+ const favors = [];
343
+ const burdens = [];
344
+ for (const f of found) {
345
+ const st = byPos(f.position);
346
+ if (!st) continue;
347
+ if (/旺|相/.test(st.monthState || '') && !st.kong && !st.monthChong) favors.push(f.name);
348
+ if (/日克|日冲/.test(st.dayRelation || '') || st.kong || st.monthChong) burdens.push(f.name);
349
+ }
350
+ const balance = found.length
351
+ ? (favors.length > burdens.length ? '条件偏有利'
352
+ : burdens.length > favors.length ? '条件偏紧,得有外力或时间'
353
+ : '一半一半,取决于你怎么推')
354
+ : '没有可判的落点';
355
+
356
+ const flags = uniq([
357
+ ...(ug.labels || []).filter((x) => /两现|伏藏|六合|六冲/.test(x)),
358
+ r.body && r.body.ben && r.body.ben.chong ? '六冲卦:事情容易散,节奏偏快' : '',
359
+ r.body && r.body.ben && r.body.ben.he ? '六合卦:事情黏住,快不起来' : '',
360
+ ]);
361
+
362
+ const yingqiBranches = uniq(((r.yingqi && r.yingqi.items) || []).map((x) => x.branch)).slice(0, 4);
363
+
364
+ return {
365
+ system: 'liuyao',
366
+ title: '先看这一卦:这卦大致在说什么',
367
+ chips: uniq([r.ben && r.ben.name, `用神 ${ug.liuqin || ''}`, month ? `${month}月` : '', r.yingqi ? r.yingqi.tempo : '']),
368
+ lead,
369
+ sections: [
370
+ { h: '代表你的是', t: shi ? `世爻在${shi.name}(${shi.branch}${shi.element}·${shi.liuqin}):${stateLine(shi) || '状态平稳'}。` : '这一卦的世爻取不到,先看用神那一条。' },
371
+ { h: '代表这件事的是', t: found.length
372
+ ? found.map((f) => `${f.name} ${f.ganzhi}(${f.liuqin}):${stateLine(byPos(f.position)) || '没有明显的月日冲克'}。`).join('')
373
+ : '用神伏藏:盘中看不见它,说明这件事现在还不在明面上,要等一个引子。' },
374
+ { h: '成不成看哪里', t: `把用神的月令与日辰放在一起,盘面给的是「${balance}」。${flags.length ? `另有:${flags.join(';')}。` : ''}` },
375
+ ],
376
+ advice: [
377
+ ...((ASK[askKind(ug.direction)] && ASK[askKind(ug.direction)].reality) || []).slice(0, 2),
378
+ yingqiBranches.length ? `应期只看地支:${yingqiBranches.join('、')}——地支只是「往哪个方向等」,不是公历日期。` : '',
379
+ ].filter(Boolean),
380
+ caveat: uniq([
381
+ found.length > 1 ? `用神两现(${positions}):本插件不替你二选一,两处条件一起看。` : '',
382
+ ug.missing ? '用神伏藏:先看伏神与飞神的关系,再看有没有东西把它冲出来。' : '',
383
+ '世应只说明「你」和「事」的相对位置,不直接等于吉凶。',
384
+ ]).join(''),
385
+ footer: '以上是把盘面符号翻译成话,不是断语;真正的判断要看现实里那几步。',
386
+ };
387
+ }
388
+
389
+ function meihuaSummary(r) {
390
+ const bu = r.bodyUse;
391
+ if (!bu || !bu.body || !bu.use) return null;
392
+ const q = String(r.question || '').trim();
393
+ return {
394
+ system: 'meihua',
395
+ title: '先看这一卦:方向大致在哪',
396
+ // 标签写体用关系(谁给谁力),不把「大吉」顶在最前面:
397
+ // 引擎的 level 是条件层面的刻度,放到标题旁边就会被读成结果。
398
+ chips: uniq([r.ben && r.ben.name, r.bian && `变${r.bian.name}`, bu.relation ? `体用 ${bu.relation.split('(')[0]}` : '']),
399
+ lead: `${q ? `你问的是「${q}」。` : ''}本卦「${r.ben && r.ben.name}」,动在第 ${r.moving} 爻,变卦「${r.bian && r.bian.name}」。`
400
+ + `体卦 ${bu.body.trigram}(${bu.body.element})代表你,用卦 ${bu.use.trigram}(${bu.use.element})代表这件事:${bu.relation}。`,
401
+ sections: [
402
+ { h: '过程与结果', t: `过程看互卦「${(r.hu && r.hu.name) || '—'}」,结果看变卦「${(r.bian && r.bian.name) || '—'}」。梅花给的是方向,不是细节——它不会告诉你几号、多少钱。` },
403
+ { h: '盘面给的分量', t: `这一卦的体用关系被标成「${bu.level}」。注意:这是条件层面的判断,不是结果已经发生;条件好但不动,一样没有结果。` },
404
+ ],
405
+ advice: [
406
+ '把它当「换个角度看这件事」:体用只分「谁在给、谁在耗」,剩下要你自己补具体动作。',
407
+ '如果要推进,先定一个最小动作(打个电话、发条消息),别等条件齐了再动。',
408
+ ],
409
+ caveat: '梅花取象的自由度大:同一卦,不同人读出的细节可以不同。本插件只取体用生克这一层。',
410
+ footer: '以上是把盘面符号翻译成话,不是断语;真正的判断要看现实里那几步。',
411
+ };
412
+ }
413
+
414
+ function daliurenSummary(r) {
415
+ const sc = r.sanChuan || {};
416
+ if (!sc.chu || !sc.zhong || !sc.mo) return null;
417
+ const q = String(r.question || '').trim();
418
+ const say = (c) => `${c.zhi}(${c.liuQin || ''}·${c.jiangFull || ''})`;
419
+ const plain = (c) => (LIUQIN_PLAIN[c.liuQin] ? `——${LIUQIN_PLAIN[c.liuQin]}` : '');
420
+ return {
421
+ system: 'daliuren',
422
+ title: '先看这一课:事情怎么走到这一步',
423
+ chips: uniq([r.geJu && r.geJu.name, `${sc.chu.zhi}→${sc.mo.zhi}`, r.yueJiang && `月将${r.yueJiang.zhi}`]),
424
+ lead: `${q ? `你问的是「${q}」。` : ''}三传是 ${sc.chu.zhi} → ${sc.zhong.zhi} → ${sc.mo.zhi};`
425
+ + `末传落 ${sc.mo.zhi}(${sc.mo.liuQin || ''}·${sc.mo.jiangFull || ''}),课体是「${(r.geJu && r.geJu.name) || '—'}」。`,
426
+ sections: [
427
+ { h: '起手(初传)', t: `${say(sc.chu)}${plain(sc.chu)}。这是事情开头的样子。` },
428
+ { h: '转折(中传)', t: `${say(sc.zhong)}${plain(sc.zhong)}。中间会经过这一段,也是容易改道的地方。` },
429
+ { h: '落点(末传)', t: `${say(sc.mo)}${plain(sc.mo)}。末传比初传更接近结果——但也只是「照现在这样走下去会到哪」。` },
430
+ ],
431
+ advice: [
432
+ '先看初传:事情是从哪儿起的?把那个起点找出来,比猜结果有用。',
433
+ '中传若与初传相冲相克,说明中途会变;这时别把计划定死。',
434
+ ],
435
+ caveat: '三传讲的是传变形态,不给具体日期;同一课不同流派对课体的命名也有差异。',
436
+ footer: '以上是把盘面符号翻译成话,不是断语;真正的判断要看现实里那几步。',
437
+ };
438
+ }
439
+
440
+ function qimenSummary(r) {
441
+ const dayGan = r.siZhu && r.siZhu.day ? String(r.siZhu.day)[0] : '';
442
+ const hourGan = r.siZhu && r.siZhu.time ? String(r.siZhu.time)[0] : '';
443
+ const palaces = r.palaces || [];
444
+ if (!palaces.length) return null;
445
+ const findStem = (g) => palaces.find((p) => p.tianPan === g)
446
+ || (r.centerPalace && r.centerPalace.tianPan === g ? r.centerPalace : null);
447
+ const mine = findStem(dayGan);
448
+ const thing = findStem(hourGan);
449
+ const q = String(r.question || '').trim();
450
+ // 「6宫」是给人看的界面里最别扭的一种写法:读奇门的人认的是乾宫、巽宫。
451
+ // 宫名从 PALACE_FIELD 的释义里取(「乾宫:…」),取不到再退回数字。
452
+ const pName = (p) => {
453
+ const f = p && PALACE[p.palace] && PALACE[p.palace].field;
454
+ return f ? f.split(':')[0] : (p ? `${p.palace}宫` : '盘中取不到');
455
+ };
456
+ const pDir = (p) => (p && PALACE[p.palace] && PALACE[p.palace].dir ? `(${PALACE[p.palace].dir})` : '');
457
+ const cell = (p) => (p ? `${pName(p)}${pDir(p)}|${p.gate || '—'}·${p.star || '—'}·${p.god || '—'}` : '盘中取不到');
458
+ const rel = (p) => {
459
+ if (!p || !p.gateRelation || !p.gateRelation.type) return '门与宫的关系没算到';
460
+ return `${p.gateRelation.type}(${p.gateRelation.note || ''})`;
461
+ };
462
+ return {
463
+ system: 'qimen',
464
+ title: '先看这一局:条件和位置在哪',
465
+ // r.ju 已经含「阴遁/阳遁」,再单列一个「阴遁」标签是重复信息,占位置不增信息
466
+ chips: uniq([r.ju, r.zhiFu && `值符${r.zhiFu.star}`]),
467
+ lead: `${q ? `你问的是「${q}」。` : ''}日干 ${dayGan} 落在${cell(mine)},时干 ${hourGan} 落在${cell(thing)}——前者代表你,后者代表这件事。`
468
+ + `你这边门与宫是${rel(mine)};事情那边是${rel(thing)}。`,
469
+ sections: [
470
+ { h: '你这边的条件', t: mine
471
+ ? `你在${PALACE[mine.palace] ? PALACE[mine.palace].field : mine.palace + '宫'};门是${mine.gate || '—'}——${GATES[mine.gate] || '门名只说明形态'}。`
472
+ : '日干落在中宫或取不到:中宫无门无神,按寄宫(多寄坤二)取用,读的时候要说清这是寄宫。' },
473
+ { h: '事情那边的条件', t: thing
474
+ ? `事情落在${PALACE[thing.palace] ? PALACE[thing.palace].field : thing.palace + '宫'};门是${thing.gate || '—'}——${GATES[thing.gate] || '门名只说明形态'}。`
475
+ : '时干落在中宫或取不到,同样按寄宫看,别当成坤二本宫。' },
476
+ { h: '门名不是吉凶', t: '开门主启动、生门主生长、休门主等待、伤门主损耗、杜门主隐藏——这些说的是「这件事现在是什么形态」,不是好坏判决。要看它与日干宫、时干宫的生克。' },
477
+ ],
478
+ advice: [
479
+ '把「我这边」和「事情那边」的宫位对一下:两边相距越远,越需要中间人、流程或时间。',
480
+ '如果门名是伤门、死门,不是叫你别做,而是提醒这件事的代价和收尾要先想清楚。',
481
+ ],
482
+ caveat: uniq([r.juMethodNote || '', '奇门重方位与时机;本插件不给具体方位建议,只把宫位与门星神摊开。']).join(''),
483
+ footer: '以上是把盘面符号翻译成话,不是断语;真正的判断要看现实里那几步。',
484
+ };
485
+ }
486
+
487
+ function tarotSummary(r) {
488
+ const cards = r.cards || [];
489
+ if (!cards.length) return null;
490
+ const q = String(r.question || '').trim();
491
+ // 「关节」取当下那一张(牌阵里通常叫现在/现况/建议),没有就取中间一张。
492
+ // 以前取的是「第一张逆位」,那是随机的——同一副牌换个抽牌顺序,结论就换了个位置。
493
+ const pivot = cards.find((c) => /现在|现况|当下|建议|核心/.test(c.position || ''))
494
+ || cards[Math.floor(cards.length / 2)];
495
+ return {
496
+ system: 'tarot',
497
+ title: '先看这几张牌:它们各在说什么',
498
+ // spread 里已经写了「3 张牌阵(过去/现在/未来)」,再挂一个「3 张」是重复
499
+ chips: uniq([r.spread]),
500
+ lead: `${q ? `你问的是「${q}」。` : ''}${r.spread || ''}:`
501
+ + cards.map((c) => `${c.position}「${c.name}」${c.orientation}`).join(',') + '。',
502
+ sections: cards.slice(0, 4).map((c) => ({
503
+ h: c.position,
504
+ t: `「${c.name}」${c.orientation}:${(c.keywords || []).slice(0, 3).join('、') || c.core || ''}。${c.positionHint || ''}`,
505
+ })),
506
+ advice: [
507
+ `最值得先看的是「${pivot.name}」(${pivot.position}·${pivot.orientation})——${(pivot.keywords || []).slice(0, 3).join('、') || '它所在的位置,就是这件事现在的着力点'}。`,
508
+ '把牌当镜子:它给的是看事情的角度,不是判决。看完写一句「那我下一步做什么」。',
509
+ ],
510
+ caveat: r.systemNote || '塔罗为象征系统,牌义与元素对应存在流派差异。',
511
+ footer: '以上是把盘面符号翻译成话,不是断语;真正的判断要看现实里那几步。',
512
+ };
513
+ }
514
+
515
+ function hepanSummary(r) {
516
+ const sc = r.score;
517
+ if (!sc) return null;
518
+ const dims = (r.dimensions || []).filter((d) => d && d.text);
519
+ // 合盘里 dayMaster 是「丁火」这样的一整串,不是 {stem,element};两种写法都接住,
520
+ // 否则标签会渲染成 undefinedundefined,比不给还难看。
521
+ const name = (x) => {
522
+ const dm = x && x.dayMaster;
523
+ if (!dm) return '';
524
+ if (typeof dm === 'string') return dm.replace(/日主$/, '');
525
+ return `${dm.stem || ''}${dm.element || ''}`;
526
+ };
527
+ const a = name(r.a);
528
+ const b = name(r.b);
529
+ return {
530
+ system: 'hepan',
531
+ title: '先看这一张:你们大致怎么相处',
532
+ chips: uniq([a && b ? `${a} × ${b}` : '', sc.level, `${sc.total}/100`]),
533
+ lead: `${a && b ? `你们是 ${a} 与 ${b}。` : ''}合盘描述的是「相处方式与互补点」,不判合不合,也不决定关系成败。`
534
+ + `整体上盘面给的是「${sc.level}」(${sc.total}/100——分数只是相对刻度,不是及格线)。`,
535
+ sections: dims.slice(0, 3).map((d) => ({ h: d.label, t: d.text })),
536
+ advice: [
537
+ '一起看,别一个人看完拿去判断对方:前者是对话,后者是审判。',
538
+ '把上面的分歧当话题清单:哪一条你们真的在意,就聊哪一条。',
539
+ ],
540
+ caveat: (r.notes && r.notes[0]) || '合盘看的是相处方式与互补点,不决定关系成败。',
541
+ footer: '以上是把盘面符号翻译成话,不是断语;真正的判断要看现实里那几步。',
542
+ };
543
+ }
544
+
545
+ function trendSummary(r) {
546
+ const pts = r.points || [];
547
+ const segs = r.segments || [];
548
+ if (!pts.length || !segs.length) return null;
549
+ // 「现在」由调用方传进来的 asOf 决定(meta.nowYear),不读挂钟:
550
+ // 同一条线在任何时候重新生成,文字必须一模一样。
551
+ const nowYear = Number.isInteger(r.meta && r.meta.nowYear) ? r.meta.nowYear : null;
552
+ const cur = nowYear ? (pts.find((p) => p.year === nowYear) || null) : null;
553
+ const seg = cur
554
+ ? (segs.find((s) => cur.age >= s.startAge && cur.age <= s.endAge) || segs[0])
555
+ : segs[0];
556
+ const next = segs[segs.indexOf(seg) + 1];
557
+ const inSeg = pts.filter((p) => p.age >= seg.startAge && p.age <= seg.endAge);
558
+ const avg = inSeg.length ? inSeg.reduce((n, p) => n + p.score, 0) / inSeg.length : 0;
559
+ const allAvg = pts.reduce((n, p) => n + p.score, 0) / pts.length;
560
+ const tone = avg >= allAvg + 5 ? '比一生平均更亮' : avg <= allAvg - 5 ? '比一生平均更闷' : '与一生平均差不多';
561
+ const best = (r.highlights && r.highlights.best) || [];
562
+ const worst = (r.highlights && r.highlights.worst) || [];
563
+ const nowLine = cur
564
+ ? `你现在 ${cur.age} 岁(${nowYear} 年),在「${seg.ganZhi}」这一步的第 ${cur.age - seg.startAge + 1} 年(${seg.startAge}–${seg.endAge} 岁)`
565
+ + `${next ? `,再过 ${seg.endAge - cur.age + 1} 年到 ${next.startAge} 岁换「${next.ganZhi}」` : ',已是最后一步'}。`
566
+ : `没给当前日期(asOf),所以不标「现在在第几步」;先看第一步「${seg.ganZhi}」(${seg.startAge}–${seg.endAge} 岁)起手的样子。`;
567
+ return {
568
+ system: 'trend',
569
+ title: '先看这一条线:这些年大致什么走势',
570
+ chips: uniq([seg.ganZhi, `${seg.startAge}–${seg.endAge} 岁`, cur ? `${cur.age} 岁` : '']),
571
+ lead: `这条线按 ${(r.meta && r.meta.toAge) || pts.length} 岁排到 ${pts[pts.length - 1].year} 年,共 ${segs.length} 步大运。`
572
+ + nowLine,
573
+ sections: [
574
+ { h: '这一步的基调', t: `这一步里各年的平均分是 ${avg.toFixed(0)},${tone}(全盘平均 ${allAvg.toFixed(0)})。分数是相对刻度,不是绝对值。` },
575
+ { h: '这些年里最亮和最闷的', t: `${best.length ? `高点:${best.slice(0, 2).join('、')}` : '没有特别高的点'};${worst.length ? `低点:${worst.slice(0, 2).join('、')}` : '没有特别低的点'}。高点适合推进,低点适合收口。` },
576
+ { h: '怎么用这条线', t: '它不是命运表,是把「顺/不顺」画成可视的起伏。真正决定走向的,是你在这几步里做了什么。' },
577
+ ],
578
+ advice: [
579
+ '把下一步换运的年龄记下来:换运前后半年,通常是最容易做决定、也最容易冲动的时候。',
580
+ '低点年别做大额投入,高点年别只观望——这两条比记分数管用。',
581
+ ],
582
+ caveat: '线按年岁等距绘制,实际每步大运十年;起点与交运按节气推算,出生时刻未知时按日估算。',
583
+ footer: '以上是把盘面符号翻译成话,不是断语;真正的判断要看现实里那几步。',
584
+ };
585
+ }
586
+
587
+ /**
588
+ * 生成盘面顶部的人话总结。
589
+ * @param {object} r compute() 的结果
590
+ * @returns {null|{system,title,chips,lead,sections,advice,caveat,footer}}
591
+ */
592
+ export function plainSummary(r) {
593
+ if (!r || typeof r !== 'object') return null;
594
+ if (r.system === 'bazi') return baziSummary(r);
595
+ if (r.system === 'ziwei') return ziweiSummary(r);
596
+ if (r.system === 'liuyao') return liuyaoSummary(r);
597
+ if (r.system === 'meihua') return meihuaSummary(r);
598
+ if (r.system === 'daliuren') return daliurenSummary(r);
599
+ if (r.system === 'qimen') return qimenSummary(r);
600
+ if (r.system === 'tarot') return tarotSummary(r);
601
+ if (r.system === 'hepan') return hepanSummary(r);
602
+ if (r.system === 'trend') return trendSummary(r);
603
+ return null;
604
+ }
package/engine/random.js CHANGED
@@ -3,7 +3,15 @@
3
3
 
4
4
  /** mulberry32:32 位状态,分布够用,实现只有几行 */
5
5
  export function mulberry32(seed) {
6
- let a = Number(seed) >>> 0;
6
+ const n = Number(seed);
7
+ // 状态只有 32 位,但不能只截低 32 位(`>>> 0`):相差 2^32 的种子会跑出同一串数。
8
+ // 拿毫秒时间戳当种子时,隔 49.7 天的两次摇卦会得到完全一样的卦——用户换了种子却看不出区别,
9
+ // 「可复现」就退成了「可猜测」。所以把高位与小数位各折一份进状态;
10
+ // 整数种子(0…2^31)的低 32 位仍原样参与,小种子不会莫名其妙换一卦。
11
+ const lo = n >>> 0;
12
+ const hi = Math.floor(Math.abs(n) / 4294967296) >>> 0;
13
+ const frac = Math.round((n - Math.trunc(n)) * 4294967296) >>> 0;
14
+ let a = (lo ^ Math.imul(hi + 1, 0x85ebca6b) ^ frac) >>> 0;
7
15
  if (!a) a = 0x9e3779b9;
8
16
  return function next() {
9
17
  a = (a + 0x6d2b79f5) >>> 0;