""" 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"])