From 44d6f3b1c51197f03ab2d1b90ed37f9bdc84e387 Mon Sep 17 00:00:00 2001 From: max Date: Tue, 8 Sep 2026 23:02:22 +0200 Subject: [PATCH] feat(config): add configuration parameters, kinematics, and distraction targets --- catser/config.py | 145 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 catser/config.py diff --git a/catser/config.py b/catser/config.py new file mode 100644 index 0000000..fdf083b --- /dev/null +++ b/catser/config.py @@ -0,0 +1,145 @@ +""" +Catser Configuration and Kinematic Constants. + +Stores system configurations, ActivityWatch parameters, distraction rules, +visual coat definitions, and exact physics constants ported from Workcat (cat.js). +""" + +from dataclasses import dataclass, field +from typing import List, Dict, Any, Tuple + + +# ============================================================================== +# Workcat Kinematic and Animation Constants +# Derived from: apps/desktop/src/features/pet-mode/petModeData.ts & cat.js +# ============================================================================== + +GAIT_FRAME_COUNT: int = 30 +WALK_SPEED: float = 20.8 +WALK_STRIDE: float = 26.0 +RUN_SPEED: float = 300.0 +RUN_STRIDE: float = 72.0 +GAIT_CADENCE: float = 0.9 +GAIT_SHIFT_X: float = -6.383 +GAIT_SHIFT_Y: float = 3.76 +CAT_BOX_WIDTH: float = 121.2 +CAT_BOX_HEIGHT: float = 92.4 +BASE_WIDTH: float = 144.0 + +PAW_FPS: int = 24 +PAW_FRAME_COUNT: int = 17 +PAW_REACH_FRAME: int = 13 # End of reaching motion +PAW_CONTACT_FRAME: int = 14 # Exact contact frame where the paw strikes the target +PAW_HOLD_FRAME: int = 15 # Frame holding the extended strike pose + +# Contact offset ratio relative to cat bounding box +# target_x = cat_x + width * CONTACT_RATIO_X (for right-facing cat) +# target_y = cat_y + height * CONTACT_RATIO_Y +CONTACT_RATIO_X: float = (143.0 - 11.4) / 121.2 # ~1.0858 (paw reaches past box edge) +CONTACT_RATIO_Y: float = (40.0 - 4.2) / 92.4 # ~0.3874 + +ALERT_HOLD_MS: int = 620 # Pause duration when target is spotted before sprint +RELEASE_SETTLE_MS: int = 320 # Settle duration after dropping before walking +DRAG_THRESHOLD_PX: int = 7 # Minimum drag distance before transitioning from pet to drag +HAPPY_MS: int = 1200 # Petting reaction duration +BLINK_DURATION_MS: int = 228 +BLINK_INTERVAL_MIN_MS: int = 4420 +BLINK_INTERVAL_MAX_MS: int = 7280 + +# Approach duration limits (ensures sprint feels dynamic regardless of distance) +APPROACH_MIN_MS: int = 620 +APPROACH_MAX_MS: int = 1150 + +LIFE_MOTION_FPS: int = 12 +LIFE_MOTION_FRAME_COUNT: int = 37 + +# Probabilities for life actions when standing vs sitting +ACTION_WEIGHTS: Dict[str, Dict[str, int]] = { + "standing": {"walk": 3, "stretch": 2, "sit": 6, "sleep": 1}, + "sitting": {"walk": 2, "stretch": 1, "sit": 3, "sleep": 6}, +} + +# Settled action intervals (ms) +LIFE_ACTION_INTERVAL_MS: int = 9000 +SETTLED_ACTION_INTERVAL_MS: int = 3400 +SIT_MIN_MS: int = 3200 +SIT_MAX_MS: int = 9000 +SLEEP_MIN_MS: int = 6400 +SLEEP_MAX_MS: int = 18000 + + +# ============================================================================== +# Cat Coat Definitions +# ============================================================================== + +@dataclass(frozen=True) +class Coat: + """Represents a cat coat color palette.""" + name: str + label_en: str + fur_color: Tuple[int, int, int] # RGB + ink_color: Tuple[int, int, int] # RGB for face features (eyes, mouth) + filter_type: str # CSS-equivalent color filter for raster assets + + +COATS: Dict[str, Coat] = { + "ivory": Coat("ivory", "Ivory", (232, 229, 218), (23, 25, 22), "none"), + "charcoal": Coat("charcoal", "Charcoal", (51, 51, 47), (242, 241, 237), "charcoal"), + "grey": Coat("grey", "Grey", (163, 161, 153), (26, 27, 24), "grey"), + "apricot": Coat("apricot", "Apricot", (217, 177, 140), (60, 43, 28), "apricot"), + "sage": Coat("sage", "Sage", (147, 168, 142), (28, 36, 26), "sage"), + "plum": Coat("plum", "Plum", (122, 95, 107), (239, 230, 234), "plum"), +} + + +# ============================================================================== +# Application Configuration +# ============================================================================== + +@dataclass +class Config: + """ + Runtime configuration for Catser. + + Attributes: + aw_url: ActivityWatch server base URL (default http://localhost:5600). + poll_interval_sec: Interval in seconds to poll ActivityWatch. + auto_fallback_to_win32: Whether to fall back to native Win32 window APIs if AW is offline. + distraction_keywords: List of substrings in window titles that trigger cat action. + distraction_apps: List of process names (lowercase) that trigger cat action. + action_on_hit: Action to take on contact ('close', 'minimize', or 'test_notify'). + cat_width: Screen pixel width for the cat sprite. + coat_name: Active coat color name ('ivory', 'charcoal', etc.). + sound_enabled: Whether to play sound effects on strike / purr. + cooldown_after_close_sec: Minimum seconds between closing actions on the same window. + """ + aw_url: str = "http://localhost:5600" + poll_interval_sec: float = 0.5 + auto_fallback_to_win32: bool = True + distraction_keywords: List[str] = field(default_factory=lambda: [ + "shorts", + "youtube shorts", + "reels", + "instagram reels", + "tiktok", + "reddit", + "twitch.tv", + ]) + distraction_apps: List[str] = field(default_factory=lambda: [ + # Users can add specific processes e.g. "discord.exe", "steam.exe" + ]) + action_on_hit: str = "close" # 'close', 'minimize', or 'test_notify' + cat_width: int = 144 # Matches BASE_WIDTH for 1:1 scale + coat_name: str = "ivory" + sound_enabled: bool = True + cooldown_after_close_sec: float = 4.0 + + @property + def cat_height(self) -> int: + """Calculates cat height proportionally based on aspect ratio 121.2 / 92.4.""" + return int(round(self.cat_width * (CAT_BOX_HEIGHT / CAT_BOX_WIDTH))) + + @property + def coat(self) -> Coat: + """Retrieves the active Coat instance.""" + return COATS.get(self.coat_name, COATS["ivory"])