File size: 6,008 Bytes
25d2d3e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
"""参数化 SVG 渲染: traits + age + stage + sick + mood → 蛐蛐形象.

设计原则: 零依赖、零成本、演化连续可见。
映射(可随时调,但保持单调直觉):
  勇 brave   → 触须张角/前肢姿态(越勇越张扬)
  萌 cute    → 眼睛大小/身体圆润度
  怨 grudge  → 体色向紫黑偏移/眉角下压
  智 wit     → 额头花纹复杂度(条纹数)
  馋 glutton → 肚子大小
  stage      → 整体尺寸 + 翅膀层数(蜕皮=大变样)
  sick       → 整体降饱和 + 绿色病气 + 眼睛变 X
"""
from __future__ import annotations

from traits import TRAIT_KEYS, molt_stage


def _lerp(a: float, b: float, t: float) -> float:
    return a + (b - a) * t


def _color(traits: dict, sick: bool) -> tuple[str, str]:
    """主体色: 健康=暖棕→怨气高偏紫黑; 生病=灰绿."""
    g = traits.get("grudge", 0.5)
    if sick:
        return "#7a8a6a", "#5a6a4a"
    r = int(_lerp(150, 90, g))
    gr = int(_lerp(95, 60, g))
    b = int(_lerp(60, 110, g))
    return f"rgb({r},{gr},{b})", f"rgb({max(r-40,0)},{max(gr-30,0)},{max(b-20,0)})"


def render_svg(traits: dict, feed_count: int = 0, sick: bool = False,
               mood: str = "calm", width: int = 360, height: int = 280) -> str:
    """返回完整 <svg> 字符串."""
    t = {k: float(traits.get(k, 0.5)) for k in TRAIT_KEYS}
    stage = molt_stage(feed_count)
    scale = _lerp(0.7, 1.15, min(stage, 3) / 3)
    body_fill, body_dark = _color(t, sick)

    cx, cy = width / 2, height / 2 + 20
    belly = _lerp(34, 52, t["glutton"]) * scale          # 馋→肚子
    body_ry = _lerp(belly * 0.78, belly * 0.95, t["cute"])  # 萌→圆润
    eye_r = _lerp(4.5, 9.5, t["cute"]) * scale            # 萌→大眼
    ant_spread = _lerp(8, 42, t["brave"])                 # 勇→触须张角(度)
    ant_len = _lerp(55, 85, t["brave"]) * scale
    stripes = 1 + int(round(t["wit"] * 4))                # 智→花纹条数
    brow_drop = _lerp(-3, 5, t["grudge"])                 # 怨→眉角下压

    head_cx = cx - belly * 0.95
    head_cy = cy - body_ry * 0.45
    head_r = _lerp(16, 22, t["cute"]) * scale

    parts: list[str] = []
    parts.append(
        f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" '
        f'viewBox="0 0 {width} {height}">'
    )
    # 地面
    parts.append(f'<ellipse cx="{cx}" cy="{cy + body_ry + 14}" rx="{belly * 1.6}" ry="10" fill="#00000014"/>')
    # 后腿(蛐蛐标志性大跳腿)
    leg = belly * 1.25
    parts.append(
        f'<path d="M {cx + belly * 0.5} {cy} q {leg * 0.55} {-leg * 0.7} {leg * 0.32} {leg * 0.05} '
        f'l {-leg * 0.12} {leg * 0.62}" stroke="{body_dark}" stroke-width="{5 * scale}" fill="none" stroke-linecap="round"/>'
    )
    # 身体
    parts.append(f'<ellipse cx="{cx}" cy="{cy}" rx="{belly}" ry="{body_ry}" fill="{body_fill}"/>')
    # 翅膀层数 = stage+1(蜕皮可见)
    for i in range(stage + 1):
        wy = cy - body_ry * 0.55 - i * 4 * scale
        parts.append(
            f'<ellipse cx="{cx + 6}" cy="{wy}" rx="{belly * 0.85}" ry="{body_ry * 0.45}" '
            f'fill="{body_dark}" opacity="{0.55 - i * 0.12}"/>'
        )
    # 智力花纹
    for i in range(stripes):
        sx = cx - belly * 0.4 + i * (belly * 0.8 / max(stripes - 1, 1))
        parts.append(
            f'<line x1="{sx}" y1="{cy - body_ry * 0.25}" x2="{sx}" y2="{cy + body_ry * 0.35}" '
            f'stroke="{body_dark}" stroke-width="{2 * scale}" opacity="0.6"/>'
        )
    # 头
    parts.append(f'<circle cx="{head_cx}" cy="{head_cy}" r="{head_r}" fill="{body_fill}"/>')
    # 触须(勇→张角)
    for sign in (-1, 1):
        ang = (-90 + sign * ant_spread) * 3.14159 / 180
        ex = head_cx + ant_len * 1.0 * __import__("math").cos(ang) * (0.6 if sign < 0 else 0.8)
        ey = head_cy + ant_len * __import__("math").sin(ang)
        mx = head_cx + sign * 10
        my = head_cy - ant_len * 0.6
        parts.append(
            f'<path d="M {head_cx} {head_cy - head_r * 0.7} Q {mx} {my} {ex} {ey}" '
            f'stroke="{body_dark}" stroke-width="{2.2 * scale}" fill="none" stroke-linecap="round"/>'
        )
    # 眼睛 / 病眼
    eye_x, eye_y = head_cx - head_r * 0.25, head_cy - head_r * 0.15
    if sick:
        s = eye_r * 0.8
        parts.append(
            f'<g stroke="#3a3a3a" stroke-width="2.4" stroke-linecap="round">'
            f'<line x1="{eye_x - s}" y1="{eye_y - s}" x2="{eye_x + s}" y2="{eye_y + s}"/>'
            f'<line x1="{eye_x - s}" y1="{eye_y + s}" x2="{eye_x + s}" y2="{eye_y - s}"/></g>'
        )
        # 病气泡
        parts.append(f'<circle cx="{head_cx - head_r * 1.4}" cy="{head_cy - head_r}" r="5" fill="#9ab87a" opacity="0.8"/>')
    else:
        parts.append(f'<circle cx="{eye_x}" cy="{eye_y}" r="{eye_r}" fill="#241a12"/>')
        parts.append(f'<circle cx="{eye_x + eye_r * 0.3}" cy="{eye_y - eye_r * 0.3}" r="{eye_r * 0.3}" fill="#fff"/>')
    # 怨气眉
    parts.append(
        f'<line x1="{eye_x - eye_r * 1.4}" y1="{eye_y - eye_r * 1.6 + brow_drop}" '
        f'x2="{eye_x + eye_r * 1.2}" y2="{eye_y - eye_r * 1.6 - brow_drop}" '
        f'stroke="#241a12" stroke-width="2.6" stroke-linecap="round"/>'
    )
    # 嘴(mood 简单弯曲)
    smile = {"happy": 6, "excited": 7, "loved": 6, "content": 4, "calm": 1,
             "sad": -5, "angry": -6, "hurt": -6, "disgusted": -4}.get(mood, 1)
    parts.append(
        f'<path d="M {head_cx - 7} {head_cy + head_r * 0.45} q 7 {smile} 14 0" '
        f'stroke="#241a12" stroke-width="2.2" fill="none" stroke-linecap="round"/>'
    )
    parts.append("</svg>")
    return "".join(parts)


def svg_params(traits: dict, feed_count: int, sick: bool, mood: str) -> dict:
    """快照里存的渲染参数(回放史料)."""
    return {
        "traits": {k: round(float(traits.get(k, 0.5)), 4) for k in TRAIT_KEYS},
        "stage": molt_stage(feed_count),
        "sick": bool(sick),
        "mood": mood,
        "feed_count": feed_count,
    }