diff --git a/src/core/animation_manager.py b/src/core/animation_manager.py index faa829b..07b6800 100644 --- a/src/core/animation_manager.py +++ b/src/core/animation_manager.py @@ -157,6 +157,16 @@ def update_frame(self): elif self.current_state == "thinking": # Наклон + покачивание painter.rotate(10 + 5 * math.sin(self.frame_counter * 0.2)) + elif self.current_state == "playing": + # Покачивание и подпрыгивание + angle = 8 * math.sin(self.frame_counter * 0.6) + painter.rotate(angle) + painter.translate(0, -abs(8 * math.sin(self.frame_counter * 0.6))) + elif self.current_state == "shaking": + # Сильное встряхивание с динамическим изменением масштаба + scale = 1.0 + 0.1 * math.sin(self.frame_counter * 0.9) + painter.scale(scale, scale) + painter.translate(random.randint(-5, 5), random.randint(-5, 5)) painter.translate(-size.width() / 2, -size.height() / 2) diff --git a/src/core/input_manager.py b/src/core/input_manager.py index 9ca9d10..6733e8d 100644 --- a/src/core/input_manager.py +++ b/src/core/input_manager.py @@ -44,6 +44,8 @@ def stop(self): self.keyboard_listener.stop() class InputManager(QObject): + laser_mode_changed = Signal(bool) + def __init__(self, pet_window, data_store=None): super().__init__() self.window = pet_window @@ -442,6 +444,7 @@ def handle_mouse(self, x, y): def toggle_laser_mode(self): self.laser_mode = not self.laser_mode + self.laser_mode_changed.emit(self.laser_mode) if self.laser_mode: self.window.animation_manager.play_state("hunting") # Создаем красивый светящийся красный лазерный курсор diff --git a/src/ui/tray_menu.py b/src/ui/tray_menu.py index b3d0392..92389fc 100644 --- a/src/ui/tray_menu.py +++ b/src/ui/tray_menu.py @@ -59,10 +59,13 @@ def setup_menu(self): self.menu.addSeparator() # Лазерная указка - laser_action = QAction("Лазерная указка 🔴", self) - laser_action.setCheckable(True) - laser_action.triggered.connect(self.toggle_laser) - self.menu.addAction(laser_action) + self.laser_action = QAction("Лазерная указка 🔴", self) + self.laser_action.setCheckable(True) + self.laser_action.triggered.connect(self.toggle_laser) + self.menu.addAction(self.laser_action) + + if self.window.input_manager: + self.window.input_manager.laser_mode_changed.connect(self.laser_action.setChecked) self.menu.addSeparator() diff --git a/tests/test_utils.py b/tests/test_utils.py index 1a72e44..0fad989 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -58,6 +58,8 @@ def test_always_on_top_logic(self): self.assertFalse(bool(flags & Qt.WindowStaysOnTopHint)) self.assertFalse(config.get("always_on_top")) + window.close() + def test_config_manager(self): config = ConfigManager(self.config_path) config.set("username", "TestUser") @@ -201,19 +203,58 @@ def test_laser_mode_transitions(self): db = DataStore(self.db_path) im = InputManager(mock_window, db) + # Подключаем проверку сигнала laser_mode_changed + signal_received = [] + im.laser_mode_changed.connect(lambda val: signal_received.append(val)) + # Переключаем лазер в True im.toggle_laser_mode() self.assertTrue(im.laser_mode) + self.assertEqual(signal_received, [True]) mock_window.animation_manager.play_state.assert_called_with("hunting") mock_window.setCursor.assert_called() # Переключаем обратно в False im.toggle_laser_mode() self.assertFalse(im.laser_mode) + self.assertEqual(signal_received, [True, False]) mock_window.animation_manager.play_state.assert_called_with("idle") db.close() + def test_procedural_svg_playing_and_shaking(self): + os.environ["QT_QPA_PLATFORM"] = "offscreen" + from PySide6.QtWidgets import QApplication, QLabel + from src.core.animation_manager import AnimationManager + from PySide6.QtCore import QSize + + app = QApplication.instance() or QApplication([]) + label = QLabel() + label.resize(QSize(100, 100)) + + config = ConfigManager(self.config_path) + am = AnimationManager(label, config) + + # Настраиваем фиктивный svg_renderer + mock_svg = MagicMock() + am.svg_renderer = mock_svg + + # Тест для состояния playing + am.current_state = "playing" + am.update_frame() + self.assertEqual(am.frame_counter, 1) + mock_svg.render.assert_called() + + # Тест для состояния shaking + mock_svg.reset_mock() + am.current_state = "shaking" + am.update_frame() + self.assertEqual(am.frame_counter, 2) + mock_svg.render.assert_called() + + am.anim_timer.stop() + label.close() + def test_force_state_delays_idle(self): mock_window = MagicMock() mock_window.animation_manager.current_state = "idle" @@ -306,6 +347,9 @@ 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")) + anim_mgr.anim_timer.stop() + label.close() + # Очистка if os.path.exists(test_svg_path): os.remove(test_svg_path) @@ -356,6 +400,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 +458,9 @@ def test_animation_manager_current_fps(self): am.current_fps = 15 self.assertEqual(am.current_fps, 15) + am.anim_timer.stop() + label.close() + def test_sound_manager_fallback(self): from src.utils.sound_manager import SoundManager from PySide6.QtMultimedia import QSoundEffect @@ -514,6 +563,8 @@ def test_stats_dialog_reset_ui_flow(self): finally: QMessageBox.question = original_question QMessageBox.information = original_information + dialog.update_timer.stop() + dialog.close() db.close() if __name__ == '__main__': diff --git a/verification/screenshots/stats_history.png b/verification/screenshots/stats_history.png index 1fa59b4..497c743 100644 Binary files a/verification/screenshots/stats_history.png and b/verification/screenshots/stats_history.png differ