fix: compute work hours from events at runtime; add Achievements button to RPG view

This commit is contained in:
2026-06-25 02:08:33 +03:30
parent 85587d563d
commit 4fcb694418
3 changed files with 104 additions and 9 deletions

View File

@@ -73,7 +73,8 @@ func (h *Handler) clockOut(chatID int64) (string, error) {
streak, _ := h.updateStreak(user.ID, today, true)
xp, leveledUp, newLevel, _ := h.computeAndAwardXP(user.ID, sessionWork, streak, today)
stats, _ := h.DB.GetOrCreateRPGStats(user.ID)
unlocked := h.checkAchievements(user.ID, stats, sessionWork, today, isWeekend)
totalHours := float64(h.totalWorkSeconds(user.ID, loc)) / secondsPerHour
unlocked := h.checkAchievements(user.ID, stats, sessionWork, today, isWeekend, totalHours)
text += fmt.Sprintf("\n\n+%d XP (Lv.%d)", xp, newLevel)
if leveledUp {

View File

@@ -198,7 +198,7 @@ func (h *Handler) HandleMessage(ctx context.Context, msg *models.Message) {
case "/setbreak":
h.handleSetBreakMsg(ctx, msg)
case "/rpg":
h.handleActionMsg(ctx, msg, h.rpgStatus)
h.handleRPGCmd(ctx, msg)
case "/achievements":
h.handleActionMsg(ctx, msg, h.achievementsList)
case "/salary":
@@ -298,6 +298,10 @@ func (h *Handler) HandleCallback(ctx context.Context, cb *models.CallbackQuery)
h.leagueToggleCallback(ctx, chatID, msgID, cb.ID)
case "reporttoggle":
h.reportToggleCallback(ctx, chatID, msgID, cb.ID)
case "rpg_achievements":
h.rpgAchievementsCallback(ctx, chatID, msgID, cb.ID)
case "rpg_back":
h.rpgBackCallback(ctx, chatID, msgID, cb.ID)
case "username":
h.usernameCallback(ctx, chatID, msgID, cb.ID)
case "currency":

View File

@@ -1,11 +1,14 @@
package bot
import (
"context"
"fmt"
"log/slog"
"strings"
"time"
"github.com/go-telegram/bot/models"
"worktimeBot/internal/db"
)
@@ -121,12 +124,12 @@ func (h *Handler) updateStreak(userID int64, date string, hasWorkToday bool) (in
}
// checkAchievements evaluates and unlocks any newly earned achievements.
func (h *Handler) checkAchievements(userID int64, stats *db.RPGStats, workSeconds int64, date string, isWeekend bool) []string {
func (h *Handler) checkAchievements(userID int64, stats *db.RPGStats, workSeconds int64, date string, isWeekend bool, totalHours float64) []string {
unlocker := newAchievementUnlocker(h, userID)
h.checkEarlyAchievements(unlocker, workSeconds)
h.checkEventTimeAchievements(unlocker, userID, date)
h.checkStreakAchievements(unlocker, stats)
h.checkHourAchievements(unlocker, stats)
h.checkHourAchievements(unlocker, stats, totalHours)
h.checkSessionAchievements(unlocker, workSeconds, isWeekend)
h.checkLevelAchievements(unlocker, stats)
return unlocker.unlocked
@@ -201,8 +204,7 @@ func (h *Handler) checkStreakAchievements(u *achievementUnlocker, stats *db.RPGS
}
}
func (h *Handler) checkHourAchievements(u *achievementUnlocker, stats *db.RPGStats) {
hoursWorked := float64(stats.TotalWorkSeconds) / secondsPerHour
func (h *Handler) checkHourAchievements(u *achievementUnlocker, stats *db.RPGStats, hoursWorked float64) {
if hoursWorked >= 100.0 {
u.unlock("hundred_hours")
}
@@ -235,8 +237,34 @@ func (h *Handler) checkLevelAchievements(u *achievementUnlocker, stats *db.RPGSt
}
}
// totalWorkSeconds computes actual total work seconds from all events for the user.
// Calculated at runtime to stay in sync with event edits.
func (h *Handler) totalWorkSeconds(userID int64, loc *time.Location) int64 {
days, err := h.DB.GetUserDaysInRange(userID, "2000-01-01", "2100-01-01")
if err != nil {
return 0
}
var total int64
for _, day := range days {
if day.IsDayOff {
continue
}
events, err := h.DB.EventsForDayByDayID(day.ID)
if err != nil || len(events) == 0 {
continue
}
y, m, d := parseGregorianDateKey(day.Date)
dayStart := time.Date(y, time.Month(m), d, 0, 0, 0, 0, loc).Unix()
dayEnd := time.Date(y, time.Month(m), d, 23, 59, 59, 0, loc).Unix()
totals := ComputeDailyTotals(events, day.MinBreakThreshold, dayStart, dayEnd)
total += totals.TotalSeconds
}
return total
}
// buildRPGStatus returns a formatted string showing the user's RPG stats.
func (h *Handler) buildRPGStatus(userID int64) string {
func (h *Handler) buildRPGStatus(userID int64, loc *time.Location) string {
stats, err := h.DB.GetOrCreateRPGStats(userID)
if err != nil {
return "RPG stats unavailable"
@@ -252,7 +280,7 @@ func (h *Handler) buildRPGStatus(userID int64) string {
b.WriteString(fmt.Sprintf("XP: %d / %d (%d%%)\n", stats.XP, nextLevelXP, xpIntoLevel*100/xpNeeded))
b.WriteString(fmt.Sprintf("Streak: %d days (best: %d)\n", stats.CurrentStreak, stats.LongestStreak))
hoursTotal := float64(stats.TotalWorkSeconds) / secondsPerHour
hoursTotal := float64(h.totalWorkSeconds(userID, loc)) / secondsPerHour
b.WriteString(fmt.Sprintf("Total work: %.1f hours\n", hoursTotal))
// Show achievements count
@@ -296,7 +324,69 @@ func (h *Handler) rpgStatus(chatID int64) (string, error) {
if !st.RPGEnabled {
return "RPG system is disabled. Enable it in Settings or use /rpgtoggle.", nil
}
return h.buildRPGStatus(user.ID), nil
loc := loadLocation(user.Timezone)
return h.buildRPGStatus(user.ID, loc), nil
}
// handleRPGCmd handles /rpg — shows stats with an Achievements button.
func (h *Handler) handleRPGCmd(ctx context.Context, msg *models.Message) {
user, err := h.getOrCreateUser(msg.Chat.ID)
if err != nil {
h.sendText(ctx, msg.Chat.ID, "Error loading profile")
return
}
st, err := h.DB.GetOrCreateSettings(user.ID)
if err != nil {
h.sendText(ctx, msg.Chat.ID, "Error loading settings")
return
}
if !st.RPGEnabled {
h.sendText(ctx, msg.Chat.ID, "RPG system is disabled. Enable it in Settings or use /rpgtoggle.")
return
}
loc := loadLocation(user.Timezone)
text := h.buildRPGStatus(user.ID, loc)
kb := models.InlineKeyboardMarkup{
InlineKeyboard: [][]models.InlineKeyboardButton{
{{Text: "Achievements", CallbackData: "rpg_achievements"}},
{{Text: "Back to Menu", CallbackData: "back_menu"}},
},
}
h.sendWithKB(ctx, msg.Chat.ID, text, kb)
}
// rpgAchievementsCallback shows achievements from the RPG view.
func (h *Handler) rpgAchievementsCallback(ctx context.Context, chatID int64, msgID int, callbackID string) {
defer h.answerCb(ctx, callbackID)
user, err := h.getOrCreateUser(chatID)
if err != nil {
return
}
text := h.buildAchievementsList(user.ID)
kb := models.InlineKeyboardMarkup{
InlineKeyboard: [][]models.InlineKeyboardButton{
{{Text: "Back to RPG", CallbackData: "rpg_back"}},
},
}
h.editText(ctx, chatID, msgID, text, &kb)
}
// rpgBackCallback returns from achievements to the RPG stats view.
func (h *Handler) rpgBackCallback(ctx context.Context, chatID int64, msgID int, callbackID string) {
defer h.answerCb(ctx, callbackID)
user, err := h.getOrCreateUser(chatID)
if err != nil {
return
}
loc := loadLocation(user.Timezone)
text := h.buildRPGStatus(user.ID, loc)
kb := models.InlineKeyboardMarkup{
InlineKeyboard: [][]models.InlineKeyboardButton{
{{Text: "Achievements", CallbackData: "rpg_achievements"}},
{{Text: "Back to Menu", CallbackData: "back_menu"}},
},
}
h.editText(ctx, chatID, msgID, text, &kb)
}
// achievementsList returns the user's unlocked achievements.