// Package synastry computes love/friend/marriage match indices from two natal charts. package synastry import ( "fmt" "github.com/yuxingu/digital-psychology/apps/api/internal/star/natal" ) // Indices are 0–100 match scores. type Indices struct { Love int `json:"love_index"` Friend int `json:"friend_index"` Marriage int `json:"marriage_index"` LoveNote string `json:"love_note"` FriendNote string `json:"friend_note"` MarriageNote string `json:"marriage_note"` Summary string `json:"summary"` } // Compute returns match indices for charts A and B. func Compute(a, b natal.Chart, aName, bName string) Indices { if aName == "" { aName = "我" } if bName == "" { bName = "TA" } sunDiff := natal.AngleDiff(a.Sun.Lon, b.Sun.Lon) moonDiff := natal.AngleDiff(a.Moon.Lon, b.Moon.Lon) venA := bodyLon(a, "venus") venB := bodyLon(b, "venus") marsA := bodyLon(a, "mars") marsB := bodyLon(b, "mars") venDiff := natal.AngleDiff(venA, venB) marsVen := natal.AngleDiff(marsA, venB) venMars := natal.AngleDiff(venA, marsB) // Closer aspects (0/60/120 soft; 90/180 hard but charged) → higher score blended love := clamp(58+aspectBoost(sunDiff)+aspectBoost(moonDiff)+aspectBoost(venDiff)+aspectBoost(marsVen)/2+aspectBoost(venMars)/2, 42, 98) friend := clamp(60+aspectBoost(sunDiff)+aspectBoost(moonDiff)*3/2+elemBoost(a.Sun, b.Sun), 45, 97) marriage := clamp(55+aspectBoost(sunDiff)+aspectBoost(moonDiff)+aspectBoost(venDiff)+houseBoost(a, b), 40, 96) return Indices{ Love: love, Friend: friend, Marriage: marriage, LoveNote: fmt.Sprintf("%s与%s恋爱指数 %d:太阳相距约%.0f°,金星互动影响吸引力。", aName, bName, love, sunDiff), FriendNote: fmt.Sprintf("友情指数 %d:月亮与太阳元素是否合拍,决定轻松感。", friend), MarriageNote: fmt.Sprintf("婚姻/长期指数 %d:看重稳定与共同节奏,而非一时火花。", marriage), Summary: fmt.Sprintf("%s × %s:恋爱 %d · 友情 %d · 婚姻 %d", aName, bName, love, friend, marriage), } } func bodyLon(c natal.Chart, key string) float64 { for _, p := range c.Planets { if p.Key == key { return p.Lon } } return 0 } func aspectBoost(diff float64) int { // reward conjunction, sextile, trine; mild for square/opposition targets := []float64{0, 60, 90, 120, 180} best := 180.0 for _, t := range targets { d := abs(diff - t) if d < best { best = d } } switch { case best <= 8: if near(diff, 90) || near(diff, 180) { return 8 } return 14 case best <= 12: return 8 default: return int(6 - best/30) } } func near(diff, target float64) bool { return abs(diff-target) <= 10 } func elemBoost(a, b natal.Body) int { if a.Element == b.Element { return 6 } // fire-air / earth-water traditionally supportive pair := a.Element + b.Element if pair == "火风" || pair == "风火" || pair == "土水" || pair == "水土" { return 4 } return 0 } func houseBoost(a, b natal.Chart) int { // same rising modality → slight boost if a.Rise.Modality == b.Rise.Modality { return 4 } return 0 } func clamp(v, lo, hi int) int { if v < lo { return lo } if v > hi { return hi } return v } func abs(x float64) float64 { if x < 0 { return -x } return x } // AsMap for JSON. func (i Indices) AsMap() map[string]any { return map[string]any{ "love_index": i.Love, "friend_index": i.Friend, "marriage_index": i.Marriage, "love_note": i.LoveNote, "friend_note": i.FriendNote, "marriage_note": i.MarriageNote, "summary": i.Summary, } }