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
10 changes: 6 additions & 4 deletions src/core/animation_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,9 @@ def update_frame(self):
angle = 5 * math.sin(self.frame_counter * 0.8)
painter.rotate(angle)
elif self.current_state == "happy":
# Прыжки
painter.translate(0, -abs(15 * math.sin(self.frame_counter * 0.5)))
# Прыжки, пропорциональные высоте окна котика
jump_amplitude = size.height() * 0.15
painter.translate(0, -abs(jump_amplitude * 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 @@ -147,9 +148,10 @@ def update_frame(self):
# Растягивание
painter.scale(0.8, 1.4)
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))
eat_offset = size.height() * 0.1
painter.translate(0, eat_offset * (scale_y - 1.0))
painter.scale(1.0, scale_y)
elif self.current_state == "thinking":
# Наклон + покачивание
Expand Down
7 changes: 5 additions & 2 deletions src/core/input_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -410,8 +410,11 @@ 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:
# Оптимизация: динамическое пороговое расстояние (60% ширины окна)
pet_pet_threshold = max(30, int(self.window.width() * 0.6))
pet_pet_threshold_sq = pet_pet_threshold * pet_pet_threshold

if dist_sq_pet < pet_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,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_val = self.config.get("pet_size") if self.config else 100
self.original_size = QSize(pet_size_val, pet_size_val)

# Начальный размер
self.resize(self.original_size)
Expand Down Expand Up @@ -91,6 +93,15 @@ def set_opacity(self, value):
"""Устанавливает прозрачность окна (0-100)"""
self.setWindowOpacity(value / 100.0)

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

def set_always_on_top(self, enabled):
"""Включает или выключает режим 'Поверх всех окон' динамически"""
if self.config:
Expand Down
26 changes: 25 additions & 1 deletion src/ui/settings_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def __init__(self, config, parent=None):
self.volume_slider = QSlider(Qt.Horizontal)
self.volume_slider.setRange(0, 100)
self.volume_slider.setValue(self.config.get("volume"))
self.volume_slider.valueChanged.connect(lambda v: self.volume_val_label.setText(f"{v}%"))
self.volume_slider.valueChanged.connect(self.on_volume_changed)
layout.addWidget(self.volume_slider)

# Прозрачность
Expand All @@ -51,6 +51,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)

# Размер котика (pet_size)
size_header_layout = QHBoxLayout()
size_header_layout.addWidget(QLabel("Размер котика (px):"))
self.size_val_label = QLabel(f"{self.config.get('pet_size')}x{self.config.get('pet_size')}")
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}x{v}"))
layout.addWidget(self.size_slider)

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

def on_volume_changed(self, value):
self.volume_val_label.setText(f"{value}%")
# Мгновенная интерактивная обратная связь - тихий тестовый "meow"
if self.parent() and hasattr(self.parent(), "sound_manager") and self.parent().sound_manager:
# На время проигрывания временно обновляем громкость в конфигурации, чтобы звук соответствовал позиции слайдера
self.config.set("volume", value)
self.parent().sound_manager.play_sound("meow")

def on_skin_changed(self, index=0):
current_skin_id = self.skin_combo.currentData()
is_custom = bool(current_skin_id and current_skin_id.startswith("custom_"))
Expand Down Expand Up @@ -161,6 +184,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
123 changes: 101 additions & 22 deletions src/ui/stats_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,27 @@
from src.utils.bonding_utils import get_level_info, ACHIEVEMENTS
from datetime import datetime, timezone

from PySide6.QtCore import QTimer

class StatsDialog(QDialog):
def __init__(self, db, parent=None):
super().__init__(parent)
self.db = db
self.setWindowTitle("Статистика Котика")
self.setFixedWidth(320)

# Список для обновления шкал прогресса заблокированных достижений на лету
self.locked_progress_bars = {}

main_layout = QVBoxLayout(self)

self.tabs = QTabWidget()
main_layout.addWidget(self.tabs)

# Вкладка 1: Прогресс
progress_tab = QWidget()
self.setup_progress_tab(progress_tab)
self.tabs.addTab(progress_tab, "Прогресс")
self.progress_tab = QWidget()
self.setup_progress_tab(self.progress_tab)
self.tabs.addTab(self.progress_tab, "Прогресс")

# Вкладка 2: История
history_tab = QWidget()
Expand All @@ -31,42 +36,48 @@ def __init__(self, db, parent=None):
close_btn.clicked.connect(self.accept)
main_layout.addWidget(close_btn)

# Таймер обновления данных в реальном времени
self.update_timer = QTimer(self)
self.update_timer.timeout.connect(self.update_stats_live)
self.update_timer.start(500)

def setup_progress_tab(self, widget):
layout = QVBoxLayout(widget)

points = self.db.get_affection_points()
if self.parent() and hasattr(self.parent(), 'input_manager') and self.parent().input_manager:
points += self.parent().input_manager.pending_points
level, title, points_in_level, points_for_next_level = get_level_info(points)

# Заголовок
title_label = QLabel(f"Уровень {level}: {title}")
title_label.setAlignment(Qt.AlignCenter)
title_label.setStyleSheet("font-size: 16px; font-weight: bold; margin-bottom: 5px;")
layout.addWidget(title_label)
self.title_label = QLabel(f"Уровень {level}: {title}")
self.title_label.setAlignment(Qt.AlignCenter)
self.title_label.setStyleSheet("font-size: 16px; font-weight: bold; margin-bottom: 5px;")
layout.addWidget(self.title_label)

# Общие очки и KPS в одной строке
stats_row = QHBoxLayout()
points_label = QLabel(f"Всего: {points} ❤️")
points_label.setStyleSheet("font-weight: bold; font-size: 12px;")
self.points_label = QLabel(f"Всего: {points} ❤️")
self.points_label.setStyleSheet("font-weight: bold; font-size: 12px;")

max_kps = self.db.get_stat("max_kps")
kps_label = QLabel(f"Рекорд: {max_kps} кл/сек ⚡")
kps_label.setStyleSheet("color: #555; font-size: 11px;")
if self.parent() and hasattr(self.parent(), 'input_manager') and self.parent().input_manager:
max_kps = max(max_kps, self.parent().input_manager.max_kps)
self.kps_label = QLabel(f"Рекорд: {max_kps} кл/сек ⚡")
self.kps_label.setStyleSheet("color: #555; font-size: 11px;")

stats_row.addWidget(points_label)
stats_row.addWidget(self.points_label)
stats_row.addStretch()
stats_row.addWidget(kps_label)
stats_row.addWidget(self.kps_label)
layout.addLayout(stats_row)

# Прогресс бар
if points_for_next_level > 0:
layout.addWidget(QLabel(f"До следующего уровня: {points_for_next_level - points_in_level}"))
progress = QProgressBar()
progress.setMaximum(points_for_next_level)
progress.setValue(points_in_level)
progress.setFormat("%v / %m")
layout.addWidget(progress)
else:
layout.addWidget(QLabel("Максимальный уровень достигнут! 🎉"))
self.next_level_desc_label = QLabel()
layout.addWidget(self.next_level_desc_label)
self.level_progress = QProgressBar()
layout.addWidget(self.level_progress)

self._update_level_progress_visuals(points, points_in_level, points_for_next_level)

layout.addSpacing(10)
layout.addWidget(QLabel("<b>Достижения:</b>"))
Expand Down Expand Up @@ -94,6 +105,7 @@ def setup_progress_tab(self, widget):

# Расчет прогресса
progress_text = ""
current_val = 0
if not is_unlocked and 'goal' in ach_info and 'stat' in ach_info:
if self.parent() and hasattr(self.parent(), 'input_manager') and self.parent().input_manager:
im = self.parent().input_manager
Expand Down Expand Up @@ -141,6 +153,8 @@ def setup_progress_tab(self, widget):
}
""")
ach_text_layout.addWidget(prog_bar)
# Сохраняем ссылку для обновления в реальном времени
self.locked_progress_bars[ach_id] = (prog_bar, info_label, ach_info)

ach_item_layout.addLayout(ach_text_layout)
ach_item_layout.addStretch()
Expand All @@ -151,6 +165,71 @@ def setup_progress_tab(self, widget):
scroll.setFixedHeight(200)
layout.addWidget(scroll)

def _update_level_progress_visuals(self, points, points_in_level, points_for_next_level):
if points_for_next_level > 0:
self.next_level_desc_label.setText(f"До следующего уровня: {points_for_next_level - points_in_level}")
self.level_progress.setMaximum(points_for_next_level)
self.level_progress.setValue(points_in_level)
self.level_progress.setFormat("%v / %m")
self.level_progress.show()
else:
self.next_level_desc_label.setText("Максимальный уровень достигнут! 🎉")
self.level_progress.hide()

def update_stats_live(self):
"""Регулярно опрашивает менеджер ввода и БД для обновления прогресса на лету."""
points = self.db.get_affection_points()
max_kps = self.db.get_stat("max_kps")

if self.parent() and hasattr(self.parent(), 'input_manager') and self.parent().input_manager:
im = self.parent().input_manager
points += im.pending_points
max_kps = max(max_kps, im.max_kps)

level, title, points_in_level, points_for_next_level = get_level_info(points)

# 1. Обновляем заголовок уровня и общие очки/KPS
self.title_label.setText(f"Уровень {level}: {title}")
self.points_label.setText(f"Всего: {points} ❤️")
self.kps_label.setText(f"Рекорд: {max_kps} кл/сек ⚡")

# 2. Обновляем уровень прогресса
self._update_level_progress_visuals(points, points_in_level, points_for_next_level)

# 3. Обновляем шкалы прогресса для закрытых достижений
unlocked = self.db.get_unlocked_achievements()
for ach_id, (prog_bar, label, ach_info) in list(self.locked_progress_bars.items()):
if ach_id in unlocked:
# Если достижение было разблокировано прямо сейчас, мы могли бы перерисовать,
# но для простоты и безопасности просто убираем его из отслеживания обновления
del self.locked_progress_bars[ach_id]
continue

current_val = 0
if self.parent() and hasattr(self.parent(), 'input_manager') and self.parent().input_manager:
im = self.parent().input_manager
if ach_info['stat'] == 'bonding_points':
current_val = im.last_affection_points + im.pending_points
elif ach_info['stat'] == 'total_clicks':
current_val = im.total_clicks_cache + im.pending_stats.get('total_clicks', 0)
elif ach_info['stat'] == 'max_kps':
current_val = im.max_kps
else:
current_val = self.db.get_stat(ach_info['stat']) + im.pending_stats.get(ach_info['stat'], 0)
else:
current_val = self.db.get_stat(ach_info['stat'])

if ach_info['stat'] == 'level':
current_val = level

goal = ach_info['goal']
prog_bar.setValue(min(current_val, goal))

progress_text = ""
if goal > 0:
progress_text = f" <span style='color: #888;'>({current_val}/{goal})</span>"
label.setText(f"<b>{ach_info['title']}</b>{progress_text}<br/><small>{ach_info['desc']}</small>")

def setup_history_tab(self, widget):
layout = QVBoxLayout(widget)

Expand Down
3 changes: 2 additions & 1 deletion 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 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
Loading