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
4 changes: 2 additions & 2 deletions src/core/animation_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ def update_frame(self):
painter.rotate(angle)
elif self.current_state == "happy":
# Прыжки
painter.translate(0, -abs(15 * math.sin(self.frame_counter * 0.5)))
painter.translate(0, -abs(size.height() * 0.15 * math.sin(self.frame_counter * 0.5)))
elif self.current_state == "sleeping":
# Глубокое медленное дыхание + наклон
scale = 1.0 + 0.05 * math.sin(self.frame_counter * 0.1)
Expand All @@ -152,7 +152,7 @@ def update_frame(self):
elif self.current_state == "eating":
# Наклоны головы вперед-назад при еде
scale_y = 1.0 + 0.1 * abs(math.sin(self.frame_counter * 0.8))
painter.translate(0, 10 * (scale_y - 1.0))
painter.translate(0, (size.height() * 0.1) * (scale_y - 1.0))
painter.scale(1.0, scale_y)
elif self.current_state == "thinking":
# Наклон + покачивание
Expand Down
18 changes: 16 additions & 2 deletions src/core/input_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -410,8 +410,22 @@ 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_width = 100
if hasattr(self.window, 'original_size'):
orig_sz = self.window.original_size
w = orig_sz.width() if callable(getattr(orig_sz, 'width', None)) else getattr(orig_sz, 'width', 100)
if isinstance(w, (int, float)):
pet_width = w
elif hasattr(self.window, 'width'):
w = self.window.width()
if isinstance(w, (int, float)):
pet_width = w

pet_radius = pet_width * 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)
Expand Down
13 changes: 12 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,16 @@ def set_opacity(self, value):
"""Устанавливает прозрачность окна (0-100)"""
self.setWindowOpacity(value / 100.0)

def set_pet_size(self, size):
"""Устанавливает базовый размер окна питомца (50-250px)"""
size = max(50, min(250, int(size)))
self.original_size = QSize(size, size)
if not self.is_dragging:
self.resize(self.original_size)
self.animation_manager.update_size(self.size())
if self.config:
self.config.set("pet_size", size)

def set_always_on_top(self, enabled):
"""Включает или выключает режим 'Поверх всех окон' динамически"""
if self.config:
Expand Down
22 changes: 22 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("Размер питомца:"))
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(self.on_size_slider_changed)
layout.addWidget(self.size_slider)

# Интервал растяжки
layout.addWidget(QLabel("Интервал растяжки (мин):"))
self.stretch_spin = QSpinBox()
Expand Down Expand Up @@ -103,6 +118,12 @@ def __init__(self, config, parent=None):
btn_layout.addWidget(cancel_btn)
layout.addLayout(btn_layout)

def on_size_slider_changed(self, value):
self.size_val_label.setText(f"{value} px")
parent = self.parent()
if parent and hasattr(parent, "set_pet_size"):
parent.set_pet_size(value)

def play_test_sound(self):
# Если есть родительское окно с менеджером звуков, проигрываем звук через него
parent = self.parent()
Expand Down Expand Up @@ -168,6 +189,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
8 changes: 4 additions & 4 deletions src/ui/tray_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
# Обновляем режим "Поверх всех окон" в реальном времени
Expand All @@ -198,10 +199,9 @@ def show_settings(self, checked=False):
self.window.timer_system.restart_stretch_timer()
self.window.show_message("Настройки сохранены! 💾")
else:
# Даже если диалог был отклонен, скин мог быть импортирован или удален мгновенно.
# Поэтому мы в любом случае синхронизируем меню выбора окрасов в трее.
# Если диалог был отменен, восстанавливаем размер питомца и скин из конфига
self.window.set_pet_size(self.window.config.get("pet_size"))
self.update_skin_menu()
# Также обновляем отображаемый скин (если удалили текущий активный, он мог сброситься на default)
self.window.animation_manager.set_skin(self.window.config.get("skin"))

def show_stats(self, checked=False):
Expand Down
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
78 changes: 78 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,84 @@ def test_input_manager_reset_logic(self):

db.close()

def test_pet_size_config_and_window_resize(self):
os.environ["QT_QPA_PLATFORM"] = "offscreen"
from PySide6.QtWidgets import QApplication
app = QApplication.instance() or QApplication([])

config = ConfigManager(self.config_path)
self.assertEqual(config.get("pet_size"), 100)

config.set("pet_size", 150)
self.assertEqual(config.get("pet_size"), 150)

window = PetWindow(config)
self.assertEqual(window.original_size.width(), 150)
self.assertEqual(window.original_size.height(), 150)

# Динамическая смена размера
window.set_pet_size(180)
self.assertEqual(window.original_size.width(), 180)
self.assertEqual(config.get("pet_size"), 180)

def test_petting_radius_scaled_by_pet_size(self):
mock_window = MagicMock()
mock_window.original_size.width.return_value = 150
mock_window.width.return_value = 150
mock_window.height.return_value = 150

# Позиция центра на (150, 150)
mock_pos = MagicMock()
mock_pos.x.return_value = 75
mock_pos.y.return_value = 75
mock_window.get_cached_pos.return_value = mock_pos

mock_cursor = MagicMock()
mock_cursor.pos.return_value = QPoint(75, 75)
mock_window.cursor.return_value = mock_cursor

mock_window.animation_manager.current_state = "idle"

db = DataStore(self.db_path)
im = InputManager(mock_window, db)

# Радиус для 150px составляет 150 * 0.6 = 90px (порог 8100)
# Мышь на 150, 150 (расстояние от центра 75,75 до 150,150 = sqrt(75^2 + 75^2) = sqrt(11250) > 8100 -> вне радиуса)
# Мышь на 120, 120 (расстояние 45, 45 -> 45^2 + 45^2 = 4050 < 8100 -> внутри радиуса)

# 1. Первая точка поглаживания в (120, 120)
im.handle_mouse(120, 120)
self.assertEqual(im.pending_stats["petting_count"], 0)

# 2. Движение > 30px внутри расширенного радиуса (150, 150) с кулдауном
im.last_pet_time -= 1.0
im.handle_mouse(155, 155) # мазок от (120,120) до (155,155) это ~49px
self.assertEqual(im.pending_stats["petting_count"], 1)

db.close()

def test_settings_dialog_pet_size_slider(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)
mock_parent = MagicMock()

dialog = SettingsDialog(config, None)
dialog.parent = lambda: mock_parent
self.assertEqual(dialog.size_slider.value(), 100)

# Сдвигаем слайдер
dialog.size_slider.setValue(120)
mock_parent.set_pet_size.assert_called_with(120)

# Сохранение настроек
dialog.save_settings()
self.assertEqual(config.get("pet_size"), 120)

def test_stats_dialog_reset_ui_flow(self):
from src.ui.stats_dialog import StatsDialog
from PySide6.QtWidgets import QMessageBox, QApplication
Expand Down
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.