dsh-metaphysics 0.13.6 → 0.14.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/README.md +24 -5
- package/client.js +3209 -2101
- package/engine/bazi.js +52 -13
- package/engine/calendar.js +88 -16
- package/engine/daliuren.js +36 -1
- package/engine/geo.js +28 -11
- package/engine/hepan.js +144 -0
- package/engine/index.js +262 -39
- package/engine/interpret.js +108 -6
- package/engine/lunar.js +46 -12
- package/engine/meihua.js +2 -2
- package/engine/qimen-interpret.js +176 -0
- package/engine/qimen.js +440 -0
- package/engine/random.js +33 -0
- package/engine/tarot.js +5 -4
- package/engine/vsop-earth.js +40 -0
- package/engine/ziwei-hepan.js +120 -0
- package/engine/ziwei.js +2 -2
- package/index.js +109 -25
- package/locale/en.json +1 -1
- package/locale/zh.json +2 -2
- package/package.json +13 -9
package/engine/qimen.js
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
// 时家奇门(转盘,拆补法定局)。
|
|
2
|
+
// 算法移植自 MIT 许可的 qimendunjia-standalone@0.1.0(作者:见其 LICENSE),
|
|
3
|
+
// 保留其定局/排盘口径(符头定三元、值符值使推演、天盘随值符旋转、中五宫寄坤二),
|
|
4
|
+
// 只把历法层换成本插件自己的 calendar/lunar 模块——节气、干支、农历都走同一套已验证的源码。
|
|
5
|
+
import { termInstantJd, termWindow, jdToParts, partsToJd, formatJd, ganzhiName, sexagenaryIndex, STEMS, BRANCHES } from './calendar.js';
|
|
6
|
+
import { solarToLunar } from './lunar.js';
|
|
7
|
+
|
|
8
|
+
const TIAN_GAN = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'];
|
|
9
|
+
const DI_ZHI = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'];
|
|
10
|
+
const SAN_QI_LIU_YI = ['戊', '己', '庚', '辛', '壬', '癸', '丁', '丙', '乙'];
|
|
11
|
+
const LUO_SHU_ORDER = ['1', '8', '3', '4', '9', '2', '7', '6'];
|
|
12
|
+
const DISPLAY_ORDER = ['9', '2', '7', '6', '1', '8', '3', '4'];
|
|
13
|
+
const BASIC_STARS = { 1: '天蓬', 8: '天任', 3: '天冲', 4: '天辅', 9: '天英', 2: '天芮', 7: '天柱', 6: '天心', 5: '天禽' };
|
|
14
|
+
const BASIC_GATES = { 1: '休门', 8: '生门', 3: '伤门', 4: '杜门', 9: '景门', 2: '死门', 7: '惊门', 6: '开门', 5: '' };
|
|
15
|
+
// 八神命名对齐参照实现:第五位作「勾陈」、第六位作「朱雀」(部分流派作白虎/玄武,位置一致)
|
|
16
|
+
const BASIC_GODS = ['值符', '螣蛇', '太阴', '六合', '勾陈', '朱雀', '九地', '九天'];
|
|
17
|
+
const STANDARD_STARS = ['天英', '天芮', '天柱', '天心', '天蓬', '天任', '天冲', '天辅'];
|
|
18
|
+
const STANDARD_GATES = ['景门', '死门', '惊门', '开门', '休门', '生门', '伤门', '杜门'];
|
|
19
|
+
const STANDARD_GODS = ['值符', '螣蛇', '太阴', '六合', '勾陈', '朱雀', '九地', '九天'];
|
|
20
|
+
const XUN_SHOU_TO_FULL_MAP = { 戊: '甲子戊', 己: '甲戌己', 庚: '甲申庚', 辛: '甲午辛', 壬: '甲辰壬', 癸: '甲寅癸' };
|
|
21
|
+
const ZHI_TO_GONG = { 子: '1', 丑: '8', 寅: '8', 卯: '3', 辰: '4', 巳: '4', 午: '9', 未: '2', 申: '2', 酉: '7', 戌: '6', 亥: '6' };
|
|
22
|
+
const COUNTER_CLOCKWISE_ORDER = ['1', '6', '7', '2', '9', '4', '3', '8'];
|
|
23
|
+
|
|
24
|
+
// 节气 → 三元局数(上/中/下),与参照实现一致
|
|
25
|
+
const JU_NUMBERS_MAP = {
|
|
26
|
+
冬至: '174', 惊蛰: '174', 小寒: '285', 大寒: '396', 春分: '396', 雨水: '963',
|
|
27
|
+
清明: '417', 立夏: '417', 立春: '852', 谷雨: '528', 小满: '528', 芒种: '639',
|
|
28
|
+
夏至: '936', 白露: '936', 小暑: '825', 大暑: '714', 秋分: '714', 立秋: '258',
|
|
29
|
+
寒露: '693', 立冬: '693', 处暑: '147', 霜降: '582', 小雪: '582', 大雪: '471',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// ── 历法适配:把本插件的时间轴转成奇门引擎要的 snapshot ──
|
|
33
|
+
const xunOf = (gz) => {
|
|
34
|
+
const idx = indexOfGz(gz);
|
|
35
|
+
if (idx < 0) return '';
|
|
36
|
+
const head = Math.floor(idx / 10) * 10;
|
|
37
|
+
return ganzhiName(head).name; // 甲子/甲戌/甲申/甲午/甲辰/甲寅
|
|
38
|
+
};
|
|
39
|
+
const indexOfGz = (gz) => {
|
|
40
|
+
for (let i = 0; i < 60; i++) if (ganzhiName(i).name === gz) return i;
|
|
41
|
+
return -1;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** 由公历时刻构建快照(本地民用时间轴) */
|
|
45
|
+
export function buildSnapshot({ year, month, day, hour, minute, lateZiHourMode = 'same-day' }) {
|
|
46
|
+
const jd = partsToJd({ year, month, day, hour, minute });
|
|
47
|
+
|
|
48
|
+
// 日干支:晚子时按「次日」或「当日」
|
|
49
|
+
const baseJdn = partsToJd({ year, month, day, hour: 12, minute: 0 });
|
|
50
|
+
const jdn = Math.floor(baseJdn + 0.5);
|
|
51
|
+
const isLateZi = hour >= 23;
|
|
52
|
+
const dayJdn = isLateZi && lateZiHourMode === 'next-day' ? jdn + 1 : jdn;
|
|
53
|
+
const dayGz = ganzhiName((((dayJdn + 49) % 60) + 60) % 60).name;
|
|
54
|
+
|
|
55
|
+
// 时干支(五鼠遁):23:00 之后时柱一律按次日的日干起子时,
|
|
56
|
+
// 这样 same-day / next-day 两种口径只有日柱不同、时柱一致(与参照实现一致)。
|
|
57
|
+
const hourBranchIdx = Math.floor(((hour + 1) % 24) / 2);
|
|
58
|
+
const hourStemBasisJdn = isLateZi ? jdn + 1 : jdn;
|
|
59
|
+
const hourBasisGz = ganzhiName((((hourStemBasisJdn + 49) % 60) + 60) % 60).name;
|
|
60
|
+
const dayStemIdx = STEMS.indexOf(hourBasisGz[0]);
|
|
61
|
+
const hourStemIdx = (dayStemIdx % 5) * 2 + hourBranchIdx;
|
|
62
|
+
const hourStem = STEMS[((hourStemIdx % 10) + 10) % 10];
|
|
63
|
+
const hourGz = ganzhiName(sexagenaryIndex(STEMS.indexOf(hourStem), hourBranchIdx)).name;
|
|
64
|
+
|
|
65
|
+
// 年月干支:用本插件已验证的八字引擎同款规则(节气月、立春年)
|
|
66
|
+
const yearGz = ganzhiName((((solarYearFor(year, month, day, hour, minute) - 4) % 60) + 60) % 60).name;
|
|
67
|
+
const monthGz = monthGanzhiOf(jd);
|
|
68
|
+
|
|
69
|
+
// 符头(最近的甲/己日):超神接气与置闰都以它为准
|
|
70
|
+
const fuTouOffset = (((dayJdn + 49) % 60) + 60) % 60 % 5;
|
|
71
|
+
const fuTouJdn = dayJdn - fuTouOffset;
|
|
72
|
+
const fuTouMid = fuTouJdn - 0.5 + 0.5; // 符头日正午(本地轴)
|
|
73
|
+
const winF = termWindow(year);
|
|
74
|
+
const pastF = winF.filter((t) => t.jd <= fuTouMid);
|
|
75
|
+
const fuTouTerm = pastF.length ? pastF[pastF.length - 1] : winF[0];
|
|
76
|
+
const fuTouLeadDays = Math.round(fuTouTerm.jd - fuTouMid);
|
|
77
|
+
// 符头到「下一个交节」的天数:超神越九日才谈置闰
|
|
78
|
+
const nextTerm = winF.find((t) => t.jd > fuTouMid) || winF[winF.length - 1];
|
|
79
|
+
const leadToNextTerm = Math.round(nextTerm.jd - fuTouMid);
|
|
80
|
+
|
|
81
|
+
// 当前节气(不晚于此刻的最后一个)
|
|
82
|
+
const win = termWindow(year);
|
|
83
|
+
const past = win.filter((t) => t.jd <= jd);
|
|
84
|
+
const cur = past.length ? past[past.length - 1] : win[0];
|
|
85
|
+
|
|
86
|
+
// 阴阳遁:冬至(含)到夏至(不含)为阳遁
|
|
87
|
+
const wsThis = termInstantJd(year, 23);
|
|
88
|
+
const wsYear = jd < wsThis ? year - 1 : year;
|
|
89
|
+
const wsJd = termInstantJd(wsYear, 23);
|
|
90
|
+
const ssJd = termInstantJd(wsYear + 1, 11);
|
|
91
|
+
|
|
92
|
+
// 十二时辰干支表(值符值使按 hourIndex 取)
|
|
93
|
+
const hours = [];
|
|
94
|
+
for (let i = 0; i < 12; i++) {
|
|
95
|
+
const st = (dayStemIdx % 5) * 2 + i;
|
|
96
|
+
const gz = ganzhiName(sexagenaryIndex(STEMS.indexOf(STEMS[((st % 10) + 10) % 10]), i)).name;
|
|
97
|
+
hours.push({ ganZhi: gz, xun: xunOf(gz) });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const lunar = solarToLunar(year, month, day);
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
yearGanZhi: yearGz,
|
|
104
|
+
monthGanZhi: monthGz,
|
|
105
|
+
dayGanZhi: dayGz,
|
|
106
|
+
dayXun: xunOf(dayGz),
|
|
107
|
+
monthXun: xunOf(monthGz),
|
|
108
|
+
yearXun: xunOf(yearGz),
|
|
109
|
+
hours,
|
|
110
|
+
solarTermName: cur.name,
|
|
111
|
+
fuTouTermName: fuTouTerm.name,
|
|
112
|
+
fuTouLeadDays,
|
|
113
|
+
nextTermName: nextTerm.name,
|
|
114
|
+
nextTermJd: nextTerm.jd,
|
|
115
|
+
leadToNextTerm,
|
|
116
|
+
julianDay: jd,
|
|
117
|
+
winterSolsticeJulianDay: wsJd,
|
|
118
|
+
summerSolsticeJulianDay: ssJd,
|
|
119
|
+
solarDate: `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`,
|
|
120
|
+
lunarDate: `${lunar.year}年${lunar.isLeap ? '闰' : ''}${lunar.month}月${lunar.day}日`,
|
|
121
|
+
solarTermTime: formatJd(cur.jd),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** 立春分界的公历年(与八字引擎一致) */
|
|
126
|
+
function solarYearFor(year, month, day, hour, minute) {
|
|
127
|
+
const jd = partsToJd({ year, month, day, hour, minute });
|
|
128
|
+
const lichun = termInstantJd(year, 2); // 立春
|
|
129
|
+
return jd < lichun ? year - 1 : year;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** 月柱:以「节」为界,五虎遁起月干 */
|
|
133
|
+
function monthGanzhiOf(jd) {
|
|
134
|
+
const parts = jdToParts(jd);
|
|
135
|
+
const win = termWindow(parts.year);
|
|
136
|
+
const jie = win.filter((t) => t.index % 2 === 0 && t.jd <= jd);
|
|
137
|
+
const cur = jie.length ? jie[jie.length - 1] : win[0];
|
|
138
|
+
// 节气索引 -> 月支:小寒(0)/大寒(1)=丑,立春(2)/雨水(3)=寅 …
|
|
139
|
+
const branchIdx = (Math.floor(cur.index / 2) + 1) % 12;
|
|
140
|
+
const yearForMonth = solarYearFor(parts.year, parts.month, parts.day, parts.hour, parts.minute);
|
|
141
|
+
const yinStem = ((STEMS.indexOf(ganzhiName((((yearForMonth - 4) % 60) + 60) % 60).name[0]) % 5) * 2 + 2) % 10;
|
|
142
|
+
const offset = ((branchIdx - 2) + 12) % 12;
|
|
143
|
+
return ganzhiName(sexagenaryIndex((yinStem + offset) % 10, branchIdx)).name;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── 排盘 ──
|
|
147
|
+
|
|
148
|
+
// ── 五行与同宫关系(断语层用,均为可复算事实)──
|
|
149
|
+
const GATE_ELEMENT = { 休门: '水', 生门: '土', 伤门: '木', 杜门: '木', 景门: '火', 死门: '土', 惊门: '金', 开门: '金' };
|
|
150
|
+
const STAR_ELEMENT = { 天蓬: '水', 天任: '土', 天冲: '木', 天辅: '木', 天英: '火', 天芮: '土', 天柱: '金', 天心: '金', 天禽: '土', 禽芮: '土' };
|
|
151
|
+
const PALACE_ELEMENT = { 1: '水', 2: '土', 3: '木', 4: '木', 5: '土', 6: '金', 7: '金', 8: '土', 9: '火' };
|
|
152
|
+
const SHENG = { 木: '火', 火: '土', 土: '金', 金: '水', 水: '木' };
|
|
153
|
+
const KE = { 木: '土', 土: '水', 水: '火', 火: '金', 金: '木' };
|
|
154
|
+
|
|
155
|
+
/** 两五行关系:axis = a 对 b */
|
|
156
|
+
export function wuxingRelation(a, b) {
|
|
157
|
+
if (!a || !b) return 'unknown';
|
|
158
|
+
if (a === b) return '比和';
|
|
159
|
+
if (SHENG[a] === b) return '生';
|
|
160
|
+
if (SHENG[b] === a) return '被生';
|
|
161
|
+
if (KE[a] === b) return '克';
|
|
162
|
+
if (KE[b] === a) return '被克';
|
|
163
|
+
return 'unknown';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 同宫门与宫的关系(严格按「门五行 vs 宫五行」,仅同宫成立)
|
|
168
|
+
* 门克宫=门迫;宫克门=宫制门(不是门迫);其余为门生宫/宫生门/比和
|
|
169
|
+
*/
|
|
170
|
+
export function gatePalaceRelation(gate, palace) {
|
|
171
|
+
const ge = GATE_ELEMENT[gate];
|
|
172
|
+
const pe = PALACE_ELEMENT[palace];
|
|
173
|
+
if (!ge || !pe) return { type: 'unknown', gate: ge || null, palace: pe || null, note: '缺少门或宫的五行,无法判断' };
|
|
174
|
+
if (KE[ge] === pe) return { type: '门迫', gate: ge, palace: pe, note: `${ge}克${pe},门克宫` };
|
|
175
|
+
if (KE[pe] === ge) return { type: '宫制门', gate: ge, palace: pe, note: `${pe}克${ge},宫克门(制,不为迫)` };
|
|
176
|
+
if (SHENG[ge] === pe) return { type: '门生宫', gate: ge, palace: pe, note: `${ge}生${pe}` };
|
|
177
|
+
if (SHENG[pe] === ge) return { type: '宫生门', gate: ge, palace: pe, note: `${pe}生${ge}` };
|
|
178
|
+
return { type: '比和', gate: ge, palace: pe, note: `${ge}与${pe}比和` };
|
|
179
|
+
}
|
|
180
|
+
function getDiPan(type, num) {
|
|
181
|
+
const result = {};
|
|
182
|
+
let gong = num;
|
|
183
|
+
for (const stem of SAN_QI_LIU_YI) {
|
|
184
|
+
result[gong.toString()] = stem;
|
|
185
|
+
if (type === 'yang') gong = gong === 9 ? 1 : gong + 1;
|
|
186
|
+
else gong = gong === 1 ? 9 : gong - 1;
|
|
187
|
+
}
|
|
188
|
+
return result;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function findStemPalace(diPan, stem) {
|
|
192
|
+
return Object.keys(diPan).find((g) => g !== '5' && diPan[g] === stem);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function distributeJiuXing(diPan, xunShou, shiGan) {
|
|
196
|
+
const zhiFuGong = findStemPalace(diPan, xunShou) || '2';
|
|
197
|
+
const zhiFuXing = BASIC_STARS[zhiFuGong];
|
|
198
|
+
if (!shiGan) return { zhiFuGong, zhiFuLuoGong: zhiFuGong, zhiFuXing, jiuXing: { ...BASIC_STARS } };
|
|
199
|
+
const zhiFuLuoGong = findStemPalace(diPan, shiGan) || (diPan['5'] === shiGan ? '2' : zhiFuGong);
|
|
200
|
+
const zhiFuIndex = LUO_SHU_ORDER.indexOf(zhiFuGong);
|
|
201
|
+
const luoGongIndex = LUO_SHU_ORDER.indexOf(zhiFuLuoGong);
|
|
202
|
+
if (zhiFuIndex === -1 || luoGongIndex === -1) return { zhiFuGong, zhiFuLuoGong, zhiFuXing, jiuXing: { ...BASIC_STARS } };
|
|
203
|
+
const steps = (luoGongIndex - zhiFuIndex + 8) % 8;
|
|
204
|
+
const jiuXing = { 1: '', 2: '', 3: '', 4: '', 5: '', 6: '', 7: '', 8: '', 9: '' };
|
|
205
|
+
for (let i = 0; i < LUO_SHU_ORDER.length; i++) {
|
|
206
|
+
const src = LUO_SHU_ORDER[i];
|
|
207
|
+
const dst = LUO_SHU_ORDER[(i + steps) % LUO_SHU_ORDER.length];
|
|
208
|
+
jiuXing[dst] = src === '2' ? '禽芮' : BASIC_STARS[src];
|
|
209
|
+
}
|
|
210
|
+
return { zhiFuGong, zhiFuLuoGong, zhiFuXing, jiuXing };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function distributeBaMen(zhiFuGong, shiGan, type) {
|
|
214
|
+
const originalGong = zhiFuGong === '5' ? '2' : zhiFuGong;
|
|
215
|
+
const zhiShiMen = BASIC_GATES[originalGong] || '';
|
|
216
|
+
const steps = Math.max(TIAN_GAN.indexOf(shiGan), 0);
|
|
217
|
+
let cur = Number(originalGong);
|
|
218
|
+
for (let i = 0; i < steps; i++) {
|
|
219
|
+
if (type === 'yang') cur = cur === 9 ? 1 : cur + 1;
|
|
220
|
+
else cur = cur === 1 ? 9 : cur - 1;
|
|
221
|
+
}
|
|
222
|
+
const zhiShiGong = (cur === 5 ? '2' : cur.toString());
|
|
223
|
+
const menIdx = LUO_SHU_ORDER.findIndex((g) => BASIC_GATES[g] === zhiShiMen);
|
|
224
|
+
const luoIdx = LUO_SHU_ORDER.indexOf(zhiShiGong);
|
|
225
|
+
if (menIdx === -1 || luoIdx === -1) return { zhiShiGong, zhiShiMen, baMen: { ...BASIC_GATES } };
|
|
226
|
+
const rot = (luoIdx - menIdx + 8) % 8;
|
|
227
|
+
const baMen = { 1: '', 2: '', 3: '', 4: '', 5: '', 6: '', 7: '', 8: '', 9: '' };
|
|
228
|
+
for (let i = 0; i < LUO_SHU_ORDER.length; i++) {
|
|
229
|
+
baMen[LUO_SHU_ORDER[(i + rot) % LUO_SHU_ORDER.length]] = BASIC_GATES[LUO_SHU_ORDER[i]];
|
|
230
|
+
}
|
|
231
|
+
return { zhiShiGong, zhiShiMen, baMen };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function distributeBaShen(zhiFuGong, type) {
|
|
235
|
+
const order = type === 'yang' ? LUO_SHU_ORDER : COUNTER_CLOCKWISE_ORDER;
|
|
236
|
+
const baShen = { 1: '', 2: '', 3: '', 4: '', 5: '', 6: '', 7: '', 8: '', 9: '' };
|
|
237
|
+
const start = zhiFuGong === '5' ? '2' : zhiFuGong;
|
|
238
|
+
const si = order.indexOf(start);
|
|
239
|
+
if (si === -1) return baShen;
|
|
240
|
+
for (let i = 0; i < BASIC_GODS.length; i++) baShen[order[(si + i) % order.length]] = BASIC_GODS[i];
|
|
241
|
+
return baShen;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function xunShouLiuYi(gz) {
|
|
245
|
+
const tg = TIAN_GAN.indexOf(gz[0]);
|
|
246
|
+
const dz = DI_ZHI.indexOf(gz[1]);
|
|
247
|
+
if (tg === -1 || dz === -1) return '戊';
|
|
248
|
+
const headDz = DI_ZHI[(dz - tg + 12) % 12];
|
|
249
|
+
const heads = ['子', '戌', '申', '午', '辰', '寅'];
|
|
250
|
+
const stems = ['戊', '己', '庚', '辛', '壬', '癸'];
|
|
251
|
+
const i = heads.indexOf(headDz);
|
|
252
|
+
return i === -1 ? '戊' : stems[i];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const KONG_WANG = { 甲子: ['戌', '亥'], 甲戌: ['申', '酉'], 甲申: ['午', '未'], 甲午: ['辰', '巳'], 甲辰: ['寅', '卯'], 甲寅: ['子', '丑'] };
|
|
256
|
+
|
|
257
|
+
function calcJuShu(snapshot, juMethod = 'chaibu') {
|
|
258
|
+
const ganZhi = snapshot.dayGanZhi;
|
|
259
|
+
const tg = TIAN_GAN.indexOf(ganZhi[0]);
|
|
260
|
+
const dz = DI_ZHI.indexOf(ganZhi[1]);
|
|
261
|
+
const fuTouDz = DI_ZHI[(dz - (tg % 5) + 12) % 12];
|
|
262
|
+
const yuan = ['子', '午', '卯', '酉'].includes(fuTouDz) ? 0 : ['寅', '申', '巳', '亥'].includes(fuTouDz) ? 1 : 2;
|
|
263
|
+
const dun = snapshot.julianDay >= snapshot.winterSolsticeJulianDay && snapshot.julianDay < snapshot.summerSolsticeJulianDay ? 'yang' : 'yin';
|
|
264
|
+
|
|
265
|
+
// ── 流派:节气取哪个 ──
|
|
266
|
+
// chaibu 拆补:以「当前实际节气」定局,符头只管上中下元(本实现已验证)
|
|
267
|
+
// chaotic 超神接气:局随符头走。符头先于节气到=超神(提前用该节局);符头晚于节气=接气(等符头再换局)
|
|
268
|
+
// zhirun 置闰:在拆补基础上,若符头比节气超前 9 天以上,则在芒种/大雪后置闰一个三元
|
|
269
|
+
let termName = snapshot.solarTermName;
|
|
270
|
+
let yuanOverride = -1;
|
|
271
|
+
let note = '拆补法:当前节气 + 符头定三元';
|
|
272
|
+
if (juMethod === 'chaoshen' && snapshot.fuTouTermName) {
|
|
273
|
+
termName = snapshot.fuTouTermName;
|
|
274
|
+
note = '超神接气:局随符头(符头先到用本节,节气先到等符头)';
|
|
275
|
+
} else if (juMethod === 'zhirun') {
|
|
276
|
+
// 置闰只可能落在「芒种后~夏至前」与「大雪后~冬至前」这两段,
|
|
277
|
+
// 其他节气即使超神越九日也不得置闰(改为按超神接气处理)。
|
|
278
|
+
const lead = snapshot.leadToNextTerm;
|
|
279
|
+
const inWindow = snapshot.nextTermName === '夏至' || snapshot.nextTermName === '冬至';
|
|
280
|
+
if (lead != null && lead >= 9 && inWindow) {
|
|
281
|
+
termName = snapshot.solarTermName;
|
|
282
|
+
yuanOverride = 2; // 闰奇:重复本节的下元
|
|
283
|
+
note = `置闰法:符头距${snapshot.nextTermName} ${lead} 天(超九日),在${snapshot.solarTermName}置闰下元`;
|
|
284
|
+
} else if (lead != null && lead >= 9) {
|
|
285
|
+
termName = snapshot.fuTouTermName || termName;
|
|
286
|
+
note = `置闰法:超神 ${lead} 天,但不在芒种/大雪窗口,按超神接气取${termName}`;
|
|
287
|
+
} else {
|
|
288
|
+
note = `置闰法:符头距${snapshot.nextTermName} ${lead} 天,未达置闰条件,按拆补处理`;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const nums = JU_NUMBERS_MAP[termName] || '111';
|
|
292
|
+
const num = Number(nums.charAt(yuan));
|
|
293
|
+
const CN = { 1: '一', 2: '二', 3: '三', 4: '四', 5: '五', 6: '六', 7: '七', 8: '八', 9: '九' };
|
|
294
|
+
const yuanName = ['上元', '中元', '下元'][yuanOverride >= 0 ? yuanOverride : yuan];
|
|
295
|
+
return {
|
|
296
|
+
jieQiName: termName,
|
|
297
|
+
juMethod,
|
|
298
|
+
juMethodNote: note,
|
|
299
|
+
type: dun,
|
|
300
|
+
number: num,
|
|
301
|
+
yuan: yuanName,
|
|
302
|
+
fullName: `${dun === 'yin' ? '阴遁' : '阳遁'}${CN[num]}局 (${yuanName})`,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function calcAnGan(zhiShiGong, shiGan, xunShou, type, diPan) {
|
|
307
|
+
const seq = ['戊', '己', '庚', '辛', '壬', '癸', '丁', '丙', '乙'];
|
|
308
|
+
const startStem = shiGan === '甲' ? xunShou : shiGan;
|
|
309
|
+
let si = seq.indexOf(startStem);
|
|
310
|
+
let gong = Number(zhiShiGong);
|
|
311
|
+
if (si === -1) si = 0;
|
|
312
|
+
if (diPan[gong.toString()] === startStem) gong = 5;
|
|
313
|
+
const anGan = { 1: '', 2: '', 3: '', 4: '', 5: '', 6: '', 7: '', 8: '', 9: '' };
|
|
314
|
+
for (let i = 0; i < 9; i++) {
|
|
315
|
+
anGan[gong.toString()] = seq[si];
|
|
316
|
+
si = (si + 1) % seq.length;
|
|
317
|
+
gong = type === 'yang' ? (gong === 9 ? 1 : gong + 1) : gong === 1 ? 9 : gong - 1;
|
|
318
|
+
}
|
|
319
|
+
return anGan;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const findGong = (obj, val) =>
|
|
323
|
+
Object.keys(obj).find((k) => obj[k] === val || (val === '天芮' && obj[k] === '禽芮')) || '9';
|
|
324
|
+
|
|
325
|
+
/** 排盘主函数 */
|
|
326
|
+
export function buildQimen(input) {
|
|
327
|
+
const snapshot = input.snapshot || buildSnapshot(input);
|
|
328
|
+
const hourIdx = input.hourIndex != null ? input.hourIndex : Math.floor((((input.hour ?? 0) + 1) % 24) / 2);
|
|
329
|
+
const hourGz = snapshot.hours[hourIdx] ? snapshot.hours[hourIdx].ganZhi : snapshot.hours[snapshot.hours.length - 1].ganZhi;
|
|
330
|
+
const juMethod = ['chaibu', 'chaoshen', 'zhirun'].includes(input.juMethod) ? input.juMethod : 'chaibu';
|
|
331
|
+
const juShu = calcJuShu(snapshot, juMethod);
|
|
332
|
+
const xunShou = xunShouLiuYi(hourGz);
|
|
333
|
+
const shiGan = hourGz[0];
|
|
334
|
+
const diPan = getDiPan(juShu.type, juShu.number);
|
|
335
|
+
const jx = distributeJiuXing(diPan, xunShou, shiGan);
|
|
336
|
+
const bm = distributeBaMen(jx.zhiFuGong, shiGan, juShu.type);
|
|
337
|
+
const zhiFuLuoGong = jx.zhiFuLuoGong || jx.zhiFuGong;
|
|
338
|
+
const baShen = distributeBaShen(zhiFuLuoGong, juShu.type);
|
|
339
|
+
const anGan = calcAnGan(bm.zhiShiGong, shiGan, xunShou, juShu.type, diPan);
|
|
340
|
+
|
|
341
|
+
const idxFu = LUO_SHU_ORDER.indexOf(jx.zhiFuGong);
|
|
342
|
+
const idxLuo = LUO_SHU_ORDER.indexOf(jx.zhiFuLuoGong);
|
|
343
|
+
const rot = (idxLuo - idxFu + 8) % 8;
|
|
344
|
+
const tianPan = { 1: '', 2: '', 3: '', 4: '', 5: '', 6: '', 7: '', 8: '', 9: '' };
|
|
345
|
+
LUO_SHU_ORDER.forEach((gong, i) => {
|
|
346
|
+
const stem = diPan[gong];
|
|
347
|
+
const target = LUO_SHU_ORDER[(i + rot) % LUO_SHU_ORDER.length];
|
|
348
|
+
// 中五宫寄坤二只影响地盘的归属,天盘仍是单个干(参照实现同此口径)
|
|
349
|
+
tianPan[target] = stem;
|
|
350
|
+
});
|
|
351
|
+
tianPan['5'] = diPan['5'] || ''; // 中五宫天盘取本宫地盘干
|
|
352
|
+
|
|
353
|
+
const kongZhi = KONG_WANG[snapshot.dayXun === xunOf(hourGz) ? xunOf(hourGz) : xunOf(hourGz)] || [];
|
|
354
|
+
const kongGong = kongZhi.map((z) => ZHI_TO_GONG[z]).filter(Boolean);
|
|
355
|
+
const maMap = { 申: '寅', 子: '寅', 辰: '寅', 寅: '申', 午: '申', 戌: '申', 亥: '巳', 卯: '巳', 未: '巳', 巳: '亥', 酉: '亥', 丑: '亥' };
|
|
356
|
+
const maZhi = maMap[hourGz[1]] || '';
|
|
357
|
+
const maGong = ZHI_TO_GONG[maZhi] || '';
|
|
358
|
+
|
|
359
|
+
// 中五宫单列(天禽寄坤二,门神为空),与参照实现的九宫结构对齐
|
|
360
|
+
const centerPalace = {
|
|
361
|
+
palace: '5',
|
|
362
|
+
diPan: diPan['5'] || '',
|
|
363
|
+
tianPan: tianPan['5'] || '',
|
|
364
|
+
star: '天禽',
|
|
365
|
+
gate: '',
|
|
366
|
+
god: '',
|
|
367
|
+
anGan: '',
|
|
368
|
+
palaceElement: PALACE_ELEMENT['5'],
|
|
369
|
+
gateElement: null,
|
|
370
|
+
starElement: STAR_ELEMENT['天禽'],
|
|
371
|
+
gateRelation: { type: 'unknown', note: '中宫无门,不参与门宫关系' },
|
|
372
|
+
kong: false,
|
|
373
|
+
ma: false,
|
|
374
|
+
center: true,
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
const palaces = DISPLAY_ORDER.map((gong) => ({
|
|
378
|
+
palace: gong,
|
|
379
|
+
diPan: diPan[gong] || '',
|
|
380
|
+
tianPan: tianPan[gong] || '',
|
|
381
|
+
star: jx.jiuXing[gong] || '',
|
|
382
|
+
gate: bm.baMen[gong] || '',
|
|
383
|
+
god: baShen[gong] || '',
|
|
384
|
+
anGan: anGan[gong] || '',
|
|
385
|
+
palaceElement: PALACE_ELEMENT[gong],
|
|
386
|
+
gateElement: GATE_ELEMENT[bm.baMen[gong]] || null,
|
|
387
|
+
starElement: STAR_ELEMENT[jx.jiuXing[gong]] || null,
|
|
388
|
+
gateRelation: gatePalaceRelation(bm.baMen[gong], gong),
|
|
389
|
+
kong: kongGong.includes(gong),
|
|
390
|
+
ma: maGong === gong,
|
|
391
|
+
}));
|
|
392
|
+
|
|
393
|
+
return {
|
|
394
|
+
system: 'qimen',
|
|
395
|
+
solarDate: snapshot.solarDate,
|
|
396
|
+
lunarDate: snapshot.lunarDate,
|
|
397
|
+
siZhu: {
|
|
398
|
+
year: snapshot.yearGanZhi,
|
|
399
|
+
month: snapshot.monthGanZhi,
|
|
400
|
+
day: snapshot.dayGanZhi,
|
|
401
|
+
time: hourGz,
|
|
402
|
+
},
|
|
403
|
+
ju: juShu.fullName,
|
|
404
|
+
dun: juShu.type === 'yin' ? '阴遁' : '阳遁',
|
|
405
|
+
juNumber: juShu.number,
|
|
406
|
+
juMethod: juShu.juMethod,
|
|
407
|
+
juMethodNote: juShu.juMethodNote,
|
|
408
|
+
yuan: juShu.yuan,
|
|
409
|
+
jieqi: juShu.jieQiName,
|
|
410
|
+
jieqiTime: snapshot.solarTermTime,
|
|
411
|
+
xunshou: XUN_SHOU_TO_FULL_MAP[xunShou] || '',
|
|
412
|
+
// gong = 值符所落之宫(与参照 core.fuGong 对齐);yuanGong = 值符星的本宫
|
|
413
|
+
zhiFu: { star: jx.zhiFuXing, gong: jx.zhiFuLuoGong || jx.zhiFuGong, yuanGong: jx.zhiFuGong },
|
|
414
|
+
zhiShi: { gate: bm.zhiShiMen, gong: bm.zhiShiGong },
|
|
415
|
+
kongWang: kongZhi,
|
|
416
|
+
ma: { zhi: maZhi, gong: maGong },
|
|
417
|
+
palaces,
|
|
418
|
+
centerPalace,
|
|
419
|
+
rules: [
|
|
420
|
+
'定局:以日干支符头定上中下元(子午卯酉上元、寅申巳亥中元、辰戌丑未下元),按节气取局数',
|
|
421
|
+
'阴阳遁:冬至至夏至前用阳遁,夏至至冬至前用阴遁',
|
|
422
|
+
'中五宫寄坤二宫;天盘随值符旋转,八神随值符落宫顺(阳)/逆(阴)布',
|
|
423
|
+
'本次未实现超神接气与置闰,采用拆补法',
|
|
424
|
+
],
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
|
package/engine/random.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// 确定性伪随机数:只用于让「同一次摇卦 / 抽牌」可以复现(seed 相同 → 结果相同)。
|
|
2
|
+
// 不用于任何安全用途,也不承担密码学强度。
|
|
3
|
+
|
|
4
|
+
/** mulberry32:32 位状态,分布够用,实现只有几行 */
|
|
5
|
+
export function mulberry32(seed) {
|
|
6
|
+
let a = Number(seed) >>> 0;
|
|
7
|
+
if (!a) a = 0x9e3779b9;
|
|
8
|
+
return function next() {
|
|
9
|
+
a = (a + 0x6d2b79f5) >>> 0;
|
|
10
|
+
let t = a;
|
|
11
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
12
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
13
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 从参数里取随机源:
|
|
19
|
+
* - 没给 seed:沿用 Math.random(保持"真随机"的手感)
|
|
20
|
+
* - 给了数字或数字字符串:mulberry32(seed),可复现
|
|
21
|
+
* - 给了别的东西:调用方应当先报错,这里兜底回 Math.random
|
|
22
|
+
*/
|
|
23
|
+
export function randomOf(seed) {
|
|
24
|
+
if (seed === undefined || seed === null || seed === '') return Math.random;
|
|
25
|
+
const n = Number(seed);
|
|
26
|
+
return Number.isFinite(n) ? mulberry32(n) : Math.random;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 是否是一个可以接受的 seed */
|
|
30
|
+
export function isSeed(v) {
|
|
31
|
+
if (v === undefined || v === null || v === '') return true;
|
|
32
|
+
return Number.isFinite(Number(v));
|
|
33
|
+
}
|
package/engine/tarot.js
CHANGED
|
@@ -353,17 +353,18 @@ export function tarotReading({ question = '', cards = [] } = {}) {
|
|
|
353
353
|
return paras;
|
|
354
354
|
}
|
|
355
355
|
|
|
356
|
-
/** 抽牌:count
|
|
357
|
-
export function drawTarot({ count = 3, question = '', at } = {}) {
|
|
356
|
+
/** 抽牌:count 张,按牌阵给出位置。random 可注入(配合 seed 复现同一次抽牌) */
|
|
357
|
+
export function drawTarot({ count = 3, question = '', at, random = Math.random } = {}) {
|
|
358
358
|
const n = [1, 3, 5].includes(count) ? count : 3;
|
|
359
|
+
const rand = typeof random === 'function' ? random : Math.random;
|
|
359
360
|
const deck = tarotDeck();
|
|
360
361
|
for (let i = deck.length - 1; i > 0; i--) {
|
|
361
|
-
const j = Math.floor(
|
|
362
|
+
const j = Math.floor(rand() * (i + 1));
|
|
362
363
|
[deck[i], deck[j]] = [deck[j], deck[i]];
|
|
363
364
|
}
|
|
364
365
|
const positions = SPREADS[n];
|
|
365
366
|
const cards = deck.slice(0, n).map((c, i) => {
|
|
366
|
-
const upright =
|
|
367
|
+
const upright = rand() < 0.5;
|
|
367
368
|
const pos = positions[i];
|
|
368
369
|
return {
|
|
369
370
|
position: pos.name,
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// 地球日心黄经 L 的 VSOP87B 截断级数(MIT,系数取自 astronomia 的 vsop87Bearth 数据表)。
|
|
2
|
+
// 只保留对 1900–2100 贡献 ≥ 阈值(L0/L1 2e-7、L2 5e-8、L3/L4 1e-8 rad)的项,
|
|
3
|
+
// 共 105 项,被舍弃项振幅合计 2.74 角秒(约 0.11 分钟),足以把节气误差压到 1 分钟内。
|
|
4
|
+
// 用法:earthL(T) 返回弧度,T 为儒略世纪(自 J2000 起)。
|
|
5
|
+
const L0 = [
|
|
6
|
+
[1.75347045673,0,0],[0.03341656453,4.66925680415,6283.0758499914],[0.00034894275,4.62610242189,12566.1516999828],[0.00003417572,2.82886579754,3.523118349],
|
|
7
|
+
[0.00003497056,2.74411783405,5753.3848848968],[0.00003135899,3.62767041756,77713.7714681205],[0.00002676218,4.41808345438,7860.4193924392],[0.00002342691,6.13516214446,3930.2096962196],
|
|
8
|
+
[0.00001273165,2.03709657878,529.6909650946],[0.00001324294,0.74246341673,11506.7697697936],[0.00000901854,2.04505446477,26.2983197998],[0.00001199167,1.10962946234,1577.3435424478],
|
|
9
|
+
[0.00000857223,3.50849152283,398.1490034082],[0.00000779786,1.17882681962,5223.6939198022],[0.0000099025,5.23268072088,5884.9268465832],[0.00000753141,2.53339052847,5507.5532386674],
|
|
10
|
+
[0.00000505267,4.58292599973,18849.2275499742],[0.00000492392,4.20505711826,775.522611324],[0.00000356672,2.91954114478,0.0673103028],[0.00000284125,1.89869240932,796.2980068164],
|
|
11
|
+
[0.00000242879,0.34481445893,5486.777843175],[0.00000317087,5.84901948512,11790.6290886588],[0.00000271112,0.31486255375,10977.078804699],[0.00000206217,4.80646631478,2544.3144198834],
|
|
12
|
+
[0.00000205478,1.86953770281,5573.1428014331],[0.00000202318,2.45767790232,6069.7767545534],[0.00000126225,1.08295459501,20.7753954924],[0.00000155516,0.83306084617,213.299095438],
|
|
13
|
+
[0.00000115132,0.64544911683,0.9803210682],[0.00000102851,0.63599845579,4694.0029547076],[0.00000101724,4.2667980198,7.1135470008],[9.9206e-7,6.20992926918,2146.1654164752],
|
|
14
|
+
[0.00000132212,3.41118292683,2942.4634232916],[9.7607e-7,0.68101342359,155.4203994342],[8.5128e-7,1.29870764804,6275.9623029906],[7.4651e-7,1.755089133,5088.6288397668],
|
|
15
|
+
[0.00000101895,0.97569280312,15720.8387848784],[8.4711e-7,3.67080093031,71430.69561812909],[7.3547e-7,4.67926633877,801.8209311238],[7.3874e-7,3.50319414955,3154.6870848956],
|
|
16
|
+
[7.8757e-7,3.03697458703,12036.4607348882],[7.9637e-7,1.80791287082,17260.1546546904],[8.5803e-7,5.9832263126,161000.6857376741],[5.6963e-7,2.78430458592,6286.5989683404],
|
|
17
|
+
[6.1148e-7,1.81839892984,7084.8967811152],[6.9627e-7,0.83297621398,9437.762934887],[5.6116e-7,4.38694865354,14143.4952424306],[6.2449e-7,3.97763912806,8827.3902698748],
|
|
18
|
+
[5.1145e-7,0.28306832879,5856.4776591154],[5.5577e-7,3.47006059924,6279.5527316424],[4.1036e-7,5.36817592855,8429.2412664666],[5.1605e-7,1.33282739866,1748.016413067],
|
|
19
|
+
[5.1992e-7,0.18914947184,12139.5535091068],[4.9e-7,0.48735014197,1194.4470102246],[3.92e-7,6.16833020996,10447.3878396044],[3.557e-7,1.775968892,6812.766815086],
|
|
20
|
+
[3.677e-7,6.04133863162,10213.285546211],[3.6596e-7,2.56957481827,1059.3819301892],[3.3296e-7,0.59310278598,17789.845619785],[3.5954e-7,1.70875808777,2352.8661537718],
|
|
21
|
+
[4.0938e-7,2.39850938714,19651.048481098],[3.0047e-7,2.73975124088,1349.8674096588],[3.0412e-7,0.44294464169,83996.84731811189],[2.3663e-7,0.48473622521,8031.0922630584],
|
|
22
|
+
[2.3574e-7,2.06528133162,3340.6124266998],[2.1089e-7,4.14825468851,951.7184062506],[2.4738e-7,0.21484762138,3.5904286518],[2.5352e-7,3.16470891653,4690.4798363586],
|
|
23
|
+
[2.2823e-7,5.22195230819,4705.7323075436],[2.1419e-7,1.42563910473,16730.4636895958],[2.1891e-7,5.55594302779,553.5694028424],[2.03e-7,0.37133792946,283.8593188652],
|
|
24
|
+
];
|
|
25
|
+
const L1 = [[6283.0758499914,0,0],[0.00206058863,2.67823455808,6283.0758499914],[0.00004303419,2.63512233481,12566.1516999828],[0.00000425264,1.59046982018,3.523118349],[0.00000109017,2.96631010675,1577.3435424478],[9.3479e-7,2.59211109542,18849.2275499742],[0.00000119305,5.79555765566,26.2983197998],[7.2121e-7,1.13840581212,529.6909650946],[6.7784e-7,1.87453300345,398.1490034082],[6.735e-7,4.40932832004,5507.5532386674],[5.9045e-7,2.88815790631,5223.6939198022],[5.5976e-7,2.17471740035,155.4203994342],[4.5411e-7,0.39799502896,796.2980068164],[3.6298e-7,0.46875437227,775.522611324],[2.8962e-7,2.64732254645,7.1135470008],[2.0844e-7,5.34138275149,0.9803210682]];
|
|
26
|
+
const L2 = [[0.00008721859,1.07253635559,6283.0758499914],[0.0000099099,3.14159265359,0],[0.00000294833,0.43717350256,12566.1516999828],[2.7338e-7,0.05295636147,3.523118349],[1.6333e-7,5.18820215724,26.2983197998],[1.5745e-7,3.68504712183,155.4203994342],[9.425e-8,0.29667114694,18849.2275499742],[8.938e-8,2.05706319592,77713.7714681205],[6.94e-8,0.82691541038,775.522611324],[5.061e-8,4.6624323168,1577.3435424478]];
|
|
27
|
+
const L3 = [[0.00000289058,5.84173149732,6283.0758499914],[2.0712e-7,6.0498393902,12566.1516999828],[2.962e-8,5.1956057957,155.4203994342],[2.527e-8,3.14159265359,0],[1.288e-8,4.7219761197,3.523118349]];
|
|
28
|
+
const L4 = [[7.714e-8,4.14117321449,6283.0758499914],[1.016e-8,3.27573644241,12566.1516999828]];
|
|
29
|
+
|
|
30
|
+
/** 地球日心黄经(弧度) */
|
|
31
|
+
export function earthL(T) {
|
|
32
|
+
let s = 0;
|
|
33
|
+
for (const [a, b, c] of L0) s += a * Math.cos(b + c * T);
|
|
34
|
+
let t = 1;
|
|
35
|
+
for (const arr of [L1, L2, L3, L4]) {
|
|
36
|
+
t *= T;
|
|
37
|
+
for (const [a, b, c] of arr) s += a * t * Math.cos(b + c * T);
|
|
38
|
+
}
|
|
39
|
+
return s;
|
|
40
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// 紫微合盘:两盘并排 + 互落宫 + 四化引动。
|
|
2
|
+
// 口径:把一方的「命宫 / 夫妻宫」地支落到对方盘的宫位上,判断影响落在对方哪一领域;
|
|
3
|
+
// 再把一方生年四化所化的星,找它在对方盘的位置,判断「你的某种特质会引动对方哪一宫」。
|
|
4
|
+
// 这是并排对照的读法,不涉及两盘叠加安星(那是另一派做法,本文不采用)。
|
|
5
|
+
import { buildZiwei } from './ziwei.js';
|
|
6
|
+
|
|
7
|
+
const NAMES = ['命宫', '兄弟', '夫妻', '子女', '财帛', '疾厄', '迁移', '交友', '官禄', '田宅', '福德', '父母'];
|
|
8
|
+
const MUT_MEAN = {
|
|
9
|
+
禄: '会把资源、机会与松弛感带过去',
|
|
10
|
+
权: '会不自觉地主导或施压',
|
|
11
|
+
科: '带来的更多是名声、体面与理性',
|
|
12
|
+
忌: '会勾起压力点与执念',
|
|
13
|
+
};
|
|
14
|
+
const MUT_SCORE = { 禄: 10, 权: 5, 科: 8, 忌: -10 };
|
|
15
|
+
// 落在对方这些宫位时影响最直接
|
|
16
|
+
const STRONG_LANDS = ['命宫', '夫妻', '福德', '财帛', '官禄'];
|
|
17
|
+
|
|
18
|
+
const palaceByBranch = (chart, branch) => (chart.palaces || []).find((p) => p.branch === branch) || null;
|
|
19
|
+
const mainStars = (p) => (p && p.stars ? p.stars.filter((s) => s.type === 'main' || s.type === '吉星' || s.type === '煞星').map((s) => s.name) : []);
|
|
20
|
+
|
|
21
|
+
/** 一方的某宫(按地支)在对方盘中落到哪个宫位 */
|
|
22
|
+
function landOn(host, branch) {
|
|
23
|
+
const p = palaceByBranch(host, branch);
|
|
24
|
+
return p ? { palace: p.palace, branch: p.branch, stars: mainStars(p) } : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function buildZiweiHepan(input) {
|
|
28
|
+
const A = buildZiwei({ ...input.a });
|
|
29
|
+
const B = buildZiwei({ ...input.b });
|
|
30
|
+
|
|
31
|
+
// ① 互落宫:命宫、夫妻宫各落到对方哪一宫
|
|
32
|
+
const soulToB = landOn(B, A.soul.branch);
|
|
33
|
+
const soulToA = landOn(A, B.soul.branch);
|
|
34
|
+
const spouseBranchA = (A.palaces.find((p) => p.palace === '夫妻') || {}).branch;
|
|
35
|
+
const spouseBranchB = (B.palaces.find((p) => p.palace === '夫妻') || {}).branch;
|
|
36
|
+
const spouseAtoB = spouseBranchA ? landOn(B, spouseBranchA) : null;
|
|
37
|
+
const spouseBtoA = spouseBranchB ? landOn(A, spouseBranchB) : null;
|
|
38
|
+
|
|
39
|
+
// ② 四化引动:A 生年四化的星,在 B 盘哪一宫
|
|
40
|
+
const hits = [];
|
|
41
|
+
const collect = (from, to, who, targetWho) => {
|
|
42
|
+
for (const p of from.palaces || []) {
|
|
43
|
+
for (const s of p.stars || []) {
|
|
44
|
+
if (!s.mutagen) continue;
|
|
45
|
+
const target = (to.palaces || []).find((q) => (q.stars || []).some((x) => x.name === s.name));
|
|
46
|
+
const land = target ? target.palace : null;
|
|
47
|
+
hits.push({
|
|
48
|
+
who,
|
|
49
|
+
star: s.name,
|
|
50
|
+
mutagen: s.mutagen,
|
|
51
|
+
ownPalace: p.palace,
|
|
52
|
+
landingPalace: land,
|
|
53
|
+
strong: land ? STRONG_LANDS.includes(land) : false,
|
|
54
|
+
text: `${who}的${s.name}化${s.mutagen}(在本命${p.palace})落在${targetWho}的${land || '盘外'}——${who}${MUT_MEAN[s.mutagen]}`,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
collect(A, B, '你', '对方');
|
|
60
|
+
collect(B, A, '对方', '你');
|
|
61
|
+
|
|
62
|
+
// ③ 评分:互落宫 + 四化
|
|
63
|
+
let score = 40;
|
|
64
|
+
const dims = [];
|
|
65
|
+
if (soulToB) {
|
|
66
|
+
const strong = STRONG_LANDS.includes(soulToB.palace);
|
|
67
|
+
const s = strong ? 12 : 5;
|
|
68
|
+
score += s;
|
|
69
|
+
dims.push({ key: 'soul', label: '命宫互落', score: s, text: `你的命宫在${A.soul.branch},落到对方盘的「${soulToB.palace}」${soulToB.stars.length ? `(同宫见 ${soulToB.stars.join('、')})` : ''}——${strong ? '影响直接,脾气与做派会明显落在对方这一块' : '影响偏间接,主要体现在日常协作上'}。` });
|
|
70
|
+
}
|
|
71
|
+
if (soulToA) {
|
|
72
|
+
dims.push({ key: 'soulBack', label: '对方命宫落你盘', score: 0, text: `对方的命宫在${B.soul.branch},落到你盘的「${soulToA.palace}」${soulToA.stars.length ? `(同宫见 ${soulToA.stars.join('、')})` : ''}。` });
|
|
73
|
+
}
|
|
74
|
+
if (spouseAtoB && spouseBtoA) {
|
|
75
|
+
const good = ['命宫', '夫妻', '福德'].includes(spouseAtoB.palace) || ['命宫', '夫妻', '福德'].includes(spouseBtoA.palace);
|
|
76
|
+
const s = good ? 8 : 2;
|
|
77
|
+
score += s;
|
|
78
|
+
dims.push({ key: 'spouse', label: '夫妻宫互落', score: s, text: `你的夫妻宫在${spouseBranchA},落在对方「${spouseAtoB.palace}」;对方的夫妻宫在${spouseBranchB},落在你「${spouseBtoA.palace}」。${good ? '双方的感情定位能对上彼此在意的事' : '彼此的期待更容易落在不同领域,需要说清楚'}` });
|
|
79
|
+
}
|
|
80
|
+
// 四化净分:禄科为正、忌为负;落在强宫加倍
|
|
81
|
+
let mutScore = 0;
|
|
82
|
+
for (const h of hits) {
|
|
83
|
+
const base = MUT_SCORE[h.mutagen] || 0;
|
|
84
|
+
mutScore += h.strong ? Math.round(base * 1.5) : base;
|
|
85
|
+
}
|
|
86
|
+
mutScore = Math.max(-25, Math.min(25, mutScore));
|
|
87
|
+
score += mutScore;
|
|
88
|
+
dims.push({ key: 'mutagen', label: '四化引动', score: mutScore, text: hits.filter((h) => h.who === '你').map((h) => h.text).join(';') || '你这边没有生年四化引动对方的宫位。' });
|
|
89
|
+
|
|
90
|
+
const total = Math.max(0, Math.min(100, Math.round(score)));
|
|
91
|
+
const level = total >= 75 ? '契合度较高' : total >= 55 ? '中等偏顺' : total >= 40 ? '需要磨合' : '差异较大';
|
|
92
|
+
|
|
93
|
+
const side = (z, who) => ({
|
|
94
|
+
who,
|
|
95
|
+
soul: { branch: z.soul.branch, palace: z.soul.palace },
|
|
96
|
+
body: { branch: z.body.branch, lands: z.body.lands },
|
|
97
|
+
bureau: z.bureau.name,
|
|
98
|
+
ziwei: z.ziwei,
|
|
99
|
+
tianfu: z.tianfu,
|
|
100
|
+
soulStars: mainStars(z.palaces.find((p) => p.palace === '命宫') || {}),
|
|
101
|
+
spouseStars: mainStars(z.palaces.find((p) => p.palace === '夫妻') || {}),
|
|
102
|
+
natalSihua: (z.palaces || []).flatMap((p) => (p.stars || []).filter((s) => s.mutagen).map((s) => `${s.name}化${s.mutagen}(${p.palace})`)),
|
|
103
|
+
summary: z.summary,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
system: 'hepan',
|
|
108
|
+
chart: 'ziwei',
|
|
109
|
+
a: side(A, '你'),
|
|
110
|
+
b: side(B, '对方'),
|
|
111
|
+
score: { total, level },
|
|
112
|
+
dimensions: dims,
|
|
113
|
+
interactions: hits,
|
|
114
|
+
notes: [
|
|
115
|
+
'紫微合盘看的是「彼此会在对方生活里的哪一块起作用」,不是关系成败的判决。',
|
|
116
|
+
'口径:命宫与夫妻宫按地支落到对方盘的宫位;四化按「一方的生年四化星在对方盘的落宫」读,落在命宫/夫妻/福德/财帛/官禄加倍计分。',
|
|
117
|
+
],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|