Files
Catser/catser/config.py
T

167 lines
6.3 KiB
Python

"""
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 pad_x(self) -> int:
"""Horizontal padding to accommodate tail wags (-15%) and paw extensions (+9%)."""
return int(round(self.cat_width * 0.20))
@property
def pad_y(self) -> int:
"""Vertical padding to accommodate ears, stretch, and tail height."""
return int(round(self.cat_height * 0.20))
@property
def canvas_width(self) -> int:
"""Total width of the transparent overlay canvas (symmetrical padding)."""
return int(round(self.cat_width * 1.40))
@property
def canvas_height(self) -> int:
"""Total height of the transparent overlay canvas."""
return int(round(self.cat_height * 1.30))
@property
def coat(self) -> Coat:
"""Retrieves the active Coat instance."""
return COATS.get(self.coat_name, COATS["ivory"])