diff --git a/.jules/bolt.md b/.jules/bolt.md
index c35890c..211b7ef 100644
--- a/.jules/bolt.md
+++ b/.jules/bolt.md
@@ -62,3 +62,11 @@
## 2026-07-04 - [Activity History for User Engagement]
**Инсайт:** Пользователи ценят возможность видеть историю своих действий (кормление, достижения). Разделение статистики на «Прогресс» и «Историю» через QTabWidget позволяет сохранить компактность окна, предоставляя при этом больше информации.
**Действие:** Для логов и списков истории использовать QTabWidget с QListWidget, чтобы не перегружать основной экран статистики.
+
+## 2026-07-05 - [UI/UX Improvement: Achievement Progress Bars]
+**Инсайт:** Текстовое отображение прогресса достижений (например, "5/10") менее информативно и визуально скучно по сравнению с графическими индикаторами. Использование компактных QProgressBar внутри списков достижений позволяет пользователю мгновенно оценить прогресс.
+**Действие:** Всегда визуализировать численный прогресс через QProgressBar, если есть четкая цель (goal).
+
+## 2026-07-05 - [Code Quality: Redundant Event Logging]
+**Инсайт:** Логирование событий одновременно в UI-слое (TrayMenu) и в слое логики (InputManager) приводит к дублированию записей в БД. Это замусоривает историю и может сбивать пользователя с толку.
+**Действие:** Централизовать логирование ключевых событий (level_up, achievement) в InputManager. В TrayMenu логировать только те действия, которые инициируются напрямую пользователем через меню (кормление, запуск таймера).
diff --git a/src/core/input_manager.py b/src/core/input_manager.py
index 26ef6c9..8f4d435 100644
--- a/src/core/input_manager.py
+++ b/src/core/input_manager.py
@@ -60,6 +60,8 @@ def __init__(self, pet_window, data_store=None):
self.last_mouse_pos = (0, 0)
self.last_input_time = time.time()
self.last_purr_time = 0
+ self.last_pet_time = 0
+ self.last_pet_mouse_pos = (0, 0)
self.laser_mode = False
self.monitor.key_pressed.connect(self.handle_key)
@@ -165,15 +167,14 @@ def add_points(self, points):
# Сохраняем в БД только когда накопилось 10 очков (примерно каждые 10 сек активной работы)
if self.pending_points >= 10:
- self.flush_points()
+ self.flush_all()
# Проверка уровня (визуально можно чаще, используя буферизованные очки)
virtual_total = self.last_affection_points + self.pending_points
new_level = get_level(virtual_total)
if new_level > old_level:
- self.flush_points() # Обязательно сбрасываем перед уведомлением
- self.db.log_event("level_up", f"Новый уровень: {new_level}")
+ self.flush_all() # Обязательно сбрасываем перед уведомлением
self.window.show_message(f"Уровень дружбы повышен: {new_level} ❤️")
self.window.sound_manager.play_sound("happy")
self.db.log_event("level_up", f"Уровень повышен до {new_level}")
@@ -334,14 +335,20 @@ def handle_mouse(self, x, y):
# Оптимизация: сравнение квадрата расстояния (порог 60px -> 3600)
if dist_sq_pet < 3600:
- if self.window.animation_manager.current_state not in ["playing", "hunting", "shaking"]:
- self.window.animation_manager.play_state("playing")
- self.pending_stats["petting_count"] += 1
- if self.pending_stats["petting_count"] % 5 == 0:
- self.check_for_achievements()
- if now - self.last_purr_time > 2.0:
- self.window.sound_manager.play_sound("purr")
- self.last_purr_time = now
+ # Анти-фарм: требуем кулдаун 500мс и движение мыши хотя бы на 30пкс с прошлого поглаживания
+ m_dx = x - self.last_pet_mouse_pos[0]
+ m_dy = y - self.last_pet_mouse_pos[1]
+ if now - self.last_pet_time > 0.5 and (m_dx*m_dx + m_dy*m_dy) > 900:
+ if self.window.animation_manager.current_state not in ["playing", "hunting", "shaking"]:
+ self.window.animation_manager.play_state("playing")
+ self.pending_stats["petting_count"] += 1
+ self.last_pet_time = now
+ self.last_pet_mouse_pos = (x, y)
+ if self.pending_stats["petting_count"] % 5 == 0:
+ self.check_for_achievements()
+ if now - self.last_purr_time > 2.0:
+ self.window.sound_manager.play_sound("purr")
+ self.last_purr_time = now
def toggle_laser_mode(self):
self.laser_mode = not self.laser_mode
diff --git a/src/ui/stats_dialog.py b/src/ui/stats_dialog.py
index afa5377..f287560 100644
--- a/src/ui/stats_dialog.py
+++ b/src/ui/stats_dialog.py
@@ -45,12 +45,14 @@ def setup_progress_tab(self, widget):
# Общие очки и KPS в одной строке
stats_row = QHBoxLayout()
points_label = QLabel(f"Всего: {points} ❤️")
+ stats_row.addWidget(points_label)
max_kps = self.db.get_stat("max_kps")
- kps_label = QLabel(f"Рекорд скорости: {max_kps} кл/сек ⚡")
- kps_label.setAlignment(Qt.AlignCenter)
- kps_label.setStyleSheet("color: #555; font-size: 11px; margin-bottom: 5px;")
- layout.addWidget(kps_label)
+ kps_label = QLabel(f"Рекорд: {max_kps} кл/сек ⚡")
+ kps_label.setAlignment(Qt.AlignRight)
+ kps_label.setStyleSheet("color: #555; font-size: 11px;")
+ stats_row.addWidget(kps_label)
+ layout.addLayout(stats_row)
# Прогресс бар
if points_for_next_level > 0:
@@ -88,8 +90,14 @@ def setup_progress_tab(self, widget):
ach_item_layout.addWidget(item_label)
# Расчет прогресса
- progress_text = ""
- if not is_unlocked and 'goal' in ach_info and 'stat' in ach_info:
+ ach_text_layout = QVBoxLayout()
+ ach_text_layout.setSpacing(2)
+
+ progress_val = 0
+ goal = ach_info.get('goal', 0)
+ show_progress_bar = False
+
+ if not is_unlocked and goal > 0 and 'stat' in ach_info:
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':
@@ -106,14 +114,34 @@ def setup_progress_tab(self, widget):
if ach_info['stat'] == 'level':
current_val, _, _, _ = get_level_info(points)
- goal = ach_info['goal']
- if goal > 0:
- progress_text = f" ({current_val}/{goal})"
+ progress_val = current_val
+ show_progress_bar = True
- info_label = QLabel(f"{ach_info['title']}{progress_text}
{ach_info['desc']}")
+ info_label = QLabel(f"{ach_info['title']}
{ach_info['desc']}")
if not is_unlocked:
info_label.setStyleSheet("color: #888;")
- ach_item_layout.addWidget(info_label)
+ ach_text_layout.addWidget(info_label)
+
+ if show_progress_bar:
+ ach_progress = QProgressBar()
+ ach_progress.setMaximum(goal)
+ ach_progress.setValue(min(progress_val, goal))
+ ach_progress.setFixedHeight(10)
+ ach_progress.setTextVisible(False)
+ ach_progress.setStyleSheet("""
+ QProgressBar {
+ border: 1px solid #ddd;
+ border-radius: 5px;
+ background-color: #f0f0f0;
+ }
+ QProgressBar::chunk {
+ background-color: #4CAF50;
+ border-radius: 4px;
+ }
+ """)
+ ach_text_layout.addWidget(ach_progress)
+
+ ach_item_layout.addLayout(ach_text_layout)
ach_item_layout.addStretch()
scroll_layout.addWidget(ach_widget)
diff --git a/src/ui/tray_menu.py b/src/ui/tray_menu.py
index 6ee6c32..55a1d1a 100644
--- a/src/ui/tray_menu.py
+++ b/src/ui/tray_menu.py
@@ -117,8 +117,6 @@ def show_message(self, title, message):
def start_work_timer(self, checked=False):
if self.window.timer_system:
self.window.timer_system.start_pomodoro("work")
- if self.window.input_manager and self.window.input_manager.db:
- self.window.input_manager.db.log_event("pomodoro_start", "Начало работы")
self.window.show_message("Пора работать! 🛠")
if self.window.input_manager and self.window.input_manager.db:
self.window.input_manager.db.log_event("pomodoro_start", "Начата сессия работы")
@@ -126,8 +124,6 @@ def start_work_timer(self, checked=False):
def start_break_timer(self, checked=False):
if self.window.timer_system:
self.window.timer_system.start_pomodoro("break")
- if self.window.input_manager and self.window.input_manager.db:
- self.window.input_manager.db.log_event("pomodoro_start", "Перерыв")
self.window.show_message("Отдыхаем! ☕")
if self.window.input_manager and self.window.input_manager.db:
self.window.input_manager.db.log_event("pomodoro_start", "Начата сессия отдыха")
@@ -152,8 +148,6 @@ def show_stats(self, checked=False):
def feed_pet(self, checked=False):
self.window.animation_manager.play_state("eating")
if self.window.input_manager:
- if self.window.input_manager.db:
- self.window.input_manager.db.log_event("feeding", "Котик покормлен")
self.window.input_manager.add_points(5)
self.window.show_message("Мням! +5 ❤️")
self.window.input_manager.pending_stats["total_feedings"] += 1
diff --git a/verification/screenshots/stats_history.png b/verification/screenshots/stats_history.png
new file mode 100644
index 0000000..a212016
Binary files /dev/null and b/verification/screenshots/stats_history.png differ
diff --git a/verification/screenshots/stats_progress.png b/verification/screenshots/stats_progress.png
new file mode 100644
index 0000000..c87fd56
Binary files /dev/null and b/verification/screenshots/stats_progress.png differ