fix(pet): человекочитаемые подписи в ленте XP питомца

Лента «последних начислений» печатала сырые коды причин из xp_log
(achievement:cr_5_lessons, tb:math-6-ch1-ach-start, lesson_complete).
Добавлен резолвер _xpReasonLabel: achievement:<slug> -> «Достижение
«<title>»» через ACHIEVEMENT_DEFS, tb:* -> «Учебник», недостающие
фиксированные причины (lesson_complete/daily_goal/daily_activity/
lab_experiment), сохранение уже-читаемых фраз, фолбэк «Награда».

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-04 12:39:43 +03:00
parent 785f85e1ef
commit 7b653d92c2
+31 -9
View File
@@ -1,6 +1,36 @@
'use strict';
const db = require('../db/db');
const { awardCoins } = require('./gamificationController');
const { ACHIEVEMENT_DEFS } = require('./gamification/_shared');
/* ── XP-log reason → человекочитаемый ярлык для ленты активности питомца ──
Сырые коды причин (`achievement:slug`, `tb:...`, `lesson_complete`, …)
не должны попадать в UID — резолвим их в понятные подписи. */
const SOURCE_LABELS = {
hangman_win: 'Виселица', crossword_win: 'Кроссворд',
test_answer: 'Тест', challenge: 'Задание',
lab_activity: 'Лаборатория', lab_experiment: 'Лаборатория',
assignment: 'Домашнее задание',
pet_petting: 'Поглаживание', pet_feeding: 'Кормёжка питомца',
mood_ecstatic: 'Бонус настроения', correct_answers: 'Тест',
test_complete: 'Тест', 'test_90+': 'Тест 90%+', test_perfect: 'Тест 100%',
daily_activity: 'Активность дня', daily_goal: 'Цель дня',
lesson_complete: 'Урок пройден',
};
const _ACH_TITLE = new Map(ACHIEVEMENT_DEFS.map(a => [a.slug, a.title]));
function _xpReasonLabel(reason) {
if (!reason) return 'Награда';
if (reason.startsWith('achievement:')) {
const t = _ACH_TITLE.get(reason.slice('achievement:'.length));
return t ? `Достижение «${t}»` : 'Достижение';
}
if (reason.startsWith('tb:')) return 'Учебник';
if (SOURCE_LABELS[reason]) return SOURCE_LABELS[reason];
// Уже читаемая фраза (например «Испытание: …») — содержит пробел/кириллицу.
if (/[А-Яа-яЁё ]/.test(reason)) return reason;
return 'Награда';
}
const BG_SHOP = [
{ id:'space', name:'Космос', price:50, desc:'Звёздный простор' },
@@ -115,19 +145,11 @@ function getPet(req, res) {
weeklyXP.push({ day: d.toLocaleDateString('ru', { weekday: 'short' }), xp: weekMap.get(dateStr) || 0 });
}
const SOURCE_LABELS = {
hangman_win: 'Виселица', crossword_win: 'Кроссворд',
test_answer: 'Тест', challenge: 'Задание',
lab_activity: 'Лаборатория', assignment: 'Домашнее задание',
pet_petting: 'Поглаживание', pet_feeding: 'Кормёжка питомца',
mood_ecstatic: 'Бонус настроения', correct_answers: 'Тест',
test_complete: 'Тест', 'test_90+': 'Тест 90%+', test_perfect: 'Тест 100%',
};
const rawActivity = db.prepare(
"SELECT amount, reason, created_at FROM xp_log WHERE user_id = ? ORDER BY created_at DESC LIMIT 6"
).all(req.user.id);
const recentActivity = rawActivity.map(r => ({
xp: r.amount, label: SOURCE_LABELS[r.reason] || r.reason, at: r.created_at,
xp: r.amount, label: _xpReasonLabel(r.reason), at: r.created_at,
}));
const pettingCooldown = user.pet_last_petted