|
|
@@ -20,6 +20,13 @@ const WIN_H: i32 = 200;
|
|
|
const FEET_ANCHOR_X: i32 = 100;
|
|
|
const FEET_ANCHOR_Y: i32 = 200;
|
|
|
|
|
|
+// ── 宠物可视包围盒(窗口内),用于「鼠标悬停在宠物身上 → 暂停活动」判定 ──
|
|
|
+// 与 index.html 的 #pet(120×140,底部居中)布局保持一致:left=(200-120)/2=40,top=200-140=60
|
|
|
+const PET_BOX_OFFSET_X: i32 = 40;
|
|
|
+const PET_BOX_OFFSET_Y: i32 = 60;
|
|
|
+const PET_BOX_W: i32 = 120;
|
|
|
+const PET_BOX_H: i32 = 140;
|
|
|
+
|
|
|
// ── 抖动检测参数 ──
|
|
|
const SHAKE_WINDOW_MS: u128 = 600;
|
|
|
const SHAKE_MIN_REVERSALS: usize = 4;
|
|
|
@@ -86,6 +93,15 @@ pub fn clamp_window_pos(x: i32, y: i32, wa_x: i32, wa_y: i32, wa_w: i32, wa_h: i
|
|
|
(x.clamp(wa_x, max_x), y.clamp(wa_y, max_y))
|
|
|
}
|
|
|
|
|
|
+/// 光标是否落在窗口内的宠物包围盒上(用于「悬停在宠物身上 → 暂停活动」)。
|
|
|
+pub fn cursor_over_pet(cursor: (f64, f64), win_pos: (i32, i32)) -> bool {
|
|
|
+ let left = (win_pos.0 + PET_BOX_OFFSET_X) as f64;
|
|
|
+ let top = (win_pos.1 + PET_BOX_OFFSET_Y) as f64;
|
|
|
+ let right = left + PET_BOX_W as f64;
|
|
|
+ let bottom = top + PET_BOX_H as f64;
|
|
|
+ cursor.0 >= left && cursor.0 <= right && cursor.1 >= top && cursor.1 <= bottom
|
|
|
+}
|
|
|
+
|
|
|
/// 工作区内的确定性随机落点(LCG 单步,便于单测)。
|
|
|
pub fn pick_random_target(wa_x: i32, wa_y: i32, wa_w: i32, wa_h: i32, seed: u64) -> (i32, i32) {
|
|
|
let s = seed
|
|
|
@@ -179,8 +195,10 @@ pub fn spawn(app: AppHandle) {
|
|
|
shake_cooldown -= 1;
|
|
|
}
|
|
|
|
|
|
- // 采样全局光标(用于抖动检测 / 站手跟随)
|
|
|
+ // 采样全局光标(用于抖动检测 / 站手跟随 / 悬停暂停)
|
|
|
+ let mut cursor: Option<(f64, f64)> = None;
|
|
|
if let Ok(cur) = win.cursor_position() {
|
|
|
+ cursor = Some((cur.x, cur.y));
|
|
|
samples.push(Sample { x: cur.x, y: cur.y, t: now });
|
|
|
if samples.len() > MAX_SAMPLES {
|
|
|
samples.remove(0);
|
|
|
@@ -229,6 +247,14 @@ pub fn spawn(app: AppHandle) {
|
|
|
emit_gait(&app, &mut last_gait, "idle", "none");
|
|
|
}
|
|
|
|
|
|
+ // 鼠标悬停在宠物身上时暂停自主动作(走/跳/飞 → 立即待机)
|
|
|
+ let hovered = cursor.map(|c| cursor_over_pet(c, last_pos)).unwrap_or(false);
|
|
|
+ if hovered && matches!(mode, Mode::Walk | Mode::Hop | Mode::Fly) {
|
|
|
+ mode = Mode::Idle;
|
|
|
+ idle_ticks = 0;
|
|
|
+ emit_gait(&app, &mut last_gait, "idle", "none");
|
|
|
+ }
|
|
|
+
|
|
|
match mode {
|
|
|
Mode::Fly => {
|
|
|
if fly_left > 0 {
|
|
|
@@ -263,23 +289,35 @@ pub fn spawn(app: AppHandle) {
|
|
|
}
|
|
|
}
|
|
|
_ => {
|
|
|
- idle_ticks += 1;
|
|
|
- let threshold = IDLE_MIN_TICKS + (next_rand(&mut seed) % IDLE_JITTER_TICKS) as u32;
|
|
|
- if idle_ticks >= threshold {
|
|
|
+ if hovered {
|
|
|
+ // 悬停期间保持静止:不累积空闲计时,不触发新的走/跳
|
|
|
idle_ticks = 0;
|
|
|
- let hop = next_rand(&mut seed) % 2 == 0;
|
|
|
- let dir: i32 = if next_rand(&mut seed) % 2 == 0 { 1 } else { -1 };
|
|
|
- let dist = 60 + (next_rand(&mut seed) % 120) as i32;
|
|
|
- let (tx, ty) =
|
|
|
- clamp_window_pos(last_pos.0 + dir * dist, last_pos.1, wa_x, wa_y, wa_w, wa_h);
|
|
|
- target = (tx, ty);
|
|
|
- mode = if hop { Mode::Hop } else { Mode::Walk };
|
|
|
- emit_gait(
|
|
|
- &app,
|
|
|
- &mut last_gait,
|
|
|
- if hop { "hop" } else { "walk" },
|
|
|
- if dir > 0 { "right" } else { "left" },
|
|
|
- );
|
|
|
+ } else {
|
|
|
+ idle_ticks += 1;
|
|
|
+ let threshold =
|
|
|
+ IDLE_MIN_TICKS + (next_rand(&mut seed) % IDLE_JITTER_TICKS) as u32;
|
|
|
+ if idle_ticks >= threshold {
|
|
|
+ idle_ticks = 0;
|
|
|
+ let hop = next_rand(&mut seed) % 2 == 0;
|
|
|
+ let dir: i32 = if next_rand(&mut seed) % 2 == 0 { 1 } else { -1 };
|
|
|
+ let dist = 60 + (next_rand(&mut seed) % 120) as i32;
|
|
|
+ let (tx, ty) = clamp_window_pos(
|
|
|
+ last_pos.0 + dir * dist,
|
|
|
+ last_pos.1,
|
|
|
+ wa_x,
|
|
|
+ wa_y,
|
|
|
+ wa_w,
|
|
|
+ wa_h,
|
|
|
+ );
|
|
|
+ target = (tx, ty);
|
|
|
+ mode = if hop { Mode::Hop } else { Mode::Walk };
|
|
|
+ emit_gait(
|
|
|
+ &app,
|
|
|
+ &mut last_gait,
|
|
|
+ if hop { "hop" } else { "walk" },
|
|
|
+ if dir > 0 { "right" } else { "left" },
|
|
|
+ );
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -360,4 +398,17 @@ mod tests {
|
|
|
assert!((0..=880).contains(&y), "y={y}");
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn cursor_over_pet_detects_hit_and_miss() {
|
|
|
+ // 窗口左上角 (100,100):宠物盒 = x∈[140,260], y∈[160,300]
|
|
|
+ assert!(cursor_over_pet((200.0, 250.0), (100, 100)));
|
|
|
+ assert!(!cursor_over_pet((10.0, 250.0), (100, 100)));
|
|
|
+ assert!(!cursor_over_pet((200.0, 10.0), (100, 100)));
|
|
|
+ // 边界包含
|
|
|
+ assert!(cursor_over_pet((140.0, 160.0), (100, 100)));
|
|
|
+ assert!(cursor_over_pet((260.0, 300.0), (100, 100)));
|
|
|
+ // 窗口移动后包围盒随之平移
|
|
|
+ assert!(cursor_over_pet((500.0, 600.0), (400, 450)));
|
|
|
+ }
|
|
|
}
|