Commit 800d7775 by luoqi

feat(web): 桌宠微调 — 打招呼气泡/画圈套绳/超人按距离/各概率

- 切患者:恢复"开始跟进「XX」"说话框(此动作保留气泡)
- 鼠标画圈手势(累计转角>330°+成环+冷却)→ 以画圈处为锚点套绳荡飞
- 边框上想去别层:套绳概率 18%→40%
- 蛀虫骑边框概率 40%→60%
- 超人模式改按"宠物-蛀虫距离"给概率 min(32%, 4%+dist/2200),越远越易触发
- 牙医组合 idle 轮盘 10%→16%

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
parent 43bcc18d
......@@ -143,11 +143,14 @@ export function usePetBrain() {
prevSeq = s.seq;
if (s.status === 'completed') play({ gesture: 'celebrate', ttlMs: 3_600 });
}
// 切患者 → 抬头看一眼(无语言)
// 切患者 → 抬头看一眼 + 打招呼气泡(此动作保留说话框)
const planId = s.current?.planId ?? null;
if (planId && planId !== prevPlanId) {
prevPlanId = planId;
if (s.current?.patientName) setGlanceSeq((g) => g + 1);
if (s.current?.patientName) {
setGlanceSeq((g) => g + 1);
play({ gesture: 'greet', bubble: `开始跟进「${s.current.patientName}」`, ttlMs: 2_600 });
}
}
});
}, [play]);
......@@ -178,11 +181,11 @@ export function usePetBrain() {
const schedule = () => {
timer = setTimeout(() => {
const r = Math.random();
if (r < 0.42) setGlanceSeq((s) => s + 1);
else if (r < 0.6) play({ gesture: 'floss', ttlMs: 3_000 });
else if (r < 0.76) play({ gesture: 'shine', ttlMs: 1_800 });
else if (r < 0.9) play({ gesture: 'chomp', ttlMs: 2_400 });
else emitPetEvent({ type: 'vet_combo' }); // ~10%:交给 widget 跑牙医诊疗组合
if (r < 0.4) setGlanceSeq((s) => s + 1);
else if (r < 0.56) play({ gesture: 'floss', ttlMs: 3_000 });
else if (r < 0.71) play({ gesture: 'shine', ttlMs: 1_800 });
else if (r < 0.84) play({ gesture: 'chomp', ttlMs: 2_400 });
else emitPetEvent({ type: 'vet_combo' }); // ~16%:交给 widget 跑牙医诊疗组合
schedule();
}, GLANCE_MIN_MS + Math.random() * (GLANCE_MAX_MS - GLANCE_MIN_MS));
};
......
......@@ -80,6 +80,8 @@ export function usePetLocomotion(
nextStrollAt: Date.now() + STROLL_IDLE_MIN_MS,
// 鼠标互动:光标位置/速度 + 惊吓/好奇冷却
cur: { x: -1e4, y: -1e4, t: 0, lastFastAt: 0 },
// 转圈手势识别(画圈套绳):累计转角 + 路径范围 + 冷却
circle: { prevH: null as number | null, accum: 0, t0: 0, len: 0, minX: 0, maxX: 0, minY: 0, maxY: 0, lastAt: 0 },
startleUntil: 0,
curiousUntil: 0,
// 拖拽测速(松手抛掷用)
......@@ -317,6 +319,51 @@ export function usePetLocomotion(
}
}
}
// 画圈手势 → 套绳荡飞:累计转角超过约一圈(且路径成环、范围合理)即触发
const cr = c.circle;
const mdx = e.clientX - c.cur.x;
const mdy = e.clientY - c.cur.y;
const seg = Math.hypot(mdx, mdy);
if (seg > 2.5) {
if (cr.prevH === null || now - cr.t0 > 1_400) {
cr.accum = 0; // 窗口重置(停顿/超时)
cr.len = 0;
cr.t0 = now;
cr.minX = cr.maxX = e.clientX;
cr.minY = cr.maxY = e.clientY;
cr.prevH = null;
}
const h = Math.atan2(mdy, mdx);
if (cr.prevH !== null) {
let d = h - cr.prevH;
while (d > Math.PI) d -= 2 * Math.PI;
while (d < -Math.PI) d += 2 * Math.PI;
cr.accum += d;
cr.len += seg;
}
cr.prevH = h;
cr.minX = Math.min(cr.minX, e.clientX);
cr.maxX = Math.max(cr.maxX, e.clientX);
cr.minY = Math.min(cr.minY, e.clientY);
cr.maxY = Math.max(cr.maxY, e.clientY);
const diag = Math.hypot(cr.maxX - cr.minX, cr.maxY - cr.minY);
if (
Math.abs(cr.accum) > 5.8 && // ≈ 330°
cr.len > 140 &&
diag > 30 &&
diag < 650 &&
now - cr.lastAt > 6_000 &&
c.motion === 'rest' &&
!getBusy()
) {
cr.lastAt = now;
cr.accum = 0;
cr.prevH = null;
startSwing({ x: e.clientX, y: e.clientY }); // 锚点 = 画圈处
}
}
c.cur.x = e.clientX;
c.cur.y = e.clientY;
c.cur.t = now;
......@@ -699,8 +746,8 @@ export function usePetLocomotion(
// 蹲够了 / 要去蹲守 AI → 走向平台边缘,自然踩空下来
const wantDown = Date.now() > c.nextStrollAt || getWatchX() !== null;
if (wantDown && !getBusy()) {
// 在边框高处也能套绳荡飞(鼠标在低处也行)→ 直接离框甩荡
if (getWatchX() === null && Math.random() < 0.18 && swingReady()) {
// 在边框上想去别的楼层时,优先甩绳荡过去(概率提高)
if (getWatchX() === null && Math.random() < 0.4 && swingReady()) {
startSwing();
scheduleStroll();
break;
......
......@@ -235,10 +235,9 @@ export function PetFab({
const spawnGerm = (forceSuper = false) => {
if (germ.current) return; // 允许宠物坐着/走着时也刷(它会下来/跳上去追)
vetAbortRef.current(); // 蛀虫出现 → 中断诊疗组合,优先去追
const superman = forceSuper || Math.random() < 0.1; // 隐藏 10%:超人模式(lab 可强制)
// 40% 概率骑在某个可见边框上,否则在地板底边
// 60% 概率骑在某个可见边框上,否则在地板底边
let perch: Element | null = null;
if (Math.random() < 0.4) {
if (Math.random() < 0.6) {
const cands: Element[] = [];
document.querySelectorAll('[data-pet-perch], [class*="border"]').forEach((p) => {
const r = p.getBoundingClientRect();
......@@ -249,29 +248,31 @@ export function PetFab({
});
perch = cands[Math.floor(Math.random() * cands.length)] ?? null;
}
// 算出蛀虫落点(x + 离地高度 yTop),再按"与宠物距离"决定超人概率(越远越可能)
let gx: number;
let gyTop: number;
if (perch) {
const r = perch.getBoundingClientRect();
germ.current = {
x: r.left + 6 + Math.random() * Math.max(r.width - 28, 1),
dir: Math.random() < 0.5 ? -1 : 1,
t0: performance.now(),
state: 'crawl',
perch,
superman,
};
gx = r.left + 6 + Math.random() * Math.max(r.width - 28, 1);
gyTop = r.top - 22;
} else {
const r = btnRef.current?.getBoundingClientRect();
const baseX = r ? r.left : window.innerWidth / 2;
const side = Math.random() < 0.5 ? -1 : 1;
germ.current = {
x: Math.min(Math.max(baseX + side * (200 + Math.random() * 160), 8), window.innerWidth - 30),
dir: side > 0 ? -1 : 1,
t0: performance.now(),
state: 'crawl',
perch: null,
superman,
};
gx = Math.min(Math.max(baseX + side * (200 + Math.random() * 160), 8), window.innerWidth - 30);
gyTop = window.innerHeight - 22 - 2;
}
const pr = btnRef.current?.getBoundingClientRect();
const dist = pr ? Math.hypot(gx + 11 - (pr.left + SIZE / 2), gyTop + 11 - (pr.top + SIZE / 2)) : 0;
const superman = forceSuper || Math.random() < Math.min(0.32, 0.04 + dist / 2200); // 距离越远越可能变超人
germ.current = {
x: gx,
dir: Math.random() < 0.5 ? -1 : 1,
t0: performance.now(),
state: 'crawl',
perch,
superman,
};
setGermAlive(true);
};
const spawnRef = useRef(spawnGerm);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment