diff --git a/src/core/input_manager.py b/src/core/input_manager.py index 9ca9d10..c66944d 100644 --- a/src/core/input_manager.py +++ b/src/core/input_manager.py @@ -410,8 +410,12 @@ def handle_mouse(self, x, y): dy_pet = y - center_y dist_sq_pet = dx_pet * dx_pet + dy_pet * dy_pet - # Оптимизация: сравнение квадрата расстояния (порог 60px -> 3600) - if dist_sq_pet < 3600: + # Динамический порог радиуса поглаживания на основе текущего размера окна (по умолчанию ~60px при 100px окне) + pet_radius = max(30, int(max(self.window.width(), self.window.height()) * 0.6)) + threshold_sq = pet_radius * pet_radius + + # Оптимизация: сравнение квадрата расстояния с динамическим порогом + if dist_sq_pet < threshold_sq: # Исключаем пассивный фарм (требуем активное поглаживание: активное движение мыши и кулдаун) if self.last_pet_time == 0: self.last_pet_mouse_pos = (x, y) diff --git a/src/ui/main_window.py b/src/ui/main_window.py index e619905..042b1b2 100644 --- a/src/ui/main_window.py +++ b/src/ui/main_window.py @@ -55,7 +55,9 @@ def __init__(self, config_manager=None): self.is_dragging = False self.shake_count = 0 self.last_shake_time = 0 - self.original_size = QSize(100, 100) + + pet_size = self.config.get("pet_size") if self.config else 100 + self.original_size = QSize(pet_size, pet_size) # Начальный размер self.resize(self.original_size) @@ -91,6 +93,14 @@ def set_opacity(self, value): """Устанавливает прозрачность окна (0-100)""" self.setWindowOpacity(value / 100.0) + def set_pet_size(self, size): + """Устанавливает базовый размер питомца в пикселях (50-250)""" + if self.config: + self.config.set("pet_size", size) + self.original_size = QSize(size, size) + self.resize(self.original_size) + self.animation_manager.update_size(self.size()) + def set_always_on_top(self, enabled): """Включает или выключает режим 'Поверх всех окон' динамически""" if self.config: diff --git a/src/ui/settings_dialog.py b/src/ui/settings_dialog.py index 5339824..f1d9378 100644 --- a/src/ui/settings_dialog.py +++ b/src/ui/settings_dialog.py @@ -52,6 +52,21 @@ def __init__(self, config, parent=None): self.opacity_slider.valueChanged.connect(lambda v: self.opacity_val_label.setText(f"{v}%")) layout.addWidget(self.opacity_slider) + # Размер питомца + size_header_layout = QHBoxLayout() + size_header_layout.addWidget(QLabel("Размер питомца:")) + self.size_val_label = QLabel(f"{self.config.get('pet_size')}px") + self.size_val_label.setStyleSheet("font-weight: bold; color: #555;") + size_header_layout.addStretch() + size_header_layout.addWidget(self.size_val_label) + layout.addLayout(size_header_layout) + + self.size_slider = QSlider(Qt.Horizontal) + self.size_slider.setRange(50, 250) + self.size_slider.setValue(self.config.get("pet_size")) + self.size_slider.valueChanged.connect(lambda v: self.size_val_label.setText(f"{v}px")) + layout.addWidget(self.size_slider) + # Интервал растяжки layout.addWidget(QLabel("Интервал растяжки (мин):")) self.stretch_spin = QSpinBox() @@ -168,6 +183,7 @@ def save_settings(self): self.config.set("always_on_top", self.always_on_top_check.isChecked()) self.config.set("volume", self.volume_slider.value()) self.config.set("opacity", self.opacity_slider.value()) + self.config.set("pet_size", self.size_slider.value()) self.config.set("stretch_interval", self.stretch_spin.value()) self.config.set("pomodoro_work", self.pomodoro_work_spin.value()) self.config.set("pomodoro_break", self.pomodoro_break_spin.value()) diff --git a/src/ui/tray_menu.py b/src/ui/tray_menu.py index b3d0392..8bcea4a 100644 --- a/src/ui/tray_menu.py +++ b/src/ui/tray_menu.py @@ -183,9 +183,10 @@ def start_break_timer(self, checked=False): def show_settings(self, checked=False): dialog = SettingsDialog(self.window.config, self.window) if dialog.exec(): - # Обновляем скин и прозрачность в реальном времени + # Обновляем скин, прозрачность и размер в реальном времени self.window.animation_manager.set_skin(self.window.config.get("skin")) self.window.set_opacity(self.window.config.get("opacity")) + self.window.set_pet_size(self.window.config.get("pet_size")) # Обновляем меню скинов self.update_skin_menu() # Обновляем режим "Поверх всех окон" в реальном времени @@ -242,5 +243,10 @@ def update_skin_menu(self): self.skin_menu.clear() for skin_id, name in CAT_SKINS.items(): action = QAction(name, self) - action.triggered.connect(lambda checked=False, sid=skin_id: self.window.animation_manager.set_skin(sid)) + action.triggered.connect(lambda checked=False, sid=skin_id: self.select_skin(sid)) self.skin_menu.addAction(action) + + def select_skin(self, skin_id): + self.window.animation_manager.set_skin(skin_id) + if self.window.config: + self.window.config.set("skin", skin_id) diff --git a/src/utils/config_manager.py b/src/utils/config_manager.py index e6ad878..fb39511 100644 --- a/src/utils/config_manager.py +++ b/src/utils/config_manager.py @@ -12,7 +12,8 @@ class ConfigManager: "volume": 70, "language": "ru", "skin": "default", - "opacity": 100 + "opacity": 100, + "pet_size": 100 } def __init__(self, config_path="settings.json"): diff --git a/tests/test_utils.py b/tests/test_utils.py index 1a72e44..1f52d0b 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -58,6 +58,53 @@ def test_always_on_top_logic(self): self.assertFalse(bool(flags & Qt.WindowStaysOnTopHint)) self.assertFalse(config.get("always_on_top")) + window.close() + + def test_pet_size_configuration_and_window(self): + os.environ["QT_QPA_PLATFORM"] = "offscreen" + from PySide6.QtWidgets import QApplication + app = QApplication.instance() or QApplication([]) + + config = ConfigManager(self.config_path) + + # 1. Проверяем дефолтное значение pet_size в ConfigManager + self.assertEqual(config.get("pet_size"), 100) + + # 2. Установка нового размера в ConfigManager + config.set("pet_size", 150) + self.assertEqual(config.get("pet_size"), 150) + + # 3. Проверяем геометрию PetWindow + window = PetWindow(config) + self.assertEqual(window.original_size.width(), 150) + self.assertEqual(window.original_size.height(), 150) + + # 4. Динамическое изменение размера окна через set_pet_size + window.set_pet_size(200) + self.assertEqual(window.original_size.width(), 200) + self.assertEqual(window.original_size.height(), 200) + self.assertEqual(config.get("pet_size"), 200) + + window.close() + + def test_tray_menu_select_skin_saves_config(self): + os.environ["QT_QPA_PLATFORM"] = "offscreen" + from PySide6.QtWidgets import QApplication + from src.ui.tray_menu import TrayMenu + + app = QApplication.instance() or QApplication([]) + + config = ConfigManager(self.config_path) + window = PetWindow(config) + tray = TrayMenu(window) + + # Выбор скина через трей должен немедленно записать его в конфиг + tray.select_skin("orange") + self.assertEqual(window.animation_manager.skin, "orange") + self.assertEqual(config.get("skin"), "orange") + + window.close() + def test_config_manager(self): config = ConfigManager(self.config_path) config.set("username", "TestUser") @@ -306,6 +353,8 @@ def test_custom_skins_integration(self): self.assertEqual(anim_mgr.skin, "custom_test_skin") self.assertTrue(anim_mgr.current_anim_path.endswith("cat_custom_test_skin.svg")) + label.close() + # Очистка if os.path.exists(test_svg_path): os.remove(test_svg_path) @@ -356,6 +405,8 @@ def test_delete_custom_skin(self): self.assertNotIn("custom_todel_skin", CAT_SKINS) self.assertEqual(config.get("skin"), "default") self.assertNotIn("custom_todel_skin", config.get("custom_skins") or {}) + + dialog.close() finally: # Восстанавливаем моки гарантированно QMessageBox.question = original_question @@ -412,6 +463,8 @@ def test_animation_manager_current_fps(self): am.current_fps = 15 self.assertEqual(am.current_fps, 15) + label.close() + def test_sound_manager_fallback(self): from src.utils.sound_manager import SoundManager from PySide6.QtMultimedia import QSoundEffect @@ -514,6 +567,7 @@ def test_stats_dialog_reset_ui_flow(self): finally: QMessageBox.question = original_question QMessageBox.information = original_information + dialog.close() db.close() if __name__ == '__main__': diff --git a/verification/screenshots/stats_history.png b/verification/screenshots/stats_history.png index 1fa59b4..4e0baad 100644 Binary files a/verification/screenshots/stats_history.png and b/verification/screenshots/stats_history.png differ