Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/core/animation_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,21 @@ 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)
jump_y = -abs(10 * math.sin(self.frame_counter * 0.6))
painter.translate(0, jump_y)
painter.rotate(angle)
elif self.current_state == "shaking":
# Сильное головокружительное встряхивание с изменением масштаба
angle = 12 * math.sin(self.frame_counter * 1.2)
shake_x = random.randint(-5, 5)
shake_y = random.randint(-3, 3)
scale = 1.0 + 0.05 * math.sin(self.frame_counter * 0.8)
painter.translate(shake_x, shake_y)
painter.rotate(angle)
painter.scale(scale, scale)

painter.translate(-size.width() / 2, -size.height() / 2)

Expand Down
8 changes: 6 additions & 2 deletions src/core/input_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -410,8 +412,9 @@ 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:
# Динамический радиус поглаживания в зависимости от размера окна питомца
pet_radius = max(30.0, min(self.window.width(), self.window.height()) * 0.6)
if dist_sq_pet < pet_radius * pet_radius:
# Исключаем пассивный фарм (требуем активное поглаживание: активное движение мыши и кулдаун)
if self.last_pet_time == 0:
self.last_pet_mouse_pos = (x, y)
Expand Down Expand Up @@ -470,6 +473,7 @@ def toggle_laser_mode(self):
else:
self.window.setCursor(Qt.ArrowCursor)
self.window.animation_manager.play_state("idle")
self.laser_mode_changed.emit(self.laser_mode)
return self.laser_mode

def reset_all_data(self):
Expand Down
10 changes: 9 additions & 1 deletion src/ui/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ 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)
Expand Down Expand Up @@ -91,6 +92,13 @@ def set_opacity(self, value):
"""Устанавливает прозрачность окна (0-100)"""
self.setWindowOpacity(value / 100.0)

def set_pet_size(self, size):
"""Устанавливает базовый размер питомца в пикселях (50-250)."""
self.original_size = QSize(size, size)
self.resize(self.original_size)
if self.animation_manager:
self.animation_manager.update_size(self.size())

def set_always_on_top(self, enabled):
"""Включает или выключает режим 'Поверх всех окон' динамически"""
if self.config:
Expand Down
16 changes: 16 additions & 0 deletions src/ui/settings_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -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("Размер питомца (px):"))
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()
Expand Down Expand Up @@ -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())
Expand Down
25 changes: 19 additions & 6 deletions src/ui/tray_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
if self.window.input_manager:
self.laser_action.setChecked(self.window.input_manager.laser_mode)
self.window.input_manager.laser_mode_changed.connect(self.laser_action.setChecked)
self.laser_action.triggered.connect(self.toggle_laser)
self.menu.addAction(self.laser_action)

self.menu.addSeparator()

Expand Down Expand Up @@ -183,9 +186,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()
# Обновляем режим "Поверх всех окон" в реальном времени
Expand Down Expand Up @@ -238,9 +242,18 @@ def toggle_laser(self, checked):
else:
self.show_message("Мини-игра", "Лазерная указка выключена.")

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)
self.update_skin_menu()

def update_skin_menu(self):
self.skin_menu.clear()
current_skin = self.window.config.get("skin") if self.window.config else "default"
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.setCheckable(True)
action.setChecked(skin_id == current_skin)
action.triggered.connect(lambda checked=False, sid=skin_id: self.select_skin(sid))
self.skin_menu.addAction(action)
3 changes: 2 additions & 1 deletion src/utils/config_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"):
Expand Down
141 changes: 141 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -516,5 +516,146 @@ def test_stats_dialog_reset_ui_flow(self):
QMessageBox.information = original_information
db.close()

def test_procedural_animations_playing_shaking(self):
os.environ["QT_QPA_PLATFORM"] = "offscreen"
from PySide6.QtWidgets import QApplication, QLabel
from src.core.animation_manager import AnimationManager
from PySide6.QtSvg import QSvgRenderer

app = QApplication.instance() or QApplication([])
label = QLabel()
label.resize(100, 100)
config = ConfigManager(self.config_path)
am = AnimationManager(label, config)

# Мокаем svg_renderer
am.svg_renderer = MagicMock(spec=QSvgRenderer)

# Проверяем кадр состояния "playing"
am.play_state("playing")
am.update_frame()
self.assertEqual(am.current_state, "playing")
self.assertIsNotNone(label.pixmap())

# Проверяем кадр состояния "shaking"
am.play_state("shaking")
am.update_frame()
self.assertEqual(am.current_state, "shaking")
self.assertIsNotNone(label.pixmap())

am.anim_timer.stop()
label.close()

def test_laser_mode_signal_and_tray_sync(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)
db = DataStore(self.db_path)
im = InputManager(window, db)
window.input_manager = im

tray = TrayMenu(window)

# Проверяем, что сигнал переключает галочку в трей-меню
im.toggle_laser_mode()
self.assertTrue(im.laser_mode)
self.assertTrue(tray.laser_action.isChecked())

im.toggle_laser_mode()
self.assertFalse(im.laser_mode)
self.assertFalse(tray.laser_action.isChecked())

im.watchdog.stop()
window.close()
tray.deleteLater()
db.close()

def test_tray_menu_skin_checkmarks(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)
config.set("skin", "orange")
window = PetWindow(config)

tray = TrayMenu(window)

# Находим action для "orange" и "default"
orange_action = None
default_action = None
for action in tray.skin_menu.actions():
if action.text() == "Рыжий":
orange_action = action
elif action.text() == "Стандартный":
default_action = action

self.assertIsNotNone(orange_action)
self.assertIsNotNone(default_action)
self.assertTrue(orange_action.isChecked())
self.assertFalse(default_action.isChecked())

# Выбираем другой скин
tray.select_skin("default")
self.assertEqual(config.get("skin"), "default")

window.close()
tray.deleteLater()

def test_pet_size_and_dynamic_petting_radius(self):
os.environ["QT_QPA_PLATFORM"] = "offscreen"
from PySide6.QtWidgets import QApplication
from src.ui.settings_dialog import SettingsDialog

app = QApplication.instance() or QApplication([])
config = ConfigManager(self.config_path)

# 1. Проверяем дефолтный pet_size в ConfigManager
self.assertEqual(config.get("pet_size"), 100)

# 2. Инициализация и изменение размера через PetWindow.set_pet_size
window = PetWindow(config)
self.assertEqual(window.width(), 100)
self.assertEqual(window.height(), 100)

window.set_pet_size(150)
self.assertEqual(window.width(), 150)
self.assertEqual(window.height(), 150)
self.assertEqual(window.original_size.width(), 150)

# 3. Настройка pet_size в SettingsDialog
dialog = SettingsDialog(config)
dialog.size_slider.setValue(180)
dialog.save_settings()
self.assertEqual(config.get("pet_size"), 180)

# 4. Проверка динамического радиуса поглаживания в InputManager
db = DataStore(self.db_path)
im = InputManager(window, db)

# При окне 150x150, центр на (175, 175) для позиционирования окна на (100, 100)
# pet_radius = max(30, 150 * 0.6) = 90px
mock_pos = MagicMock()
mock_pos.x.return_value = 100
mock_pos.y.return_value = 100
window.get_cached_pos = MagicMock(return_value=mock_pos)

# Поглаживание в пределах 90px от центра (175, 175) - например на (235, 235)
im.handle_mouse(175, 175)
im.last_pet_time -= 1.0
im.last_mouse_time -= 1.0 # симулируем нормальную скорость движения мыши (не мгновенный скачок)
im.handle_mouse(235, 235) # Расстояние от 175 до 235 = 60px (< 90px радиус), вектор движения ~85px > 30px
self.assertEqual(im.pending_stats["petting_count"], 1)

im.watchdog.stop()
window.close()
dialog.close()
db.close()

if __name__ == '__main__':
unittest.main()
Binary file modified verification/screenshots/stats_history.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.